Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DDING-95] 지원자 상세 조회 API 구현 #240

Merged
merged 7 commits into from
Feb 6, 2025
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ddingdong.ddingdongBE.domain.formapplication.api;

import ddingdong.ddingdongBE.auth.PrincipalDetails;
import ddingdong.ddingdongBE.domain.formapplication.controller.dto.response.FormApplicationResponse;
import ddingdong.ddingdongBE.domain.formapplication.controller.dto.response.MyFormApplicationPageResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
Expand Down Expand Up @@ -31,4 +32,16 @@ MyFormApplicationPageResponse getMyFormApplicationPage(
@AuthenticationPrincipal PrincipalDetails principalDetails
);

@Operation(summary = "지원자 상세 조회 API")
@ApiResponse(responseCode = "200", description = "지원자 상세 조회 성공",
content = @Content(schema = @Schema(implementation = FormApplicationResponse.class)))
@ResponseStatus(HttpStatus.OK)
@SecurityRequirement(name = "AccessToken")
@GetMapping("/my/forms/{formId}/applications/{applicationId}")
FormApplicationResponse getFormApplication(
@PathVariable("formId") Long formId,
@PathVariable("applicationId") Long applicationId,
@AuthenticationPrincipal PrincipalDetails principalDetails
);

}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package ddingdong.ddingdongBE.domain.formapplication.controller;

import ddingdong.ddingdongBE.auth.PrincipalDetails;
import ddingdong.ddingdongBE.domain.formapplication.controller.dto.response.FormApplicationResponse;
import ddingdong.ddingdongBE.domain.formapplication.service.FacadeCentralFormApplicationService;
import ddingdong.ddingdongBE.domain.formapplication.api.CentralFormApplicationApi;
import ddingdong.ddingdongBE.domain.formapplication.controller.dto.response.MyFormApplicationPageResponse;
import ddingdong.ddingdongBE.domain.formapplication.service.dto.query.FormApplicationQuery;
import ddingdong.ddingdongBE.domain.formapplication.service.dto.query.MyFormApplicationPageQuery;
import ddingdong.ddingdongBE.domain.user.entity.User;
import lombok.RequiredArgsConstructor;
Expand All @@ -21,4 +23,11 @@ public MyFormApplicationPageResponse getMyFormApplicationPage(Long formId, int s
MyFormApplicationPageQuery query = facadeCentralFormService.getMyFormApplicationPage(formId, user, size, currentCursorId);
return MyFormApplicationPageResponse.from(query);
}

@Override
public FormApplicationResponse getFormApplication(Long formId, Long applicationId, PrincipalDetails principalDetails) {
User user = principalDetails.getUser();
FormApplicationQuery query = facadeCentralFormService.getFormApplication(formId, applicationId, user);
return FormApplicationResponse.from(query);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package ddingdong.ddingdongBE.domain.formapplication.controller.dto.response;

import ddingdong.ddingdongBE.domain.form.entity.FieldType;
import ddingdong.ddingdongBE.domain.formapplication.entity.FormApplicationStatus;
import ddingdong.ddingdongBE.domain.formapplication.service.dto.query.FormApplicationQuery;
import ddingdong.ddingdongBE.domain.formapplication.service.dto.query.FormApplicationQuery.FormFieldAnswerListQuery;

import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;

import java.time.LocalDateTime;
import java.util.List;

@Builder
public record FormApplicationResponse (
@Schema(description = "제출일시", example = "2025-01-01T00:00")
LocalDateTime submittedAt,
@Schema(description = "지원자 이름", example = "김띵동")
String name,
@Schema(description = "지원자 학번", example = "60201111")
String studentNumber,
@Schema(description = "지원자 학과", example = "융합소프트웨어학부")
String department,
@Schema(description = "status", example = "SUBMITTED")
FormApplicationStatus status,
@ArraySchema(schema = @Schema(implementation = FormFieldAnswerListResponse.class))
List<FormFieldAnswerListResponse> formFieldAnswers
){
@Builder
record FormFieldAnswerListResponse (
@Schema(description = "폼지 질문 ID", example = "1")
Long fieldId,
@Schema(description = "폼지 질문", example = "성별이 무엇입니까??")
String question,
@Schema(description = "폼지 질문 유형", example = "RADIO", allowableValues = {"CHECK_BOX", "RADIO", "TEXT", "LONG_TEXT", "FILE"})
FieldType type,
@Schema(description = "폼지 지문", example = "[\"여성\", \"남성\"]")
List<String> options,
@Schema(description = "필수 여부", example = "true")
Boolean required,
@Schema(description = "질문 순서", example = "1")
Integer order,
@Schema(description = "섹션", example = "공통")
String section,
@Schema(description = "질문 답변 값", example = "[\"지문1\"]")
List<String> value
) {
public static FormFieldAnswerListResponse from(FormFieldAnswerListQuery formFieldAnswerListQuery) {
return FormFieldAnswerListResponse.builder()
.fieldId(formFieldAnswerListQuery.fieldId())
.question(formFieldAnswerListQuery.question())
.type(formFieldAnswerListQuery.type())
.options(formFieldAnswerListQuery.options())
.required(formFieldAnswerListQuery.required())
.order(formFieldAnswerListQuery.order())
.section(formFieldAnswerListQuery.section())
.value(formFieldAnswerListQuery.value())
.build();
}
}
public static FormApplicationResponse from(FormApplicationQuery formApplicationQuery) {
List<FormFieldAnswerListResponse> responses = formApplicationQuery.formFieldAnswers().stream()
.map(FormFieldAnswerListResponse::from)
.toList();

return FormApplicationResponse.builder()
.submittedAt(formApplicationQuery.createdAt())
.name(formApplicationQuery.name())
.studentNumber(formApplicationQuery.studentNumber())
.department(formApplicationQuery.department())
.status(formApplicationQuery.status())
.formFieldAnswers(responses)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package ddingdong.ddingdongBE.domain.formapplication.repository;

import ddingdong.ddingdongBE.domain.formapplication.entity.FormAnswer;
import ddingdong.ddingdongBE.domain.formapplication.entity.FormApplication;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface FormAnswerRepository extends JpaRepository<FormAnswer, Long> {
List<FormAnswer> findAllByFormApplication(FormApplication formApplication);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package ddingdong.ddingdongBE.domain.formapplication.service;

import ddingdong.ddingdongBE.domain.formapplication.service.dto.query.FormApplicationQuery;
import ddingdong.ddingdongBE.domain.formapplication.service.dto.query.MyFormApplicationPageQuery;
import ddingdong.ddingdongBE.domain.user.entity.User;

public interface FacadeCentralFormApplicationService {

MyFormApplicationPageQuery getMyFormApplicationPage(Long formId, User user, int size, Long currentCursorId);

FormApplicationQuery getFormApplication(Long formId, Long applicationId, User user);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import ddingdong.ddingdongBE.domain.club.entity.Club;
import ddingdong.ddingdongBE.domain.club.service.ClubService;
import ddingdong.ddingdongBE.domain.formapplication.entity.FormAnswer;
import ddingdong.ddingdongBE.domain.formapplication.service.dto.query.FormApplicationQuery;
import ddingdong.ddingdongBE.domain.formapplication.service.dto.query.PagingQuery;
import ddingdong.ddingdongBE.domain.form.entity.Form;
import ddingdong.ddingdongBE.domain.form.service.FormService;
Expand All @@ -11,7 +13,6 @@
import ddingdong.ddingdongBE.domain.user.entity.User;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Slice;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -22,17 +23,11 @@
@Transactional(readOnly = true)
public class FacadeCentralFormApplicationServiceImpl implements FacadeCentralFormApplicationService {

private final ClubService clubService;
private final FormService formService;
private final FormApplicationService formApplicationService;
private final FormAnswerService formAnswerService;

@Override
public MyFormApplicationPageQuery getMyFormApplicationPage(Long formId, User user, int size, Long currentCursorId) {
Club club = clubService.getByUserId(user.getId());
Form form = formService.getById(formId);
if (!form.getClub().equals(club)) {
throw new AccessDeniedException("권한이 없습니다.");
}
Slice<FormApplication> formApplicationPage = formApplicationService.getFormApplicationPageByFormId(formId, size, currentCursorId);
if (formApplicationPage == null) {
return MyFormApplicationPageQuery.createEmpty();
Expand All @@ -44,6 +39,12 @@ public MyFormApplicationPageQuery getMyFormApplicationPage(Long formId, User use
PagingQuery pagingQuery = PagingQuery.of(currentCursorId, completeFormApplications, formApplicationPage.hasNext());

return MyFormApplicationPageQuery.of(formApplicationListQueries, pagingQuery);
}

@Override
public FormApplicationQuery getFormApplication(Long formId, Long applicationId, User user) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

p3)
formId, user 파라미터는 해당 메서드에서 사용을 안하는데 따로 받는 이유가 있을까요?
없다면 제거해도 괜찮을거 같습니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

추후 해당 폼이 로그인한 사용자 소유의 폼인지 유효성 검사를 해야 할 것 같아서 받아왔는데 일단 삭제해두는 게 맞을까요?

FormApplication formApplication = formApplicationService.getById(applicationId);
List<FormAnswer> formAnswers = formAnswerService.getAllByApplication(formApplication);
Copy link
Collaborator

Choose a reason for hiding this comment

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

p1) form으로 fields를 찾는 것보다 formAnswer내부의 field를 사용하면 dto내 복잡한 로직을 줄일 수 있을 것 같아요

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

아하...!!! 제가 엔터티가 객체랑 매핑되어 있는 걸 잊었네요... 테이블에 있는 것처럼 id만 있다고 생각 했더니 이렇게 됐습니다. 알려주셔서 감사합니다! dto 재사용이 조심스러워야 한다는 점도 처음 알았습니다. 감사합니다. 수정 완료했습니다!

return FormApplicationQuery.of(formApplication, formAnswers);
}
Comment on lines +44 to 49
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

필수 유효성 검사가 누락되었습니다!

getFormApplication 메서드에 다음과 같은 중요한 검증 로직이 누락되었습니다:

  1. 사용자의 접근 권한 검증
  2. formApplication null 체크
  3. formIdapplication의 연관성 검증

다음과 같이 수정하는 것을 제안드립니다:

 @Override
 public FormApplicationQuery getFormApplication(Long formId, Long applicationId, User user) {
+    Form form = formService.getById(formId);
+    if (!form.getClub().equals(user.getClub())) {
+        throw new AccessDeniedException("해당 폼에 대한 접근 권한이 없습니다.");
+    }
     FormApplication formApplication = formApplicationService.getById(applicationId);
+    if (formApplication == null) {
+        throw new EntityNotFoundException("지원서를 찾을 수 없습니다.");
+    }
+    if (!formApplication.getForm().getId().equals(formId)) {
+        throw new InvalidArgumentException("지원서가 해당 폼에 속하지 않습니다.");
+    }
     List<FormAnswer> formAnswers = formAnswerService.getAllByApplication(formApplication);
     return FormApplicationQuery.of(formApplication, formAnswers);
 }
📝 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
@Override
public FormApplicationQuery getFormApplication(Long formId, Long applicationId, User user) {
FormApplication formApplication = formApplicationService.getById(applicationId);
List<FormAnswer> formAnswers = formAnswerService.getAllByApplication(formApplication);
return FormApplicationQuery.of(formApplication, formAnswers);
}
@Override
public FormApplicationQuery getFormApplication(Long formId, Long applicationId, User user) {
Form form = formService.getById(formId);
if (!form.getClub().equals(user.getClub())) {
throw new AccessDeniedException("해당 폼에 대한 접근 권한이 없습니다.");
}
FormApplication formApplication = formApplicationService.getById(applicationId);
if (formApplication == null) {
throw new EntityNotFoundException("지원서를 찾을 수 없습니다.");
}
if (!formApplication.getForm().getId().equals(formId)) {
throw new InvalidArgumentException("지원서가 해당 폼에 속하지 않습니다.");
}
List<FormAnswer> formAnswers = formAnswerService.getAllByApplication(formApplication);
return FormApplicationQuery.of(formApplication, formAnswers);
}

}
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package ddingdong.ddingdongBE.domain.formapplication.service;

import ddingdong.ddingdongBE.domain.formapplication.entity.FormAnswer;
import ddingdong.ddingdongBE.domain.formapplication.entity.FormApplication;

import java.util.List;

public interface FormAnswerService {

void createAll(List<FormAnswer> formAnswers);

List<FormAnswer> getAllByApplication(FormApplication formApplication);

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ public interface FormApplicationService {
FormApplication create(FormApplication formApplication);

Slice<FormApplication> getFormApplicationPageByFormId(Long formId, int size, Long currentCursorId);

FormApplication getById(Long applicationId);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ddingdong.ddingdongBE.domain.formapplication.service;

import ddingdong.ddingdongBE.domain.formapplication.entity.FormAnswer;
import ddingdong.ddingdongBE.domain.formapplication.entity.FormApplication;
import ddingdong.ddingdongBE.domain.formapplication.repository.FormAnswerRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
Expand All @@ -21,4 +22,8 @@ public void createAll(List<FormAnswer> formAnswers) {
formAnswerRepository.saveAll(formAnswers);
}

@Override
public List<FormAnswer> getAllByApplication(FormApplication formApplication) {
return formAnswerRepository.findAllByFormApplication(formApplication);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package ddingdong.ddingdongBE.domain.formapplication.service;

import ddingdong.ddingdongBE.common.exception.PersistenceException.ResourceNotFound;
import ddingdong.ddingdongBE.domain.formapplication.entity.FormApplication;
import ddingdong.ddingdongBE.domain.formapplication.repository.FormApplicationRepository;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -31,6 +32,12 @@ public Slice<FormApplication> getFormApplicationPageByFormId(Long formId, int si
return buildSlice(formApplicationPages, size);
}

@Override
public FormApplication getById(Long applicationId) {
return formApplicationRepository.findById(applicationId)
.orElseThrow(() -> new ResourceNotFound("주어진 id로 해당 지원자를 찾을 수 없습니다.:"+applicationId));
}

private Slice<FormApplication> buildSlice(Slice<FormApplication> originalSlice, int size) {
List<FormApplication> content = new ArrayList<>(originalSlice.getContent());
if (content.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package ddingdong.ddingdongBE.domain.formapplication.service.dto.query;

import ddingdong.ddingdongBE.domain.form.entity.FieldType;

import ddingdong.ddingdongBE.domain.formapplication.entity.FormAnswer;
import ddingdong.ddingdongBE.domain.formapplication.entity.FormApplication;
import ddingdong.ddingdongBE.domain.formapplication.entity.FormApplicationStatus;

import lombok.Builder;

import java.time.LocalDateTime;
import java.util.List;

@Builder
public record FormApplicationQuery (
LocalDateTime createdAt,
String name,
String studentNumber,
String department,
FormApplicationStatus status,
List<FormFieldAnswerListQuery> formFieldAnswers
) {
@Builder
public record FormFieldAnswerListQuery (
Long fieldId,
String question,
FieldType type,
List<String> options,
Boolean required,
Integer order,
String section,
List<String> value
) {
public static FormFieldAnswerListQuery from(FormAnswer formAnswer) {
return FormFieldAnswerListQuery.builder()
.fieldId(formAnswer.getFormField().getId())
.question(formAnswer.getFormField().getQuestion())
.type(formAnswer.getFormField().getFieldType())
.options(formAnswer.getFormField().getOptions())
.required(formAnswer.getFormField().isRequired())
.order(formAnswer.getFormField().getFieldOrder())
.section(formAnswer.getFormField().getSection())
.value(formAnswer.getValue())
.build();
}
}
public static FormApplicationQuery of(FormApplication formApplication, List<FormAnswer> formAnswers) {
List<FormFieldAnswerListQuery> formFieldAnswerListQueries = formAnswers.stream()
.map(FormFieldAnswerListQuery::from)
.toList();
return FormApplicationQuery.builder()
.createdAt(formApplication.getCreatedAt())
.name(formApplication.getName())
.studentNumber(formApplication.getStudentNumber())
.department(formApplication.getDepartment())
.status(formApplication.getStatus())
.formFieldAnswers(formFieldAnswerListQueries)
.build();
}
}
Loading
Loading