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-89] 동아리 지원 폼지 상세조회 API 구현 #238

Merged
merged 8 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import ddingdong.ddingdongBE.domain.form.controller.dto.request.CreateFormRequest;
import ddingdong.ddingdongBE.domain.form.controller.dto.request.UpdateFormRequest;
import ddingdong.ddingdongBE.domain.form.controller.dto.response.FormListResponse;
import ddingdong.ddingdongBE.domain.form.controller.dto.response.FormResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
Expand Down Expand Up @@ -70,4 +71,14 @@ void deleteForm(
List<FormListResponse> getAllMyForm(
@AuthenticationPrincipal PrincipalDetails principalDetails
);

@Operation(summary = "동아리 지원 폼지 상세조회 API")
@ApiResponse(responseCode = "200", description = "동아리 지원 폼지 상세조회 성공",
content = @Content(schema = @Schema(implementation = FormResponse.class)))
@ResponseStatus(HttpStatus.OK)
@SecurityRequirement(name = "AccessToken")
@GetMapping("/my/forms/{formId}")
FormResponse getForm(
@PathVariable("formId") Long formId
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
import ddingdong.ddingdongBE.domain.form.controller.dto.request.CreateFormRequest;
import ddingdong.ddingdongBE.domain.form.controller.dto.request.UpdateFormRequest;
import ddingdong.ddingdongBE.domain.form.controller.dto.response.FormListResponse;
import ddingdong.ddingdongBE.domain.form.controller.dto.response.FormResponse;
import ddingdong.ddingdongBE.domain.form.service.FacadeCentralFormService;
import ddingdong.ddingdongBE.domain.form.service.dto.query.FormListQuery;
import ddingdong.ddingdongBE.domain.form.service.dto.query.FormQuery;
import ddingdong.ddingdongBE.domain.user.entity.User;
import java.util.List;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -50,4 +52,10 @@ public List<FormListResponse> getAllMyForm(PrincipalDetails principalDetails) {
.map(FormListResponse::from)
.toList();
}

@Override
public FormResponse getForm(Long formId) {
FormQuery query = facadeCentralFormService.getForm(formId);
return FormResponse.from(query);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public record FormListResponse(
@Schema(description = "지원 폼지 시작일", example = "2001-01-01")
LocalDate startDate,
@Schema(description = "지원 폼지 종료일", example = "2001-01-02")
LocalDate endData,
LocalDate endDate,
@Schema(description = "활성화 여부", example = "true")
boolean isActive
) {
Expand All @@ -24,7 +24,7 @@ public static FormListResponse from(FormListQuery query) {
.formId(query.formId())
.title(query.title())
.startDate(query.startDate())
.endData(query.endData())
.endDate(query.endDate())
.isActive(query.isActive())
.build();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package ddingdong.ddingdongBE.domain.form.controller.dto.response;

import ddingdong.ddingdongBE.domain.form.entity.FieldType;
import ddingdong.ddingdongBE.domain.form.service.dto.query.FormQuery;
import ddingdong.ddingdongBE.domain.form.service.dto.query.FormQuery.FormFieldListQuery;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDate;
import java.util.List;
import lombok.Builder;

@Builder
public record FormResponse(
@Schema(description = "폼지 제목", example = "카우 1기 폼지")
String title,
@Schema(description = "폼지 설명", example = "폼지 설명입니다")
String description,
@Schema(description = "폼지 시작일", example = "2001-01-01")
LocalDate startDate,
@Schema(description = "폼지 종료일", example = "2001-01-02")
LocalDate endDate,
@Schema(description = "면접 여부", example = "true")
boolean hasInterview,
@Schema(description = "생성한 섹션들", example = "[공통,서버,웹]")
List<String> sections,
@ArraySchema(schema = @Schema(implementation = FormFieldListResponse.class))
List<FormFieldListResponse> formFields
) {

@Builder
record FormFieldListResponse(
@Schema(description = "폼지 질문", example = "당신의 이름은?")
String question,
@Schema(description = "폼지 질문 유형", example = "CHECK_BOX")
FieldType type,
@Schema(description = "폼지 지문", example = "[지문1, 지문2]")
List<String> options,
@Schema(description = "필수 여부", example = "true")
boolean required,
@Schema(description = "폼지 질문 순서", example = "1")
int order,
@Schema(description = "폼지 섹션", example = "공통")
String section
) {

public static FormFieldListResponse from(FormFieldListQuery formFieldListQuery) {
return FormFieldListResponse.builder()
.question(formFieldListQuery.question())
.type(formFieldListQuery.type())
.options(formFieldListQuery.options())
.required(formFieldListQuery.required())
.order(formFieldListQuery.order())
.section(formFieldListQuery.section())
.build();
}
}

public static FormResponse from(FormQuery formQuery) {
List<FormFieldListResponse> responses = formQuery.formFields().stream()
.map(FormFieldListResponse::from)
.toList();

return FormResponse.builder()
.title(formQuery.title())
.description(formQuery.description())
.startDate(formQuery.startDate())
.endDate(formQuery.endDate())
.hasInterview(formQuery.hasInterview())
.sections(formQuery.sections())
.formFields(responses)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package ddingdong.ddingdongBE.domain.form.entity;

import ddingdong.ddingdongBE.common.BaseEntity;
import ddingdong.ddingdongBE.common.converter.StringListConverter;
import ddingdong.ddingdongBE.domain.club.entity.Club;
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import java.time.LocalDate;
import java.util.List;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
Expand Down Expand Up @@ -38,6 +41,10 @@ public class Form extends BaseEntity {
@Column(nullable = false)
private boolean hasInterview;

@Column(nullable = false)
@Convert(converter = StringListConverter.class)
private List<String> sections;

@ManyToOne(fetch = FetchType.LAZY)
private Club club;

Expand All @@ -48,13 +55,15 @@ private Form(
LocalDate startDate,
LocalDate endDate,
boolean hasInterview,
List<String> sections,
Club club
) {
this.title = title;
this.description = description;
this.startDate = startDate;
this.endDate = endDate;
this.hasInterview = hasInterview;
this.sections = sections;
this.club = club;
}

Expand All @@ -63,6 +72,7 @@ public void update(Form updateForm) {
this.description = updateForm.getDescription();
this.startDate = updateForm.getStartDate();
this.endDate = updateForm.getEndDate();
this.sections = updateForm.getSections();
this.hasInterview = updateForm.isHasInterview();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@
public interface FormFieldRepository extends JpaRepository<FormField, Long> {

List<FormField> findAllByForm(Form form);

List<FormField> findAllByFormAndSection(Form form, String section);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import ddingdong.ddingdongBE.domain.form.service.dto.command.CreateFormCommand;
import ddingdong.ddingdongBE.domain.form.service.dto.command.UpdateFormCommand;
import ddingdong.ddingdongBE.domain.form.service.dto.query.FormListQuery;
import ddingdong.ddingdongBE.domain.form.service.dto.query.FormQuery;
import ddingdong.ddingdongBE.domain.user.entity.User;
import java.util.List;

Expand All @@ -15,4 +16,6 @@ public interface FacadeCentralFormService {
void deleteForm(Long formId, User user);

List<FormListQuery> getAllMyForm(User user);

FormQuery getForm(Long formId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import ddingdong.ddingdongBE.domain.form.service.dto.command.UpdateFormCommand;
import ddingdong.ddingdongBE.domain.form.service.dto.command.UpdateFormCommand.UpdateFormFieldCommand;
import ddingdong.ddingdongBE.domain.form.service.dto.query.FormListQuery;
import ddingdong.ddingdongBE.domain.form.service.dto.query.FormQuery;
import ddingdong.ddingdongBE.domain.user.entity.User;
import java.time.LocalDate;
import java.util.List;
Expand Down Expand Up @@ -71,6 +72,13 @@ public List<FormListQuery> getAllMyForm(User user) {
.toList();
}

@Override
public FormQuery getForm(Long formId) {
Form form = formService.getById(formId);
List<FormField> formFields = formFieldService.findAllByForm(form);
return FormQuery.of(form, formFields);
}
Comment on lines +75 to +80
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

폼 상세 조회 로직에 권한 검증이 필요합니다.

현재 구현에서는 누구나 formId만 알면 폼을 조회할 수 있습니다. 클럽 소유자만 자신의 폼을 조회할 수 있도록 권한 검증이 필요합니다.

다음과 같이 수정을 제안합니다:

 @Override
 public FormQuery getForm(Long formId) {
     Form form = formService.getById(formId);
+    Club club = form.getClub();
+    validateEqualsClub(club, form);
     List<FormField> formFields = formFieldService.findAllByForm(form);
     return FormQuery.of(form, formFields);
 }
📝 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 FormQuery getForm(Long formId) {
Form form = formService.getById(formId);
List<FormField> formFields = formFieldService.findAllByForm(form);
return FormQuery.of(form, formFields);
}
@Override
public FormQuery getForm(Long formId) {
Form form = formService.getById(formId);
Club club = form.getClub();
validateEqualsClub(club, form);
List<FormField> formFields = formFieldService.findAllByForm(form);
return FormQuery.of(form, formFields);
}


private FormListQuery buildFormListQuery(Form form) {
boolean isActive = TimeUtils.isDateInRange(LocalDate.now(), form.getStartDate(), form.getEndDate());
return FormListQuery.from(form, isActive);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import ddingdong.ddingdongBE.domain.form.entity.Form;
import ddingdong.ddingdongBE.domain.form.entity.FormField;
import java.util.List;
import java.util.Optional;

public interface FormFieldService {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public record FormListQuery(
Long formId,
String title,
LocalDate startDate,
LocalDate endData,
LocalDate endDate,
boolean isActive
) {

Expand All @@ -18,7 +18,7 @@ public static FormListQuery from(Form form, boolean isActive) {
.formId(form.getId())
.title(form.getTitle())
.startDate(form.getStartDate())
.endData(form.getEndDate())
.endDate(form.getEndDate())
.isActive(isActive)
.build();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package ddingdong.ddingdongBE.domain.form.service.dto.query;

import ddingdong.ddingdongBE.domain.form.entity.FieldType;
import ddingdong.ddingdongBE.domain.form.entity.Form;
import ddingdong.ddingdongBE.domain.form.entity.FormField;
import java.time.LocalDate;
import java.util.List;
import lombok.Builder;

@Builder
public record FormQuery(
String title,
String description,
LocalDate startDate,
LocalDate endDate,
boolean hasInterview,
List<String> sections,
List<FormFieldListQuery> formFields
) {

@Builder
public record FormFieldListQuery(
String question,
FieldType type,
List<String> options,
boolean required,
int order,
String section
) {
public static FormFieldListQuery from(FormField formField) {
return FormFieldListQuery.builder()
.question(formField.getQuestion())
.type(formField.getFieldType())
.options(formField.getOptions())
.required(formField.isRequired())
.order(formField.getFieldOrder())
.section(formField.getSection())
.build();
}
}

public static FormQuery of(Form form, List<FormField> formFields) {
List<FormFieldListQuery> formFieldListQueries = formFields.stream()
.map(FormFieldListQuery::from)
.toList();

return FormQuery.builder()
.title(form.getTitle())
.description(form.getDescription())
.startDate(form.getStartDate())
.endDate(form.getEndDate())
.hasInterview(form.isHasInterview())
.sections(form.getSections())
.formFields(formFieldListQueries)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE form ADD COLUMN sections TEXT NOT NULL DEFAULT '[]';
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,11 @@
import ddingdong.ddingdongBE.domain.form.service.dto.command.UpdateFormCommand;
import ddingdong.ddingdongBE.domain.form.service.dto.command.UpdateFormCommand.UpdateFormFieldCommand;
import ddingdong.ddingdongBE.domain.form.service.dto.query.FormListQuery;
import ddingdong.ddingdongBE.domain.form.service.dto.query.FormQuery;
import ddingdong.ddingdongBE.domain.user.entity.Role;
import ddingdong.ddingdongBE.domain.user.entity.User;
import ddingdong.ddingdongBE.domain.user.repository.UserRepository;
import jakarta.persistence.EntityManager;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -49,16 +48,8 @@ class FacadeCentralFormServiceImplTest extends TestContainerSupport {
@Autowired
private FormFieldRepository formFieldRepository;

@Autowired
private EntityManager entityManager;

private static final FixtureMonkey fixtureMonkey = FixtureMonkeyFactory.getNotNullBuilderIntrospectorMonkey();

@BeforeEach
void setUp() {
entityManager.clear();
}

@DisplayName("폼지와 폼지 질문을 생성할 수 있다.")
@Test
void createForm() {
Expand Down Expand Up @@ -247,4 +238,26 @@ void getAllMyForm() {
assertThat(queries.get(1).title()).isEqualTo("제목2");

}

@DisplayName("동아리는 폼지를 상세조회 할 수 있다.")
@Test
void getForm() {
// given
Form form = fixtureMonkey.giveMeBuilder(Form.class)
.set("id", 1L)
.set("title", "제목1")
.set("club", null)
.sample();
Form form2 = fixtureMonkey.giveMeBuilder(Form.class)
.set("id", 2L)
.set("title", "제목2")
.set("club", null)
.sample();
formService.create(form);
formService.create(form2);
// when
FormQuery formQuery = facadeCentralFormService.getForm(1L);
// then
assertThat(formQuery.title()).isEqualTo("제목1");
}
Comment on lines +242 to +262
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

getForm 테스트에 권한 검증 케이스가 누락되었습니다.

현재 테스트는 기본적인 조회 기능만 검증하고 있습니다. 다음 사항들을 추가로 검증해야 합니다:

  • 클럽 소유자가 자신의 폼을 조회하는 케이스
  • 다른 클럽의 폼을 조회할 때 예외가 발생하는 케이스

다음과 같이 테스트 케이스 추가를 제안합니다:

 @DisplayName("동아리는 폼지를 상세조회 할 수 있다.")
 @Test
 void getForm() {
   // given
+    User user = fixtureMonkey.giveMeBuilder(User.class)
+            .set("id", 1L)
+            .set("Role", Role.CLUB)
+            .set("deletedAt", null)
+            .sample();
+    User savedUser = userRepository.save(user);
+    Club club = fixtureMonkey.giveMeBuilder(Club.class)
+            .set("id", 1L)
+            .set("user", savedUser)
+            .set("score", null)
+            .set("clubMembers", null)
+            .set("deletedAt", null)
+            .sample();
+    clubRepository.save(club);
     Form form = fixtureMonkey.giveMeBuilder(Form.class)
             .set("id", 1L)
             .set("title", "제목1")
-            .set("club", null)
+            .set("club", club)
             .sample();
     Form form2 = fixtureMonkey.giveMeBuilder(Form.class)
             .set("id", 2L)
             .set("title", "제목2")
-            .set("club", null)
+            .set("club", club)
             .sample();
     formService.create(form);
     formService.create(form2);
   // when
     FormQuery formQuery = facadeCentralFormService.getForm(1L);
     // then
     assertThat(formQuery.title()).isEqualTo("제목1");
+    assertThat(formQuery.club().getId()).isEqualTo(club.getId());
 }

+@DisplayName("동아리는 다른 동아리의 폼지를 상세조회 할 수 없다.")
+@Test
+void getFormWithNonHaveAuthority() {
+    // given
+    User user1 = fixtureMonkey.giveMeBuilder(User.class)
+            .set("id", 1L)
+            .set("Role", Role.CLUB)
+            .set("deletedAt", null)
+            .sample();
+    User savedUser1 = userRepository.save(user1);
+    Club club1 = fixtureMonkey.giveMeBuilder(Club.class)
+            .set("id", 1L)
+            .set("user", savedUser1)
+            .set("score", null)
+            .set("clubMembers", null)
+            .set("deletedAt", null)
+            .sample();
+    clubRepository.save(club1);
+
+    User user2 = fixtureMonkey.giveMeBuilder(User.class)
+            .set("id", 2L)
+            .set("Role", Role.CLUB)
+            .set("deletedAt", null)
+            .sample();
+    User savedUser2 = userRepository.save(user2);
+    Club club2 = fixtureMonkey.giveMeBuilder(Club.class)
+            .set("id", 2L)
+            .set("user", savedUser2)
+            .set("score", null)
+            .set("clubMembers", null)
+            .set("deletedAt", null)
+            .sample();
+    clubRepository.save(club2);
+
+    Form form = fixtureMonkey.giveMeBuilder(Form.class)
+            .set("id", 1L)
+            .set("title", "제목1")
+            .set("club", club1)
+            .sample();
+    formService.create(form);
+
+    // when/then
+    assertThrows(NonHaveAuthority.class, () -> {
+        facadeCentralFormService.getForm(1L);
+    });
+}
📝 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
@DisplayName("동아리는 폼지를 상세조회 할 수 있다.")
@Test
void getForm() {
// given
Form form = fixtureMonkey.giveMeBuilder(Form.class)
.set("id", 1L)
.set("title", "제목1")
.set("club", null)
.sample();
Form form2 = fixtureMonkey.giveMeBuilder(Form.class)
.set("id", 2L)
.set("title", "제목2")
.set("club", null)
.sample();
formService.create(form);
formService.create(form2);
// when
FormQuery formQuery = facadeCentralFormService.getForm(1L);
// then
assertThat(formQuery.title()).isEqualTo("제목1");
}
@DisplayName("동아리는 폼지를 상세조회 할 수 있다.")
@Test
void getForm() {
// given
User user = fixtureMonkey.giveMeBuilder(User.class)
.set("id", 1L)
.set("Role", Role.CLUB)
.set("deletedAt", null)
.sample();
User savedUser = userRepository.save(user);
Club club = fixtureMonkey.giveMeBuilder(Club.class)
.set("id", 1L)
.set("user", savedUser)
.set("score", null)
.set("clubMembers", null)
.set("deletedAt", null)
.sample();
clubRepository.save(club);
Form form = fixtureMonkey.giveMeBuilder(Form.class)
.set("id", 1L)
.set("title", "제목1")
.set("club", club)
.sample();
Form form2 = fixtureMonkey.giveMeBuilder(Form.class)
.set("id", 2L)
.set("title", "제목2")
.set("club", club)
.sample();
formService.create(form);
formService.create(form2);
// when
FormQuery formQuery = facadeCentralFormService.getForm(1L);
// then
assertThat(formQuery.title()).isEqualTo("제목1");
assertThat(formQuery.club().getId()).isEqualTo(club.getId());
}
@DisplayName("동아리는 다른 동아리의 폼지를 상세조회 할 수 없다.")
@Test
void getFormWithNonHaveAuthority() {
// given
User user1 = fixtureMonkey.giveMeBuilder(User.class)
.set("id", 1L)
.set("Role", Role.CLUB)
.set("deletedAt", null)
.sample();
User savedUser1 = userRepository.save(user1);
Club club1 = fixtureMonkey.giveMeBuilder(Club.class)
.set("id", 1L)
.set("user", savedUser1)
.set("score", null)
.set("clubMembers", null)
.set("deletedAt", null)
.sample();
clubRepository.save(club1);
User user2 = fixtureMonkey.giveMeBuilder(User.class)
.set("id", 2L)
.set("Role", Role.CLUB)
.set("deletedAt", null)
.sample();
User savedUser2 = userRepository.save(user2);
Club club2 = fixtureMonkey.giveMeBuilder(Club.class)
.set("id", 2L)
.set("user", savedUser2)
.set("score", null)
.set("clubMembers", null)
.set("deletedAt", null)
.sample();
clubRepository.save(club2);
Form form = fixtureMonkey.giveMeBuilder(Form.class)
.set("id", 1L)
.set("title", "제목1")
.set("club", club1)
.sample();
formService.create(form);
// when/then
assertThrows(NonHaveAuthority.class, () -> {
facadeCentralFormService.getForm(1L);
});
}

}
Loading