diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 00fadce..84a98ba 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -4,9 +4,6 @@ on: push: branches: - develop - pull_request: - branches: - - develop permissions: contents: read diff --git a/build.gradle b/build.gradle index 83bcbce..3be3e43 100644 --- a/build.gradle +++ b/build.gradle @@ -67,6 +67,10 @@ dependencies { annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" annotationProcessor "jakarta.annotation:jakarta.annotation-api" annotationProcessor "jakarta.persistence:jakarta.persistence-api" + + // AWS SDK (S3) + implementation 'com.amazonaws:aws-java-sdk-s3:1.12.611' + implementation 'software.amazon.awssdk:s3:2.30.2' } tasks.named('test') { diff --git a/src/main/java/UMC/career_mate/domain/answer/Answer.java b/src/main/java/UMC/career_mate/domain/answer/Answer.java index 04395d0..b4c355e 100644 --- a/src/main/java/UMC/career_mate/domain/answer/Answer.java +++ b/src/main/java/UMC/career_mate/domain/answer/Answer.java @@ -3,8 +3,20 @@ import UMC.career_mate.domain.member.Member; import UMC.career_mate.domain.question.Question; import UMC.career_mate.global.entity.BaseEntity; -import jakarta.persistence.*; -import lombok.*; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; import org.hibernate.annotations.SQLRestriction; @Entity @@ -24,6 +36,9 @@ public class Answer extends BaseEntity { @Column(nullable = false) private String content; + @Column(nullable = false) + private Long sequence; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id") private Member member; @@ -32,9 +47,6 @@ public class Answer extends BaseEntity { @JoinColumn(name = "question_id") private Question question; - @Column(nullable = false) - private Long sequence; - public void updateContent(String content) { this.content = content; } diff --git a/src/main/java/UMC/career_mate/domain/answer/controller/AnswerController.java b/src/main/java/UMC/career_mate/domain/answer/controller/AnswerController.java index 0baac82..bee3efb 100644 --- a/src/main/java/UMC/career_mate/domain/answer/controller/AnswerController.java +++ b/src/main/java/UMC/career_mate/domain/answer/controller/AnswerController.java @@ -17,18 +17,20 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; 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.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; import java.util.List; import static UMC.career_mate.global.response.result.code.CommonResultCode.*; @RestController @RequestMapping("/answers") -@Tag(name = "05. 답변 API", description = "답변 도메인의 API 입니다.") +@Tag(name = "답변 API", description = "답변 도메인의 API 입니다.") @RequiredArgsConstructor public class AnswerController { private final AnswerCommandService answerCommandService; @@ -39,23 +41,20 @@ public class AnswerController { summary = "커리어 작성 API", description = """ - 커리어를 작성하는 API 입니다. - 커리어를 처음 작성할 때, 반드시 2개의 `answerInfoList` 데이터를 요청해야 합니다. 입력하지 않을 경우엔 빈 문자열로 요청을 할 수 있습니다. + 커리어를 작성하는 API입니다. + 커리어를 처음 작성할 때, 반드시 2개의 `answerInfoList` 데이터를 요청해야 합니다. + 입력하지 않을 경우엔 빈 문자열로 요청할 수 있습니다. ## 하나의 커리어만 작성하는 경우 ### Example REQUEST JSON: ```json { - "answerList": [ + "answerGroupDTOList": [ { "sequence": 1, - "answerInfoList": [ - { - "questionId": 30, - "content": "삼성전자 / IoT 개발팀" - }, + "answerInfoDTOList": [ { "questionId": 31, - "content": "백엔드 개발자" + "content": "카카오뱅크 / 대출상품기획팀" }, { "questionId": 32, @@ -63,25 +62,29 @@ public class AnswerController { }, { "questionId": 33, - "content": "IoT 디바이스 통신 프로토콜을 설계하며, 효율적인 데이터 전송 방식에 대해 배웠습니다." + "content": "회사명 / IoT 디바이스 팀" }, { "questionId": 34, - "content": "스마트 홈 서비스의 핵심 모듈을 개발하며, 사용자 경험 중심의 설계 중요성을 깨달았습니다." + "content": "IoT 디바이스 통신 프로토콜을 설계하며, 효율적인 데이터 전송 방식에 대해 배웠습니다." }, { "questionId": 35, + "content": "스마트 홈 서비스의 핵심 모듈을 개발하며, 사용자 경험 중심의 설계 중요성을 깨달았습니다." + }, + { + "questionId": 36, "content": "팀원들과의 코드 리뷰를 통해 문제를 다양한 시각으로 바라보는 법을 배웠습니다." + }, + { + "questionId": 37, + "content": "복잡한 문제를 해결하는 과정에서 논리적인 사고력과 협업 능력이 향상되었습니다." } ] }, { "sequence": 2, - "answerInfoList": [ - { - "questionId": 30, - "content": "" - }, + "answerInfoDTOList": [ { "questionId": 31, "content": "" @@ -101,6 +104,14 @@ public class AnswerController { { "questionId": 35, "content": "" + }, + { + "questionId": 36, + "content": "" + }, + { + "questionId": 37, + "content": "" } ] } @@ -110,8 +121,9 @@ public class AnswerController { """ ) public ApiResponse saveAnswerList(@LoginMember Member member, - @Valid @RequestBody AnswerCreateOrUpdateDTO answerCreateOrUpdateDTO) { - answerCommandService.saveAnswerList(member, answerCreateOrUpdateDTO); + @RequestPart(value = "data") @Valid AnswerCreateOrUpdateDTO answerCreateOrUpdateDTO, + @RequestPart(value = "image", required = false) List imageFileList) throws IOException { + answerCommandService.saveAnswerList(member, answerCreateOrUpdateDTO, imageFileList); return ApiResponse.onSuccess(CREATE_ANSWER_LIST); } @@ -143,62 +155,70 @@ public ApiResponse> getAnswerList(@LoginMember Member me ### Example REQUEST JSON: ```json { - "answerList": [ + "answerGroupDTOList": [ { "sequence": 1, - "answerInfoList": [ - { - "questionId": 30, - "content": "삼성전자 / IoT 개발팀" - }, + "answerInfoDTOList": [ { "questionId": 31, - "content": "백엔드 개발자" + "content": "네이버 / 데이터 분석팀" }, { "questionId": 32, - "content": "2022.03.01~2022.08.01" + "content": "2021.05.01~2022.02.28" }, { "questionId": 33, - "content": "IoT 디바이스 통신 프로토콜을 설계하며, 효율적인 데이터 전송 방식에 대해 배웠습니다." + "content": "스타트업 / 마케팅 데이터 분석팀" }, { "questionId": 34, - "content": "스마트 홈 서비스의 핵심 모듈을 개발하며, 사용자 경험 중심의 설계 중요성을 깨달았습니다." + "content": "A/B 테스트를 설계하여 마케팅 캠페인의 효과를 분석하고 최적의 전략을 도출했습니다." }, { "questionId": 35, - "content": "팀원들과의 코드 리뷰를 통해 문제를 다양한 시각으로 바라보는 법을 배웠습니다." + "content": "데이터 시각화를 통해 인사이트를 제공하며, 의사결정 과정을 지원하는 방법을 배웠습니다." + }, + { + "questionId": 36, + "content": "Python과 SQL을 활용한 데이터 분석 프로젝트를 진행하며, 데이터 처리 및 분석 역량을 키웠습니다." + }, + { + "questionId": 37, + "content": "데이터 기반 의사결정의 중요성을 체감하며, 문제 해결력을 향상시킬 수 있었습니다." } ] }, { "sequence": 2, - "answerInfoList": [ - { - "questionId": 30, - "content": "LG CNS / 클라우드 플랫폼 개발팀" - }, + "answerInfoDTOList": [ { "questionId": 31, - "content": "풀스택 개발자" + "content": "쿠팡 / 물류 최적화 팀" }, { "questionId": 32, - "content": "2021.09.01~2022.02.01" + "content": "2022.06.01~2022.12.31" }, { "questionId": 33, - "content": "클라우드 기반 서비스 배포 자동화를 구현하며 DevOps의 핵심 개념을 익혔습니다." + "content": "이커머스 기업 / 물류 데이터 분석팀" }, { "questionId": 34, - "content": "대규모 사용자 트래픽을 처리하며 안정적인 시스템 운영 경험을 쌓았습니다." + "content": "배송 최적화를 위한 데이터 분석을 진행하며, 머신러닝을 활용한 수요 예측 모델을 개발했습니다." }, { "questionId": 35, - "content": "다양한 클라우드 서비스를 연동하며 기술적 시야를 넓힐 수 있었습니다." + "content": "물류 데이터 분석을 통해 비용 절감과 효율성을 높이는 경험을 했습니다." + }, + { + "questionId": 36, + "content": "팀원들과 협업하여 데이터 기반 개선안을 도출하고, 이를 현업에 적용하는 과정을 배웠습니다." + }, + { + "questionId": 37, + "content": "데이터를 기반으로 문제를 해결하는 과정에서 논리적 사고력과 커뮤니케이션 능력을 키웠습니다." } ] } @@ -208,8 +228,9 @@ public ApiResponse> getAnswerList(@LoginMember Member me """ ) public ApiResponse updateAnswerList(@LoginMember Member member, - @Valid @RequestBody AnswerCreateOrUpdateDTO answerCreateOrUpdateDTO) { - answerCommandService.updateAnswerList(member, answerCreateOrUpdateDTO); + @RequestPart("data") @Valid AnswerCreateOrUpdateDTO answerCreateOrUpdateDTO, + @RequestPart(value = "image", required = false) List imageFileList) throws IOException { + answerCommandService.updateAnswerList(member, answerCreateOrUpdateDTO, imageFileList); return ApiResponse.onSuccess(UPDATE_ANSWER_LIST); } @@ -225,7 +246,6 @@ public ApiResponse updateAnswerList(@LoginMember Member member 3. 기타 활동 (OTHER_ACTIVITIES)\s 4. 보유 기술 (TECHNICAL_SKILLS)\s 5. 최종 정리 (SUMMARY) - ### Example Response JSON: ```json { diff --git a/src/main/java/UMC/career_mate/domain/answer/converter/AnswerConverter.java b/src/main/java/UMC/career_mate/domain/answer/converter/AnswerConverter.java index 83a14cc..07ba584 100644 --- a/src/main/java/UMC/career_mate/domain/answer/converter/AnswerConverter.java +++ b/src/main/java/UMC/career_mate/domain/answer/converter/AnswerConverter.java @@ -1,11 +1,10 @@ package UMC.career_mate.domain.answer.converter; import UMC.career_mate.domain.answer.Answer; -import UMC.career_mate.domain.answer.dto.request.AnswerCreateOrUpdateDTO; import UMC.career_mate.domain.answer.dto.response.AnswerCompletionStatusInfoDTO; import UMC.career_mate.domain.answer.dto.response.AnswerCompletionStatusInfoListDTO; -import UMC.career_mate.domain.answer.dto.response.AnswerInfoListDTO; import UMC.career_mate.domain.answer.dto.response.AnswerInfoDTO; +import UMC.career_mate.domain.answer.dto.response.AnswerInfoListDTO; import UMC.career_mate.domain.member.Member; import UMC.career_mate.domain.question.Question; import UMC.career_mate.domain.template.enums.TemplateType; @@ -14,9 +13,9 @@ import java.util.stream.Collectors; public class AnswerConverter { - public static Answer toAnswer(AnswerCreateOrUpdateDTO.AnswerInfo answerInfo, Member member, Question question, long sequence) { + public static Answer toAnswer(String content, Member member, Question question, long sequence) { return Answer.builder() - .content(answerInfo.content()) + .content(content) .member(member) .question(question) .sequence(sequence) @@ -24,7 +23,7 @@ public static Answer toAnswer(AnswerCreateOrUpdateDTO.AnswerInfo answerInfo, Mem } public static AnswerInfoDTO toAnswerInfoDTO(Answer answer) { - return AnswerInfoDTO.builder() + return UMC.career_mate.domain.answer.dto.response.AnswerInfoDTO.builder() .questionId(answer.getQuestion().getId()) .questionName(answer.getQuestion().getContent()) .content(answer.getContent()) @@ -40,7 +39,7 @@ public static List toAnswerInfoDTOList(List answerList) { public static AnswerInfoListDTO toAnswerListResponseDTO(Long sequence, List answerList) { return AnswerInfoListDTO.builder() .sequence(sequence) - .answerList(toAnswerInfoDTOList(answerList)) + .answerInfoDTOList(toAnswerInfoDTOList(answerList)) .build(); } diff --git a/src/main/java/UMC/career_mate/domain/answer/dto/request/AnswerCreateOrUpdateDTO.java b/src/main/java/UMC/career_mate/domain/answer/dto/request/AnswerCreateOrUpdateDTO.java index 18da8a4..2574377 100644 --- a/src/main/java/UMC/career_mate/domain/answer/dto/request/AnswerCreateOrUpdateDTO.java +++ b/src/main/java/UMC/career_mate/domain/answer/dto/request/AnswerCreateOrUpdateDTO.java @@ -2,12 +2,12 @@ import java.util.List; -public record AnswerCreateOrUpdateDTO(List answerList) { +public record AnswerCreateOrUpdateDTO(List answerGroupDTOList) { - public record AnswerList(Long sequence, List answerInfoList) { + public record AnswerGroupDTO(Long sequence, List answerInfoDTOList) { } - public record AnswerInfo(Long questionId, String content) { + public record AnswerInfoDTO(Long questionId, String content) { } } diff --git a/src/main/java/UMC/career_mate/domain/answer/dto/response/AnswerInfoListDTO.java b/src/main/java/UMC/career_mate/domain/answer/dto/response/AnswerInfoListDTO.java index 072fa3a..465cf32 100644 --- a/src/main/java/UMC/career_mate/domain/answer/dto/response/AnswerInfoListDTO.java +++ b/src/main/java/UMC/career_mate/domain/answer/dto/response/AnswerInfoListDTO.java @@ -7,7 +7,7 @@ @Builder public record AnswerInfoListDTO( Long sequence, - List answerList + List answerInfoDTOList ) { } diff --git a/src/main/java/UMC/career_mate/domain/answer/service/AnswerCommandService.java b/src/main/java/UMC/career_mate/domain/answer/service/AnswerCommandService.java index f4d9626..34d6469 100644 --- a/src/main/java/UMC/career_mate/domain/answer/service/AnswerCommandService.java +++ b/src/main/java/UMC/career_mate/domain/answer/service/AnswerCommandService.java @@ -3,52 +3,95 @@ import UMC.career_mate.domain.answer.Answer; import UMC.career_mate.domain.answer.converter.AnswerConverter; import UMC.career_mate.domain.answer.dto.request.AnswerCreateOrUpdateDTO; -import UMC.career_mate.domain.answer.dto.request.AnswerCreateOrUpdateDTO.AnswerInfo; -import UMC.career_mate.domain.answer.dto.request.AnswerCreateOrUpdateDTO.AnswerList; +import UMC.career_mate.domain.answer.dto.request.AnswerCreateOrUpdateDTO.AnswerInfoDTO; +import UMC.career_mate.domain.answer.dto.request.AnswerCreateOrUpdateDTO.AnswerGroupDTO; import UMC.career_mate.domain.answer.repository.AnswerRepository; import UMC.career_mate.domain.member.Member; +import UMC.career_mate.domain.member.repository.MemberRepository; import UMC.career_mate.domain.question.Question; import UMC.career_mate.domain.question.repository.QuestionRepository; import UMC.career_mate.global.response.exception.GeneralException; import UMC.career_mate.global.response.exception.code.CommonErrorCode; +import UMC.career_mate.global.s3.service.S3Uploader; import lombok.RequiredArgsConstructor; 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; @Service @RequiredArgsConstructor public class AnswerCommandService { private final AnswerRepository answerRepository; private final QuestionRepository questionRepository; + private final S3Uploader s3Uploader; + private final MemberRepository memberRepository; @Transactional - public void saveAnswerList(Member member, AnswerCreateOrUpdateDTO answerCreateOrUpdateDTO) { - long start_sequence = 1L; + public void saveAnswerList(Member member, AnswerCreateOrUpdateDTO answerCreateOrUpdateDTO, List imageFileList) throws IOException { + // 이미지 업로드 및 URL 저장 + List imageUrlList = new ArrayList<>(); + if (imageFileList != null && !imageFileList.isEmpty()) { + for (MultipartFile imageFile : imageFileList) { + if (!imageFile.isEmpty()) { + imageUrlList.add(s3Uploader.uploadImage(imageFile)); // S3 업로드 후 URL 리스트에 저장 + } + } + } - for (AnswerList answerList : answerCreateOrUpdateDTO.answerList()) { - for (AnswerInfo answerInfo : answerList.answerInfoList()) { + long sequence = 1L; + for (AnswerGroupDTO answerGroupDTO : answerCreateOrUpdateDTO.answerGroupDTOList()) { + String assignedImageUrl = (sequence <= imageUrlList.size()) ? imageUrlList.get((int) sequence - 1) : null; + + for (AnswerInfoDTO answerInfo : answerGroupDTO.answerInfoDTOList()) { Question question = questionRepository.findById(answerInfo.questionId()) .orElseThrow(() -> new GeneralException(CommonErrorCode.NOT_FOUND_QUESTION)); - Answer answer = AnswerConverter.toAnswer(answerInfo, member, question, start_sequence); + // 특정 질문에 대해서만 이미지 URL 저장 + String content = (assignedImageUrl != null && question.getId().equals(103L)) + ? assignedImageUrl + : answerInfo.content(); + + Answer answer = AnswerConverter.toAnswer(content, member, question, sequence); answerRepository.save(answer); } - start_sequence++; + sequence++; } } @Transactional - public void updateAnswerList(Member member, AnswerCreateOrUpdateDTO answerCreateOrUpdateDTO) { - for (AnswerList answerList : answerCreateOrUpdateDTO.answerList()) { - for (AnswerInfo answerInfo : answerList.answerInfoList()) { - Question question = questionRepository.findById(answerInfo.questionId()) + public void updateAnswerList(Member member, AnswerCreateOrUpdateDTO answerCreateOrUpdateDTO, List imageFileList) throws IOException { + // 이미지 업로드 및 URL 저장 + List imageUrlList = new ArrayList<>(); + if (imageFileList != null && !imageFileList.isEmpty()) { + for (MultipartFile imageFile : imageFileList) { + if (!imageFile.isEmpty()) { + imageUrlList.add(s3Uploader.uploadImage(imageFile)); // S3 업로드 후 URL 리스트에 저장 + } + } + } + + for (AnswerGroupDTO answerGroupDTO : answerCreateOrUpdateDTO.answerGroupDTOList()) { + String assignedImageUrl = (answerGroupDTO.sequence() <= imageUrlList.size()) + ? imageUrlList.get((int) (answerGroupDTO.sequence() - 1)) : null; + + for (AnswerInfoDTO answerInfoDTO : answerGroupDTO.answerInfoDTOList()) { + Question question = questionRepository.findById(answerInfoDTO.questionId()) .orElseThrow(() -> new GeneralException(CommonErrorCode.NOT_FOUND_QUESTION)); // 해당 회원, 질문, 시퀀스를 기준으로 기존 답변 조회 - Answer existingAnswer = answerRepository.findByMemberAndQuestionAndSequence(member, question, answerList.sequence()) + Answer existingAnswer = answerRepository.findByMemberAndQuestionAndSequence(member, question, answerGroupDTO.sequence()) .orElseThrow(() -> new GeneralException(CommonErrorCode.NOT_FOUND_ANSWER)); - existingAnswer.updateContent(answerInfo.content()); + // 특정 질문에 대해서만 이미지 URL 저장 + String content = (assignedImageUrl != null && question.getId().equals(103L)) + ? assignedImageUrl + : answerInfoDTO.content(); + + existingAnswer.updateContent(content); // 질문 order 1의 답변 sequence 1은 수정일을 매번 업데이트 -> recruit 조회 로직에서 사용 if (question.getOrder() == 1 && existingAnswer.getSequence() == 1) { @@ -57,4 +100,5 @@ public void updateAnswerList(Member member, AnswerCreateOrUpdateDTO answerCreate } } } + } diff --git a/src/main/java/UMC/career_mate/domain/content/controller/ContentController.java b/src/main/java/UMC/career_mate/domain/content/controller/ContentController.java index 0258f92..5511ad2 100644 --- a/src/main/java/UMC/career_mate/domain/content/controller/ContentController.java +++ b/src/main/java/UMC/career_mate/domain/content/controller/ContentController.java @@ -10,6 +10,7 @@ import UMC.career_mate.global.common.PageResponseDTO; import UMC.career_mate.global.response.ApiResponse; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @@ -17,6 +18,7 @@ @RestController @RequiredArgsConstructor +@Tag(name = "컨텐츠 API", description = "컨텐츠 도메인의 API 입니다.") @RequestMapping("/content") public class ContentController { @@ -119,4 +121,4 @@ public ApiResponse>> getScrapConte @LoginMember Member member) { return ApiResponse.onSuccess(contentScrapService.getScrapContents(member, page, size)); } -} \ No newline at end of file +} diff --git a/src/main/java/UMC/career_mate/domain/content/service/ContentService.java b/src/main/java/UMC/career_mate/domain/content/service/ContentService.java index 516b189..6e47d53 100644 --- a/src/main/java/UMC/career_mate/domain/content/service/ContentService.java +++ b/src/main/java/UMC/career_mate/domain/content/service/ContentService.java @@ -54,7 +54,7 @@ public PageResponseDTO> getContentsByJobId(int page, in // 로그인한 사용자의 직무 ID 확인 Long jobId = member.getJob().getId(); if (jobId == null) { - throw new GeneralException(CommonErrorCode.NOT_FOUND_JOB); + throw new GeneralException(CommonErrorCode.NOT_FOUND_BY_JOB_ID); } PageRequest pageRequest = PageRequest.of(page - 1, size); @@ -79,4 +79,4 @@ public PageResponseDTO> getContentsByJobId(int page, in .result(contentList) .build(); } -} \ No newline at end of file +} diff --git a/src/main/java/UMC/career_mate/domain/job/controller/JobController.java b/src/main/java/UMC/career_mate/domain/job/controller/JobController.java index 94cb9c5..306980f 100644 --- a/src/main/java/UMC/career_mate/domain/job/controller/JobController.java +++ b/src/main/java/UMC/career_mate/domain/job/controller/JobController.java @@ -5,6 +5,7 @@ import UMC.career_mate.global.response.ApiResponse; import UMC.career_mate.global.response.result.code.CommonResultCode; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -14,6 +15,7 @@ @RestController @RequiredArgsConstructor +@Tag(name = "직무 API", description = "직무 도메인의 API 입니다.") @RequestMapping("/job") public class JobController { diff --git a/src/main/java/UMC/career_mate/domain/member/controller/MemberController.java b/src/main/java/UMC/career_mate/domain/member/controller/MemberController.java index de0dba2..a56ed0a 100644 --- a/src/main/java/UMC/career_mate/domain/member/controller/MemberController.java +++ b/src/main/java/UMC/career_mate/domain/member/controller/MemberController.java @@ -8,11 +8,13 @@ import UMC.career_mate.global.response.ApiResponse; import UMC.career_mate.global.response.result.code.CommonResultCode; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @RestController @RequiredArgsConstructor +@Tag(name = "회원 API", description = "회원 도메인의 API 입니다.") @RequestMapping("/member") public class MemberController { diff --git a/src/main/java/UMC/career_mate/domain/planner/controller/PlannerController.java b/src/main/java/UMC/career_mate/domain/planner/controller/PlannerController.java index 86821c5..c930b03 100644 --- a/src/main/java/UMC/career_mate/domain/planner/controller/PlannerController.java +++ b/src/main/java/UMC/career_mate/domain/planner/controller/PlannerController.java @@ -8,11 +8,13 @@ import UMC.career_mate.global.annotation.LoginMember; import UMC.career_mate.global.response.ApiResponse; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @RestController @RequiredArgsConstructor +@Tag(name = "플래너 API", description = "플래너 도메인의 API 입니다.") @RequestMapping("/planner") public class PlannerController { diff --git a/src/main/java/UMC/career_mate/domain/recruit/controller/RecruitController.java b/src/main/java/UMC/career_mate/domain/recruit/controller/RecruitController.java index 1b36018..30cd06c 100644 --- a/src/main/java/UMC/career_mate/domain/recruit/controller/RecruitController.java +++ b/src/main/java/UMC/career_mate/domain/recruit/controller/RecruitController.java @@ -11,6 +11,7 @@ import UMC.career_mate.global.common.PageResponseDTO; import UMC.career_mate.global.response.ApiResponse; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -22,6 +23,7 @@ @RestController @RequiredArgsConstructor +@Tag(name = "채용 공고 API", description = "채용 공고 도메인의 API 입니다.") @RequestMapping("/recruits") public class RecruitController { @@ -29,8 +31,8 @@ public class RecruitController { private final RecruitQueryService recruitQueryService; @Operation( - summary = "추천 채용 공고 조회 API", - description = """ + summary = "추천 채용 공고 조회 API", + description = """ 추천 채용 공고를 조회하는 API입니다.\n\n page의 값은 1부터 시작이고, 기본 값은 1입니다.\n\n size의 기본값은 6입니다.\n\n @@ -41,26 +43,26 @@ public class RecruitController { """) @GetMapping public ApiResponse> getRecommendRecruitList( - @RequestParam(defaultValue = "1", required = false) int page, - @RequestParam(defaultValue = "6", required = false) int size, - @RequestParam(defaultValue = "POSTING_DESC", required = false) RecruitSortType recruitSortType, - @LoginMember Member member + @RequestParam(defaultValue = "1", required = false) int page, + @RequestParam(defaultValue = "6", required = false) int size, + @RequestParam(defaultValue = "POSTING_DESC", required = false) RecruitSortType recruitSortType, + @LoginMember Member member ) { return ApiResponse.onSuccess( - recruitQueryService.getRecommendRecruitList(page, size, recruitSortType, member)); + recruitQueryService.getRecommendRecruitList(page, size, recruitSortType, member)); } @Operation( - summary = "채용 공고 요약 페이지 조회 API", - description = """ + summary = "채용 공고 요약 페이지 조회 API", + description = """ 채용 공고 요약 페이지를 조회하는 API입니다.\n\n recruitId : 조회하려는 채용 공고 pk 값 """) @GetMapping("/{recruitId}") public ResponseEntity> getRecruitInfo(@PathVariable Long recruitId, - @LoginMember Member member) { + @LoginMember Member member) { return ResponseEntity.ok( - ApiResponse.onSuccess(recruitQueryService.findRecruitInfo(member, recruitId))); + ApiResponse.onSuccess(recruitQueryService.findRecruitInfo(member, recruitId))); } @Deprecated diff --git a/src/main/java/UMC/career_mate/domain/recruitScrap/controller/RecruitScrapController.java b/src/main/java/UMC/career_mate/domain/recruitScrap/controller/RecruitScrapController.java index 7b1977f..5c0279c 100644 --- a/src/main/java/UMC/career_mate/domain/recruitScrap/controller/RecruitScrapController.java +++ b/src/main/java/UMC/career_mate/domain/recruitScrap/controller/RecruitScrapController.java @@ -8,6 +8,8 @@ import UMC.career_mate.global.response.ApiResponse; import io.swagger.v3.oas.annotations.Operation; import java.util.List; + +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -18,6 +20,7 @@ @RestController @RequiredArgsConstructor +@Tag(name = "채용 공고 스크랩 API", description = "채용 공고 스크랩 도메인의 API 입니다.") @RequestMapping("/scrap/recruits") public class RecruitScrapController { diff --git a/src/main/java/UMC/career_mate/domain/template/controller/TemplateController.java b/src/main/java/UMC/career_mate/domain/template/controller/TemplateController.java index 64fe71b..923f95e 100644 --- a/src/main/java/UMC/career_mate/domain/template/controller/TemplateController.java +++ b/src/main/java/UMC/career_mate/domain/template/controller/TemplateController.java @@ -7,6 +7,7 @@ import UMC.career_mate.global.annotation.LoginMember; import UMC.career_mate.global.response.ApiResponse; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -17,6 +18,7 @@ @RestController @RequiredArgsConstructor +@Tag(name = "템플릿 API", description = "템플릿 도메인의 API 입니다.") @RequestMapping("/templates") public class TemplateController { private final TemplateQueryService templateQueryService; diff --git a/src/main/java/UMC/career_mate/global/response/exception/code/CommonErrorCode.java b/src/main/java/UMC/career_mate/global/response/exception/code/CommonErrorCode.java index 0301d0e..056e834 100644 --- a/src/main/java/UMC/career_mate/global/response/exception/code/CommonErrorCode.java +++ b/src/main/java/UMC/career_mate/global/response/exception/code/CommonErrorCode.java @@ -27,9 +27,6 @@ public enum CommonErrorCode implements ErrorCode { // Recruit 도메인 NOT_FOUND_RECRUIT(400, "ERE000", "해당 채용 공고를 찾을 수 없습니다."), - // Job 도메인 - NOT_FOUND_JOB(400, "ERJ000", "해당 직업을 찾을 수 없습니다."), - // Template 도메인 NOT_FOUND_TEMPLATE(400, "ERT000", "해당 템플릿을 찾을 수 없습니다."), diff --git a/src/main/java/UMC/career_mate/global/s3/config/S3Config.java b/src/main/java/UMC/career_mate/global/s3/config/S3Config.java new file mode 100644 index 0000000..41efb66 --- /dev/null +++ b/src/main/java/UMC/career_mate/global/s3/config/S3Config.java @@ -0,0 +1,34 @@ +package UMC.career_mate.global.s3.config; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class S3Config { + + @Value("${cloud.aws.credentials.access-key}") + private String accessKey; + + @Value("${cloud.aws.credentials.secret-key}") + private String secretKey; + + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public AmazonS3Client amazonS3Client() { + //accessKey, secretKey, region 값으로 S3에 접근 가능한 객체 등록 + BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey); + + return (AmazonS3Client) AmazonS3ClientBuilder + .standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(credentials)) + .build(); + } +} diff --git a/src/main/java/UMC/career_mate/global/s3/service/S3Uploader.java b/src/main/java/UMC/career_mate/global/s3/service/S3Uploader.java new file mode 100644 index 0000000..2dafeb3 --- /dev/null +++ b/src/main/java/UMC/career_mate/global/s3/service/S3Uploader.java @@ -0,0 +1,33 @@ +package UMC.career_mate.global.s3.service; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.ObjectMetadata; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.UUID; + +@Component +@RequiredArgsConstructor +public class S3Uploader { + private final AmazonS3 amazonS3; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + public String uploadImage(MultipartFile file) throws IOException { + // S3에 저장할 고유한 파일 이름 생성 + String fileName = UUID.randomUUID() + "_" + file.getOriginalFilename(); + + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentLength(file.getSize()); + metadata.setContentType(file.getContentType()); + + // 파일 업로드 + amazonS3.putObject(bucket, fileName, file.getInputStream(), metadata); + return amazonS3.getUrl(bucket, fileName).toString(); + } +} diff --git a/src/main/java/UMC/career_mate/global/security/SecurityController.java b/src/main/java/UMC/career_mate/global/security/SecurityController.java index 5d965f6..c01877a 100644 --- a/src/main/java/UMC/career_mate/global/security/SecurityController.java +++ b/src/main/java/UMC/career_mate/global/security/SecurityController.java @@ -4,6 +4,7 @@ import UMC.career_mate.global.security.service.RefreshTokenService; import UMC.career_mate.global.security.service.SecurityService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @@ -13,6 +14,7 @@ @RestController @RequiredArgsConstructor +@Tag(name = "인증 API", description = "인증 도메인의 API 입니다.") @RequestMapping("/auth") public class SecurityController {