Skip to content
3 changes: 0 additions & 3 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@ on:
push:
branches:
- develop
pull_request:
branches:
- develop

permissions:
contents: read
Expand Down
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
22 changes: 17 additions & 5 deletions src/main/java/UMC/career_mate/domain/answer/Answer.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -39,49 +41,50 @@ 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,
"content": "2022.03.01~2022.08.01"
},
{
"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": ""
Expand All @@ -101,6 +104,14 @@ public class AnswerController {
{
"questionId": 35,
"content": ""
},
{
"questionId": 36,
"content": ""
},
{
"questionId": 37,
"content": ""
}
]
}
Expand All @@ -110,8 +121,9 @@ public class AnswerController {
"""
)
public ApiResponse<CommonResultCode> saveAnswerList(@LoginMember Member member,
@Valid @RequestBody AnswerCreateOrUpdateDTO answerCreateOrUpdateDTO) {
answerCommandService.saveAnswerList(member, answerCreateOrUpdateDTO);
@RequestPart(value = "data") @Valid AnswerCreateOrUpdateDTO answerCreateOrUpdateDTO,
@RequestPart(value = "image", required = false) List<MultipartFile> imageFileList) throws IOException {
answerCommandService.saveAnswerList(member, answerCreateOrUpdateDTO, imageFileList);
Comment on lines +124 to +126
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

파일 업로드 처리에 대한 보안 및 유효성 검증이 필요합니다.

이미지 파일 업로드 처리 시 다음 사항들을 고려해야 합니다:

  1. 파일 크기 제한
  2. 허용된 파일 형식 검증
  3. 악성 파일 검사
  4. IOException에 대한 구체적인 예외 처리

다음과 같은 검증 로직을 추가하는 것을 권장합니다:

+    @PostMapping
+    public ApiResponse<CommonResultCode> saveAnswerList(
+        @LoginMember Member member,
+        @RequestPart(value = "data") @Valid AnswerCreateOrUpdateDTO answerCreateOrUpdateDTO,
+        @RequestPart(value = "image", required = false) List<MultipartFile> imageFileList) throws IOException {
+        
+        if (imageFileList != null) {
+            for (MultipartFile file : imageFileList) {
+                validateImageFile(file);
+            }
+        }
+        
+        answerCommandService.saveAnswerList(member, answerCreateOrUpdateDTO, imageFileList);
+        return ApiResponse.onSuccess(CREATE_ANSWER_LIST);
+    }
+
+    private void validateImageFile(MultipartFile file) {
+        if (file.getSize() > 5_000_000) { // 5MB
+            throw new IllegalArgumentException("File size exceeds maximum limit");
+        }
+        String contentType = file.getContentType();
+        if (contentType == null || !contentType.startsWith("image/")) {
+            throw new IllegalArgumentException("Only image files are allowed");
+        }
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@RequestPart(value = "data") @Valid AnswerCreateOrUpdateDTO answerCreateOrUpdateDTO,
@RequestPart(value = "image", required = false) List<MultipartFile> imageFileList) throws IOException {
answerCommandService.saveAnswerList(member, answerCreateOrUpdateDTO, imageFileList);
@PostMapping
public ApiResponse<CommonResultCode> saveAnswerList(
@LoginMember Member member,
@RequestPart(value = "data") @Valid AnswerCreateOrUpdateDTO answerCreateOrUpdateDTO,
@RequestPart(value = "image", required = false) List<MultipartFile> imageFileList) throws IOException {
if (imageFileList != null) {
for (MultipartFile file : imageFileList) {
validateImageFile(file);
}
}
answerCommandService.saveAnswerList(member, answerCreateOrUpdateDTO, imageFileList);
return ApiResponse.onSuccess(CREATE_ANSWER_LIST);
}
private void validateImageFile(MultipartFile file) {
if (file.getSize() > 5_000_000) { // 5MB
throw new IllegalArgumentException("File size exceeds maximum limit");
}
String contentType = file.getContentType();
if (contentType == null || !contentType.startsWith("image/")) {
throw new IllegalArgumentException("Only image files are allowed");
}
}

return ApiResponse.onSuccess(CREATE_ANSWER_LIST);
}

Expand Down Expand Up @@ -143,62 +155,70 @@ public ApiResponse<List<AnswerInfoListDTO>> 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": "데이터를 기반으로 문제를 해결하는 과정에서 논리적 사고력과 커뮤니케이션 능력을 키웠습니다."
}
]
}
Expand All @@ -208,8 +228,9 @@ public ApiResponse<List<AnswerInfoListDTO>> getAnswerList(@LoginMember Member me
"""
)
public ApiResponse<CommonResultCode> updateAnswerList(@LoginMember Member member,
@Valid @RequestBody AnswerCreateOrUpdateDTO answerCreateOrUpdateDTO) {
answerCommandService.updateAnswerList(member, answerCreateOrUpdateDTO);
@RequestPart("data") @Valid AnswerCreateOrUpdateDTO answerCreateOrUpdateDTO,
@RequestPart(value = "image", required = false) List<MultipartFile> imageFileList) throws IOException {
answerCommandService.updateAnswerList(member, answerCreateOrUpdateDTO, imageFileList);
return ApiResponse.onSuccess(UPDATE_ANSWER_LIST);
Comment on lines +231 to 234
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

updateAnswerList 메소드도 동일한 파일 검증이 필요합니다.

saveAnswerList와 동일한 파일 업로드 보안 및 유효성 검증을 적용해야 합니다.

}

Expand All @@ -225,7 +246,6 @@ public ApiResponse<CommonResultCode> updateAnswerList(@LoginMember Member member
3. 기타 활동 (OTHER_ACTIVITIES)\s
4. 보유 기술 (TECHNICAL_SKILLS)\s
5. 최종 정리 (SUMMARY)

### Example Response JSON:
```json
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -14,17 +13,17 @@
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)
.build();
}
Comment on lines +16 to 23
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

toAnswer 메소드의 매개변수 유효성 검증이 필요합니다.

매개변수에 대한 null 체크가 없습니다. 다음과 같은 방어적 프로그래밍을 추가하는 것이 좋습니다:

 public static Answer toAnswer(String content, Member member, Question question, long sequence) {
+    Objects.requireNonNull(member, "Member cannot be null");
+    Objects.requireNonNull(question, "Question cannot be null");
+    if (content == null) {
+        content = "";
+    }
     return Answer.builder()
             .content(content)
             .member(member)
             .question(question)
             .sequence(sequence)
             .build();
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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)
.build();
}
public static Answer toAnswer(String content, Member member, Question question, long sequence) {
Objects.requireNonNull(member, "Member cannot be null");
Objects.requireNonNull(question, "Question cannot be null");
if (content == null) {
content = "";
}
return Answer.builder()
.content(content)
.member(member)
.question(question)
.sequence(sequence)
.build();
}


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())
Expand All @@ -40,7 +39,7 @@ public static List<AnswerInfoDTO> toAnswerInfoDTOList(List<Answer> answerList) {
public static AnswerInfoListDTO toAnswerListResponseDTO(Long sequence, List<Answer> answerList) {
return AnswerInfoListDTO.builder()
.sequence(sequence)
.answerList(toAnswerInfoDTOList(answerList))
.answerInfoDTOList(toAnswerInfoDTOList(answerList))
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

import java.util.List;

public record AnswerCreateOrUpdateDTO(List<AnswerList> answerList) {
public record AnswerCreateOrUpdateDTO(List<AnswerGroupDTO> answerGroupDTOList) {

public record AnswerList(Long sequence, List<AnswerInfo> answerInfoList) {
public record AnswerGroupDTO(Long sequence, List<AnswerInfoDTO> answerInfoDTOList) {
}

public record AnswerInfo(Long questionId, String content) {
public record AnswerInfoDTO(Long questionId, String content) {
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
@Builder
public record AnswerInfoListDTO(
Long sequence,
List<AnswerInfoDTO> answerList
List<AnswerInfoDTO> answerInfoDTOList
) {

}
Loading