-
Notifications
You must be signed in to change notification settings - Fork 0
[Feat/#23] 행사 신청서 폼 조회·신청 API 추가 #26
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
Changes from 6 commits
f2384fe
23eb79b
78b8284
41bb3b2
8e538dd
53eac7b
f7c3449
fc4208f
606cb89
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| package kr.ac.kookmin.stream.event; | ||
|
|
||
| import jakarta.validation.Valid; | ||
| import kr.ac.kookmin.stream.ApiResponse; | ||
| import kr.ac.kookmin.stream.app.AppApiUser; | ||
| import kr.ac.kookmin.stream.event.domain.event.service.EventService; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.web.bind.annotation.GetMapping; | ||
| import org.springframework.web.bind.annotation.PathVariable; | ||
| 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.RestController; | ||
|
|
||
| @RestController | ||
| @RequestMapping("/v1/app/events") | ||
| @RequiredArgsConstructor | ||
| public class AppEventController { | ||
|
|
||
| private final EventService eventService; | ||
|
|
||
| @GetMapping("/{eventId}/form") | ||
| public ApiResponse<EventFormResponse> getApplicationForm(@PathVariable Long eventId) { | ||
| return ApiResponse.success(EventFormResponse.from(eventService.getApplicationForm(eventId))); | ||
| } | ||
|
|
||
| @PostMapping("/{eventId}/applications") | ||
| public ApiResponse<EventApplyResponse> apply( | ||
| AppApiUser apiUser, | ||
| @PathVariable Long eventId, | ||
| @Valid @RequestBody EventApplyRequest request | ||
| ) { | ||
| return ApiResponse.success( | ||
| EventApplyResponse.from(eventService.apply(eventId, apiUser.userId(), request.toCommand())) | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| package kr.ac.kookmin.stream.event; | ||
|
|
||
| import jakarta.validation.Valid; | ||
| import jakarta.validation.constraints.NotNull; | ||
| import java.util.List; | ||
| import kr.ac.kookmin.stream.event.domain.event.domain.EventApplyCommand; | ||
|
|
||
| public record EventApplyRequest( | ||
| @NotNull(message = "신청서 답변 목록을 입력해 주세요.") | ||
| @Valid | ||
| List<@NotNull(message = "답변 항목이 비어 있습니다.") AnswerRequest> answers | ||
| ) { | ||
|
|
||
| public EventApplyCommand toCommand() { | ||
| return new EventApplyCommand(answers.stream().map(AnswerRequest::toCommand).toList()); | ||
| } | ||
|
|
||
| /** | ||
| * @param selectedOptions 선택형 질문에서 고른 선택지의 0-based 인덱스 | ||
| */ | ||
| public record AnswerRequest( | ||
| @NotNull(message = "답변 대상 질문을 입력해 주세요.") | ||
| Long questionId, | ||
| String answerText, | ||
| List<Integer> selectedOptions | ||
| ) { | ||
|
|
||
| public EventApplyCommand.AnswerCommand toCommand() { | ||
| return new EventApplyCommand.AnswerCommand(questionId, answerText, selectedOptions); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| package kr.ac.kookmin.stream.event; | ||
|
|
||
| import java.time.LocalDateTime; | ||
| import kr.ac.kookmin.stream.event.domain.event.domain.Event; | ||
| import kr.ac.kookmin.stream.event.domain.event.domain.EventApplicationResult; | ||
|
|
||
| public record EventApplyResponse( | ||
| Long applicationId, | ||
| String title, | ||
| LocalDateTime eventStartAt, | ||
| String place | ||
| ) { | ||
|
|
||
| public static EventApplyResponse from(EventApplicationResult result) { | ||
| Event event = result.getEvent(); | ||
| return new EventApplyResponse( | ||
| result.getApplicationId(), | ||
| event.getTitle(), | ||
| event.getEventStartAt(), | ||
| event.getPlace() | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| package kr.ac.kookmin.stream.event; | ||
|
|
||
| import java.util.List; | ||
| import kr.ac.kookmin.stream.event.domain.event.domain.EventQuestion; | ||
| import kr.ac.kookmin.stream.event.domain.event.domain.QuestionType; | ||
|
|
||
| public record EventFormQuestionResponse( | ||
| Long questionId, | ||
| String questionText, | ||
| QuestionType questionType, | ||
| boolean isRequired, | ||
| int displayOrder, | ||
| List<String> options, | ||
| Integer maxLength | ||
| ) { | ||
|
|
||
| public static EventFormQuestionResponse from(EventQuestion question) { | ||
| QuestionType questionType = question.getQuestionType(); | ||
| return new EventFormQuestionResponse( | ||
| question.getId(), | ||
| question.getQuestionText(), | ||
| questionType, | ||
| question.isRequired(), | ||
| question.getDisplayOrder(), | ||
| question.getOptions() == null ? List.of() : question.getOptions(), | ||
| questionType.maxLength() | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| package kr.ac.kookmin.stream.event; | ||
|
|
||
| import java.time.LocalDateTime; | ||
| import java.util.List; | ||
| import kr.ac.kookmin.stream.event.domain.event.domain.Event; | ||
| import kr.ac.kookmin.stream.event.domain.event.domain.EventApplicationForm; | ||
|
|
||
| public record EventFormResponse( | ||
| Long eventId, | ||
| String title, | ||
| LocalDateTime eventStartAt, | ||
| String place, | ||
| List<EventFormQuestionResponse> questions | ||
| ) { | ||
|
|
||
| public static EventFormResponse from(EventApplicationForm form) { | ||
| Event event = form.getEvent(); | ||
| return new EventFormResponse( | ||
| event.getId(), | ||
| event.getTitle(), | ||
| event.getEventStartAt(), | ||
| event.getPlace(), | ||
| form.getQuestions().stream().map(EventFormQuestionResponse::from).toList() | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| package kr.ac.kookmin.stream.event.domain.event.domain; | ||
|
|
||
| import java.util.List; | ||
| import lombok.AccessLevel; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.EqualsAndHashCode; | ||
| import lombok.Getter; | ||
|
|
||
| /** | ||
| * 신청서 폼 조회 결과. 행사 요약과 질문 목록을 함께 돌려주기 위한 읽기 모델이다. | ||
| */ | ||
| @Getter | ||
| @EqualsAndHashCode | ||
| @AllArgsConstructor(access = AccessLevel.PRIVATE) | ||
| public class EventApplicationForm { | ||
|
|
||
| private Event event; | ||
| private List<EventQuestion> questions; | ||
|
|
||
| public static EventApplicationForm of(Event event, List<EventQuestion> questions) { | ||
| return new EventApplicationForm(event, questions); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package kr.ac.kookmin.stream.event.domain.event.domain; | ||
|
|
||
| import lombok.AccessLevel; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.EqualsAndHashCode; | ||
| import lombok.Getter; | ||
|
|
||
| /** | ||
| * 행사 신청 결과. 생성된 신청 식별자와 행사 요약을 함께 돌려주기 위한 읽기 모델이다. | ||
| */ | ||
| @Getter | ||
| @EqualsAndHashCode | ||
| @AllArgsConstructor(access = AccessLevel.PRIVATE) | ||
| public class EventApplicationResult { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이런 vo들은 record로 만들어도 좋을 거 같아요~
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 말씀해주신대로 record로 수정했습니다 ! |
||
|
|
||
| private Long applicationId; | ||
| private Event event; | ||
|
|
||
| public static EventApplicationResult of(Long applicationId, Event event) { | ||
| return new EventApplicationResult(applicationId, event); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| package kr.ac.kookmin.stream.event.domain.event.domain; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| /** | ||
| * 행사 신청 요청. 질문별 답변 목록을 담는다. | ||
| */ | ||
| public record EventApplyCommand(List<AnswerCommand> answers) { | ||
|
|
||
| /** | ||
| * 질문 하나에 대한 답변. | ||
| * | ||
| * @param answerText 단답형·장문형 답변. 선택형이면 null | ||
| * @param selectedOptions 선택형 질문에서 고른 선택지의 0-based 인덱스. 주관식이면 빈 목록 | ||
| */ | ||
| public record AnswerCommand(Long questionId, String answerText, List<Integer> selectedOptions) { | ||
|
|
||
| /** | ||
| * 답변 내용이 비어 있는지. 선택 질문은 생략과 빈 답변을 같게 취급하므로 판정 기준을 한곳에 둔다. | ||
| */ | ||
| public boolean isEmpty() { | ||
| return (answerText == null || answerText.isBlank()) | ||
| && (selectedOptions == null || selectedOptions.isEmpty()); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package kr.ac.kookmin.stream.event.domain.event.domain; | ||
|
|
||
| import kr.ac.kookmin.stream.common.ErrorCode; | ||
| import kr.ac.kookmin.stream.common.ErrorStatus; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Getter; | ||
| import lombok.experimental.Accessors; | ||
|
|
||
| @Getter | ||
| @Accessors(fluent = true) | ||
| @AllArgsConstructor | ||
| public enum EventErrorCode implements ErrorCode { | ||
|
|
||
| EVENT_NOT_FOUND(ErrorStatus.NOT_FOUND, "행사를 찾을 수 없습니다."), | ||
| ALREADY_CLOSED(ErrorStatus.CONFLICT, "행사 마감되었습니다."), | ||
| CAPACITY_FULL(ErrorStatus.CONFLICT, "모집 정원이 마감되었습니다."), | ||
| ALREADY_APPLIED(ErrorStatus.CONFLICT, "이미 신청한 행사입니다."), | ||
| INVALID_ANSWER(ErrorStatus.BAD_REQUEST, "신청서 답변 형식이 올바르지 않습니다."); | ||
|
|
||
| private final int status; | ||
| private final String message; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,20 @@ | ||
| package kr.ac.kookmin.stream.event.domain.event.domain; | ||
|
|
||
| import lombok.AccessLevel; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Getter; | ||
| import lombok.experimental.Accessors; | ||
|
|
||
| @Getter | ||
| @Accessors(fluent = true) | ||
| @AllArgsConstructor(access = AccessLevel.PRIVATE) | ||
| public enum QuestionType { | ||
| SHORT_TEXT, | ||
| LONG_TEXT, | ||
| SINGLE_CHOICE, | ||
| MULTIPLE_CHOICE | ||
|
|
||
| SHORT_TEXT(50), | ||
| LONG_TEXT(500), | ||
| SINGLE_CHOICE(null), | ||
| MULTIPLE_CHOICE(null); | ||
|
|
||
| /** 답변 길이 제한. 선택형 질문은 답변이 텍스트가 아니라 null이다. */ | ||
| private final Integer maxLength; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| package kr.ac.kookmin.stream.event.domain.event.repository; | ||
|
|
||
| import java.util.List; | ||
| import java.util.Optional; | ||
| import kr.ac.kookmin.stream.event.domain.event.domain.Event; | ||
| import kr.ac.kookmin.stream.event.domain.event.domain.EventApplication; | ||
| import kr.ac.kookmin.stream.event.domain.event.domain.EventApplicationAnswer; | ||
| import kr.ac.kookmin.stream.event.domain.event.domain.EventQuestion; | ||
|
|
||
| public interface EventRepository { | ||
|
|
||
| Optional<Event> findById(Long id); | ||
|
|
||
| List<EventQuestion> findQuestionsByEventId(Long eventId); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 다른 엔티티를 조회하는 레퍼지토리지만 같은 도메인이라 이렇게 묶어놓은 거 너무 좋네요 |
||
|
|
||
| long countAppliedByEventId(Long eventId); | ||
|
|
||
| boolean existsAppliedByEventIdAndMemberId(Long eventId, Long memberId); | ||
|
|
||
| EventApplication saveApplication(EventApplication application); | ||
|
|
||
| List<EventApplicationAnswer> saveAnswers(List<EventApplicationAnswer> answers); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package kr.ac.kookmin.stream.event.domain.event.service; | ||
|
|
||
| import kr.ac.kookmin.stream.event.domain.event.domain.EventApplicationForm; | ||
| import kr.ac.kookmin.stream.event.domain.event.domain.EventApplicationResult; | ||
| import kr.ac.kookmin.stream.event.domain.event.domain.EventApplyCommand; | ||
|
|
||
| public interface EventService { | ||
|
|
||
| EventApplicationForm getApplicationForm(Long eventId); | ||
|
|
||
| EventApplicationResult apply(Long eventId, Long memberId, EventApplyCommand command); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이렇게 도메인 안에서 도메인 자체의 메서드를 만들어서 검증하는 방식 너무 좋습니다!!
아주 좋아요~