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

Feature/#86 컨퍼런스 목록 조회 api 구현 #108

Merged
16 changes: 16 additions & 0 deletions backend/emm-sale/src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,19 @@ include::{snippets}/find-event/http-response.adoc[]

.HTTP response 설명
include::{snippets}/find-event/response-fields.adoc[]

== Event

=== `GET`: 행사 목록 조회

.HTTP request
include::{snippets}/find-events/http-request.adoc[]

.HTTP request 설명
include::{snippets}/find-events/request-parameters.adoc[]

.HTTP response
include::{snippets}/find-events/http-response.adoc[]

.HTTP response 설명
include::{snippets}/find-events/response-fields.adoc[]
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,34 @@

import com.emmsale.event.application.EventService;
import com.emmsale.event.application.dto.EventDetailResponse;
import com.emmsale.event.application.dto.EventResponse;
import java.time.LocalDate;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/events")
@RequiredArgsConstructor
public class EventApi {

private final EventService eventService;

@GetMapping("/events/{id}")
@GetMapping("/{id}")
public ResponseEntity<EventDetailResponse> findEventById(@PathVariable final String id) {
return ResponseEntity.ok(eventService.findEvent(Long.parseLong(id)));
}
@GetMapping
public ResponseEntity<List<EventResponse>> findEvents(@RequestParam final int year,
@RequestParam final int month, @RequestParam(required = false) final String tag,
@RequestParam(required = false) final String status) {

return ResponseEntity.ok(eventService.findEvents(LocalDate.now(), year, month, tag, status));
}

}
Original file line number Diff line number Diff line change
@@ -1,11 +1,30 @@
package com.emmsale.event.application;

import static com.emmsale.event.exception.EventExceptionType.EVENT_NOT_FOUND_EXCEPTION;
import static com.emmsale.event.exception.EventExceptionType.INVALID_MONTH;
import static com.emmsale.event.exception.EventExceptionType.INVALID_STATUS;
import static com.emmsale.event.exception.EventExceptionType.INVALID_YEAR;
import static com.emmsale.tag.exception.TagExceptionType.NOT_FOUND_TAG;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;

import com.emmsale.event.application.dto.EventDetailResponse;
import com.emmsale.event.application.dto.EventResponse;
import com.emmsale.event.domain.Event;
import com.emmsale.event.domain.EventStatus;
import com.emmsale.event.domain.EventTag;
import com.emmsale.event.domain.EventTagRepository;
import com.emmsale.event.domain.repository.EventRepository;
import com.emmsale.event.exception.EventException;
import com.emmsale.tag.domain.Tag;
import com.emmsale.tag.domain.TagRepository;
import com.emmsale.tag.exception.TagException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -15,12 +34,130 @@
@RequiredArgsConstructor
public class EventService {

private static final int MIN_MONTH = 1;
private static final int MAX_MONTH = 12;
private static final int MIN_YEAR = 2015;
Copy link
Collaborator

Choose a reason for hiding this comment

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

MIN_YEAR를 정한 이유가 있을까요?


private final EventRepository eventRepository;
private final EventTagRepository eventTagRepository;
private final TagRepository tagRepository;

@Transactional(readOnly = true)
public EventDetailResponse findEvent(final Long id) {
final Event event = eventRepository.findById(id)
.orElseThrow(() -> new EventException(EVENT_NOT_FOUND_EXCEPTION));
return EventDetailResponse.from(event);
}

@Transactional(readOnly = true)
public List<EventResponse> findEvents(final LocalDate nowDate, final int year, final int month,
final String tagName, final String statusName) {
validateYearAndMonth(year, month);
List<Event> events = filterEventsByTag(tagName);

final EnumMap<EventStatus, List<Event>> sortAndGroupByStatus
= groupByEventStatus(nowDate, events, year, month);

return filterEventResponsesByStatus(statusName, sortAndGroupByStatus);
}

private void validateYearAndMonth(final int year, final int month) {
if (year < MIN_YEAR) {
throw new EventException(INVALID_YEAR);
}
if (month < MIN_MONTH || month > MAX_MONTH) {
throw new EventException(INVALID_MONTH);
}
}

private List<Event> filterEventsByTag(final String tagName) {
if (isExistTagName(tagName)) {
Tag tag = tagRepository.findByName(tagName)
.orElseThrow(() -> new TagException(NOT_FOUND_TAG));

return eventTagRepository.findEventTagsByTag(tag)
.stream()
.map(EventTag::getEvent)
.collect(toList());
}
return eventRepository.findAll();
}

private boolean isExistTagName(final String tagName) {
return tagName != null;
}

private EnumMap<EventStatus, List<Event>> groupByEventStatus(final LocalDate nowDate,
final List<Event> events, final int year, final int month) {
return events.stream()
.filter(event -> isOverlapToMonth(year, month,
event.getStartDate().toLocalDate(), event.getEndDate().toLocalDate())
)
.sorted(comparing(Event::getStartDate))
.collect(
groupingBy(event -> extractEventStatus(nowDate, event.getStartDate().toLocalDate(),
event.getEndDate().toLocalDate()), () -> new EnumMap<>(EventStatus.class), toList())
);
}

private boolean isOverlapToMonth(final int year, final int month,
final LocalDate eventStart, final LocalDate eventEnd) {
LocalDate monthStart = LocalDate.of(year, month, 1);
LocalDate monthEnd = LocalDate.of(year, month, monthStart.lengthOfMonth());

return (isBeforeOrEquals(eventStart, monthEnd) && isBeforeOrEquals(monthStart, eventEnd))
|| (isBeforeOrEquals(monthStart, eventStart) && isBeforeOrEquals(eventStart, monthEnd))
|| (isBeforeOrEquals(monthStart, eventEnd) && isBeforeOrEquals(eventEnd, monthEnd));
}

private boolean isBeforeOrEquals(LocalDate criteria, LocalDate comparison) {
return criteria.isBefore(comparison) || criteria.isEqual(comparison);
}

private EventStatus extractEventStatus(LocalDate now, LocalDate startDate, LocalDate endDate) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

이 메서드를 Event 안으로 밀어넣어 처리할 수 있지 않을까요?
(Event의 메서드를 정의한다는 뜻입니다.)

if (now.isBefore(startDate)) {
return EventStatus.UPCOMING;
}
if (now.isAfter(endDate)) {
return EventStatus.ENDED;
}
return EventStatus.IN_PROGRESS;
}

private List<EventResponse> filterEventResponsesByStatus(final String statusName,
final EnumMap<EventStatus, List<Event>> sortAndGroupByEventStatus) {
if (isaBoolean(statusName)) {
EventStatus status = findEventStatusByValue(statusName);
return makeEventResponsesByStatus(status, sortAndGroupByEventStatus.get(status));
}
return mergeEventResponses(sortAndGroupByEventStatus);
}

private boolean isaBoolean(final String statusName) {
return statusName != null;
}

private EventStatus findEventStatusByValue(final String value) {
return Arrays.stream(EventStatus.values())
.filter(status -> status.isSameValue(value))
.findFirst()
.orElseThrow(() -> new EventException(INVALID_STATUS));
}

private List<EventResponse> makeEventResponsesByStatus(EventStatus status,
List<Event> events) {
return events.stream()
.map(event -> EventResponse.from(status, event))
.collect(toList());
}

private List<EventResponse> mergeEventResponses(
final EnumMap<EventStatus, List<Event>> groupByEventStatus) {
return groupByEventStatus.entrySet().stream()
.map(entry -> makeEventResponsesByStatus(entry.getKey(), entry.getValue()))
.reduce(new ArrayList<>(), (combinedEvents, eventsToAdd) -> {
combinedEvents.addAll(eventsToAdd);
return combinedEvents;
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.emmsale.event.application.dto;

import com.emmsale.event.domain.Event;
import com.emmsale.event.domain.EventStatus;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public class EventResponse {

private final Long id;
private final String name;
@JsonFormat(pattern = "yyyy:MM:dd:HH:mm:ss")
private final LocalDateTime startDate;
@JsonFormat(pattern = "yyyy:MM:dd:HH:mm:ss")
private final LocalDateTime endDate;
private final List<String> tags;
private final String status;

public static EventResponse from(EventStatus status, Event event) {
return
new EventResponse(event.getId(), event.getName(), event.getStartDate(), event.getEndDate(),
event.getTags()
.stream()
.map(tag -> tag.getTag().getName())
.collect(Collectors.toList()), status.getValue());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.emmsale.event.domain;

import lombok.Getter;

@Getter
public enum EventStatus {

IN_PROGRESS("진행 중"),
UPCOMING("진행 예정"),
ENDED("종료된 행사");

private final String value;

EventStatus(final String value) {
this.value = value;
}

public boolean isSameValue(String value) {
return this.value.equals(value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class EventTag {

@Id
Expand All @@ -21,4 +26,9 @@ public class EventTag {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "tag_id", nullable = false)
private Tag tag;

public EventTag(Event event, Tag tag) {
this.event = event;
this.tag = tag;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.emmsale.event.domain;

import com.emmsale.tag.domain.Tag;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;

public interface EventTagRepository extends JpaRepository<EventTag, Long> {

List<EventTag> findEventTagsByTag(Tag tag);

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,19 @@
@RequiredArgsConstructor
public enum EventExceptionType implements BaseExceptionType {

EVENT_NOT_FOUND_EXCEPTION(HttpStatus.NOT_FOUND, "해당하는 행사를 찾을 수 없습니다.");
EVENT_NOT_FOUND_EXCEPTION(HttpStatus.NOT_FOUND, "해당하는 행사를 찾을 수 없습니다."),
INVALID_STATUS(
HttpStatus.BAD_REQUEST,
"요청하신 상태는 유효하지 않는 값입니다."
),
INVALID_YEAR(
HttpStatus.BAD_REQUEST,
"연도 값은 2015 이상이어야 합니다."
),
INVALID_MONTH(
HttpStatus.BAD_REQUEST,
"월은 1에서 12 사이여야 합니다."
);

private final HttpStatus httpStatus;
private final String errorMessage;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,22 @@
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Tag {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;

public Tag(String name) {
this.name = name;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.emmsale.tag.domain;

import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface TagRepository extends JpaRepository<Tag, Long> {

Optional<Tag> findByName(String name);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.emmsale.tag.exception;

import com.emmsale.base.BaseException;
import com.emmsale.base.BaseExceptionType;

public class TagException extends BaseException {

private final TagExceptionType exceptionType;

public TagException(final TagExceptionType exceptionType) {
super(exceptionType.errorMessage());
this.exceptionType = exceptionType;
}

@Override
public BaseExceptionType exceptionType() {
return exceptionType;
}
}
Loading