diff --git a/src/main/java/com/weeth/domain/schedule/application/annotation/ScheduleTimeCheck.java b/src/main/java/com/weeth/domain/schedule/application/annotation/ScheduleTimeCheck.java deleted file mode 100644 index 7e8ec584..00000000 --- a/src/main/java/com/weeth/domain/schedule/application/annotation/ScheduleTimeCheck.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.weeth.domain.schedule.application.annotation; - -import jakarta.validation.Constraint; -import jakarta.validation.Payload; -import com.weeth.domain.schedule.application.validator.ScheduleTimeCheckValidator; - -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -@Target({FIELD}) -@Retention(RUNTIME) -@Constraint(validatedBy = ScheduleTimeCheckValidator.class) -public @interface ScheduleTimeCheck { - - String message() default "마감 시간이 시작 시간보다 빠를 수 없습니다."; - - Class[] groups() default {}; - - Class[] payload() default {}; - -} diff --git a/src/main/java/com/weeth/domain/schedule/application/dto/EventDTO.java b/src/main/java/com/weeth/domain/schedule/application/dto/EventDTO.java deleted file mode 100644 index ee949d3a..00000000 --- a/src/main/java/com/weeth/domain/schedule/application/dto/EventDTO.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.weeth.domain.schedule.application.dto; - -import com.weeth.domain.schedule.domain.entity.enums.Type; - -import java.time.LocalDateTime; - -public class EventDTO { - - public record Response( - Long id, - String title, - String content, - String location, - String requiredItem, - String name, - Integer cardinal, - Type type, - LocalDateTime start, - LocalDateTime end, - LocalDateTime createdAt, - LocalDateTime modifiedAt - ) {} -} - diff --git a/src/main/java/com/weeth/domain/schedule/application/dto/MeetingDTO.java b/src/main/java/com/weeth/domain/schedule/application/dto/MeetingDTO.java deleted file mode 100644 index 09b89017..00000000 --- a/src/main/java/com/weeth/domain/schedule/application/dto/MeetingDTO.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.weeth.domain.schedule.application.dto; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.weeth.domain.schedule.domain.entity.enums.Type; - -import java.time.LocalDateTime; -import java.util.List; - -public class MeetingDTO { - - @JsonInclude(JsonInclude.Include.NON_NULL) - public record Response( - Long id, - String title, - String content, - String location, - String requiredItem, - String name, - Integer cardinal, - Type type, - Integer code, - LocalDateTime start, - LocalDateTime end, - LocalDateTime createdAt, - LocalDateTime modifiedAt - ) {} - - public record Info( - Long id, - Integer cardinal, - String title, - LocalDateTime start - ) {} - - public record Infos( - Info thisWeek, - List meetings - ) {} - - -} diff --git a/src/main/java/com/weeth/domain/schedule/application/dto/ScheduleDTO.java b/src/main/java/com/weeth/domain/schedule/application/dto/ScheduleDTO.java deleted file mode 100644 index 0aecfa05..00000000 --- a/src/main/java/com/weeth/domain/schedule/application/dto/ScheduleDTO.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.weeth.domain.schedule.application.dto; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import com.weeth.domain.schedule.domain.entity.enums.Type; -import org.springframework.format.annotation.DateTimeFormat; - -import java.time.LocalDateTime; - -public class ScheduleDTO { - - public record Response( - Long id, - String title, - LocalDateTime start, - LocalDateTime end, - Boolean isMeeting - ) {} - - public record Time( - @NotNull @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime start, - @NotNull @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime end - ) {} - - public record Save( - @NotBlank String title, - @NotBlank String content, - @NotBlank String location, - String requiredItem, - @NotNull Type type, - @NotNull Integer cardinal, - @NotNull @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime start, - @NotNull @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime end - ) {} - - public record Update( - @NotBlank String title, - @NotBlank String content, - @NotBlank String location, - String requiredItem, - @NotNull Type type, - @NotNull @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime start, - @NotNull @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime end - ) {} -} diff --git a/src/main/java/com/weeth/domain/schedule/application/exception/EventErrorCode.java b/src/main/java/com/weeth/domain/schedule/application/exception/EventErrorCode.java deleted file mode 100644 index 591dbaca..00000000 --- a/src/main/java/com/weeth/domain/schedule/application/exception/EventErrorCode.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.weeth.domain.schedule.application.exception; - -import com.weeth.global.common.exception.ErrorCodeInterface; -import com.weeth.global.common.exception.ExplainError; -import lombok.AllArgsConstructor; -import lombok.Getter; -import org.springframework.http.HttpStatus; - -@Getter -@AllArgsConstructor -public enum EventErrorCode implements ErrorCodeInterface { - - @ExplainError("요청한 일정 ID에 해당하는 일정이 존재하지 않을 때 발생합니다.") - EVENT_NOT_FOUND(2700, HttpStatus.NOT_FOUND, "존재하지 않는 일정입니다."); - - private final int code; - private final HttpStatus status; - private final String message; -} diff --git a/src/main/java/com/weeth/domain/schedule/application/exception/EventNotFoundException.java b/src/main/java/com/weeth/domain/schedule/application/exception/EventNotFoundException.java deleted file mode 100644 index 48856ea5..00000000 --- a/src/main/java/com/weeth/domain/schedule/application/exception/EventNotFoundException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.weeth.domain.schedule.application.exception; - -import com.weeth.global.common.exception.BaseException; - -public class EventNotFoundException extends BaseException { - public EventNotFoundException() { - super(EventErrorCode.EVENT_NOT_FOUND); - } -} diff --git a/src/main/java/com/weeth/domain/schedule/application/exception/MeetingErrorCode.java b/src/main/java/com/weeth/domain/schedule/application/exception/MeetingErrorCode.java deleted file mode 100644 index ad96ff71..00000000 --- a/src/main/java/com/weeth/domain/schedule/application/exception/MeetingErrorCode.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.weeth.domain.schedule.application.exception; - -import com.weeth.global.common.exception.ErrorCodeInterface; -import com.weeth.global.common.exception.ExplainError; -import lombok.AllArgsConstructor; -import lombok.Getter; -import org.springframework.http.HttpStatus; - -@Getter -@AllArgsConstructor -public enum MeetingErrorCode implements ErrorCodeInterface { - - @ExplainError("요청한 정기모임 ID에 해당하는 정기모임이 존재하지 않을 때 발생합니다.") - MEETING_NOT_FOUND(2701, HttpStatus.NOT_FOUND, "존재하지 않는 정기모임입니다."); - - private final int code; - private final HttpStatus status; - private final String message; -} diff --git a/src/main/java/com/weeth/domain/schedule/application/exception/MeetingNotFoundException.java b/src/main/java/com/weeth/domain/schedule/application/exception/MeetingNotFoundException.java deleted file mode 100644 index d9b4d4d7..00000000 --- a/src/main/java/com/weeth/domain/schedule/application/exception/MeetingNotFoundException.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.weeth.domain.schedule.application.exception; - -import com.weeth.global.common.exception.BaseException; - -public class MeetingNotFoundException extends BaseException { - public MeetingNotFoundException() {super(MeetingErrorCode.MEETING_NOT_FOUND);} -} diff --git a/src/main/java/com/weeth/domain/schedule/application/mapper/EventMapper.java b/src/main/java/com/weeth/domain/schedule/application/mapper/EventMapper.java deleted file mode 100644 index c297891c..00000000 --- a/src/main/java/com/weeth/domain/schedule/application/mapper/EventMapper.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.weeth.domain.schedule.application.mapper; - -import com.weeth.domain.schedule.application.dto.ScheduleDTO; -import com.weeth.domain.schedule.domain.entity.Event; -import com.weeth.domain.user.domain.entity.User; -import org.mapstruct.*; - -import static com.weeth.domain.schedule.application.dto.EventDTO.Response; - -@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, unmappedTargetPolicy = ReportingPolicy.IGNORE) -public interface EventMapper { - - @Mapping(target = "name", source = "event.user.name") - @Mapping(target = "type", expression = "java(Type.EVENT)") - Response to(Event event); - - @Mappings({ - @Mapping(target = "id", ignore = true), - @Mapping(target = "user", source = "user") - }) - Event from(ScheduleDTO.Save dto, User user); -} diff --git a/src/main/java/com/weeth/domain/schedule/application/mapper/MeetingMapper.java b/src/main/java/com/weeth/domain/schedule/application/mapper/MeetingMapper.java deleted file mode 100644 index c81c72bc..00000000 --- a/src/main/java/com/weeth/domain/schedule/application/mapper/MeetingMapper.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.weeth.domain.schedule.application.mapper; - -import com.weeth.domain.schedule.application.dto.ScheduleDTO; -import com.weeth.domain.schedule.domain.entity.Meeting; -import com.weeth.domain.user.domain.entity.User; -import org.mapstruct.*; - -import java.util.Random; - -import static com.weeth.domain.schedule.application.dto.MeetingDTO.Info; -import static com.weeth.domain.schedule.application.dto.MeetingDTO.Response; - -@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, unmappedTargetPolicy = ReportingPolicy.IGNORE) -public interface MeetingMapper { - - @Mapping(target = "name", source = "user.name") - @Mapping(target = "code", ignore = true) - @Mapping(target = "type", expression = "java(Type.MEETING)") - Response to(Meeting meeting); - - Info toInfo(Meeting meeting); - - @Mapping(target = "name", source = "user.name") - @Mapping(target = "type", expression = "java(Type.MEETING)") - Response toAdminResponse(Meeting meeting); - - @Mappings({ - @Mapping(target = "id", ignore = true), - @Mapping(target = "code", expression = "java( generateCode() )"), - @Mapping(target = "user", source = "user") - }) - Meeting from(ScheduleDTO.Save dto, User user); - - default Integer generateCode() { - return new Random().nextInt(9000) + 1000; - } - - /* - 차후 필히 리팩토링 할 것 - -> 정기 모임의 참여하는 인원의 멤버수를 어떻게 관리할지. - 해당 코드는 일시적인 대안책임 - */ -// default Integer getMemberCount(Meeting meeting) { -// return (int)meeting.getAttendances().stream() -// .filter(attendance -> !attendance.getUser().getStatus().equals(Status.BANNED)) -// .filter(attendance -> !attendance.getUser().getStatus().equals(Status.LEFT)) -// .count(); -// } -} diff --git a/src/main/java/com/weeth/domain/schedule/application/mapper/ScheduleMapper.java b/src/main/java/com/weeth/domain/schedule/application/mapper/ScheduleMapper.java deleted file mode 100644 index c61299bd..00000000 --- a/src/main/java/com/weeth/domain/schedule/application/mapper/ScheduleMapper.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.weeth.domain.schedule.application.mapper; - -import com.weeth.domain.schedule.application.dto.ScheduleDTO; -import com.weeth.domain.schedule.domain.entity.Schedule; -import org.mapstruct.Mapper; -import org.mapstruct.MappingConstants; -import org.mapstruct.ReportingPolicy; - -@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, unmappedTargetPolicy = ReportingPolicy.IGNORE) -public interface ScheduleMapper { - - ScheduleDTO.Response toScheduleDTO(Schedule schedule, Boolean isMeeting); -} diff --git a/src/main/java/com/weeth/domain/schedule/application/usecase/EventUseCase.java b/src/main/java/com/weeth/domain/schedule/application/usecase/EventUseCase.java deleted file mode 100644 index e0227af7..00000000 --- a/src/main/java/com/weeth/domain/schedule/application/usecase/EventUseCase.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.weeth.domain.schedule.application.usecase; - -import com.weeth.domain.schedule.application.dto.ScheduleDTO; - -import static com.weeth.domain.schedule.application.dto.EventDTO.*; - -public interface EventUseCase { - - Response find(Long eventId); - - void save(ScheduleDTO.Save dto, Long userId); - - void update(Long eventId, ScheduleDTO.Update dto, Long userId); - - void delete(Long eventId); -} diff --git a/src/main/java/com/weeth/domain/schedule/application/usecase/EventUseCaseImpl.java b/src/main/java/com/weeth/domain/schedule/application/usecase/EventUseCaseImpl.java deleted file mode 100644 index 21a8ae35..00000000 --- a/src/main/java/com/weeth/domain/schedule/application/usecase/EventUseCaseImpl.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.weeth.domain.schedule.application.usecase; - -import com.weeth.domain.schedule.application.dto.ScheduleDTO; -import com.weeth.domain.schedule.application.mapper.EventMapper; -import com.weeth.domain.schedule.domain.entity.Event; -import com.weeth.domain.schedule.domain.service.EventDeleteService; -import com.weeth.domain.schedule.domain.service.EventGetService; -import com.weeth.domain.schedule.domain.service.EventSaveService; -import com.weeth.domain.schedule.domain.service.EventUpdateService; -import com.weeth.domain.user.domain.entity.User; -import com.weeth.domain.user.domain.repository.CardinalReader; -import com.weeth.domain.user.domain.repository.UserReader; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import static com.weeth.domain.schedule.application.dto.EventDTO.Response; - -@Service -@RequiredArgsConstructor -public class EventUseCaseImpl implements EventUseCase { - - private final UserReader userReader; - private final EventGetService eventGetService; - private final EventSaveService eventSaveService; - private final EventUpdateService eventUpdateService; - private final EventDeleteService eventDeleteService; - private final CardinalReader cardinalReader; - private final EventMapper mapper; - - @Override - public Response find(Long eventId) { - return mapper.to(eventGetService.find(eventId)); - } - - @Override - @Transactional - public void save(ScheduleDTO.Save dto, Long userId) { - User user = userReader.getById(userId); - cardinalReader.getByCardinalNumber(dto.cardinal()); - - eventSaveService.save(mapper.from(dto, user)); - } - - @Override - @Transactional - public void update(Long eventId, ScheduleDTO.Update dto, Long userId) { - User user = userReader.getById(userId); - Event event = eventGetService.find(eventId); - eventUpdateService.update(event, dto, user); - } - - @Override - @Transactional - public void delete(Long eventId) { - Event event = eventGetService.find(eventId); - eventDeleteService.delete(event); - } -} diff --git a/src/main/java/com/weeth/domain/schedule/application/usecase/MeetingUseCase.java b/src/main/java/com/weeth/domain/schedule/application/usecase/MeetingUseCase.java deleted file mode 100644 index 857de980..00000000 --- a/src/main/java/com/weeth/domain/schedule/application/usecase/MeetingUseCase.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.weeth.domain.schedule.application.usecase; - -import com.weeth.domain.schedule.application.dto.MeetingDTO; -import com.weeth.domain.schedule.application.dto.ScheduleDTO; - -import java.util.List; - -import static com.weeth.domain.schedule.application.dto.MeetingDTO.Info; -import static com.weeth.domain.schedule.application.dto.MeetingDTO.Response; - -public interface MeetingUseCase { - - Response find(Long userId, Long eventId); - - MeetingDTO.Infos find(Integer cardinal); - - void save(ScheduleDTO.Save dto, Long userId); - - void update(ScheduleDTO.Update dto, Long userId, Long meetingId); - - void delete(Long meetingId); -} diff --git a/src/main/java/com/weeth/domain/schedule/application/usecase/MeetingUseCaseImpl.java b/src/main/java/com/weeth/domain/schedule/application/usecase/MeetingUseCaseImpl.java deleted file mode 100644 index de95fef2..00000000 --- a/src/main/java/com/weeth/domain/schedule/application/usecase/MeetingUseCaseImpl.java +++ /dev/null @@ -1,147 +0,0 @@ -package com.weeth.domain.schedule.application.usecase; - -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; -import com.weeth.domain.attendance.domain.entity.Attendance; -import com.weeth.domain.attendance.domain.service.AttendanceDeleteService; -import com.weeth.domain.attendance.domain.service.AttendanceGetService; -import com.weeth.domain.attendance.domain.service.AttendanceSaveService; -import com.weeth.domain.attendance.domain.service.AttendanceUpdateService; -import com.weeth.domain.schedule.application.dto.MeetingDTO; -import com.weeth.domain.schedule.application.dto.ScheduleDTO; -import com.weeth.domain.schedule.application.mapper.MeetingMapper; -import com.weeth.domain.schedule.domain.entity.Meeting; -import com.weeth.domain.schedule.domain.service.MeetingDeleteService; -import com.weeth.domain.schedule.domain.service.MeetingGetService; -import com.weeth.domain.schedule.domain.service.MeetingSaveService; -import com.weeth.domain.schedule.domain.service.MeetingUpdateService; -import com.weeth.domain.user.domain.entity.Cardinal; -import com.weeth.domain.user.domain.entity.User; -import com.weeth.domain.user.domain.entity.enums.Role; -import com.weeth.domain.user.domain.entity.enums.Status; -import com.weeth.domain.user.domain.repository.CardinalReader; -import com.weeth.domain.user.domain.repository.UserReader; -import com.weeth.domain.user.domain.repository.UserRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.DayOfWeek; -import java.time.LocalDate; -import java.time.temporal.TemporalAdjusters; -import java.util.Comparator; -import java.util.List; - -import static com.weeth.domain.schedule.application.dto.MeetingDTO.Response; - -@Slf4j -@Service -@RequiredArgsConstructor -public class MeetingUseCaseImpl implements MeetingUseCase { - - private final MeetingGetService meetingGetService; - private final MeetingMapper mapper; - private final MeetingSaveService meetingSaveService; - private final UserReader userReader; - private final UserRepository userRepository; - private final MeetingUpdateService meetingUpdateService; - private final MeetingDeleteService meetingDeleteService; - private final AttendanceGetService attendanceGetService; - private final AttendanceSaveService attendanceSaveService; - private final AttendanceDeleteService attendanceDeleteService; - private final AttendanceUpdateService attendanceUpdateService; - private final CardinalReader cardinalReader; - - @PersistenceContext - private EntityManager em; - - @Override - public Response find(Long userId, Long meetingId) { - User user = userReader.getById(userId); - Meeting meeting = meetingGetService.find(meetingId); - - if (Role.ADMIN == user.getRole()) { - return mapper.toAdminResponse(meeting) ; - } - - return mapper.to(meeting); - } - - @Override - public MeetingDTO.Infos find(Integer cardinal) { - List meetings; - - if (cardinal == null) { - meetings = meetingGetService.findAll(); - } else { - meetings = meetingGetService.findMeetingByCardinal(cardinal); - } - - Meeting thisWeek = findThisWeek(meetings); - List sorted = sortMeetings(meetings); - - return new MeetingDTO.Infos( - thisWeek != null ? mapper.toInfo(thisWeek) : null, - sorted.stream().map(mapper::toInfo).toList()); - } - - @Override - @Transactional - public void save(ScheduleDTO.Save dto, Long userId) { - User user = userReader.getById(userId); - Cardinal cardinal = cardinalReader.getByCardinalNumber(dto.cardinal()); - - List userList = userRepository.findAllByCardinalAndStatus(cardinal, Status.ACTIVE); - - Meeting meeting = mapper.from(dto, user); - meetingSaveService.save(meeting); - - attendanceSaveService.saveAll(userList, meeting); - } - - @Override - @Transactional - public void update(ScheduleDTO.Update dto, Long userId, Long meetingId) { - Meeting meeting = meetingGetService.find(meetingId); - User user = userReader.getById(userId); - meetingUpdateService.update(dto, user, meeting); - } - - @Override - @Transactional - public void delete(Long meetingId) { - Meeting meeting = meetingGetService.find(meetingId); - List attendances = attendanceGetService.findAllByMeeting(meeting); - - attendanceUpdateService.updateUserAttendanceByStatus(attendances); - - em.flush(); - em.clear(); - - attendanceDeleteService.deleteAll(meeting); - meetingDeleteService.delete(meeting); - } - - private List sortMeetings(List meetings) { - return meetings.stream() - .sorted(Comparator.comparing(Meeting::getStart).reversed()) - .toList(); - } - - - private Meeting findThisWeek(List meetings) { - LocalDate today = LocalDate.now(); - LocalDate startOfWeek = today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)); - LocalDate endOfWeek = today.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)); - - return meetings.stream() - .filter(m -> { - LocalDate d = m.getStart().toLocalDate(); - return !d.isBefore(startOfWeek) && !d.isAfter(endOfWeek); - }) - .findFirst() - .orElse(null); - } - -} diff --git a/src/main/java/com/weeth/domain/schedule/application/usecase/ScheduleUseCase.java b/src/main/java/com/weeth/domain/schedule/application/usecase/ScheduleUseCase.java deleted file mode 100644 index 4676a026..00000000 --- a/src/main/java/com/weeth/domain/schedule/application/usecase/ScheduleUseCase.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.weeth.domain.schedule.application.usecase; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.Map; - -import static com.weeth.domain.schedule.application.dto.ScheduleDTO.Response; - -public interface ScheduleUseCase { - - List findByMonthly(LocalDateTime start, LocalDateTime end); - - Map> findByYearly(Integer year, Integer semester); - -} diff --git a/src/main/java/com/weeth/domain/schedule/application/usecase/ScheduleUseCaseImpl.java b/src/main/java/com/weeth/domain/schedule/application/usecase/ScheduleUseCaseImpl.java deleted file mode 100644 index 79fdcb36..00000000 --- a/src/main/java/com/weeth/domain/schedule/application/usecase/ScheduleUseCaseImpl.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.weeth.domain.schedule.application.usecase; - -import com.weeth.domain.schedule.domain.service.EventGetService; -import com.weeth.domain.schedule.domain.service.MeetingGetService; -import com.weeth.domain.user.domain.entity.Cardinal; -import com.weeth.domain.user.application.exception.CardinalNotFoundException; -import com.weeth.domain.user.domain.repository.CardinalRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -import java.time.LocalDateTime; -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import java.util.stream.Stream; - -import static com.weeth.domain.schedule.application.dto.ScheduleDTO.Response; - -@Service -@RequiredArgsConstructor -public class ScheduleUseCaseImpl implements ScheduleUseCase { - - private final EventGetService eventGetService; - private final MeetingGetService meetingGetService; - private final CardinalRepository cardinalRepository; - - @Override - public List findByMonthly(LocalDateTime start, LocalDateTime end) { - List events = eventGetService.find(start, end); - List meetings = meetingGetService.find(start, end); - - return Stream.of(events, meetings) - .flatMap(Collection::stream) - .sorted(Comparator.comparing(Response::start)) - .toList(); - } - - @Override - public Map> findByYearly(Integer year, Integer semester) { - Cardinal cardinal = cardinalRepository.findByYearAndSemester(year, semester) - .orElseThrow(CardinalNotFoundException::new); - - List events = eventGetService.find(cardinal.getCardinalNumber()); - List meetings = meetingGetService.findByCardinal(cardinal.getCardinalNumber()); - - return Stream.of(events, meetings) - .flatMap(Collection::stream) // 병합 - .sorted(Comparator.comparing(Response::start)) // 스케줄 시작 시간으로 정렬 - .flatMap(schedule -> { - List> monthEventPairs = new ArrayList<>(); - - int left = schedule.start().getMonthValue(); - int right = schedule.end().getMonthValue() + 1; - IntStream.range(left, right) // 기간 내 포함된 달 계산 - .forEach(month -> monthEventPairs.add( - new AbstractMap.SimpleEntry<>(month, schedule)) - ); - - return monthEventPairs.stream(); - }) - .collect(Collectors.groupingBy( - Map.Entry::getKey, - Collectors.mapping(Map.Entry::getValue, Collectors.toList()) - )); - } -} diff --git a/src/main/java/com/weeth/domain/schedule/application/validator/ScheduleTimeCheckValidator.java b/src/main/java/com/weeth/domain/schedule/application/validator/ScheduleTimeCheckValidator.java deleted file mode 100644 index 62668e4b..00000000 --- a/src/main/java/com/weeth/domain/schedule/application/validator/ScheduleTimeCheckValidator.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.weeth.domain.schedule.application.validator; - -import jakarta.validation.ConstraintValidator; -import jakarta.validation.ConstraintValidatorContext; -import com.weeth.domain.schedule.application.annotation.ScheduleTimeCheck; -import com.weeth.domain.schedule.application.dto.ScheduleDTO.Time; - -public class ScheduleTimeCheckValidator implements ConstraintValidator { - - @Override - public void initialize(ScheduleTimeCheck constraintAnnotation) { - ConstraintValidator.super.initialize(constraintAnnotation); - } - - @Override - public boolean isValid(Time time, ConstraintValidatorContext context) { - return time.start().isBefore(time.end().plusMinutes(1)); - } -} \ No newline at end of file diff --git a/src/main/java/com/weeth/domain/schedule/domain/entity/Event.java b/src/main/java/com/weeth/domain/schedule/domain/entity/Event.java deleted file mode 100644 index b9b5253f..00000000 --- a/src/main/java/com/weeth/domain/schedule/domain/entity/Event.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.weeth.domain.schedule.domain.entity; - -import jakarta.persistence.Entity; -import com.weeth.domain.schedule.application.dto.ScheduleDTO; -import com.weeth.domain.user.domain.entity.User; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.experimental.SuperBuilder; - -@Entity -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@SuperBuilder -public class Event extends Schedule { - - public void update(ScheduleDTO.Update dto, User user) { - this.updateUpperClass(dto, user); - } -} diff --git a/src/main/java/com/weeth/domain/schedule/domain/entity/Meeting.java b/src/main/java/com/weeth/domain/schedule/domain/entity/Meeting.java deleted file mode 100644 index 30ad37fe..00000000 --- a/src/main/java/com/weeth/domain/schedule/domain/entity/Meeting.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.weeth.domain.schedule.domain.entity; - -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.PrePersist; -import com.weeth.domain.schedule.application.dto.ScheduleDTO; -import com.weeth.domain.schedule.domain.entity.enums.MeetingStatus; -import com.weeth.domain.user.domain.entity.User; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.experimental.SuperBuilder; - -@Entity -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -@SuperBuilder -public class Meeting extends Schedule { - - private Integer code; - - @Enumerated(EnumType.STRING) - private MeetingStatus meetingStatus; - - public void update(ScheduleDTO.Update dto, User user) { - this.updateUpperClass(dto, user); - } - - @PrePersist - public void init() { - this.meetingStatus = MeetingStatus.OPEN; - } - - public void close() { - this.meetingStatus = MeetingStatus.CLOSE; - } -} diff --git a/src/main/java/com/weeth/domain/schedule/domain/entity/Schedule.java b/src/main/java/com/weeth/domain/schedule/domain/entity/Schedule.java deleted file mode 100644 index 3c232518..00000000 --- a/src/main/java/com/weeth/domain/schedule/domain/entity/Schedule.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.weeth.domain.schedule.domain.entity; - -import jakarta.persistence.*; -import com.weeth.domain.schedule.application.dto.ScheduleDTO; -import com.weeth.domain.user.domain.entity.User; -import com.weeth.global.common.entity.BaseEntity; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.experimental.SuperBuilder; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; - -import java.time.LocalDateTime; - - -@Getter -@MappedSuperclass -@EntityListeners(AuditingEntityListener.class) -@SuperBuilder -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Schedule extends BaseEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String title; - - @Column(columnDefinition = "TEXT") - private String content; - - private String location; - - private Integer cardinal; - - private String requiredItem; - - private LocalDateTime start; - - private LocalDateTime end; - - @ManyToOne - @JoinColumn(name = "user_id") - private User user; - - public void updateUpperClass(ScheduleDTO.Update dto, User user) { - this.title = dto.title(); - this.content = dto.content(); - this.location = dto.location(); - this.requiredItem = dto.requiredItem(); - this.start = dto.start(); - this.end = dto.end(); - this.user = user; - } -} diff --git a/src/main/java/com/weeth/domain/schedule/domain/entity/enums/MeetingStatus.java b/src/main/java/com/weeth/domain/schedule/domain/entity/enums/MeetingStatus.java deleted file mode 100644 index 7ca34280..00000000 --- a/src/main/java/com/weeth/domain/schedule/domain/entity/enums/MeetingStatus.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.weeth.domain.schedule.domain.entity.enums; - -public enum MeetingStatus { - OPEN, - CLOSE -} diff --git a/src/main/java/com/weeth/domain/schedule/domain/entity/enums/Type.java b/src/main/java/com/weeth/domain/schedule/domain/entity/enums/Type.java deleted file mode 100644 index ca0c721d..00000000 --- a/src/main/java/com/weeth/domain/schedule/domain/entity/enums/Type.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.weeth.domain.schedule.domain.entity.enums; - -public enum Type { - EVENT, MEETING -} diff --git a/src/main/java/com/weeth/domain/schedule/domain/repository/EventRepository.java b/src/main/java/com/weeth/domain/schedule/domain/repository/EventRepository.java deleted file mode 100644 index a281ed08..00000000 --- a/src/main/java/com/weeth/domain/schedule/domain/repository/EventRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.weeth.domain.schedule.domain.repository; - -import com.weeth.domain.schedule.domain.entity.Event; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.time.LocalDateTime; -import java.util.List; - -public interface EventRepository extends JpaRepository { - - List findByStartLessThanEqualAndEndGreaterThanEqualOrderByStartAsc(LocalDateTime end, LocalDateTime start); - - List findAllByCardinal(int cardinal); -} diff --git a/src/main/java/com/weeth/domain/schedule/domain/repository/MeetingRepository.java b/src/main/java/com/weeth/domain/schedule/domain/repository/MeetingRepository.java deleted file mode 100644 index 8b3c1128..00000000 --- a/src/main/java/com/weeth/domain/schedule/domain/repository/MeetingRepository.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.weeth.domain.schedule.domain.repository; - -import com.weeth.domain.schedule.domain.entity.Meeting; -import com.weeth.domain.schedule.domain.entity.enums.MeetingStatus; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.time.LocalDateTime; -import java.util.List; - -public interface MeetingRepository extends JpaRepository { - - List findByStartLessThanEqualAndEndGreaterThanEqualOrderByStartAsc(LocalDateTime start, LocalDateTime end); - - List findAllByCardinalOrderByStartAsc(int cardinal); - - List findAllByCardinalOrderByStartDesc(int cardinal); - - List findAllByCardinal(int cardinal); - - List findAllByCardinalInOrderByCardinalAscStartAsc(List cardinals); - - List findAllByMeetingStatusAndEndBeforeOrderByEndAsc(MeetingStatus status, LocalDateTime end); - - List findAllByOrderByStartDesc(); -} diff --git a/src/main/java/com/weeth/domain/schedule/domain/service/EventDeleteService.java b/src/main/java/com/weeth/domain/schedule/domain/service/EventDeleteService.java deleted file mode 100644 index 1bcef851..00000000 --- a/src/main/java/com/weeth/domain/schedule/domain/service/EventDeleteService.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.weeth.domain.schedule.domain.service; - -import com.weeth.domain.schedule.domain.entity.Event; -import com.weeth.domain.schedule.domain.repository.EventRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class EventDeleteService { - - private final EventRepository eventRepository; - - public void delete(Event event) { - eventRepository.delete(event); - } -} diff --git a/src/main/java/com/weeth/domain/schedule/domain/service/EventGetService.java b/src/main/java/com/weeth/domain/schedule/domain/service/EventGetService.java deleted file mode 100644 index 71546635..00000000 --- a/src/main/java/com/weeth/domain/schedule/domain/service/EventGetService.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.weeth.domain.schedule.domain.service; - -import com.weeth.domain.schedule.application.dto.ScheduleDTO; -import com.weeth.domain.schedule.application.mapper.ScheduleMapper; -import com.weeth.domain.schedule.domain.entity.Event; -import com.weeth.domain.schedule.domain.repository.EventRepository; -import com.weeth.domain.schedule.application.exception.EventNotFoundException; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -import java.time.LocalDateTime; -import java.util.List; - -@Service -@RequiredArgsConstructor -public class EventGetService { - - private final EventRepository eventRepository; - private final ScheduleMapper mapper; - - public Event find(Long eventId) { - return eventRepository.findById(eventId) - .orElseThrow(EventNotFoundException::new); - } - - public List find(LocalDateTime start, LocalDateTime end) { - return eventRepository.findByStartLessThanEqualAndEndGreaterThanEqualOrderByStartAsc(end, start).stream() - .map(event -> mapper.toScheduleDTO(event, false)) - .toList(); - } - - public List find(Integer cardinal) { - return eventRepository.findAllByCardinal(cardinal).stream() - .map(event -> mapper.toScheduleDTO(event, false)) - .toList(); - } -} diff --git a/src/main/java/com/weeth/domain/schedule/domain/service/EventSaveService.java b/src/main/java/com/weeth/domain/schedule/domain/service/EventSaveService.java deleted file mode 100644 index b2c82831..00000000 --- a/src/main/java/com/weeth/domain/schedule/domain/service/EventSaveService.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.weeth.domain.schedule.domain.service; - -import com.weeth.domain.schedule.domain.entity.Event; -import com.weeth.domain.schedule.domain.repository.EventRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class EventSaveService { - - private final EventRepository eventRepository; - - public void save(Event event) { - eventRepository.save(event); - } -} diff --git a/src/main/java/com/weeth/domain/schedule/domain/service/EventUpdateService.java b/src/main/java/com/weeth/domain/schedule/domain/service/EventUpdateService.java deleted file mode 100644 index 905af593..00000000 --- a/src/main/java/com/weeth/domain/schedule/domain/service/EventUpdateService.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.weeth.domain.schedule.domain.service; - -import jakarta.transaction.Transactional; -import com.weeth.domain.schedule.application.dto.ScheduleDTO; -import com.weeth.domain.schedule.domain.entity.Event; -import com.weeth.domain.user.domain.entity.User; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@Transactional -@RequiredArgsConstructor -public class EventUpdateService { - - public void update(Event event, ScheduleDTO.Update dto, User user) { - event.update(dto, user); - } -} diff --git a/src/main/java/com/weeth/domain/schedule/domain/service/MeetingDeleteService.java b/src/main/java/com/weeth/domain/schedule/domain/service/MeetingDeleteService.java deleted file mode 100644 index 39fecb02..00000000 --- a/src/main/java/com/weeth/domain/schedule/domain/service/MeetingDeleteService.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.weeth.domain.schedule.domain.service; - -import com.weeth.domain.schedule.domain.entity.Meeting; -import com.weeth.domain.schedule.domain.repository.MeetingRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class MeetingDeleteService { - - private final MeetingRepository meetingRepository; - - public void delete(Meeting meeting) { - meetingRepository.delete(meeting); - } -} diff --git a/src/main/java/com/weeth/domain/schedule/domain/service/MeetingGetService.java b/src/main/java/com/weeth/domain/schedule/domain/service/MeetingGetService.java deleted file mode 100644 index 6eebc011..00000000 --- a/src/main/java/com/weeth/domain/schedule/domain/service/MeetingGetService.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.weeth.domain.schedule.domain.service; - -import com.weeth.domain.schedule.application.dto.ScheduleDTO; -import com.weeth.domain.schedule.application.mapper.ScheduleMapper; -import com.weeth.domain.schedule.domain.entity.Meeting; -import com.weeth.domain.schedule.domain.entity.enums.MeetingStatus; -import com.weeth.domain.schedule.domain.repository.MeetingRepository; -import com.weeth.domain.schedule.application.exception.MeetingNotFoundException; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -import java.time.LocalDateTime; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -@Service -@RequiredArgsConstructor -public class MeetingGetService { - - private final MeetingRepository meetingRepository; - private final ScheduleMapper mapper; - - public Meeting find(Long meetingId) { - return meetingRepository.findById(meetingId) - .orElseThrow(MeetingNotFoundException::new); - } - - public List find(LocalDateTime start, LocalDateTime end) { - return meetingRepository.findByStartLessThanEqualAndEndGreaterThanEqualOrderByStartAsc(end, start).stream() - .map(meeting -> mapper.toScheduleDTO(meeting, true)) - .toList(); - } - - public List find(Integer cardinal) { - return meetingRepository.findAllByCardinalOrderByStartAsc(cardinal); - } - - public Map> findByCardinals(List cardinals) { - if (cardinals == null || cardinals.isEmpty()) { - return Map.of(); - } - return meetingRepository.findAllByCardinalInOrderByCardinalAscStartAsc(cardinals).stream() - .collect(Collectors.groupingBy(Meeting::getCardinal, LinkedHashMap::new, Collectors.toList())); - } - - public List findMeetingByCardinal(Integer cardinal) { - return meetingRepository.findAllByCardinalOrderByStartDesc(cardinal); - } - - public List findAll() { - return meetingRepository.findAllByOrderByStartDesc(); - } - - public List findByCardinal(Integer cardinal) { - return meetingRepository.findAllByCardinal(cardinal).stream() - .map(meeting -> mapper.toScheduleDTO(meeting, true)) - .toList(); - } - - public List findAllOpenMeetingsBeforeNow() { - return meetingRepository.findAllByMeetingStatusAndEndBeforeOrderByEndAsc(MeetingStatus.OPEN, LocalDateTime.now()); - } -} diff --git a/src/main/java/com/weeth/domain/schedule/domain/service/MeetingSaveService.java b/src/main/java/com/weeth/domain/schedule/domain/service/MeetingSaveService.java deleted file mode 100644 index ba671f62..00000000 --- a/src/main/java/com/weeth/domain/schedule/domain/service/MeetingSaveService.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.weeth.domain.schedule.domain.service; - -import com.weeth.domain.schedule.domain.entity.Meeting; -import com.weeth.domain.schedule.domain.repository.MeetingRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class MeetingSaveService { - - private final MeetingRepository meetingRepository; - - public void save(Meeting meeting) { - meetingRepository.save(meeting); - } -} diff --git a/src/main/java/com/weeth/domain/schedule/domain/service/MeetingUpdateService.java b/src/main/java/com/weeth/domain/schedule/domain/service/MeetingUpdateService.java deleted file mode 100644 index e89301c7..00000000 --- a/src/main/java/com/weeth/domain/schedule/domain/service/MeetingUpdateService.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.weeth.domain.schedule.domain.service; - -import com.weeth.domain.schedule.application.dto.ScheduleDTO; -import com.weeth.domain.schedule.domain.entity.Meeting; -import com.weeth.domain.user.domain.entity.User; -import org.springframework.stereotype.Service; - -@Service -public class MeetingUpdateService { - - public void update(ScheduleDTO.Update dto, User user, Meeting meeting) { - meeting.update(dto, user); - } -} diff --git a/src/main/java/com/weeth/domain/schedule/presentation/EventAdminController.java b/src/main/java/com/weeth/domain/schedule/presentation/EventAdminController.java deleted file mode 100644 index 2a8f2a99..00000000 --- a/src/main/java/com/weeth/domain/schedule/presentation/EventAdminController.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.weeth.domain.schedule.presentation; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import com.weeth.domain.schedule.application.dto.ScheduleDTO; -import com.weeth.domain.schedule.application.exception.EventErrorCode; -import com.weeth.domain.schedule.application.usecase.EventUseCase; -import com.weeth.domain.schedule.application.usecase.MeetingUseCase; -import com.weeth.domain.schedule.domain.entity.enums.Type; -import com.weeth.global.auth.annotation.CurrentUser; -import com.weeth.global.common.exception.ApiErrorCodeExample; -import com.weeth.global.common.response.CommonResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.*; - -import static com.weeth.domain.schedule.presentation.ScheduleResponseCode.*; - -@Tag(name = "EVENT ADMIN", description = "[ADMIN] 일정 어드민 API") -@RestController -@RequiredArgsConstructor -@RequestMapping("/api/v1/admin/events") -@ApiErrorCodeExample(EventErrorCode.class) -public class EventAdminController { - - private final EventUseCase eventUseCase; - private final MeetingUseCase meetingUseCase; - - @PostMapping - @Operation(summary = "일정/정기모임 생성") - public CommonResponse save(@Valid @RequestBody ScheduleDTO.Save dto, - @Parameter(hidden = true) @CurrentUser Long userId) { - if (dto.type() == Type.EVENT) { - eventUseCase.save(dto, userId); - } else { - meetingUseCase.save(dto, userId); - } - - return CommonResponse.success(EVENT_SAVE_SUCCESS); - } - - @PatchMapping("/{eventId}") - @Operation(summary = "일정 수정 (type은 변경할 수 없게 해주세요.)") - public CommonResponse update(@PathVariable Long eventId, @Valid @RequestBody ScheduleDTO.Update dto, - @Parameter(hidden = true) @CurrentUser Long userId) { - if (dto.type() == Type.EVENT) { - eventUseCase.update(eventId, dto, userId); - } else { - meetingUseCase.update(dto, userId, eventId); - } - - return CommonResponse.success(EVENT_UPDATE_SUCCESS); - } - - @DeleteMapping("/{eventId}") - @Operation(summary = "일정 삭제") - public CommonResponse delete(@PathVariable Long eventId) { - eventUseCase.delete(eventId); - - return CommonResponse.success(EVENT_DELETE_SUCCESS); - } -} diff --git a/src/main/java/com/weeth/domain/schedule/presentation/EventController.java b/src/main/java/com/weeth/domain/schedule/presentation/EventController.java deleted file mode 100644 index d94165e9..00000000 --- a/src/main/java/com/weeth/domain/schedule/presentation/EventController.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.weeth.domain.schedule.presentation; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import com.weeth.domain.schedule.application.exception.EventErrorCode; -import com.weeth.domain.schedule.application.usecase.EventUseCase; -import com.weeth.global.common.exception.ApiErrorCodeExample; -import com.weeth.global.common.response.CommonResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import static com.weeth.domain.schedule.application.dto.EventDTO.Response; -import static com.weeth.domain.schedule.presentation.ScheduleResponseCode.EVENT_FIND_SUCCESS; - -@Tag(name = "EVENT", description = "일정 API") -@RestController -@RequiredArgsConstructor -@RequestMapping("/api/v1/events") -@ApiErrorCodeExample(EventErrorCode.class) -public class EventController { - - private final EventUseCase eventUseCase; - - @GetMapping("/{eventId}") - @Operation(summary="일정 상세 조회") - public CommonResponse find(@PathVariable Long eventId) { - return CommonResponse.success(EVENT_FIND_SUCCESS, - eventUseCase.find(eventId)); - } - -} diff --git a/src/main/java/com/weeth/domain/schedule/presentation/MeetingAdminController.java b/src/main/java/com/weeth/domain/schedule/presentation/MeetingAdminController.java deleted file mode 100644 index dcbc3e19..00000000 --- a/src/main/java/com/weeth/domain/schedule/presentation/MeetingAdminController.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.weeth.domain.schedule.presentation; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import com.weeth.domain.schedule.application.exception.MeetingErrorCode; -import com.weeth.domain.schedule.application.usecase.MeetingUseCase; -import com.weeth.global.common.exception.ApiErrorCodeExample; -import com.weeth.global.common.response.CommonResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import static com.weeth.domain.schedule.presentation.ScheduleResponseCode.MEETING_DELETE_SUCCESS; - -@Tag(name = "MEETING ADMIN", description = "[ADMIN] 정기모임 어드민 API") -@RestController -@RequiredArgsConstructor -@RequestMapping("/api/v1/admin/meetings") -@ApiErrorCodeExample(MeetingErrorCode.class) -public class MeetingAdminController { - - private final MeetingUseCase meetingUseCase; - - @DeleteMapping("/{meetingId}") - @Operation(summary = "정기모임 삭제") - public CommonResponse delete(@PathVariable Long meetingId) { - meetingUseCase.delete(meetingId); - return CommonResponse.success(MEETING_DELETE_SUCCESS); - } -} diff --git a/src/main/java/com/weeth/domain/schedule/presentation/MeetingController.java b/src/main/java/com/weeth/domain/schedule/presentation/MeetingController.java deleted file mode 100644 index 3af43758..00000000 --- a/src/main/java/com/weeth/domain/schedule/presentation/MeetingController.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.weeth.domain.schedule.presentation; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.tags.Tag; -import com.weeth.domain.schedule.application.dto.MeetingDTO; -import com.weeth.domain.schedule.application.exception.MeetingErrorCode; -import com.weeth.domain.schedule.application.usecase.MeetingUseCase; -import com.weeth.global.auth.annotation.CurrentUser; -import com.weeth.global.common.exception.ApiErrorCodeExample; -import com.weeth.global.common.response.CommonResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import static com.weeth.domain.schedule.presentation.ScheduleResponseCode.MEETING_FIND_SUCCESS; - -@Tag(name = "MEETING", description = "정기모임 API") -@RestController -@RequiredArgsConstructor -@RequestMapping("/api/v1/meetings") -@ApiErrorCodeExample(MeetingErrorCode.class) -public class MeetingController { - - private final MeetingUseCase meetingUseCase; - - @GetMapping("/{meetingId}") - @Operation(summary="정기모임 상세 조회") - public CommonResponse find(@Parameter(hidden = true) @CurrentUser Long userId, - @PathVariable Long meetingId) { - return CommonResponse.success(MEETING_FIND_SUCCESS, meetingUseCase.find(userId, meetingId)); - } -} diff --git a/src/main/java/com/weeth/domain/schedule/presentation/ScheduleController.java b/src/main/java/com/weeth/domain/schedule/presentation/ScheduleController.java deleted file mode 100644 index 7e3203c5..00000000 --- a/src/main/java/com/weeth/domain/schedule/presentation/ScheduleController.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.weeth.domain.schedule.presentation; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import com.weeth.domain.schedule.application.exception.EventErrorCode; -import com.weeth.domain.schedule.application.exception.MeetingErrorCode; -import com.weeth.domain.schedule.application.usecase.ScheduleUseCase; -import com.weeth.global.common.exception.ApiErrorCodeExample; -import com.weeth.global.common.response.CommonResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.format.annotation.DateTimeFormat; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.Map; - -import static com.weeth.domain.schedule.application.dto.ScheduleDTO.Response; -import static com.weeth.domain.schedule.presentation.ScheduleResponseCode.SCHEDULE_MONTHLY_FIND_SUCCESS; -import static com.weeth.domain.schedule.presentation.ScheduleResponseCode.SCHEDULE_YEARLY_FIND_SUCCESS; - -@Tag(name = "SCHEDULE", description = "캘린더 조회 API") -@RestController -@RequiredArgsConstructor -@RequestMapping("/api/v1/schedules") -@ApiErrorCodeExample({EventErrorCode.class, MeetingErrorCode.class}) -public class ScheduleController { - - private final ScheduleUseCase scheduleUseCase; - - @GetMapping("/monthly") - @Operation(summary="월별 일정 조회") - public CommonResponse> findByMonthly(@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime start, - @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime end) { - return CommonResponse.success(SCHEDULE_MONTHLY_FIND_SUCCESS,scheduleUseCase.findByMonthly(start, end)); - } - - @GetMapping("/yearly") - @Operation(summary="연도별 일정 조회") - public CommonResponse>> findByYearly(@RequestParam Integer year, - @RequestParam Integer semester) { - return CommonResponse.success(SCHEDULE_YEARLY_FIND_SUCCESS,scheduleUseCase.findByYearly(year, semester)); - } -} diff --git a/src/main/java/com/weeth/domain/schedule/presentation/ScheduleResponseCode.java b/src/main/java/com/weeth/domain/schedule/presentation/ScheduleResponseCode.java deleted file mode 100644 index 73655542..00000000 --- a/src/main/java/com/weeth/domain/schedule/presentation/ScheduleResponseCode.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.weeth.domain.schedule.presentation; - -import com.weeth.global.common.response.ResponseCodeInterface; -import lombok.Getter; -import org.springframework.http.HttpStatus; - -@Getter -public enum ScheduleResponseCode implements ResponseCodeInterface { - // EventAdminController 관련 - EVENT_SAVE_SUCCESS(1700, HttpStatus.OK, "일정/정기모임이 성공적으로 생성되었습니다."), - EVENT_UPDATE_SUCCESS(1701, HttpStatus.OK, "일정/정기모임이 성공적으로 수정되었습니다."), - EVENT_DELETE_SUCCESS(1702, HttpStatus.OK, "일정이 성공적으로 삭제되었습니다."), - // EventController 관련 - EVENT_FIND_SUCCESS(1703, HttpStatus.OK, "일정이 성공적으로 조회되었습니다."), - // MeetingAdminController 관련 - MEETING_SAVE_SUCCESS(1704, HttpStatus.OK, "정기모임 일정이 성공적으로 생성되었습니다."), - MEETING_UPDATE_SUCCESS(1705, HttpStatus.OK, "정기모임 일정이 성공적으로 수정되었습니다."), - MEETING_DELETE_SUCCESS(1706, HttpStatus.OK, "정기모임 일정이 성공적으로 삭제되었습니다."), - MEETING_CARDINAL_FIND_SUCCESS(1707, HttpStatus.OK, "특정 기수 정기모임이 성공적으로 조회되었습니다."), - MEETING_ALL_FIND_SUCCESS(1708, HttpStatus.OK, "정기모임 전체일정이 성공적으로 조회되었습니다."), - // MeetingController 관련 - MEETING_FIND_SUCCESS(1709, HttpStatus.OK, "정기모임이 성공적으로 조회되었습니다."), - // ScheduleController 관련 - SCHEDULE_MONTHLY_FIND_SUCCESS(1710, HttpStatus.OK, "월별 일정이 성공적으로 조회되었습니다."), - SCHEDULE_YEARLY_FIND_SUCCESS(1711, HttpStatus.OK, "연도별 일정이 성공적으로 조회되었습니다."); - - private final int code; - private final HttpStatus status; - private final String message; - - ScheduleResponseCode(int code, HttpStatus status, String message) { - this.code = code; - this.status = status; - this.message = message; - } -} diff --git a/src/main/kotlin/com/weeth/domain/attendance/application/dto/response/AttendanceInfoResponse.kt b/src/main/kotlin/com/weeth/domain/attendance/application/dto/response/AttendanceInfoResponse.kt index 1b72be7a..e6f36bbb 100644 --- a/src/main/kotlin/com/weeth/domain/attendance/application/dto/response/AttendanceInfoResponse.kt +++ b/src/main/kotlin/com/weeth/domain/attendance/application/dto/response/AttendanceInfoResponse.kt @@ -1,13 +1,13 @@ package com.weeth.domain.attendance.application.dto.response -import com.weeth.domain.attendance.domain.enums.Status +import com.weeth.domain.attendance.domain.entity.enums.AttendanceStatus import io.swagger.v3.oas.annotations.media.Schema data class AttendanceInfoResponse( @field:Schema(description = "출석 ID", example = "1") val id: Long, @field:Schema(description = "출석 상태", example = "ATTEND") - val status: Status?, + val status: AttendanceStatus?, @field:Schema(description = "사용자 이름", example = "이지훈") val name: String?, @field:Schema(description = "소속 학과", example = "컴퓨터공학과") diff --git a/src/main/kotlin/com/weeth/domain/attendance/application/dto/response/AttendanceResponse.kt b/src/main/kotlin/com/weeth/domain/attendance/application/dto/response/AttendanceResponse.kt index e99559af..7ef78c9a 100644 --- a/src/main/kotlin/com/weeth/domain/attendance/application/dto/response/AttendanceResponse.kt +++ b/src/main/kotlin/com/weeth/domain/attendance/application/dto/response/AttendanceResponse.kt @@ -1,6 +1,6 @@ package com.weeth.domain.attendance.application.dto.response -import com.weeth.domain.attendance.domain.enums.Status +import com.weeth.domain.attendance.domain.entity.enums.AttendanceStatus import io.swagger.v3.oas.annotations.media.Schema import java.time.LocalDateTime @@ -8,7 +8,7 @@ data class AttendanceResponse( @field:Schema(description = "출석 ID", example = "1") val id: Long, @field:Schema(description = "출석 상태", example = "ATTEND") - val status: Status?, + val status: AttendanceStatus?, @field:Schema(description = "정기모임 제목", example = "1주차 정기모임") val title: String?, @field:Schema(description = "정기모임 시작 시간") diff --git a/src/main/kotlin/com/weeth/domain/attendance/application/dto/response/AttendanceSummaryResponse.kt b/src/main/kotlin/com/weeth/domain/attendance/application/dto/response/AttendanceSummaryResponse.kt index f53b633f..7a445550 100644 --- a/src/main/kotlin/com/weeth/domain/attendance/application/dto/response/AttendanceSummaryResponse.kt +++ b/src/main/kotlin/com/weeth/domain/attendance/application/dto/response/AttendanceSummaryResponse.kt @@ -1,6 +1,6 @@ package com.weeth.domain.attendance.application.dto.response -import com.weeth.domain.attendance.domain.enums.Status +import com.weeth.domain.attendance.domain.entity.enums.AttendanceStatus import io.swagger.v3.oas.annotations.media.Schema import java.time.LocalDateTime @@ -10,7 +10,7 @@ data class AttendanceSummaryResponse( @field:Schema(description = "정기모임 제목", example = "1주차 정기모임") val title: String?, @field:Schema(description = "출석 상태", example = "ATTEND") - val status: Status?, + val status: AttendanceStatus?, @field:Schema(description = "어드민인 경우 출석 코드 노출", example = "1234") val code: Int?, @field:Schema(description = "정기모임 시작 시간") diff --git a/src/main/kotlin/com/weeth/domain/attendance/application/mapper/AttendanceMapper.kt b/src/main/kotlin/com/weeth/domain/attendance/application/mapper/AttendanceMapper.kt index 8ccf206b..fffc3d3a 100644 --- a/src/main/kotlin/com/weeth/domain/attendance/application/mapper/AttendanceMapper.kt +++ b/src/main/kotlin/com/weeth/domain/attendance/application/mapper/AttendanceMapper.kt @@ -17,12 +17,12 @@ class AttendanceMapper { ): AttendanceSummaryResponse = AttendanceSummaryResponse( attendanceRate = user.attendanceRate, - title = attendance?.meeting?.title, + title = attendance?.session?.title, status = attendance?.status, - code = if (isAdmin) attendance?.meeting?.code else null, - start = attendance?.meeting?.start, - end = attendance?.meeting?.end, - location = attendance?.meeting?.location, + code = if (isAdmin) attendance?.session?.code else null, + start = attendance?.session?.start, + end = attendance?.session?.end, + location = attendance?.session?.location, ) fun toDetailResponse( @@ -40,10 +40,10 @@ class AttendanceMapper { AttendanceResponse( id = attendance.id, status = attendance.status, - title = attendance.meeting.title, - start = attendance.meeting.start, - end = attendance.meeting.end, - location = attendance.meeting.location, + title = attendance.session.title, + start = attendance.session.start, + end = attendance.session.end, + location = attendance.session.location, ) fun toInfoResponse(attendance: Attendance): AttendanceInfoResponse = diff --git a/src/main/kotlin/com/weeth/domain/attendance/application/usecase/command/CheckInAttendanceUseCase.kt b/src/main/kotlin/com/weeth/domain/attendance/application/usecase/command/CheckInAttendanceUseCase.kt deleted file mode 100644 index 7d0efcd6..00000000 --- a/src/main/kotlin/com/weeth/domain/attendance/application/usecase/command/CheckInAttendanceUseCase.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.weeth.domain.attendance.application.usecase.command - -import com.weeth.domain.attendance.application.exception.AttendanceCodeMismatchException -import com.weeth.domain.attendance.application.exception.AttendanceNotFoundException -import com.weeth.domain.attendance.domain.enums.Status -import com.weeth.domain.attendance.domain.repository.AttendanceRepository -import com.weeth.domain.user.domain.repository.UserReader -import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional -import java.time.LocalDateTime - -@Service -class CheckInAttendanceUseCase( - private val userReader: UserReader, - private val attendanceRepository: AttendanceRepository, -) { - @Transactional - fun checkIn( - userId: Long, - code: Int, - ) { - val user = userReader.getById(userId) - val now = LocalDateTime.now() - - val todayAttendance = - attendanceRepository.findCurrentByUserId(userId, now, now.plusMinutes(10)) - ?: throw AttendanceNotFoundException() - - if (todayAttendance.isWrong(code)) { - throw AttendanceCodeMismatchException() - } - - if (todayAttendance.status != Status.ATTEND) { - todayAttendance.attend() - user.attend() - } - } -} diff --git a/src/main/kotlin/com/weeth/domain/attendance/application/usecase/command/CloseAttendanceUseCase.kt b/src/main/kotlin/com/weeth/domain/attendance/application/usecase/command/CloseAttendanceUseCase.kt deleted file mode 100644 index 7d860987..00000000 --- a/src/main/kotlin/com/weeth/domain/attendance/application/usecase/command/CloseAttendanceUseCase.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.weeth.domain.attendance.application.usecase.command - -import com.weeth.domain.attendance.domain.entity.Attendance -import com.weeth.domain.attendance.domain.repository.AttendanceRepository -import com.weeth.domain.schedule.application.exception.MeetingNotFoundException -import com.weeth.domain.schedule.domain.service.MeetingGetService -import com.weeth.domain.user.domain.entity.enums.Status -import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional -import java.time.LocalDate - -@Service -class CloseAttendanceUseCase( - private val meetingGetService: MeetingGetService, - private val attendanceRepository: AttendanceRepository, -) { - @Transactional - fun close( - now: LocalDate, - cardinal: Int, - ) { - val meetings = meetingGetService.find(cardinal) - - val targetMeeting = - meetings.firstOrNull { meeting -> - meeting.start.toLocalDate().isEqual(now) && - meeting.end.toLocalDate().isEqual(now) - } ?: throw MeetingNotFoundException() - - val attendanceList = attendanceRepository.findAllByMeetingAndUserStatus(targetMeeting, Status.ACTIVE) - closePendingAttendances(attendanceList) - } - - @Transactional - fun autoClose() { - val meetings = meetingGetService.findAllOpenMeetingsBeforeNow() - - meetings.forEach { meeting -> - meeting.close() - val attendanceList = attendanceRepository.findAllByMeetingAndUserStatus(meeting, Status.ACTIVE) - closePendingAttendances(attendanceList) - } - } - - private fun closePendingAttendances(attendances: List) { - attendances - .filter { it.isPending } - .forEach { attendance -> - attendance.close() - attendance.user.absent() - } - } -} diff --git a/src/main/kotlin/com/weeth/domain/attendance/application/usecase/command/ManageAttendanceUseCase.kt b/src/main/kotlin/com/weeth/domain/attendance/application/usecase/command/ManageAttendanceUseCase.kt new file mode 100644 index 00000000..fcdfee1d --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/attendance/application/usecase/command/ManageAttendanceUseCase.kt @@ -0,0 +1,102 @@ +package com.weeth.domain.attendance.application.usecase.command + +import com.weeth.domain.attendance.application.dto.request.UpdateAttendanceStatusRequest +import com.weeth.domain.attendance.application.exception.AttendanceCodeMismatchException +import com.weeth.domain.attendance.application.exception.AttendanceNotFoundException +import com.weeth.domain.attendance.domain.entity.Attendance +import com.weeth.domain.attendance.domain.entity.enums.AttendanceStatus +import com.weeth.domain.attendance.domain.repository.AttendanceRepository +import com.weeth.domain.session.application.exception.SessionNotFoundException +import com.weeth.domain.session.domain.entity.enums.SessionStatus +import com.weeth.domain.session.domain.repository.SessionReader +import com.weeth.domain.user.domain.entity.enums.Status +import com.weeth.domain.user.domain.repository.UserReader +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate +import java.time.LocalDateTime + +@Service +class ManageAttendanceUseCase( + private val userReader: UserReader, + private val sessionReader: SessionReader, + private val attendanceRepository: AttendanceRepository, +) { + @Transactional + fun checkIn( + userId: Long, + code: Int, + ) { + val user = userReader.getById(userId) + val now = LocalDateTime.now() + val todayAttendance = + attendanceRepository.findCurrentByUserId(userId, now, now.plusMinutes(10)) + ?: throw AttendanceNotFoundException() + if (todayAttendance.isWrong(code)) { + throw AttendanceCodeMismatchException() + } + val lockedAttendance = + attendanceRepository.findBySessionAndUserWithLock(todayAttendance.session, user) + ?: throw AttendanceNotFoundException() + if (lockedAttendance.status != AttendanceStatus.ATTEND) { + lockedAttendance.attend() + user.attend() + } + } + + @Transactional + fun close( + now: LocalDate, + cardinal: Int, + ) { + val targetSession = + sessionReader + .findAllByCardinalOrderByStartAsc(cardinal) + .firstOrNull { session -> session.start.toLocalDate().isEqual(now) && session.end.toLocalDate().isEqual(now) } + ?: throw SessionNotFoundException() + val attendances = attendanceRepository.findAllBySessionAndUserStatus(targetSession, Status.ACTIVE) + closePendingAttendances(attendances) + } + + @Transactional + fun autoClose() { + val sessions = sessionReader.findAllByStatusAndEndBeforeOrderByEndAsc(SessionStatus.OPEN, LocalDateTime.now()) + sessions.forEach { session -> + session.close() + val attendances = attendanceRepository.findAllBySessionAndUserStatus(session, Status.ACTIVE) + closePendingAttendances(attendances) + } + } + + @Transactional + fun updateStatus(attendanceUpdates: List) { + attendanceUpdates.forEach { update -> + val attendance = + attendanceRepository.findByIdWithUser(update.attendanceId) + ?: throw AttendanceNotFoundException() + val user = attendance.user + val newStatus = AttendanceStatus.valueOf(update.status) + + if (attendance.status == newStatus) return@forEach + + val prevStatus = attendance.status + attendance.adminOverride(newStatus) + if (newStatus == AttendanceStatus.ABSENT) { + if (prevStatus == AttendanceStatus.ATTEND) user.removeAttend() + user.absent() + } else { + if (prevStatus == AttendanceStatus.ABSENT) user.removeAbsent() + user.attend() + } + } + } + + private fun closePendingAttendances(attendances: List) { + attendances + .filter { it.isPending() } + .forEach { attendance -> + attendance.close() + attendance.user.absent() + } + } +} diff --git a/src/main/kotlin/com/weeth/domain/attendance/application/usecase/command/UpdateAttendanceStatusUseCase.kt b/src/main/kotlin/com/weeth/domain/attendance/application/usecase/command/UpdateAttendanceStatusUseCase.kt deleted file mode 100644 index 14d984bf..00000000 --- a/src/main/kotlin/com/weeth/domain/attendance/application/usecase/command/UpdateAttendanceStatusUseCase.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.weeth.domain.attendance.application.usecase.command - -import com.weeth.domain.attendance.application.dto.request.UpdateAttendanceStatusRequest -import com.weeth.domain.attendance.application.exception.AttendanceNotFoundException -import com.weeth.domain.attendance.domain.enums.Status -import com.weeth.domain.attendance.domain.repository.AttendanceRepository -import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional - -@Service -class UpdateAttendanceStatusUseCase( - private val attendanceRepository: AttendanceRepository, -) { - @Transactional - fun updateStatus(attendanceUpdates: List) { - attendanceUpdates.forEach { update -> - val attendance = - attendanceRepository.findByIdWithUser(update.attendanceId) - ?: throw AttendanceNotFoundException() - val user = attendance.user - val newStatus = Status.valueOf(update.status) - - if (newStatus == Status.ABSENT) { - attendance.close() - user.removeAttend() - user.absent() - } else { - attendance.attend() - user.removeAbsent() - user.attend() - } - } - } -} diff --git a/src/main/kotlin/com/weeth/domain/attendance/application/usecase/query/GetAttendanceQueryService.kt b/src/main/kotlin/com/weeth/domain/attendance/application/usecase/query/GetAttendanceQueryService.kt index 59a39143..9da8d254 100644 --- a/src/main/kotlin/com/weeth/domain/attendance/application/usecase/query/GetAttendanceQueryService.kt +++ b/src/main/kotlin/com/weeth/domain/attendance/application/usecase/query/GetAttendanceQueryService.kt @@ -5,7 +5,7 @@ import com.weeth.domain.attendance.application.dto.response.AttendanceInfoRespon import com.weeth.domain.attendance.application.dto.response.AttendanceSummaryResponse import com.weeth.domain.attendance.application.mapper.AttendanceMapper import com.weeth.domain.attendance.domain.repository.AttendanceRepository -import com.weeth.domain.schedule.domain.service.MeetingGetService +import com.weeth.domain.session.domain.repository.SessionReader import com.weeth.domain.user.domain.entity.enums.Role import com.weeth.domain.user.domain.entity.enums.Status import com.weeth.domain.user.domain.repository.UserReader @@ -19,9 +19,9 @@ import java.time.LocalDate class GetAttendanceQueryService( private val userReader: UserReader, private val userCardinalPolicy: UserCardinalPolicy, - private val meetingGetService: MeetingGetService, + private val sessionReader: SessionReader, private val attendanceRepository: AttendanceRepository, - private val mapper: AttendanceMapper, + private val attendanceMapper: AttendanceMapper, ) { fun findAttendance(userId: Long): AttendanceSummaryResponse { val user = userReader.getById(userId) @@ -34,7 +34,7 @@ class GetAttendanceQueryService( today.plusDays(1).atStartOfDay(), ) - return mapper.toSummaryResponse(user, todayAttendance, isAdmin = user.role == Role.ADMIN) + return attendanceMapper.toSummaryResponse(user, todayAttendance, isAdmin = user.role == Role.ADMIN) } fun findAllDetailsByCurrentCardinal(userId: Long): AttendanceDetailResponse { @@ -44,14 +44,14 @@ class GetAttendanceQueryService( val responses = attendanceRepository .findAllByUserIdAndCardinal(userId, currentCardinal.cardinalNumber) - .map(mapper::toResponse) + .map(attendanceMapper::toResponse) - return mapper.toDetailResponse(user, responses) + return attendanceMapper.toDetailResponse(user, responses) } - fun findAllAttendanceByMeeting(meetingId: Long): List { - val meeting = meetingGetService.find(meetingId) - val attendances = attendanceRepository.findAllByMeetingAndUserStatus(meeting, Status.ACTIVE) - return attendances.map(mapper::toInfoResponse) + fun findAllAttendanceBySession(sessionId: Long): List { + val session = sessionReader.getById(sessionId) + val attendances = attendanceRepository.findAllBySessionAndUserStatus(session, Status.ACTIVE) + return attendances.map(attendanceMapper::toInfoResponse) } } diff --git a/src/main/kotlin/com/weeth/domain/attendance/domain/entity/Attendance.kt b/src/main/kotlin/com/weeth/domain/attendance/domain/entity/Attendance.kt index f40184ba..be54e167 100644 --- a/src/main/kotlin/com/weeth/domain/attendance/domain/entity/Attendance.kt +++ b/src/main/kotlin/com/weeth/domain/attendance/domain/entity/Attendance.kt @@ -1,7 +1,7 @@ package com.weeth.domain.attendance.domain.entity -import com.weeth.domain.attendance.domain.enums.Status -import com.weeth.domain.schedule.domain.entity.Meeting +import com.weeth.domain.attendance.domain.entity.enums.AttendanceStatus +import com.weeth.domain.session.domain.entity.Session import com.weeth.domain.user.domain.entity.User import com.weeth.global.common.entity.BaseEntity import jakarta.persistence.Column @@ -14,40 +14,51 @@ import jakarta.persistence.GenerationType import jakarta.persistence.Id import jakarta.persistence.JoinColumn import jakarta.persistence.ManyToOne -import jakarta.persistence.PrePersist +import org.hibernate.annotations.OnDelete +import org.hibernate.annotations.OnDeleteAction @Entity -class Attendance - @JvmOverloads - constructor( - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "meeting_id") - val meeting: Meeting, - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") - val user: User, - @Enumerated(EnumType.STRING) - var status: Status? = null, - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "attendance_id") - val id: Long = 0, - ) : BaseEntity() { - @PrePersist - fun init() { - status = Status.PENDING - } - - fun attend() { - status = Status.ATTEND - } - - fun close() { - status = Status.ABSENT - } - - val isPending: Boolean - get() = status == Status.PENDING - - fun isWrong(code: Int): Boolean = meeting.getCode() != code +class Attendance( + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "meeting_id") + val session: Session, + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + @OnDelete(action = OnDeleteAction.CASCADE) + val user: User, + @Enumerated(EnumType.STRING) + var status: AttendanceStatus = AttendanceStatus.PENDING, +) : BaseEntity() { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "attendance_id") + val id: Long = 0 + + fun attend() { + check(status == AttendanceStatus.PENDING) { "이미 처리된 출석입니다" } + status = AttendanceStatus.ATTEND + } + + fun absent() { + check(status == AttendanceStatus.PENDING) { "이미 처리된 출석입니다" } + status = AttendanceStatus.ABSENT + } + + // 기존 close() 는 absent() 로 대체 (AttendanceUpdateService 호환 유지) + fun close() = absent() + + fun adminOverride(newStatus: AttendanceStatus) { + status = newStatus + } + + fun isPending(): Boolean = status == AttendanceStatus.PENDING + + fun isWrong(code: Int): Boolean = !session.isCodeMatch(code) + + companion object { + fun create( + session: Session, + user: User, + ): Attendance = Attendance(session = session, user = user) } +} diff --git a/src/main/kotlin/com/weeth/domain/attendance/domain/entity/enums/AttendanceStatus.kt b/src/main/kotlin/com/weeth/domain/attendance/domain/entity/enums/AttendanceStatus.kt new file mode 100644 index 00000000..f54fc9f0 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/attendance/domain/entity/enums/AttendanceStatus.kt @@ -0,0 +1,7 @@ +package com.weeth.domain.attendance.domain.entity.enums + +enum class AttendanceStatus { + ATTEND, + PENDING, + ABSENT, +} diff --git a/src/main/kotlin/com/weeth/domain/attendance/domain/enums/Status.kt b/src/main/kotlin/com/weeth/domain/attendance/domain/enums/Status.kt deleted file mode 100644 index 6184bf45..00000000 --- a/src/main/kotlin/com/weeth/domain/attendance/domain/enums/Status.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.weeth.domain.attendance.domain.enums - -enum class Status { - ATTEND, - PENDING, - ABSENT, -} diff --git a/src/main/kotlin/com/weeth/domain/attendance/domain/repository/AttendanceRepository.kt b/src/main/kotlin/com/weeth/domain/attendance/domain/repository/AttendanceRepository.kt index cd0d82a0..96c162a8 100644 --- a/src/main/kotlin/com/weeth/domain/attendance/domain/repository/AttendanceRepository.kt +++ b/src/main/kotlin/com/weeth/domain/attendance/domain/repository/AttendanceRepository.kt @@ -1,32 +1,53 @@ package com.weeth.domain.attendance.domain.repository import com.weeth.domain.attendance.domain.entity.Attendance -import com.weeth.domain.schedule.domain.entity.Meeting +import com.weeth.domain.session.domain.entity.Session +import com.weeth.domain.user.domain.entity.User import com.weeth.domain.user.domain.entity.enums.Status +import jakarta.persistence.LockModeType +import jakarta.persistence.QueryHint import org.springframework.data.jpa.repository.EntityGraph import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Lock import org.springframework.data.jpa.repository.Modifying import org.springframework.data.jpa.repository.Query +import org.springframework.data.jpa.repository.QueryHints import org.springframework.data.repository.query.Param import java.time.LocalDateTime interface AttendanceRepository : JpaRepository { + @Lock(LockModeType.PESSIMISTIC_WRITE) + @QueryHints(QueryHint(name = "jakarta.persistence.lock.timeout", value = "2000")) + @Query("SELECT a FROM Attendance a JOIN FETCH a.user WHERE a.session = :session AND a.user = :user") + fun findBySessionAndUserWithLock( + @Param("session") session: Session, + @Param("user") user: User, + ): Attendance? + @EntityGraph(attributePaths = ["user"]) - fun findAllByMeetingAndUserStatus( - meeting: Meeting, + fun findAllBySessionAndUserStatus( + session: Session, status: Status, ): List + @Lock(LockModeType.PESSIMISTIC_WRITE) + @QueryHints(QueryHint(name = "jakarta.persistence.lock.timeout", value = "2000")) + @Query("SELECT a FROM Attendance a JOIN FETCH a.user WHERE a.session = :session AND a.user.status = :status") + fun findAllBySessionAndUserStatusWithLock( + @Param("session") session: Session, + @Param("status") status: Status, + ): List + @Query("SELECT a FROM Attendance a JOIN FETCH a.user WHERE a.id = :id") fun findByIdWithUser(id: Long): Attendance? @Query( """ SELECT a FROM Attendance a - JOIN FETCH a.meeting m + JOIN FETCH a.session s WHERE a.user.id = :userId - AND m.start <= :checkInEnd - AND m.end > :now + AND s.start <= :checkInEnd + AND s.end > :now """, ) fun findCurrentByUserId( @@ -38,10 +59,10 @@ interface AttendanceRepository : JpaRepository { @Query( """ SELECT a FROM Attendance a - JOIN FETCH a.meeting m + JOIN FETCH a.session s WHERE a.user.id = :userId - AND m.start >= :dayStart - AND m.end < :dayEnd + AND s.start >= :dayStart + AND s.end < :dayEnd """, ) fun findTodayByUserId( @@ -53,10 +74,10 @@ interface AttendanceRepository : JpaRepository { @Query( """ SELECT a FROM Attendance a - JOIN FETCH a.meeting m + JOIN FETCH a.session s WHERE a.user.id = :userId - AND m.cardinal = :cardinal - ORDER BY m.start + AND s.cardinal = :cardinal + ORDER BY s.start """, ) fun findAllByUserIdAndCardinal( @@ -64,7 +85,13 @@ interface AttendanceRepository : JpaRepository { @Param("cardinal") cardinal: Int, ): List - @Modifying - @Query("DELETE FROM Attendance a WHERE a.meeting = :meeting") - fun deleteAllByMeeting(meeting: Meeting) + // TODO: QR 코드 출석 기능 구현 시 사용 예정 (여러 세션의 출석자 배치 조회) + @Query("SELECT a FROM Attendance a JOIN FETCH a.user WHERE a.session IN :sessions") + fun findAllBySessionIn( + @Param("sessions") sessions: List, + ): List + + @Modifying(flushAutomatically = true, clearAutomatically = true) + @Query("DELETE FROM Attendance a WHERE a.session = :session") + fun deleteAllBySession(session: Session) } diff --git a/src/main/kotlin/com/weeth/domain/attendance/domain/service/AttendanceDeleteService.kt b/src/main/kotlin/com/weeth/domain/attendance/domain/service/AttendanceDeleteService.kt deleted file mode 100644 index d4a05bb7..00000000 --- a/src/main/kotlin/com/weeth/domain/attendance/domain/service/AttendanceDeleteService.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.weeth.domain.attendance.domain.service - -import com.weeth.domain.attendance.domain.repository.AttendanceRepository -import com.weeth.domain.schedule.domain.entity.Meeting -import org.springframework.stereotype.Service - -@Service -class AttendanceDeleteService( - private val attendanceRepository: AttendanceRepository, -) { - fun deleteAll(meeting: Meeting) { - attendanceRepository.deleteAllByMeeting(meeting) - } -} diff --git a/src/main/kotlin/com/weeth/domain/attendance/domain/service/AttendanceGetService.kt b/src/main/kotlin/com/weeth/domain/attendance/domain/service/AttendanceGetService.kt deleted file mode 100644 index 647910b7..00000000 --- a/src/main/kotlin/com/weeth/domain/attendance/domain/service/AttendanceGetService.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.weeth.domain.attendance.domain.service - -import com.weeth.domain.attendance.domain.entity.Attendance -import com.weeth.domain.attendance.domain.repository.AttendanceRepository -import com.weeth.domain.schedule.domain.entity.Meeting -import com.weeth.domain.user.domain.entity.enums.Status -import org.springframework.stereotype.Service - -@Service -class AttendanceGetService( - private val attendanceRepository: AttendanceRepository, -) { - fun findAllByMeeting(meeting: Meeting): List = attendanceRepository.findAllByMeetingAndUserStatus(meeting, Status.ACTIVE) -} diff --git a/src/main/kotlin/com/weeth/domain/attendance/domain/service/AttendanceSaveService.kt b/src/main/kotlin/com/weeth/domain/attendance/domain/service/AttendanceSaveService.kt deleted file mode 100644 index f44045c4..00000000 --- a/src/main/kotlin/com/weeth/domain/attendance/domain/service/AttendanceSaveService.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.weeth.domain.attendance.domain.service - -import com.weeth.domain.attendance.domain.entity.Attendance -import com.weeth.domain.attendance.domain.repository.AttendanceRepository -import com.weeth.domain.schedule.domain.entity.Meeting -import com.weeth.domain.user.domain.entity.User -import org.springframework.stereotype.Service - -@Service -class AttendanceSaveService( - private val attendanceRepository: AttendanceRepository, -) { - fun init( - user: User, - meetings: List?, - ) { - meetings?.forEach { meeting -> - attendanceRepository.save(Attendance(meeting, user)) - } - } - - fun saveAll( - userList: List, - meeting: Meeting, - ) { - val attendances = userList.map { user -> Attendance(meeting, user) } - attendanceRepository.saveAll(attendances) - } -} diff --git a/src/main/kotlin/com/weeth/domain/attendance/domain/service/AttendanceUpdateService.kt b/src/main/kotlin/com/weeth/domain/attendance/domain/service/AttendanceUpdateService.kt deleted file mode 100644 index cc8f98ac..00000000 --- a/src/main/kotlin/com/weeth/domain/attendance/domain/service/AttendanceUpdateService.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.weeth.domain.attendance.domain.service - -import com.weeth.domain.attendance.domain.entity.Attendance -import com.weeth.domain.attendance.domain.enums.Status -import org.springframework.stereotype.Service - -@Service -class AttendanceUpdateService { - fun attend(attendance: Attendance) { - attendance.attend() - attendance.user.attend() - } - - fun close(attendances: List) { - attendances - .filter { it.isPending } - .forEach { attendance -> - attendance.close() - attendance.user.absent() - } - } - - fun updateUserAttendanceByStatus(attendances: List) { - attendances.forEach { attendance -> - val user = attendance.user - if (attendance.status == Status.ATTEND) { - user.removeAttend() - } else { - user.removeAbsent() - } - } - } -} diff --git a/src/main/kotlin/com/weeth/domain/attendance/infrastructure/AttendanceScheduler.kt b/src/main/kotlin/com/weeth/domain/attendance/infrastructure/AttendanceScheduler.kt index a41113e6..9890be6a 100644 --- a/src/main/kotlin/com/weeth/domain/attendance/infrastructure/AttendanceScheduler.kt +++ b/src/main/kotlin/com/weeth/domain/attendance/infrastructure/AttendanceScheduler.kt @@ -1,15 +1,15 @@ package com.weeth.domain.attendance.infrastructure -import com.weeth.domain.attendance.application.usecase.command.CloseAttendanceUseCase +import com.weeth.domain.attendance.application.usecase.command.ManageAttendanceUseCase import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Component @Component class AttendanceScheduler( - private val closeAttendanceUseCase: CloseAttendanceUseCase, + private val manageAttendanceUseCase: ManageAttendanceUseCase, ) { @Scheduled(cron = "0 0 22 * * THU", zone = "Asia/Seoul") fun autoCloseAttendance() { - closeAttendanceUseCase.autoClose() + manageAttendanceUseCase.autoClose() } } diff --git a/src/main/kotlin/com/weeth/domain/attendance/presentation/AttendanceAdminController.kt b/src/main/kotlin/com/weeth/domain/attendance/presentation/AttendanceAdminController.kt index 4e8ca2a2..86b82904 100644 --- a/src/main/kotlin/com/weeth/domain/attendance/presentation/AttendanceAdminController.kt +++ b/src/main/kotlin/com/weeth/domain/attendance/presentation/AttendanceAdminController.kt @@ -3,11 +3,8 @@ package com.weeth.domain.attendance.presentation import com.weeth.domain.attendance.application.dto.request.UpdateAttendanceStatusRequest import com.weeth.domain.attendance.application.dto.response.AttendanceInfoResponse import com.weeth.domain.attendance.application.exception.AttendanceErrorCode -import com.weeth.domain.attendance.application.usecase.command.CloseAttendanceUseCase -import com.weeth.domain.attendance.application.usecase.command.UpdateAttendanceStatusUseCase +import com.weeth.domain.attendance.application.usecase.command.ManageAttendanceUseCase import com.weeth.domain.attendance.application.usecase.query.GetAttendanceQueryService -import com.weeth.domain.schedule.application.dto.MeetingDTO -import com.weeth.domain.schedule.application.usecase.MeetingUseCase import com.weeth.global.common.exception.ApiErrorCodeExample import com.weeth.global.common.response.CommonResponse import io.swagger.v3.oas.annotations.Operation @@ -24,41 +21,30 @@ import java.time.LocalDate @Tag(name = "ATTENDANCE ADMIN", description = "[ADMIN] 출석 어드민 API") @RestController -@RequestMapping("/api/v1/admin/attendances") +@RequestMapping("/api/v4/admin/attendances") @ApiErrorCodeExample(AttendanceErrorCode::class) class AttendanceAdminController( - private val closeAttendanceUseCase: CloseAttendanceUseCase, - private val updateAttendanceStatusUseCase: UpdateAttendanceStatusUseCase, + private val manageAttendanceUseCase: ManageAttendanceUseCase, private val getAttendanceQueryService: GetAttendanceQueryService, - private val meetingUseCase: MeetingUseCase, ) { - @PatchMapping + @PatchMapping("/close") @Operation(summary = "출석 마감") fun close( @RequestParam now: LocalDate, @RequestParam cardinal: Int, ): CommonResponse { - closeAttendanceUseCase.close(now, cardinal) + manageAttendanceUseCase.close(now, cardinal) return CommonResponse.success(AttendanceResponseCode.ATTENDANCE_CLOSE_SUCCESS) } - @GetMapping("/meetings") - @Operation(summary = "정기모임 조회") - fun getMeetings( - @RequestParam(required = false) cardinal: Int?, - ): CommonResponse { - val response = meetingUseCase.find(cardinal) - return CommonResponse.success(AttendanceResponseCode.MEETING_FIND_SUCCESS, response) - } - - @GetMapping("/{meetingId}") + @GetMapping("/{sessionId}") @Operation(summary = "모든 인원 정기모임 출석 정보 조회") fun getAllAttendance( - @PathVariable meetingId: Long, + @PathVariable sessionId: Long, ): CommonResponse> = CommonResponse.success( AttendanceResponseCode.ATTENDANCE_FIND_DETAIL_SUCCESS, - getAttendanceQueryService.findAllAttendanceByMeeting(meetingId), + getAttendanceQueryService.findAllAttendanceBySession(sessionId), ) @PatchMapping("/status") @@ -66,7 +52,7 @@ class AttendanceAdminController( fun updateAttendanceStatus( @RequestBody @Valid attendanceUpdates: List, ): CommonResponse { - updateAttendanceStatusUseCase.updateStatus(attendanceUpdates) + manageAttendanceUseCase.updateStatus(attendanceUpdates) return CommonResponse.success(AttendanceResponseCode.ATTENDANCE_UPDATED_SUCCESS) } } diff --git a/src/main/kotlin/com/weeth/domain/attendance/presentation/AttendanceController.kt b/src/main/kotlin/com/weeth/domain/attendance/presentation/AttendanceController.kt index 8a1256b7..37aa0ec9 100644 --- a/src/main/kotlin/com/weeth/domain/attendance/presentation/AttendanceController.kt +++ b/src/main/kotlin/com/weeth/domain/attendance/presentation/AttendanceController.kt @@ -4,7 +4,7 @@ import com.weeth.domain.attendance.application.dto.request.CheckInRequest import com.weeth.domain.attendance.application.dto.response.AttendanceDetailResponse import com.weeth.domain.attendance.application.dto.response.AttendanceSummaryResponse import com.weeth.domain.attendance.application.exception.AttendanceErrorCode -import com.weeth.domain.attendance.application.usecase.command.CheckInAttendanceUseCase +import com.weeth.domain.attendance.application.usecase.command.ManageAttendanceUseCase import com.weeth.domain.attendance.application.usecase.query.GetAttendanceQueryService import com.weeth.global.auth.annotation.CurrentUser import com.weeth.global.common.exception.ApiErrorCodeExample @@ -13,26 +13,26 @@ import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Parameter import io.swagger.v3.oas.annotations.tags.Tag 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.RestController @Tag(name = "ATTENDANCE", description = "출석 API") @RestController -@RequestMapping("/api/v1/attendances") +@RequestMapping("/api/v4/attendances") @ApiErrorCodeExample(AttendanceErrorCode::class) class AttendanceController( - private val checkInAttendanceUseCase: CheckInAttendanceUseCase, + private val manageAttendanceUseCase: ManageAttendanceUseCase, private val getAttendanceQueryService: GetAttendanceQueryService, ) { - @PatchMapping + @PostMapping("/check-in") @Operation(summary = "출석체크") fun checkIn( @Parameter(hidden = true) @CurrentUser userId: Long, @RequestBody checkIn: CheckInRequest, ): CommonResponse { - checkInAttendanceUseCase.checkIn(userId, checkIn.code) + manageAttendanceUseCase.checkIn(userId, checkIn.code) return CommonResponse.success(AttendanceResponseCode.ATTENDANCE_CHECKIN_SUCCESS) } diff --git a/src/main/kotlin/com/weeth/domain/attendance/presentation/AttendanceResponseCode.kt b/src/main/kotlin/com/weeth/domain/attendance/presentation/AttendanceResponseCode.kt index 54ad6348..e67ef08a 100644 --- a/src/main/kotlin/com/weeth/domain/attendance/presentation/AttendanceResponseCode.kt +++ b/src/main/kotlin/com/weeth/domain/attendance/presentation/AttendanceResponseCode.kt @@ -12,10 +12,9 @@ enum class AttendanceResponseCode( ATTENDANCE_CLOSE_SUCCESS(1200, HttpStatus.OK, "출석이 성공적으로 마감되었습니다."), ATTENDANCE_UPDATED_SUCCESS(1201, HttpStatus.OK, "개별 출석 상태가 성공적으로 수정되었습니다."), ATTENDANCE_FIND_DETAIL_SUCCESS(1202, HttpStatus.OK, "모든 인원의 정기모임 출석 정보가 성공적으로 조회되었습니다."), - MEETING_FIND_SUCCESS(1203, HttpStatus.OK, "기수별 정기모임 리스트를 성공적으로 조회했습니다."), // AttendanceController 관련 - ATTENDANCE_CHECKIN_SUCCESS(1204, HttpStatus.OK, "출석이 성공적으로 처리되었습니다."), - ATTENDANCE_FIND_SUCCESS(1205, HttpStatus.OK, "사용자의 출석 정보가 성공적으로 조회되었습니다."), - ATTENDANCE_FIND_ALL_SUCCESS(1206, HttpStatus.OK, "사용자의 상세 출석 정보가 성공적으로 조회되었습니다."), + ATTENDANCE_CHECKIN_SUCCESS(1203, HttpStatus.OK, "출석이 성공적으로 처리되었습니다."), + ATTENDANCE_FIND_SUCCESS(1204, HttpStatus.OK, "사용자의 출석 정보가 성공적으로 조회되었습니다."), + ATTENDANCE_FIND_ALL_SUCCESS(1205, HttpStatus.OK, "사용자의 상세 출석 정보가 성공적으로 조회되었습니다."), } diff --git a/src/main/kotlin/com/weeth/domain/schedule/application/annotation/ScheduleTimeCheck.kt b/src/main/kotlin/com/weeth/domain/schedule/application/annotation/ScheduleTimeCheck.kt new file mode 100644 index 00000000..46202a1d --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/schedule/application/annotation/ScheduleTimeCheck.kt @@ -0,0 +1,15 @@ +package com.weeth.domain.schedule.application.annotation + +import com.weeth.domain.schedule.application.validator.ScheduleTimeCheckValidator +import jakarta.validation.Constraint +import jakarta.validation.Payload +import kotlin.reflect.KClass + +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +@Constraint(validatedBy = [ScheduleTimeCheckValidator::class]) +annotation class ScheduleTimeCheck( + val message: String = "마감 시간이 시작 시간보다 빠를 수 없습니다.", + val groups: Array> = [], + val payload: Array> = [], +) diff --git a/src/main/kotlin/com/weeth/domain/schedule/application/dto/request/ScheduleSaveRequest.kt b/src/main/kotlin/com/weeth/domain/schedule/application/dto/request/ScheduleSaveRequest.kt new file mode 100644 index 00000000..942a6608 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/schedule/application/dto/request/ScheduleSaveRequest.kt @@ -0,0 +1,32 @@ +package com.weeth.domain.schedule.application.dto.request + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Size +import org.springframework.format.annotation.DateTimeFormat +import java.time.LocalDateTime + +data class ScheduleSaveRequest( + @field:Schema(description = "일정 제목", example = "MT") + @field:NotBlank + val title: String, + @field:Schema(description = "일정 내용", example = "1박 2일 MT입니다.") + @field:NotBlank + @field:Size(max = 500) + val content: String, + @field:Schema(description = "장소", example = "가평") + @field:NotBlank + val location: String, + @field:Schema(description = "기수", example = "4") + @field:NotNull + val cardinal: Int, + @field:Schema(description = "시작 시간", example = "2024-03-01T10:00:00") + @field:NotNull + @field:DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + val start: LocalDateTime, + @field:Schema(description = "종료 시간", example = "2024-03-01T12:00:00") + @field:NotNull + @field:DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + val end: LocalDateTime, +) diff --git a/src/main/kotlin/com/weeth/domain/schedule/application/dto/request/ScheduleTimeRequest.kt b/src/main/kotlin/com/weeth/domain/schedule/application/dto/request/ScheduleTimeRequest.kt new file mode 100644 index 00000000..debc0e90 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/schedule/application/dto/request/ScheduleTimeRequest.kt @@ -0,0 +1,17 @@ +package com.weeth.domain.schedule.application.dto.request + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotNull +import org.springframework.format.annotation.DateTimeFormat +import java.time.LocalDateTime + +data class ScheduleTimeRequest( + @field:Schema(description = "시작 시간", example = "2024-03-01T10:00:00") + @field:NotNull + @field:DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + val start: LocalDateTime, + @field:Schema(description = "종료 시간", example = "2024-03-01T12:00:00") + @field:NotNull + @field:DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + val end: LocalDateTime, +) diff --git a/src/main/kotlin/com/weeth/domain/schedule/application/dto/request/ScheduleUpdateRequest.kt b/src/main/kotlin/com/weeth/domain/schedule/application/dto/request/ScheduleUpdateRequest.kt new file mode 100644 index 00000000..e9694e21 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/schedule/application/dto/request/ScheduleUpdateRequest.kt @@ -0,0 +1,29 @@ +package com.weeth.domain.schedule.application.dto.request + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Size +import org.springframework.format.annotation.DateTimeFormat +import java.time.LocalDateTime + +data class ScheduleUpdateRequest( + @field:Schema(description = "일정 제목", example = "MT") + @field:NotBlank + val title: String, + @field:Schema(description = "일정 내용", example = "1박 2일 MT입니다.") + @field:NotBlank + @field:Size(max = 500) + val content: String, + @field:Schema(description = "장소", example = "가평") + @field:NotBlank + val location: String, + @field:Schema(description = "시작 시간", example = "2024-03-01T10:00:00") + @field:NotNull + @field:DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + val start: LocalDateTime, + @field:Schema(description = "종료 시간", example = "2024-03-01T12:00:00") + @field:NotNull + @field:DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + val end: LocalDateTime, +) diff --git a/src/main/kotlin/com/weeth/domain/schedule/application/dto/response/EventResponse.kt b/src/main/kotlin/com/weeth/domain/schedule/application/dto/response/EventResponse.kt new file mode 100644 index 00000000..f6d476a4 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/schedule/application/dto/response/EventResponse.kt @@ -0,0 +1,30 @@ +package com.weeth.domain.schedule.application.dto.response + +import com.weeth.domain.schedule.domain.entity.enums.Type +import io.swagger.v3.oas.annotations.media.Schema +import java.time.LocalDateTime + +data class EventResponse( + @field:Schema(description = "일정 ID", example = "1") + val id: Long, + @field:Schema(description = "일정 제목", example = "MT") + val title: String, + @field:Schema(description = "일정 내용") + val content: String, + @field:Schema(description = "장소", example = "가평") + val location: String, + @field:Schema(description = "작성자 이름", example = "이지훈") + val name: String?, + @field:Schema(description = "기수", example = "4") + val cardinal: Int, + @field:Schema(description = "일정 타입", example = "EVENT") + val type: Type, + @field:Schema(description = "시작 시간") + val start: LocalDateTime, + @field:Schema(description = "종료 시간") + val end: LocalDateTime, + @field:Schema(description = "생성 시간") + val createdAt: LocalDateTime?, + @field:Schema(description = "수정 시간") + val modifiedAt: LocalDateTime?, +) diff --git a/src/main/kotlin/com/weeth/domain/schedule/application/dto/response/ScheduleResponse.kt b/src/main/kotlin/com/weeth/domain/schedule/application/dto/response/ScheduleResponse.kt new file mode 100644 index 00000000..1a0d883f --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/schedule/application/dto/response/ScheduleResponse.kt @@ -0,0 +1,17 @@ +package com.weeth.domain.schedule.application.dto.response + +import io.swagger.v3.oas.annotations.media.Schema +import java.time.LocalDateTime + +data class ScheduleResponse( + @field:Schema(description = "일정 ID", example = "1") + val id: Long, + @field:Schema(description = "제목", example = "1차 정기모임") + val title: String, + @field:Schema(description = "시작 시간") + val start: LocalDateTime, + @field:Schema(description = "종료 시간") + val end: LocalDateTime, + @field:Schema(description = "정기모임 여부") + val isSession: Boolean, +) diff --git a/src/main/kotlin/com/weeth/domain/schedule/application/dto/response/SessionInfoResponse.kt b/src/main/kotlin/com/weeth/domain/schedule/application/dto/response/SessionInfoResponse.kt new file mode 100644 index 00000000..2a93789e --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/schedule/application/dto/response/SessionInfoResponse.kt @@ -0,0 +1,15 @@ +package com.weeth.domain.schedule.application.dto.response + +import io.swagger.v3.oas.annotations.media.Schema +import java.time.LocalDateTime + +data class SessionInfoResponse( + @field:Schema(description = "정기모임 ID", example = "1") + val id: Long, + @field:Schema(description = "기수", example = "4") + val cardinal: Int, + @field:Schema(description = "제목", example = "1차 정기모임") + val title: String, + @field:Schema(description = "시작 시간") + val start: LocalDateTime, +) diff --git a/src/main/kotlin/com/weeth/domain/schedule/application/dto/response/SessionInfosResponse.kt b/src/main/kotlin/com/weeth/domain/schedule/application/dto/response/SessionInfosResponse.kt new file mode 100644 index 00000000..88409aa5 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/schedule/application/dto/response/SessionInfosResponse.kt @@ -0,0 +1,10 @@ +package com.weeth.domain.schedule.application.dto.response + +import io.swagger.v3.oas.annotations.media.Schema + +data class SessionInfosResponse( + @field:Schema(description = "이번 주 정기모임") + val thisWeek: SessionInfoResponse?, + @field:Schema(description = "정기모임 목록") + val sessions: List, +) diff --git a/src/main/kotlin/com/weeth/domain/schedule/application/dto/response/SessionResponse.kt b/src/main/kotlin/com/weeth/domain/schedule/application/dto/response/SessionResponse.kt new file mode 100644 index 00000000..7c06371f --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/schedule/application/dto/response/SessionResponse.kt @@ -0,0 +1,34 @@ +package com.weeth.domain.schedule.application.dto.response + +import com.fasterxml.jackson.annotation.JsonInclude +import com.weeth.domain.schedule.domain.entity.enums.Type +import io.swagger.v3.oas.annotations.media.Schema +import java.time.LocalDateTime + +@JsonInclude(JsonInclude.Include.NON_NULL) +data class SessionResponse( + @field:Schema(description = "정기모임 ID", example = "1") + val id: Long, + @field:Schema(description = "제목", example = "1차 정기모임") + val title: String, + @field:Schema(description = "내용") + val content: String?, + @field:Schema(description = "장소", example = "공학관 401호") + val location: String?, + @field:Schema(description = "작성자 이름", example = "이지훈") + val name: String?, + @field:Schema(description = "기수", example = "4") + val cardinal: Int, + @field:Schema(description = "일정 타입", example = "MEETING") + val type: Type, + @field:Schema(description = "출석 코드", example = "1234") + val code: Int?, + @field:Schema(description = "시작 시간") + val start: LocalDateTime, + @field:Schema(description = "종료 시간") + val end: LocalDateTime, + @field:Schema(description = "생성 시간") + val createdAt: LocalDateTime?, + @field:Schema(description = "수정 시간") + val modifiedAt: LocalDateTime?, +) diff --git a/src/main/kotlin/com/weeth/domain/schedule/application/exception/EventErrorCode.kt b/src/main/kotlin/com/weeth/domain/schedule/application/exception/EventErrorCode.kt new file mode 100644 index 00000000..2e266bb0 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/schedule/application/exception/EventErrorCode.kt @@ -0,0 +1,21 @@ +package com.weeth.domain.schedule.application.exception + +import com.weeth.global.common.exception.ErrorCodeInterface +import com.weeth.global.common.exception.ExplainError +import org.springframework.http.HttpStatus + +enum class EventErrorCode( + private val code: Int, + private val status: HttpStatus, + private val message: String, +) : ErrorCodeInterface { + @ExplainError("요청한 일정 ID에 해당하는 일정이 존재하지 않을 때 발생합니다.") + EVENT_NOT_FOUND(2700, HttpStatus.NOT_FOUND, "존재하지 않는 일정입니다."), + ; + + override fun getCode(): Int = code + + override fun getStatus(): HttpStatus = status + + override fun getMessage(): String = message +} diff --git a/src/main/kotlin/com/weeth/domain/schedule/application/exception/EventNotFoundException.kt b/src/main/kotlin/com/weeth/domain/schedule/application/exception/EventNotFoundException.kt new file mode 100644 index 00000000..56968357 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/schedule/application/exception/EventNotFoundException.kt @@ -0,0 +1,5 @@ +package com.weeth.domain.schedule.application.exception + +import com.weeth.global.common.exception.BaseException + +class EventNotFoundException : BaseException(EventErrorCode.EVENT_NOT_FOUND) diff --git a/src/main/kotlin/com/weeth/domain/schedule/application/mapper/EventMapper.kt b/src/main/kotlin/com/weeth/domain/schedule/application/mapper/EventMapper.kt new file mode 100644 index 00000000..a1caa781 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/schedule/application/mapper/EventMapper.kt @@ -0,0 +1,40 @@ +package com.weeth.domain.schedule.application.mapper + +import com.weeth.domain.schedule.application.dto.request.ScheduleSaveRequest +import com.weeth.domain.schedule.application.dto.response.EventResponse +import com.weeth.domain.schedule.domain.entity.Event +import com.weeth.domain.schedule.domain.entity.enums.Type +import com.weeth.domain.user.domain.entity.User +import org.springframework.stereotype.Component + +@Component +class EventMapper { + fun toResponse(event: Event): EventResponse = + EventResponse( + id = event.id, + title = event.title, + content = event.content, + location = event.location, + name = event.user?.name, + cardinal = event.cardinal, + type = Type.EVENT, + start = event.start, + end = event.end, + createdAt = event.createdAt, + modifiedAt = event.modifiedAt, + ) + + fun toEntity( + request: ScheduleSaveRequest, + user: User, + ): Event = + Event.create( + title = request.title, + content = request.content, + location = request.location, + cardinal = request.cardinal, + start = request.start, + end = request.end, + user = user, + ) +} diff --git a/src/main/kotlin/com/weeth/domain/schedule/application/mapper/ScheduleMapper.kt b/src/main/kotlin/com/weeth/domain/schedule/application/mapper/ScheduleMapper.kt new file mode 100644 index 00000000..efe5ac27 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/schedule/application/mapper/ScheduleMapper.kt @@ -0,0 +1,33 @@ +package com.weeth.domain.schedule.application.mapper + +import com.weeth.domain.schedule.application.dto.response.ScheduleResponse +import com.weeth.domain.schedule.domain.entity.Event +import com.weeth.domain.session.domain.entity.Session +import org.springframework.stereotype.Component + +@Component +class ScheduleMapper { + fun toResponse( + event: Event, + isSession: Boolean, + ): ScheduleResponse = + ScheduleResponse( + id = event.id, + title = event.title, + start = event.start, + end = event.end, + isSession = isSession, + ) + + fun toResponse( + session: Session, + isSession: Boolean, + ): ScheduleResponse = + ScheduleResponse( + id = session.id, + title = session.title, + start = session.start, + end = session.end, + isSession = isSession, + ) +} diff --git a/src/main/kotlin/com/weeth/domain/schedule/application/mapper/SessionMapper.kt b/src/main/kotlin/com/weeth/domain/schedule/application/mapper/SessionMapper.kt new file mode 100644 index 00000000..8d587724 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/schedule/application/mapper/SessionMapper.kt @@ -0,0 +1,76 @@ +package com.weeth.domain.schedule.application.mapper + +import com.weeth.domain.schedule.application.dto.request.ScheduleSaveRequest +import com.weeth.domain.schedule.application.dto.response.SessionInfoResponse +import com.weeth.domain.schedule.application.dto.response.SessionInfosResponse +import com.weeth.domain.schedule.application.dto.response.SessionResponse +import com.weeth.domain.schedule.domain.entity.enums.Type +import com.weeth.domain.session.domain.entity.Session +import com.weeth.domain.user.domain.entity.User +import org.springframework.stereotype.Component + +@Component +class SessionMapper { + fun toResponse(session: Session): SessionResponse = + SessionResponse( + id = session.id, + title = session.title, + content = session.content, + location = session.location, + name = session.user?.name, + cardinal = session.cardinal, + type = Type.SESSION, + code = null, + start = session.start, + end = session.end, + createdAt = session.createdAt, + modifiedAt = session.modifiedAt, + ) + + fun toAdminResponse(session: Session): SessionResponse = + SessionResponse( + id = session.id, + title = session.title, + content = session.content, + location = session.location, + name = session.user?.name, + cardinal = session.cardinal, + type = Type.SESSION, + code = session.code, + start = session.start, + end = session.end, + createdAt = session.createdAt, + modifiedAt = session.modifiedAt, + ) + + fun toInfo(session: Session): SessionInfoResponse = + SessionInfoResponse( + id = session.id, + cardinal = session.cardinal, + title = session.title, + start = session.start, + ) + + fun toInfos( + thisWeek: Session?, + sessions: List, + ): SessionInfosResponse = + SessionInfosResponse( + thisWeek = thisWeek?.let { toInfo(it) }, + sessions = sessions.map { toInfo(it) }, + ) + + fun toEntity( + request: ScheduleSaveRequest, + user: User, + ): Session = + Session.create( + title = request.title, + content = request.content, + location = request.location, + cardinal = request.cardinal, + start = request.start, + end = request.end, + user = user, + ) +} diff --git a/src/main/kotlin/com/weeth/domain/schedule/application/usecase/command/ManageEventUseCase.kt b/src/main/kotlin/com/weeth/domain/schedule/application/usecase/command/ManageEventUseCase.kt new file mode 100644 index 00000000..af641a8a --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/schedule/application/usecase/command/ManageEventUseCase.kt @@ -0,0 +1,47 @@ +package com.weeth.domain.schedule.application.usecase.command + +import com.weeth.domain.schedule.application.dto.request.ScheduleSaveRequest +import com.weeth.domain.schedule.application.dto.request.ScheduleUpdateRequest +import com.weeth.domain.schedule.application.exception.EventNotFoundException +import com.weeth.domain.schedule.application.mapper.EventMapper +import com.weeth.domain.schedule.domain.repository.EventRepository +import com.weeth.domain.user.domain.repository.CardinalReader +import com.weeth.domain.user.domain.repository.UserReader +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class ManageEventUseCase( + private val eventRepository: EventRepository, + private val userReader: UserReader, + private val cardinalReader: CardinalReader, + private val eventMapper: EventMapper, +) { + @Transactional + fun create( + request: ScheduleSaveRequest, + userId: Long, + ) { + val user = userReader.getById(userId) + cardinalReader.getByCardinalNumber(request.cardinal) + eventRepository.save(eventMapper.toEntity(request, user)) + } + + @Transactional + fun update( + eventId: Long, + request: ScheduleUpdateRequest, + userId: Long, + ) { + val user = userReader.getById(userId) + val event = eventRepository.findByIdOrNull(eventId) ?: throw EventNotFoundException() + event.update(request.title, request.content, request.location, request.start, request.end, user) + } + + @Transactional + fun delete(eventId: Long) { + val event = eventRepository.findByIdOrNull(eventId) ?: throw EventNotFoundException() + eventRepository.delete(event) + } +} diff --git a/src/main/kotlin/com/weeth/domain/schedule/application/usecase/query/GetScheduleQueryService.kt b/src/main/kotlin/com/weeth/domain/schedule/application/usecase/query/GetScheduleQueryService.kt new file mode 100644 index 00000000..e07112b8 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/schedule/application/usecase/query/GetScheduleQueryService.kt @@ -0,0 +1,66 @@ +package com.weeth.domain.schedule.application.usecase.query + +import com.weeth.domain.schedule.application.dto.response.EventResponse +import com.weeth.domain.schedule.application.dto.response.ScheduleResponse +import com.weeth.domain.schedule.application.exception.EventNotFoundException +import com.weeth.domain.schedule.application.mapper.EventMapper +import com.weeth.domain.schedule.application.mapper.ScheduleMapper +import com.weeth.domain.schedule.domain.repository.EventRepository +import com.weeth.domain.session.domain.repository.SessionReader +import com.weeth.domain.user.domain.repository.CardinalReader +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime + +@Service +@Transactional(readOnly = true) +class GetScheduleQueryService( + private val eventRepository: EventRepository, + private val sessionReader: SessionReader, + private val cardinalReader: CardinalReader, + private val scheduleMapper: ScheduleMapper, + private val eventMapper: EventMapper, +) { + fun findEvent(eventId: Long): EventResponse = + eventRepository + .findByIdOrNull(eventId) + ?.let { eventMapper.toResponse(it) } + ?: throw EventNotFoundException() + + fun findMonthly( + start: LocalDateTime, + end: LocalDateTime, + ): List { + val events = + eventRepository + .findByStartLessThanEqualAndEndGreaterThanEqualOrderByStartAsc(end, start) + .map { scheduleMapper.toResponse(it, false) } + val sessions = + sessionReader + .findByStartLessThanEqualAndEndGreaterThanEqualOrderByStartAsc(end, start) + .map { scheduleMapper.toResponse(it, true) } + return (events + sessions).sortedBy { it.start } + } + + fun findYearly( + year: Int, + semester: Int, + ): Map> { + val cardinal = cardinalReader.getByYearAndSemester(year, semester) + val events = + eventRepository + .findAllByCardinal(cardinal.cardinalNumber) + .map { scheduleMapper.toResponse(it, false) } + val sessions = + sessionReader + .findAllByCardinal(cardinal.cardinalNumber) + .map { scheduleMapper.toResponse(it, true) } + + return (events + sessions) + .sortedBy { it.start } + .flatMap { schedule -> + (schedule.start.monthValue..schedule.end.monthValue).map { month -> month to schedule } + }.groupBy({ it.first }, { it.second }) + } +} diff --git a/src/main/kotlin/com/weeth/domain/schedule/application/validator/ScheduleTimeCheckValidator.kt b/src/main/kotlin/com/weeth/domain/schedule/application/validator/ScheduleTimeCheckValidator.kt new file mode 100644 index 00000000..84c8dc76 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/schedule/application/validator/ScheduleTimeCheckValidator.kt @@ -0,0 +1,13 @@ +package com.weeth.domain.schedule.application.validator + +import com.weeth.domain.schedule.application.annotation.ScheduleTimeCheck +import com.weeth.domain.schedule.application.dto.request.ScheduleTimeRequest +import jakarta.validation.ConstraintValidator +import jakarta.validation.ConstraintValidatorContext + +class ScheduleTimeCheckValidator : ConstraintValidator { + override fun isValid( + time: ScheduleTimeRequest?, + context: ConstraintValidatorContext, + ): Boolean = time == null || time.start.isBefore(time.end.plusMinutes(1)) +} diff --git a/src/main/kotlin/com/weeth/domain/schedule/domain/entity/Event.kt b/src/main/kotlin/com/weeth/domain/schedule/domain/entity/Event.kt new file mode 100644 index 00000000..3bfdd4e6 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/schedule/domain/entity/Event.kt @@ -0,0 +1,73 @@ +package com.weeth.domain.schedule.domain.entity + +import com.weeth.domain.user.domain.entity.User +import com.weeth.global.common.entity.BaseEntity +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 java.time.LocalDateTime + +@Entity +class Event( + var title: String, + @Column(length = 500) + var content: String, + var location: String, + var cardinal: Int, + var start: LocalDateTime, + var end: LocalDateTime, + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + var user: User? = null, +) : BaseEntity() { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0 + + fun update( + title: String, + content: String, + location: String, + start: LocalDateTime, + end: LocalDateTime, + user: User?, + ) { + require(title.isNotBlank()) { "제목은 필수입니다" } + require(!end.isBefore(start)) { "종료 시간은 시작 시간 이후여야 합니다" } + this.title = title + this.content = content + this.location = location + this.start = start + this.end = end + this.user = user + } + + companion object { + fun create( + title: String, + content: String, + location: String, + cardinal: Int, + start: LocalDateTime, + end: LocalDateTime, + user: User?, + ): Event { + require(title.isNotBlank()) { "제목은 필수입니다" } + require(!end.isBefore(start)) { "종료 시간은 시작 시간 이후여야 합니다" } + return Event( + title = title, + content = content, + location = location, + cardinal = cardinal, + start = start, + end = end, + user = user, + ) + } + } +} diff --git a/src/main/kotlin/com/weeth/domain/schedule/domain/entity/enums/Type.kt b/src/main/kotlin/com/weeth/domain/schedule/domain/entity/enums/Type.kt new file mode 100644 index 00000000..bbb663b4 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/schedule/domain/entity/enums/Type.kt @@ -0,0 +1,6 @@ +package com.weeth.domain.schedule.domain.entity.enums + +enum class Type { + EVENT, + SESSION, +} diff --git a/src/main/kotlin/com/weeth/domain/schedule/domain/repository/EventRepository.kt b/src/main/kotlin/com/weeth/domain/schedule/domain/repository/EventRepository.kt new file mode 100644 index 00000000..a24b1804 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/schedule/domain/repository/EventRepository.kt @@ -0,0 +1,14 @@ +package com.weeth.domain.schedule.domain.repository + +import com.weeth.domain.schedule.domain.entity.Event +import org.springframework.data.jpa.repository.JpaRepository +import java.time.LocalDateTime + +interface EventRepository : JpaRepository { + fun findByStartLessThanEqualAndEndGreaterThanEqualOrderByStartAsc( + end: LocalDateTime, + start: LocalDateTime, + ): List + + fun findAllByCardinal(cardinal: Int): List +} diff --git a/src/main/kotlin/com/weeth/domain/schedule/presentation/EventAdminController.kt b/src/main/kotlin/com/weeth/domain/schedule/presentation/EventAdminController.kt new file mode 100644 index 00000000..bc3c6434 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/schedule/presentation/EventAdminController.kt @@ -0,0 +1,58 @@ +package com.weeth.domain.schedule.presentation + +import com.weeth.domain.schedule.application.dto.request.ScheduleSaveRequest +import com.weeth.domain.schedule.application.dto.request.ScheduleUpdateRequest +import com.weeth.domain.schedule.application.exception.EventErrorCode +import com.weeth.domain.schedule.application.usecase.command.ManageEventUseCase +import com.weeth.global.auth.annotation.CurrentUser +import com.weeth.global.common.exception.ApiErrorCodeExample +import com.weeth.global.common.response.CommonResponse +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.validation.Valid +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.PatchMapping +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 + +@Tag(name = "EVENT ADMIN", description = "[ADMIN] 일정 어드민 API") +@RestController +@RequestMapping("/api/v4/admin/events") +@ApiErrorCodeExample(EventErrorCode::class) +class EventAdminController( + private val manageEventUseCase: ManageEventUseCase, +) { + @PostMapping + @Operation(summary = "일정 생성") + fun create( + @Valid @RequestBody dto: ScheduleSaveRequest, + @Parameter(hidden = true) @CurrentUser userId: Long, + ): CommonResponse { + manageEventUseCase.create(dto, userId) + return CommonResponse.success(ScheduleResponseCode.EVENT_SAVE_SUCCESS) + } + + @PatchMapping("/{eventId}") + @Operation(summary = "일정 수정") + fun update( + @PathVariable eventId: Long, + @Valid @RequestBody dto: ScheduleUpdateRequest, + @Parameter(hidden = true) @CurrentUser userId: Long, + ): CommonResponse { + manageEventUseCase.update(eventId, dto, userId) + return CommonResponse.success(ScheduleResponseCode.EVENT_UPDATE_SUCCESS) + } + + @DeleteMapping("/{eventId}") + @Operation(summary = "일정 삭제") + fun delete( + @PathVariable eventId: Long, + ): CommonResponse { + manageEventUseCase.delete(eventId) + return CommonResponse.success(ScheduleResponseCode.EVENT_DELETE_SUCCESS) + } +} diff --git a/src/main/kotlin/com/weeth/domain/schedule/presentation/EventController.kt b/src/main/kotlin/com/weeth/domain/schedule/presentation/EventController.kt new file mode 100644 index 00000000..ca6d17c1 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/schedule/presentation/EventController.kt @@ -0,0 +1,28 @@ +package com.weeth.domain.schedule.presentation + +import com.weeth.domain.schedule.application.dto.response.EventResponse +import com.weeth.domain.schedule.application.exception.EventErrorCode +import com.weeth.domain.schedule.application.usecase.query.GetScheduleQueryService +import com.weeth.global.common.exception.ApiErrorCodeExample +import com.weeth.global.common.response.CommonResponse +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@Tag(name = "EVENT", description = "일정 API") +@RestController +@RequestMapping("/api/v4/events") +@ApiErrorCodeExample(EventErrorCode::class) +class EventController( + private val getScheduleQueryService: GetScheduleQueryService, +) { + @GetMapping("/{eventId}") + @Operation(summary = "일정 상세 조회") + fun getEvent( + @PathVariable eventId: Long, + ): CommonResponse = + CommonResponse.success(ScheduleResponseCode.EVENT_FIND_SUCCESS, getScheduleQueryService.findEvent(eventId)) +} diff --git a/src/main/kotlin/com/weeth/domain/schedule/presentation/ScheduleController.kt b/src/main/kotlin/com/weeth/domain/schedule/presentation/ScheduleController.kt new file mode 100644 index 00000000..3e2224b6 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/schedule/presentation/ScheduleController.kt @@ -0,0 +1,36 @@ +package com.weeth.domain.schedule.presentation + +import com.weeth.domain.schedule.application.dto.response.ScheduleResponse +import com.weeth.domain.schedule.application.usecase.query.GetScheduleQueryService +import com.weeth.global.common.response.CommonResponse +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.format.annotation.DateTimeFormat +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController +import java.time.LocalDateTime + +@Tag(name = "SCHEDULE", description = "캘린더 조회 API") +@RestController +@RequestMapping("/api/v4/schedules") +class ScheduleController( + private val getScheduleQueryService: GetScheduleQueryService, +) { + @GetMapping("/monthly") + @Operation(summary = "월별 일정 조회") + fun findByMonthly( + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) start: LocalDateTime, + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) end: LocalDateTime, + ): CommonResponse> = + CommonResponse.success(ScheduleResponseCode.SCHEDULE_MONTHLY_FIND_SUCCESS, getScheduleQueryService.findMonthly(start, end)) + + @GetMapping("/yearly") + @Operation(summary = "연도별 일정 조회") + fun findByYearly( + @RequestParam year: Int, + @RequestParam semester: Int, + ): CommonResponse>> = + CommonResponse.success(ScheduleResponseCode.SCHEDULE_YEARLY_FIND_SUCCESS, getScheduleQueryService.findYearly(year, semester)) +} diff --git a/src/main/kotlin/com/weeth/domain/schedule/presentation/ScheduleResponseCode.kt b/src/main/kotlin/com/weeth/domain/schedule/presentation/ScheduleResponseCode.kt new file mode 100644 index 00000000..92230c61 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/schedule/presentation/ScheduleResponseCode.kt @@ -0,0 +1,17 @@ +package com.weeth.domain.schedule.presentation + +import com.weeth.global.common.response.ResponseCodeInterface +import org.springframework.http.HttpStatus + +enum class ScheduleResponseCode( + override val code: Int, + override val status: HttpStatus, + override val message: String, +) : ResponseCodeInterface { + EVENT_SAVE_SUCCESS(1700, HttpStatus.OK, "일정이 성공적으로 생성되었습니다."), + EVENT_UPDATE_SUCCESS(1701, HttpStatus.OK, "일정이 성공적으로 수정되었습니다."), + EVENT_DELETE_SUCCESS(1702, HttpStatus.OK, "일정이 성공적으로 삭제되었습니다."), + EVENT_FIND_SUCCESS(1703, HttpStatus.OK, "일정이 성공적으로 조회되었습니다."), + SCHEDULE_MONTHLY_FIND_SUCCESS(1704, HttpStatus.OK, "월별 일정이 성공적으로 조회되었습니다."), + SCHEDULE_YEARLY_FIND_SUCCESS(1705, HttpStatus.OK, "연도별 일정이 성공적으로 조회되었습니다."), +} diff --git a/src/main/kotlin/com/weeth/domain/session/application/exception/SessionErrorCode.kt b/src/main/kotlin/com/weeth/domain/session/application/exception/SessionErrorCode.kt new file mode 100644 index 00000000..a965bf4a --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/session/application/exception/SessionErrorCode.kt @@ -0,0 +1,21 @@ +package com.weeth.domain.session.application.exception + +import com.weeth.global.common.exception.ErrorCodeInterface +import com.weeth.global.common.exception.ExplainError +import org.springframework.http.HttpStatus + +enum class SessionErrorCode( + private val code: Int, + private val status: HttpStatus, + private val message: String, +) : ErrorCodeInterface { + @ExplainError("요청한 정기모임 ID에 해당하는 정기모임이 존재하지 않을 때 발생합니다.") + SESSION_NOT_FOUND(2203, HttpStatus.NOT_FOUND, "존재하지 않는 정기모임입니다."), + ; + + override fun getCode(): Int = code + + override fun getStatus(): HttpStatus = status + + override fun getMessage(): String = message +} diff --git a/src/main/kotlin/com/weeth/domain/session/application/exception/SessionNotFoundException.kt b/src/main/kotlin/com/weeth/domain/session/application/exception/SessionNotFoundException.kt new file mode 100644 index 00000000..8e866880 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/session/application/exception/SessionNotFoundException.kt @@ -0,0 +1,5 @@ +package com.weeth.domain.session.application.exception + +import com.weeth.global.common.exception.BaseException + +class SessionNotFoundException : BaseException(SessionErrorCode.SESSION_NOT_FOUND) diff --git a/src/main/kotlin/com/weeth/domain/session/application/usecase/command/ManageSessionUseCase.kt b/src/main/kotlin/com/weeth/domain/session/application/usecase/command/ManageSessionUseCase.kt new file mode 100644 index 00000000..965df991 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/session/application/usecase/command/ManageSessionUseCase.kt @@ -0,0 +1,63 @@ +package com.weeth.domain.session.application.usecase.command + +import com.weeth.domain.attendance.domain.entity.Attendance +import com.weeth.domain.attendance.domain.entity.enums.AttendanceStatus +import com.weeth.domain.attendance.domain.repository.AttendanceRepository +import com.weeth.domain.schedule.application.dto.request.ScheduleSaveRequest +import com.weeth.domain.schedule.application.dto.request.ScheduleUpdateRequest +import com.weeth.domain.schedule.application.mapper.SessionMapper +import com.weeth.domain.session.application.exception.SessionNotFoundException +import com.weeth.domain.session.domain.repository.SessionRepository +import com.weeth.domain.user.domain.entity.enums.Status +import com.weeth.domain.user.domain.repository.CardinalReader +import com.weeth.domain.user.domain.repository.UserReader +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class ManageSessionUseCase( + private val sessionRepository: SessionRepository, + private val attendanceRepository: AttendanceRepository, + private val userReader: UserReader, + private val cardinalReader: CardinalReader, + private val sessionMapper: SessionMapper, +) { + @Transactional + fun create( + request: ScheduleSaveRequest, + userId: Long, + ) { + val user = userReader.getById(userId) + val cardinal = cardinalReader.getByCardinalNumber(request.cardinal) + val users = userReader.findAllByCardinalAndStatus(cardinal, Status.ACTIVE) + val session = sessionMapper.toEntity(request, user) + sessionRepository.save(session) + attendanceRepository.saveAll(users.map { Attendance.Companion.create(session, it) }) + } + + @Transactional + fun update( + sessionId: Long, + request: ScheduleUpdateRequest, + userId: Long, + ) { + val session = sessionRepository.findByIdWithLock(sessionId) ?: throw SessionNotFoundException() + val user = userReader.getById(userId) + session.updateInfo(request.title, request.content, request.location, request.start, request.end, user) + } + + @Transactional + fun delete(sessionId: Long) { + val session = sessionRepository.findByIdWithLock(sessionId) ?: throw SessionNotFoundException() + val attendances = attendanceRepository.findAllBySessionAndUserStatusWithLock(session, Status.ACTIVE) + attendances.forEach { a -> + when (a.status) { + AttendanceStatus.ATTEND -> a.user.removeAttend() + AttendanceStatus.ABSENT -> a.user.removeAbsent() + else -> Unit + } + } + attendanceRepository.deleteAllBySession(session) + sessionRepository.delete(session) + } +} diff --git a/src/main/kotlin/com/weeth/domain/session/application/usecase/query/GetSessionQueryService.kt b/src/main/kotlin/com/weeth/domain/session/application/usecase/query/GetSessionQueryService.kt new file mode 100644 index 00000000..45a37895 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/session/application/usecase/query/GetSessionQueryService.kt @@ -0,0 +1,58 @@ +package com.weeth.domain.session.application.usecase.query + +import com.weeth.domain.schedule.application.dto.response.SessionInfosResponse +import com.weeth.domain.schedule.application.dto.response.SessionResponse +import com.weeth.domain.schedule.application.mapper.SessionMapper +import com.weeth.domain.session.application.exception.SessionNotFoundException +import com.weeth.domain.session.domain.entity.Session +import com.weeth.domain.session.domain.repository.SessionRepository +import com.weeth.domain.user.domain.entity.enums.Role +import com.weeth.domain.user.domain.repository.UserReader +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.DayOfWeek +import java.time.LocalDate +import java.time.temporal.TemporalAdjusters + +@Service +@Transactional(readOnly = true) +class GetSessionQueryService( + private val sessionRepository: SessionRepository, + private val userReader: UserReader, + private val sessionMapper: SessionMapper, +) { + fun findSession( + userId: Long, + sessionId: Long, + ): SessionResponse { + val user = userReader.getById(userId) + val session = sessionRepository.findByIdOrNull(sessionId) ?: throw SessionNotFoundException() + return if (user.role == Role.ADMIN) { + sessionMapper.toAdminResponse(session) + } else { + sessionMapper.toResponse(session) + } + } + + fun findSessionInfos(cardinal: Int?): SessionInfosResponse { + val sessions = + if (cardinal == null) { + sessionRepository.findAllByOrderByStartDesc() + } else { + sessionRepository.findAllByCardinalOrderByStartDesc(cardinal) + } + val thisWeek = findThisWeek(sessions) + return sessionMapper.toInfos(thisWeek, sessions) + } + + private fun findThisWeek(sessions: List): Session? { + val today = LocalDate.now() + val startOfWeek = today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)) + val endOfWeek = today.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)) + return sessions.firstOrNull { s -> + val d = s.start.toLocalDate() + !d.isBefore(startOfWeek) && !d.isAfter(endOfWeek) + } + } +} diff --git a/src/main/kotlin/com/weeth/domain/session/domain/entity/Session.kt b/src/main/kotlin/com/weeth/domain/session/domain/entity/Session.kt new file mode 100644 index 00000000..09fd0f03 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/session/domain/entity/Session.kt @@ -0,0 +1,93 @@ +package com.weeth.domain.session.domain.entity + +import com.weeth.domain.session.domain.entity.enums.SessionStatus +import com.weeth.domain.user.domain.entity.User +import com.weeth.global.common.entity.BaseEntity +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +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 java.time.LocalDateTime + +@Entity +@Table(name = "meeting") +class Session( + var title: String, + @Column(length = 500) + var content: String? = null, + var location: String? = null, + var cardinal: Int, + var start: LocalDateTime, + var end: LocalDateTime, + var code: Int, + @Enumerated(EnumType.STRING) + var status: SessionStatus = SessionStatus.OPEN, + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + var user: User? = null, +) : BaseEntity() { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0 + + fun close() { + check(status == SessionStatus.OPEN) { "이미 종료된 세션입니다" } + status = SessionStatus.CLOSED + } + + fun updateInfo( + title: String, + content: String?, + location: String?, + start: LocalDateTime, + end: LocalDateTime, + user: User?, + ) { + require(title.isNotBlank()) { "제목은 필수입니다" } + require(!end.isBefore(start)) { "종료 시간은 시작 시간 이후여야 합니다" } + this.title = title + this.content = content + this.location = location + this.start = start + this.end = end + this.user = user + } + + fun isCodeMatch(code: Int): Boolean = this.code == code + + fun isInProgress(now: LocalDateTime): Boolean = !now.isBefore(start) && !now.isAfter(end) + + companion object { + fun create( + title: String, + content: String?, + location: String?, + cardinal: Int, + start: LocalDateTime, + end: LocalDateTime, + user: User?, + ): Session { + require(title.isNotBlank()) { "제목은 필수입니다" } + require(!end.isBefore(start)) { "종료 시간은 시작 시간 이후여야 합니다" } + return Session( + title = title, + content = content, + location = location, + cardinal = cardinal, + start = start, + end = end, + code = generateCode(), + user = user, + ) + } + + private fun generateCode(): Int = (1000..9999).random() + } +} diff --git a/src/main/kotlin/com/weeth/domain/session/domain/entity/enums/SessionStatus.kt b/src/main/kotlin/com/weeth/domain/session/domain/entity/enums/SessionStatus.kt new file mode 100644 index 00000000..6f148579 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/session/domain/entity/enums/SessionStatus.kt @@ -0,0 +1,6 @@ +package com.weeth.domain.session.domain.entity.enums + +enum class SessionStatus { + OPEN, + CLOSED, +} diff --git a/src/main/kotlin/com/weeth/domain/session/domain/repository/SessionReader.kt b/src/main/kotlin/com/weeth/domain/session/domain/repository/SessionReader.kt new file mode 100644 index 00000000..430a3138 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/session/domain/repository/SessionReader.kt @@ -0,0 +1,31 @@ +package com.weeth.domain.session.domain.repository + +import com.weeth.domain.session.domain.entity.Session +import com.weeth.domain.session.domain.entity.enums.SessionStatus +import java.time.LocalDateTime + +interface SessionReader { + fun getById(sessionId: Long): Session + + // TODO: QR 코드 출석 기능 구현 시 사용 예정 (현재 시간 기준 진행 중인 세션 조회) + fun findAllByStartBetween( + start: LocalDateTime, + end: LocalDateTime, + ): List + + fun findByStartLessThanEqualAndEndGreaterThanEqualOrderByStartAsc( + end: LocalDateTime, + start: LocalDateTime, + ): List + + fun findAllByCardinal(cardinal: Int): List + + fun findAllByCardinalIn(cardinals: List): List + + fun findAllByCardinalOrderByStartAsc(cardinal: Int): List + + fun findAllByStatusAndEndBeforeOrderByEndAsc( + status: SessionStatus, + end: LocalDateTime, + ): List +} diff --git a/src/main/kotlin/com/weeth/domain/session/domain/repository/SessionRepository.kt b/src/main/kotlin/com/weeth/domain/session/domain/repository/SessionRepository.kt new file mode 100644 index 00000000..66c4c864 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/session/domain/repository/SessionRepository.kt @@ -0,0 +1,47 @@ +package com.weeth.domain.session.domain.repository + +import com.weeth.domain.session.application.exception.SessionNotFoundException +import com.weeth.domain.session.domain.entity.Session +import com.weeth.domain.session.domain.entity.enums.SessionStatus +import jakarta.persistence.LockModeType +import jakarta.persistence.QueryHint +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Lock +import org.springframework.data.jpa.repository.Query +import org.springframework.data.jpa.repository.QueryHints +import org.springframework.data.repository.query.Param +import java.time.LocalDateTime + +interface SessionRepository : + JpaRepository, + SessionReader { + @Lock(LockModeType.PESSIMISTIC_WRITE) + @QueryHints(QueryHint(name = "jakarta.persistence.lock.timeout", value = "2000")) + @Query("SELECT s FROM Session s WHERE s.id = :id") + fun findByIdWithLock(id: Long): Session? + + override fun findByStartLessThanEqualAndEndGreaterThanEqualOrderByStartAsc( + end: LocalDateTime, + start: LocalDateTime, + ): List + + override fun findAllByCardinalOrderByStartAsc(cardinal: Int): List + + fun findAllByCardinalOrderByStartDesc(cardinal: Int): List + + override fun findAllByCardinal(cardinal: Int): List + + override fun findAllByStatusAndEndBeforeOrderByEndAsc( + status: SessionStatus, + end: LocalDateTime, + ): List + + fun findAllByOrderByStartDesc(): List + + @Query("SELECT s FROM Session s WHERE s.cardinal IN :cardinals") + override fun findAllByCardinalIn( + @Param("cardinals") cardinals: List, + ): List + + override fun getById(sessionId: Long): Session = findById(sessionId).orElseThrow { SessionNotFoundException() } +} diff --git a/src/main/kotlin/com/weeth/domain/session/presentation/SessionAdminController.kt b/src/main/kotlin/com/weeth/domain/session/presentation/SessionAdminController.kt new file mode 100644 index 00000000..06e2a3ee --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/session/presentation/SessionAdminController.kt @@ -0,0 +1,73 @@ +package com.weeth.domain.session.presentation + +import com.weeth.domain.schedule.application.dto.request.ScheduleSaveRequest +import com.weeth.domain.schedule.application.dto.request.ScheduleUpdateRequest +import com.weeth.domain.schedule.application.dto.response.SessionInfosResponse +import com.weeth.domain.session.application.exception.SessionErrorCode +import com.weeth.domain.session.application.usecase.command.ManageSessionUseCase +import com.weeth.domain.session.application.usecase.query.GetSessionQueryService +import com.weeth.global.auth.annotation.CurrentUser +import com.weeth.global.common.exception.ApiErrorCodeExample +import com.weeth.global.common.response.CommonResponse +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.validation.Valid +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PatchMapping +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.RequestParam +import org.springframework.web.bind.annotation.RestController + +@Tag(name = "SESSION ADMIN", description = "[ADMIN] 정기모임 어드민 API") +@RestController +@RequestMapping("/api/v4/admin/sessions") +@ApiErrorCodeExample(SessionErrorCode::class) +class SessionAdminController( + private val manageSessionUseCase: ManageSessionUseCase, + private val getSessionQueryService: GetSessionQueryService, +) { + @PostMapping + @Operation(summary = "정기모임 생성") + fun create( + @Valid @RequestBody dto: ScheduleSaveRequest, + @Parameter(hidden = true) @CurrentUser userId: Long, + ): CommonResponse { + manageSessionUseCase.create(dto, userId) + return CommonResponse.success(SessionResponseCode.SESSION_SAVE_SUCCESS) + } + + @PatchMapping("/{sessionId}") + @Operation(summary = "정기모임 수정") + fun update( + @PathVariable sessionId: Long, + @Valid @RequestBody dto: ScheduleUpdateRequest, + @Parameter(hidden = true) @CurrentUser userId: Long, + ): CommonResponse { + manageSessionUseCase.update(sessionId, dto, userId) + return CommonResponse.success(SessionResponseCode.SESSION_UPDATE_SUCCESS) + } + + @DeleteMapping("/{sessionId}") + @Operation(summary = "정기모임 삭제") + fun delete( + @PathVariable sessionId: Long, + ): CommonResponse { + manageSessionUseCase.delete(sessionId) + return CommonResponse.success(SessionResponseCode.SESSION_DELETE_SUCCESS) + } + + @GetMapping + @Operation(summary = "정기모임 목록 조회") + fun getSessionInfos( + @RequestParam(required = false) cardinal: Int?, + ): CommonResponse = + CommonResponse.success( + SessionResponseCode.SESSION_INFOS_FIND_SUCCESS, + getSessionQueryService.findSessionInfos(cardinal), + ) +} diff --git a/src/main/kotlin/com/weeth/domain/session/presentation/SessionController.kt b/src/main/kotlin/com/weeth/domain/session/presentation/SessionController.kt new file mode 100644 index 00000000..310282b1 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/session/presentation/SessionController.kt @@ -0,0 +1,31 @@ +package com.weeth.domain.session.presentation + +import com.weeth.domain.schedule.application.dto.response.SessionResponse +import com.weeth.domain.session.application.exception.SessionErrorCode +import com.weeth.domain.session.application.usecase.query.GetSessionQueryService +import com.weeth.global.auth.annotation.CurrentUser +import com.weeth.global.common.exception.ApiErrorCodeExample +import com.weeth.global.common.response.CommonResponse +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@Tag(name = "SESSION", description = "정기모임 API") +@RestController +@RequestMapping("/api/v4/sessions") +@ApiErrorCodeExample(SessionErrorCode::class) +class SessionController( + private val getSessionQueryService: GetSessionQueryService, +) { + @GetMapping("/{sessionId}") + @Operation(summary = "정기모임 상세 조회") + fun getSession( + @Parameter(hidden = true) @CurrentUser userId: Long, + @PathVariable sessionId: Long, + ): CommonResponse = + CommonResponse.success(SessionResponseCode.SESSION_FIND_SUCCESS, getSessionQueryService.findSession(userId, sessionId)) +} diff --git a/src/main/kotlin/com/weeth/domain/session/presentation/SessionResponseCode.kt b/src/main/kotlin/com/weeth/domain/session/presentation/SessionResponseCode.kt new file mode 100644 index 00000000..78f20ead --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/session/presentation/SessionResponseCode.kt @@ -0,0 +1,19 @@ +package com.weeth.domain.session.presentation + +import com.weeth.global.common.response.ResponseCodeInterface +import org.springframework.http.HttpStatus + +enum class SessionResponseCode( + override val code: Int, + override val status: HttpStatus, + override val message: String, +) : ResponseCodeInterface { + // SessionAdminController 관련 + SESSION_INFOS_FIND_SUCCESS(1206, HttpStatus.OK, "기수별 정기모임 리스트를 성공적으로 조회했습니다."), + SESSION_SAVE_SUCCESS(1207, HttpStatus.OK, "정기모임이 성공적으로 생성되었습니다."), + SESSION_UPDATE_SUCCESS(1208, HttpStatus.OK, "정기모임이 성공적으로 수정되었습니다."), + SESSION_DELETE_SUCCESS(1209, HttpStatus.OK, "정기모임이 성공적으로 삭제되었습니다."), + + // SessionController 관련 + SESSION_FIND_SUCCESS(1210, HttpStatus.OK, "정기모임이 성공적으로 조회되었습니다."), +} diff --git a/src/main/kotlin/com/weeth/domain/user/application/usecase/command/AdminUserUseCase.kt b/src/main/kotlin/com/weeth/domain/user/application/usecase/command/AdminUserUseCase.kt index df19f100..af9fccfb 100644 --- a/src/main/kotlin/com/weeth/domain/user/application/usecase/command/AdminUserUseCase.kt +++ b/src/main/kotlin/com/weeth/domain/user/application/usecase/command/AdminUserUseCase.kt @@ -1,8 +1,8 @@ package com.weeth.domain.user.application.usecase.command -import com.weeth.domain.attendance.domain.service.AttendanceSaveService -import com.weeth.domain.schedule.domain.entity.Meeting -import com.weeth.domain.schedule.domain.service.MeetingGetService +import com.weeth.domain.attendance.domain.entity.Attendance +import com.weeth.domain.attendance.domain.repository.AttendanceRepository +import com.weeth.domain.session.domain.repository.SessionReader import com.weeth.domain.user.application.dto.request.UserApplyObRequest import com.weeth.domain.user.application.dto.request.UserIdsRequest import com.weeth.domain.user.application.dto.request.UserRoleUpdateRequest @@ -20,8 +20,8 @@ import org.springframework.transaction.annotation.Transactional @Service class AdminUserUseCase( private val userReader: UserReader, - private val attendanceSaveService: AttendanceSaveService, - private val meetingGetService: MeetingGetService, + private val sessionReader: SessionReader, + private val attendanceRepository: AttendanceRepository, private val cardinalRepository: CardinalRepository, private val userCardinalRepository: UserCardinalRepository, private val userCardinalPolicy: UserCardinalPolicy, @@ -33,8 +33,8 @@ class AdminUserUseCase( val cardinal = userCardinalPolicy.getCurrentCardinal(user).cardinalNumber if (user.isInactive()) { user.accept() - val meetings: List = meetingGetService.find(cardinal) - attendanceSaveService.init(user, meetings) + val sessions = sessionReader.findAllByCardinal(cardinal) + attendanceRepository.saveAll(sessions.map { Attendance.create(it, user) }) } } } @@ -88,10 +88,13 @@ class AdminUserUseCase( } if (initNeededByCardinal.isNotEmpty()) { - val meetingsMap = meetingGetService.findByCardinals(initNeededByCardinal.keys.toList()) + val sessionsByCardinal = + sessionReader.findAllByCardinalIn(initNeededByCardinal.keys.toList()).groupBy { it.cardinal } initNeededByCardinal.forEach { (cardinalNumber, usersToInit) -> - val meetings = meetingsMap[cardinalNumber] ?: emptyList() - usersToInit.forEach { attendanceSaveService.init(it, meetings) } + val sessions = sessionsByCardinal[cardinalNumber] ?: emptyList() + usersToInit.forEach { user -> + attendanceRepository.saveAll(sessions.map { Attendance.create(it, user) }) + } } } diff --git a/src/main/kotlin/com/weeth/domain/user/domain/repository/CardinalReader.kt b/src/main/kotlin/com/weeth/domain/user/domain/repository/CardinalReader.kt index ae02e8bf..9b2a50cb 100644 --- a/src/main/kotlin/com/weeth/domain/user/domain/repository/CardinalReader.kt +++ b/src/main/kotlin/com/weeth/domain/user/domain/repository/CardinalReader.kt @@ -5,6 +5,11 @@ import com.weeth.domain.user.domain.entity.Cardinal interface CardinalReader { fun getByCardinalNumber(cardinalNumber: Int): Cardinal + fun getByYearAndSemester( + year: Int, + semester: Int, + ): Cardinal + fun findByIdOrNull(cardinalId: Long): Cardinal? fun findAllByCardinalNumberDesc(): List diff --git a/src/main/kotlin/com/weeth/domain/user/domain/repository/CardinalRepository.kt b/src/main/kotlin/com/weeth/domain/user/domain/repository/CardinalRepository.kt index 0fd0d865..e10a284d 100644 --- a/src/main/kotlin/com/weeth/domain/user/domain/repository/CardinalRepository.kt +++ b/src/main/kotlin/com/weeth/domain/user/domain/repository/CardinalRepository.kt @@ -27,6 +27,11 @@ interface CardinalRepository : override fun getByCardinalNumber(cardinalNumber: Int): Cardinal = findByCardinalNumber(cardinalNumber).orElseThrow { CardinalNotFoundException() } + override fun getByYearAndSemester( + year: Int, + semester: Int, + ): Cardinal = findByYearAndSemester(year, semester).orElseThrow { CardinalNotFoundException() } + override fun findByIdOrNull(cardinalId: Long): Cardinal? = findById(cardinalId).orElse(null) override fun findAllByCardinalNumberDesc(): List = findAllByOrderByCardinalNumberDesc() diff --git a/src/main/kotlin/com/weeth/domain/user/domain/repository/UserReader.kt b/src/main/kotlin/com/weeth/domain/user/domain/repository/UserReader.kt index 8e7c60fb..6092e9b4 100644 --- a/src/main/kotlin/com/weeth/domain/user/domain/repository/UserReader.kt +++ b/src/main/kotlin/com/weeth/domain/user/domain/repository/UserReader.kt @@ -1,6 +1,8 @@ package com.weeth.domain.user.domain.repository +import com.weeth.domain.user.domain.entity.Cardinal import com.weeth.domain.user.domain.entity.User +import com.weeth.domain.user.domain.entity.enums.Status interface UserReader { fun getById(userId: Long): User @@ -10,4 +12,9 @@ interface UserReader { fun findByIdOrNull(userId: Long): User? fun findAllByIds(userIds: List): List + + fun findAllByCardinalAndStatus( + cardinal: Cardinal, + status: Status, + ): List } diff --git a/src/main/kotlin/com/weeth/domain/user/domain/repository/UserRepository.kt b/src/main/kotlin/com/weeth/domain/user/domain/repository/UserRepository.kt index 47ec7f96..c11fefcf 100644 --- a/src/main/kotlin/com/weeth/domain/user/domain/repository/UserRepository.kt +++ b/src/main/kotlin/com/weeth/domain/user/domain/repository/UserRepository.kt @@ -55,7 +55,7 @@ interface UserRepository : fun findAllByOrderByNameAsc(): List @Query("SELECT uc.user FROM UserCardinal uc WHERE uc.cardinal = :cardinal AND uc.user.status = :status") - fun findAllByCardinalAndStatus( + override fun findAllByCardinalAndStatus( @Param("cardinal") cardinal: Cardinal, @Param("status") status: Status, ): List diff --git a/src/main/kotlin/com/weeth/global/common/controller/ExceptionDocController.kt b/src/main/kotlin/com/weeth/global/common/controller/ExceptionDocController.kt index 1d67f2c0..1bc82ac0 100644 --- a/src/main/kotlin/com/weeth/global/common/controller/ExceptionDocController.kt +++ b/src/main/kotlin/com/weeth/global/common/controller/ExceptionDocController.kt @@ -6,7 +6,7 @@ import com.weeth.domain.board.application.exception.BoardErrorCode import com.weeth.domain.comment.application.exception.CommentErrorCode import com.weeth.domain.penalty.application.exception.PenaltyErrorCode import com.weeth.domain.schedule.application.exception.EventErrorCode -import com.weeth.domain.schedule.application.exception.MeetingErrorCode +import com.weeth.domain.session.application.exception.SessionErrorCode import com.weeth.domain.user.application.exception.UserErrorCode import com.weeth.global.auth.jwt.application.exception.JwtErrorCode import com.weeth.global.common.exception.ApiErrorCodeExample @@ -28,7 +28,7 @@ class ExceptionDocController { @GetMapping("/attendance") @Operation(summary = "Attendance 도메인 에러 코드 목록") - @ApiErrorCodeExample(AttendanceErrorCode::class) + @ApiErrorCodeExample(AttendanceErrorCode::class, SessionErrorCode::class) fun attendanceErrorCodes() { } @@ -46,7 +46,7 @@ class ExceptionDocController { @GetMapping("/schedule") @Operation(summary = "Schedule 도메인 에러 코드 목록") - @ApiErrorCodeExample(EventErrorCode::class, MeetingErrorCode::class) + @ApiErrorCodeExample(EventErrorCode::class) fun scheduleErrorCodes() { } diff --git a/src/main/kotlin/com/weeth/global/config/SwaggerConfig.kt b/src/main/kotlin/com/weeth/global/config/SwaggerConfig.kt index a6763ffe..d174e817 100644 --- a/src/main/kotlin/com/weeth/global/config/SwaggerConfig.kt +++ b/src/main/kotlin/com/weeth/global/config/SwaggerConfig.kt @@ -33,7 +33,7 @@ private const val SWAGGER_DESCRIPTION = "| Domain | Success | Error |\n" + "|--------|---------|------|\n" + "| Account | 11xx | 21xx |\n" + - "| Attendance | 12xx | 22xx |\n" + + "| Attendance/Session | 12xx | 22xx |\n" + "| Board | 13xx | 23xx |\n" + "| Comment | 14xx | 24xx |\n" + "| File | 15xx | 25xx |\n" + diff --git a/src/test/kotlin/com/weeth/domain/attendance/application/mapper/AttendanceMapperTest.kt b/src/test/kotlin/com/weeth/domain/attendance/application/mapper/AttendanceMapperTest.kt index d9b6a8b6..700475d2 100644 --- a/src/test/kotlin/com/weeth/domain/attendance/application/mapper/AttendanceMapperTest.kt +++ b/src/test/kotlin/com/weeth/domain/attendance/application/mapper/AttendanceMapperTest.kt @@ -3,10 +3,10 @@ package com.weeth.domain.attendance.application.mapper import com.weeth.domain.attendance.fixture.AttendanceTestFixture.createActiveUser import com.weeth.domain.attendance.fixture.AttendanceTestFixture.createAdminUser import com.weeth.domain.attendance.fixture.AttendanceTestFixture.createAttendance -import com.weeth.domain.attendance.fixture.AttendanceTestFixture.createOneDayMeeting import com.weeth.domain.attendance.fixture.AttendanceTestFixture.enrichUserProfile import com.weeth.domain.attendance.fixture.AttendanceTestFixture.setAttendanceId import com.weeth.domain.attendance.fixture.AttendanceTestFixture.setUserAttendanceStats +import com.weeth.domain.session.fixture.SessionTestFixture.createOneDaySession import io.kotest.core.spec.style.DescribeSpec import io.kotest.matchers.nulls.shouldBeNull import io.kotest.matchers.nulls.shouldNotBeNull @@ -20,18 +20,18 @@ class AttendanceMapperTest : describe("toSummaryResponse") { it("사용자 + 당일 출석 객체를 MainResponse로 매핑한다") { val today = LocalDate.now() - val meeting = createOneDayMeeting(today, 1, 1111, "Today") + val session = createOneDaySession(today, 1, 1111, "Today") val user = createActiveUser("이지훈") - val attendance = createAttendance(meeting, user) + val attendance = createAttendance(session, user) val main = mapper.toSummaryResponse(user, attendance) main.shouldNotBeNull() - main.title shouldBe meeting.title + main.title shouldBe session.title main.status shouldBe attendance.status - main.start shouldBe meeting.start - main.end shouldBe meeting.end - main.location shouldBe meeting.location + main.start shouldBe session.start + main.end shouldBe session.end + main.location shouldBe session.location } it("attendance가 null이면 필드는 null로 매핑") { @@ -48,57 +48,57 @@ class AttendanceMapperTest : it("일반 유저는 출석 코드가 null로 매핑된다") { val today = LocalDate.now() - val meeting = createOneDayMeeting(today, 1, 1234, "Today") + val session = createOneDaySession(today, 1, 1234, "Today") val user = createActiveUser("일반유저") - val attendance = createAttendance(meeting, user) + val attendance = createAttendance(session, user) val main = mapper.toSummaryResponse(user, attendance) main.shouldNotBeNull() main.code.shouldBeNull() - main.title shouldBe meeting.title + main.title shouldBe session.title main.status shouldBe attendance.status } it("ADMIN 유저는 출석 코드가 포함된다") { val today = LocalDate.now() val expectedCode = 1234 - val meeting = createOneDayMeeting(today, 1, expectedCode, "Today") + val session = createOneDaySession(today, 1, expectedCode, "Today") val adminUser = createAdminUser("관리자") - val attendance = createAttendance(meeting, adminUser) + val attendance = createAttendance(session, adminUser) val main = mapper.toSummaryResponse(adminUser, attendance, isAdmin = true) main.shouldNotBeNull() main.code shouldBe expectedCode - main.title shouldBe meeting.title - main.start shouldBe meeting.start - main.end shouldBe meeting.end - main.location shouldBe meeting.location + main.title shouldBe session.title + main.start shouldBe session.start + main.end shouldBe session.end + main.location shouldBe session.location } } describe("toResponse") { it("단일 출석을 AttendanceResponse로 매핑한다") { - val meeting = createOneDayMeeting(LocalDate.now().minusDays(1), 1, 2222, "D-1") + val session = createOneDaySession(LocalDate.now().minusDays(1), 1, 2222, "D-1") val user = createActiveUser("사용자A") - val attendance = createAttendance(meeting, user) + val attendance = createAttendance(session, user) val response = mapper.toResponse(attendance) response.shouldNotBeNull() - response.title shouldBe meeting.title - response.start shouldBe meeting.start - response.end shouldBe meeting.end - response.location shouldBe meeting.location + response.title shouldBe session.title + response.start shouldBe session.start + response.end shouldBe session.end + response.location shouldBe session.location } } describe("toDetailResponse") { it("사용자 + Response 리스트를 DetailResponse로 매핑(total = attend + absence)") { val base = LocalDate.now() - val m1 = createOneDayMeeting(base.minusDays(2), 1, 1000, "D-2") - val m2 = createOneDayMeeting(base.minusDays(1), 1, 1001, "D-1") + val m1 = createOneDaySession(base.minusDays(2), 1, 1000, "D-2") + val m2 = createOneDaySession(base.minusDays(1), 1, 1001, "D-1") val user = createActiveUser("이지훈") setUserAttendanceStats(user, 3, 2) @@ -118,11 +118,11 @@ class AttendanceMapperTest : describe("toInfoResponse") { it("Attendance를 InfoResponse로 매핑") { - val meeting = createOneDayMeeting(LocalDate.now(), 1, 3333, "Info") + val session = createOneDaySession(LocalDate.now(), 1, 3333, "Info") val user = createActiveUser("유저B") enrichUserProfile(user, "컴퓨터공학과", "20201234") - val attendance = createAttendance(meeting, user) + val attendance = createAttendance(session, user) setAttendanceId(attendance, 10L) val info = mapper.toInfoResponse(attendance) diff --git a/src/test/kotlin/com/weeth/domain/attendance/application/usecase/command/CheckInAttendanceUseCaseTest.kt b/src/test/kotlin/com/weeth/domain/attendance/application/usecase/command/CheckInAttendanceUseCaseTest.kt deleted file mode 100644 index 72fdd612..00000000 --- a/src/test/kotlin/com/weeth/domain/attendance/application/usecase/command/CheckInAttendanceUseCaseTest.kt +++ /dev/null @@ -1,88 +0,0 @@ -package com.weeth.domain.attendance.application.usecase.command - -import com.weeth.domain.attendance.application.exception.AttendanceCodeMismatchException -import com.weeth.domain.attendance.application.exception.AttendanceNotFoundException -import com.weeth.domain.attendance.domain.entity.Attendance -import com.weeth.domain.attendance.domain.enums.Status -import com.weeth.domain.attendance.domain.repository.AttendanceRepository -import com.weeth.domain.user.domain.entity.User -import com.weeth.domain.user.domain.repository.UserReader -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.core.spec.style.DescribeSpec -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify - -class CheckInAttendanceUseCaseTest : - DescribeSpec({ - - val userId = 10L - val userReader = mockk() - val attendanceRepository = mockk() - - val useCase = CheckInAttendanceUseCase(userReader, attendanceRepository) - - describe("checkIn") { - context("진행 중 정기모임이고 코드 일치하며 상태가 ATTEND가 아닐 때") { - it("출석 처리된다") { - val user = mockk() - val attendance = mockk(relaxUnitFun = true) - every { attendance.isWrong(1234) } returns false - every { attendance.status } returns Status.PENDING - - every { userReader.getById(userId) } returns user - every { attendanceRepository.findCurrentByUserId(eq(userId), any(), any()) } returns attendance - every { user.attend() } returns Unit - - useCase.checkIn(userId, 1234) - - verify { attendance.attend() } - verify { user.attend() } - } - } - - context("진행 중 정기모임이 없을 때") { - it("AttendanceNotFoundException") { - val user = mockk() - every { userReader.getById(userId) } returns user - every { attendanceRepository.findCurrentByUserId(eq(userId), any(), any()) } returns null - - shouldThrow { - useCase.checkIn(userId, 1234) - } - } - } - - context("코드 불일치 시") { - it("AttendanceCodeMismatchException") { - val user = mockk() - val attendance = mockk() - every { attendance.isWrong(9999) } returns true - - every { userReader.getById(userId) } returns user - every { attendanceRepository.findCurrentByUserId(eq(userId), any(), any()) } returns attendance - - shouldThrow { - useCase.checkIn(userId, 9999) - } - } - } - - context("이미 ATTEND일 때") { - it("추가 처리 없이 종료") { - val user = mockk() - val attendance = mockk() - every { attendance.isWrong(1234) } returns false - every { attendance.status } returns Status.ATTEND - - every { userReader.getById(userId) } returns user - every { attendanceRepository.findCurrentByUserId(eq(userId), any(), any()) } returns attendance - - useCase.checkIn(userId, 1234) - - verify(exactly = 0) { attendance.attend() } - verify(exactly = 0) { user.attend() } - } - } - } - }) diff --git a/src/test/kotlin/com/weeth/domain/attendance/application/usecase/command/CloseAttendanceUseCaseTest.kt b/src/test/kotlin/com/weeth/domain/attendance/application/usecase/command/CloseAttendanceUseCaseTest.kt deleted file mode 100644 index 63c3dd68..00000000 --- a/src/test/kotlin/com/weeth/domain/attendance/application/usecase/command/CloseAttendanceUseCaseTest.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.weeth.domain.attendance.application.usecase.command - -import com.weeth.domain.attendance.domain.entity.Attendance -import com.weeth.domain.attendance.domain.repository.AttendanceRepository -import com.weeth.domain.attendance.fixture.AttendanceTestFixture.createOneDayMeeting -import com.weeth.domain.schedule.application.exception.MeetingNotFoundException -import com.weeth.domain.schedule.domain.service.MeetingGetService -import com.weeth.domain.user.domain.entity.User -import com.weeth.domain.user.domain.entity.enums.Status -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.core.spec.style.DescribeSpec -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify -import java.time.LocalDate - -class CloseAttendanceUseCaseTest : - DescribeSpec({ - - val meetingGetService = mockk() - val attendanceRepository = mockk() - - val useCase = CloseAttendanceUseCase(meetingGetService, attendanceRepository) - - describe("close") { - it("당일 정기모임을 찾아 pending 출석을 close") { - val now = LocalDate.now() - val targetMeeting = createOneDayMeeting(now, 1, 1111, "Today") - val otherMeeting = createOneDayMeeting(now.minusDays(1), 1, 9999, "Yesterday") - - val pendingAttendance = mockk(relaxUnitFun = true) - val attendedAttendance = mockk(relaxUnitFun = true) - val pendingUser = mockk(relaxUnitFun = true) - - every { pendingAttendance.isPending } returns true - every { pendingAttendance.user } returns pendingUser - every { attendedAttendance.isPending } returns false - - every { meetingGetService.find(1) } returns listOf(targetMeeting, otherMeeting) - every { - attendanceRepository.findAllByMeetingAndUserStatus(targetMeeting, Status.ACTIVE) - } returns listOf(pendingAttendance, attendedAttendance) - - useCase.close(now, 1) - - verify { pendingAttendance.close() } - verify { pendingUser.absent() } - verify(exactly = 0) { attendedAttendance.close() } - } - - it("당일 정기모임이 없으면 MeetingNotFoundException") { - val now = LocalDate.now() - val otherDayMeeting = createOneDayMeeting(now.minusDays(1), 1, 9999, "Yesterday") - - every { meetingGetService.find(1) } returns listOf(otherDayMeeting) - - shouldThrow { - useCase.close(now, 1) - } - } - } - }) diff --git a/src/test/kotlin/com/weeth/domain/attendance/application/usecase/command/UpdateAttendanceStatusUseCaseTest.kt b/src/test/kotlin/com/weeth/domain/attendance/application/usecase/command/UpdateAttendanceStatusUseCaseTest.kt deleted file mode 100644 index ebbecb4e..00000000 --- a/src/test/kotlin/com/weeth/domain/attendance/application/usecase/command/UpdateAttendanceStatusUseCaseTest.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.weeth.domain.attendance.application.usecase.command - -import com.weeth.domain.attendance.application.dto.request.UpdateAttendanceStatusRequest -import com.weeth.domain.attendance.application.exception.AttendanceNotFoundException -import com.weeth.domain.attendance.domain.entity.Attendance -import com.weeth.domain.attendance.domain.repository.AttendanceRepository -import com.weeth.domain.user.domain.entity.User -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.core.spec.style.DescribeSpec -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify - -class UpdateAttendanceStatusUseCaseTest : - DescribeSpec({ - - val attendanceRepository = mockk() - - val useCase = UpdateAttendanceStatusUseCase(attendanceRepository) - - describe("updateStatus") { - context("ABSENT로 변경 시") { - it("close + removeAttend + absent 호출") { - val user = mockk(relaxUnitFun = true) - val attendance = mockk(relaxUnitFun = true) - every { attendance.user } returns user - - every { attendanceRepository.findByIdWithUser(1L) } returns attendance - - val request = UpdateAttendanceStatusRequest(attendanceId = 1L, status = "ABSENT") - useCase.updateStatus(listOf(request)) - - verify { attendance.close() } - verify { user.removeAttend() } - verify { user.absent() } - } - } - - context("ATTEND로 변경 시") { - it("attend + removeAbsent + attend 호출") { - val user = mockk(relaxUnitFun = true) - val attendance = mockk(relaxUnitFun = true) - every { attendance.user } returns user - - every { attendanceRepository.findByIdWithUser(1L) } returns attendance - - val request = UpdateAttendanceStatusRequest(attendanceId = 1L, status = "ATTEND") - useCase.updateStatus(listOf(request)) - - verify { attendance.attend() } - verify { user.removeAbsent() } - verify { user.attend() } - } - } - - context("출석 정보가 없을 때") { - it("AttendanceNotFoundException") { - every { attendanceRepository.findByIdWithUser(999L) } returns null - - val request = UpdateAttendanceStatusRequest(attendanceId = 999L, status = "ABSENT") - - shouldThrow { - useCase.updateStatus(listOf(request)) - } - } - } - } - }) diff --git a/src/test/kotlin/com/weeth/domain/attendance/application/usecase/query/GetAttendanceQueryServiceTest.kt b/src/test/kotlin/com/weeth/domain/attendance/application/usecase/query/GetAttendanceQueryServiceTest.kt index a3474d13..d098b66d 100644 --- a/src/test/kotlin/com/weeth/domain/attendance/application/usecase/query/GetAttendanceQueryServiceTest.kt +++ b/src/test/kotlin/com/weeth/domain/attendance/application/usecase/query/GetAttendanceQueryServiceTest.kt @@ -8,8 +8,8 @@ import com.weeth.domain.attendance.application.mapper.AttendanceMapper import com.weeth.domain.attendance.domain.entity.Attendance import com.weeth.domain.attendance.domain.repository.AttendanceRepository import com.weeth.domain.attendance.fixture.AttendanceTestFixture.createActiveUser -import com.weeth.domain.schedule.domain.entity.Meeting -import com.weeth.domain.schedule.domain.service.MeetingGetService +import com.weeth.domain.session.domain.entity.Session +import com.weeth.domain.session.domain.repository.SessionReader import com.weeth.domain.user.domain.entity.Cardinal import com.weeth.domain.user.domain.entity.enums.Status import com.weeth.domain.user.domain.repository.UserReader @@ -25,7 +25,7 @@ class GetAttendanceQueryServiceTest : val userReader = mockk() val userCardinalPolicy = mockk() - val meetingGetService = mockk() + val sessionReader = mockk() val attendanceRepository = mockk() val attendanceMapper = mockk() @@ -33,7 +33,7 @@ class GetAttendanceQueryServiceTest : GetAttendanceQueryService( userReader, userCardinalPolicy, - meetingGetService, + sessionReader, attendanceRepository, attendanceMapper, ) @@ -103,23 +103,23 @@ class GetAttendanceQueryServiceTest : } } - describe("findAllAttendanceByMeeting") { + describe("findAllAttendanceBySession") { it("해당 정기모임의 출석 정보를 조회") { - val meetingId = 1L - val meeting = mockk() + val sessionId = 1L + val session = mockk() val attendance1 = mockk() val attendance2 = mockk() val response1 = mockk() val response2 = mockk() - every { meetingGetService.find(meetingId) } returns meeting + every { sessionReader.getById(sessionId) } returns session every { - attendanceRepository.findAllByMeetingAndUserStatus(meeting, Status.ACTIVE) + attendanceRepository.findAllBySessionAndUserStatus(session, Status.ACTIVE) } returns listOf(attendance1, attendance2) every { attendanceMapper.toInfoResponse(attendance1) } returns response1 every { attendanceMapper.toInfoResponse(attendance2) } returns response2 - val result = queryService.findAllAttendanceByMeeting(meetingId) + val result = queryService.findAllAttendanceBySession(sessionId) result shouldBe listOf(response1, response2) } diff --git a/src/test/kotlin/com/weeth/domain/attendance/domain/entity/AttendanceTest.kt b/src/test/kotlin/com/weeth/domain/attendance/domain/entity/AttendanceTest.kt index bc61965e..f58eb13a 100644 --- a/src/test/kotlin/com/weeth/domain/attendance/domain/entity/AttendanceTest.kt +++ b/src/test/kotlin/com/weeth/domain/attendance/domain/entity/AttendanceTest.kt @@ -1,89 +1,70 @@ package com.weeth.domain.attendance.domain.entity -import com.weeth.domain.attendance.domain.enums.Status +import com.weeth.domain.attendance.domain.entity.enums.AttendanceStatus import com.weeth.domain.attendance.fixture.AttendanceTestFixture.createActiveUser import com.weeth.domain.attendance.fixture.AttendanceTestFixture.createAttendance -import com.weeth.domain.schedule.fixture.ScheduleTestFixture.createMeeting +import com.weeth.domain.session.fixture.SessionTestFixture.createOneDaySession import io.kotest.core.spec.style.DescribeSpec import io.kotest.matchers.shouldBe +import java.time.LocalDate class AttendanceTest : DescribeSpec({ + val session = createOneDaySession(LocalDate.now(), 1, 1234, "테스트") + describe("attend") { it("상태를 ATTEND로 변경한다") { - val meeting = createMeeting() val user = createActiveUser("테스트유저") - val attendance = createAttendance(meeting, user) - attendance.init() + val attendance = createAttendance(session, user) attendance.attend() - attendance.status shouldBe Status.ATTEND + attendance.status shouldBe AttendanceStatus.ATTEND } } describe("close") { it("상태를 ABSENT로 변경한다") { - val meeting = createMeeting() val user = createActiveUser("테스트유저") - val attendance = createAttendance(meeting, user) - attendance.init() + val attendance = createAttendance(session, user) attendance.close() - attendance.status shouldBe Status.ABSENT + attendance.status shouldBe AttendanceStatus.ABSENT } } describe("isPending") { it("상태가 PENDING이면 true를 반환한다") { - val meeting = createMeeting() val user = createActiveUser("테스트유저") - val attendance = createAttendance(meeting, user) - attendance.init() + val attendance = createAttendance(session, user) - attendance.isPending shouldBe true + attendance.isPending() shouldBe true } it("상태가 PENDING이 아니면 false를 반환한다") { - val meeting = createMeeting() val user = createActiveUser("테스트유저") - val attendance = createAttendance(meeting, user) - attendance.init() + val attendance = createAttendance(session, user) attendance.attend() - attendance.isPending shouldBe false + attendance.isPending() shouldBe false } } describe("isWrong") { it("코드가 일치하지 않으면 true를 반환한다") { - val meeting = createMeeting() val user = createActiveUser("테스트유저") - val attendance = createAttendance(meeting, user) + val attendance = createAttendance(session, user) attendance.isWrong(9999) shouldBe true } it("코드가 일치하면 false를 반환한다") { - val meeting = createMeeting() val user = createActiveUser("테스트유저") - val attendance = createAttendance(meeting, user) + val attendance = createAttendance(session, user) attendance.isWrong(1234) shouldBe false } } - - describe("init") { - it("상태를 PENDING으로 초기화한다") { - val meeting = createMeeting() - val user = createActiveUser("테스트유저") - val attendance = createAttendance(meeting, user) - - attendance.init() - - attendance.status shouldBe Status.PENDING - } - } }) diff --git a/src/test/kotlin/com/weeth/domain/attendance/domain/repository/AttendanceRepositoryTest.kt b/src/test/kotlin/com/weeth/domain/attendance/domain/repository/AttendanceRepositoryTest.kt index 6744c1e2..1b4fd3c8 100644 --- a/src/test/kotlin/com/weeth/domain/attendance/domain/repository/AttendanceRepositoryTest.kt +++ b/src/test/kotlin/com/weeth/domain/attendance/domain/repository/AttendanceRepositoryTest.kt @@ -2,9 +2,9 @@ package com.weeth.domain.attendance.domain.repository import com.weeth.config.TestContainersConfig import com.weeth.domain.attendance.domain.entity.Attendance -import com.weeth.domain.schedule.domain.entity.Meeting -import com.weeth.domain.schedule.domain.entity.enums.MeetingStatus -import com.weeth.domain.schedule.domain.repository.MeetingRepository +import com.weeth.domain.session.domain.entity.Session +import com.weeth.domain.session.domain.entity.enums.SessionStatus +import com.weeth.domain.session.domain.repository.SessionRepository import com.weeth.domain.user.domain.entity.User import com.weeth.domain.user.domain.entity.enums.Status import com.weeth.domain.user.domain.repository.UserRepository @@ -22,26 +22,25 @@ import java.time.LocalDateTime @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) class AttendanceRepositoryTest( private val attendanceRepository: AttendanceRepository, - private val meetingRepository: MeetingRepository, + private val sessionRepository: SessionRepository, private val userRepository: UserRepository, ) : DescribeSpec({ - lateinit var meeting: Meeting + lateinit var session: Session lateinit var activeUser1: User lateinit var activeUser2: User beforeEach { - meeting = - Meeting - .builder() - .title("1차 정기모임") - .start(LocalDateTime.now().minusHours(1)) - .end(LocalDateTime.now().plusHours(1)) - .code(1234) - .cardinal(1) - .meetingStatus(MeetingStatus.OPEN) - .build() - meetingRepository.save(meeting) + session = + Session( + title = "1차 정기모임", + start = LocalDateTime.now().minusHours(1), + end = LocalDateTime.now().plusHours(1), + code = 1234, + cardinal = 1, + status = SessionStatus.OPEN, + ) + sessionRepository.save(session) activeUser1 = User( @@ -58,22 +57,22 @@ class AttendanceRepositoryTest( activeUser2.accept() userRepository.saveAll(listOf(activeUser1, activeUser2)) - attendanceRepository.save(Attendance(meeting, activeUser1)) - attendanceRepository.save(Attendance(meeting, activeUser2)) + attendanceRepository.save(Attendance.create(session, activeUser1)) + attendanceRepository.save(Attendance.create(session, activeUser2)) } - describe("findAllByMeetingAndUserStatus") { - it("특정 정기모임 + 사용자 상태로 출석 목록 조회") { - val attendances = attendanceRepository.findAllByMeetingAndUserStatus(meeting, Status.ACTIVE) + describe("findAllBySessionAndUserStatus") { + it("특정 세션 + 사용자 상태로 출석 목록 조회") { + val attendances = attendanceRepository.findAllBySessionAndUserStatus(session, Status.ACTIVE) attendances shouldHaveSize 2 attendances.map { it.user.name } shouldContainExactlyInAnyOrder listOf("이지훈", "이강혁") } } - describe("deleteAllByMeeting") { - it("특정 정기모임의 모든 출석 레코드 삭제") { - attendanceRepository.deleteAllByMeeting(meeting) + describe("deleteAllBySession") { + it("특정 세션의 모든 출석 레코드 삭제") { + attendanceRepository.deleteAllBySession(session) attendanceRepository.findAll().shouldBeEmpty() } diff --git a/src/test/kotlin/com/weeth/domain/attendance/domain/service/AttendanceSaveServiceTest.kt b/src/test/kotlin/com/weeth/domain/attendance/domain/service/AttendanceSaveServiceTest.kt deleted file mode 100644 index eec1deac..00000000 --- a/src/test/kotlin/com/weeth/domain/attendance/domain/service/AttendanceSaveServiceTest.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.weeth.domain.attendance.domain.service - -import com.weeth.domain.attendance.domain.entity.Attendance -import com.weeth.domain.attendance.domain.repository.AttendanceRepository -import com.weeth.domain.attendance.fixture.AttendanceTestFixture.createActiveUser -import com.weeth.domain.schedule.fixture.ScheduleTestFixture.createMeeting -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.collections.shouldHaveSize -import io.kotest.matchers.shouldBe -import io.mockk.every -import io.mockk.mockk -import io.mockk.slot -import io.mockk.verify - -class AttendanceSaveServiceTest : - DescribeSpec({ - - val attendanceRepository = mockk() - val attendanceSaveService = AttendanceSaveService(attendanceRepository) - - describe("init") { - it("각 정기모임에 대한 Attendance를 저장한다") { - val user = mockk() - val meetingFirst = createMeeting() - val meetingSecond = createMeeting() - - every { attendanceRepository.save(any()) } answers { firstArg() } - - attendanceSaveService.init(user, listOf(meetingFirst, meetingSecond)) - - verify(exactly = 2) { attendanceRepository.save(any()) } - } - } - - describe("saveAll") { - it("사용자 수만큼 Attendance 생성 후 saveAll 호출") { - val meeting = createMeeting() - val userFirst = createActiveUser("이지훈") - val userSecond = createActiveUser("이강혁") - - val listSlot = slot>() - every { attendanceRepository.saveAll(capture(listSlot)) } answers { firstArg() } - - attendanceSaveService.saveAll(listOf(userFirst, userSecond), meeting) - - val savedAttendances = listSlot.captured - savedAttendances shouldHaveSize 2 - savedAttendances.forEach { it.meeting shouldBe meeting } - savedAttendances.map { it.user } shouldBe listOf(userFirst, userSecond) - } - } - }) diff --git a/src/test/kotlin/com/weeth/domain/attendance/fixture/AttendanceTestFixture.kt b/src/test/kotlin/com/weeth/domain/attendance/fixture/AttendanceTestFixture.kt index 53f73aab..f20c8f7f 100644 --- a/src/test/kotlin/com/weeth/domain/attendance/fixture/AttendanceTestFixture.kt +++ b/src/test/kotlin/com/weeth/domain/attendance/fixture/AttendanceTestFixture.kt @@ -1,14 +1,12 @@ package com.weeth.domain.attendance.fixture import com.weeth.domain.attendance.domain.entity.Attendance -import com.weeth.domain.schedule.domain.entity.Meeting +import com.weeth.domain.session.domain.entity.Session import com.weeth.domain.user.domain.entity.User import com.weeth.domain.user.domain.entity.enums.Role import com.weeth.domain.user.domain.entity.enums.Status import com.weeth.domain.user.domain.vo.AttendanceStats import org.springframework.test.util.ReflectionTestUtils -import java.time.LocalDate -import java.time.LocalDateTime object AttendanceTestFixture { fun createActiveUser(name: String): User = @@ -25,40 +23,9 @@ object AttendanceTestFixture { ) fun createAttendance( - meeting: Meeting, + session: Session, user: User, - ): Attendance = Attendance(meeting, user) - - fun createOneDayMeeting( - date: LocalDate, - cardinal: Int, - code: Int, - title: String, - ): Meeting = - Meeting - .builder() - .title(title) - .location("Test Location") - .start(date.atTime(10, 0)) - .end(date.atTime(12, 0)) - .code(code) - .cardinal(cardinal) - .build() - - fun createInProgressMeeting( - cardinal: Int, - code: Int, - title: String, - ): Meeting = - Meeting - .builder() - .title(title) - .location("Test Location") - .start(LocalDateTime.now().minusMinutes(5)) - .end(LocalDateTime.now().plusMinutes(5)) - .code(code) - .cardinal(cardinal) - .build() + ): Attendance = Attendance.create(session, user) fun setAttendanceId( attendance: Attendance, diff --git a/src/test/kotlin/com/weeth/domain/schedule/fixture/ScheduleTestFixture.kt b/src/test/kotlin/com/weeth/domain/schedule/fixture/ScheduleTestFixture.kt index 8c1b03d0..39fb7995 100644 --- a/src/test/kotlin/com/weeth/domain/schedule/fixture/ScheduleTestFixture.kt +++ b/src/test/kotlin/com/weeth/domain/schedule/fixture/ScheduleTestFixture.kt @@ -1,28 +1,30 @@ package com.weeth.domain.schedule.fixture import com.weeth.domain.schedule.domain.entity.Event -import com.weeth.domain.schedule.domain.entity.Meeting +import org.springframework.test.util.ReflectionTestUtils import java.time.LocalDateTime object ScheduleTestFixture { - fun createEvent(): Event = - Event - .builder() - .title("Test Meeting") - .location("Test Location") - .start(LocalDateTime.now()) - .end(LocalDateTime.now().plusDays(2)) - .cardinal(1) - .build() - - fun createMeeting(): Meeting = - Meeting - .builder() - .title("Test Meeting") - .location("Test Location") - .start(LocalDateTime.now()) - .end(LocalDateTime.now().plusDays(2)) - .code(1234) - .cardinal(1) - .build() + fun createEvent( + id: Long = 0L, + title: String = "Test Event", + content: String = "Test Content", + location: String = "Test Location", + cardinal: Int = 1, + start: LocalDateTime = LocalDateTime.of(2026, 3, 1, 10, 0), + end: LocalDateTime = LocalDateTime.of(2026, 3, 1, 12, 0), + ): Event { + val event = + Event.create( + title = title, + content = content, + location = location, + cardinal = cardinal, + start = start, + end = end, + user = null, + ) + if (id != 0L) ReflectionTestUtils.setField(event, "id", id) + return event + } } diff --git a/src/test/kotlin/com/weeth/domain/session/domain/entity/SessionTest.kt b/src/test/kotlin/com/weeth/domain/session/domain/entity/SessionTest.kt new file mode 100644 index 00000000..22aa40b7 --- /dev/null +++ b/src/test/kotlin/com/weeth/domain/session/domain/entity/SessionTest.kt @@ -0,0 +1,26 @@ +package com.weeth.domain.session.domain.entity + +import com.weeth.domain.session.domain.entity.enums.SessionStatus +import com.weeth.domain.session.fixture.SessionTestFixture +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe + +class SessionTest : + StringSpec({ + "close는 status를 CLOSED로 변경한다" { + val session = SessionTestFixture.createSession(status = SessionStatus.OPEN) + + session.close() + + session.status shouldBe SessionStatus.CLOSED + } + + "이미 CLOSED 상태에서 close 호출 시 예외가 발생한다" { + val session = SessionTestFixture.createSession(status = SessionStatus.CLOSED) + + shouldThrow { + session.close() + } + } + }) diff --git a/src/test/kotlin/com/weeth/domain/session/fixture/SessionTestFixture.kt b/src/test/kotlin/com/weeth/domain/session/fixture/SessionTestFixture.kt new file mode 100644 index 00000000..584c6b4e --- /dev/null +++ b/src/test/kotlin/com/weeth/domain/session/fixture/SessionTestFixture.kt @@ -0,0 +1,64 @@ +package com.weeth.domain.session.fixture + +import com.weeth.domain.session.domain.entity.Session +import com.weeth.domain.session.domain.entity.enums.SessionStatus +import org.springframework.test.util.ReflectionTestUtils +import java.time.LocalDate +import java.time.LocalDateTime + +object SessionTestFixture { + fun createSession( + id: Long = 0L, + title: String = "Test Session", + content: String = "Test Content", + location: String = "Test Location", + cardinal: Int = 1, + code: Int = 1234, + status: SessionStatus = SessionStatus.OPEN, + start: LocalDateTime = LocalDateTime.of(2026, 3, 1, 10, 0), + end: LocalDateTime = LocalDateTime.of(2026, 3, 1, 12, 0), + ): Session { + val session = + Session( + title = title, + content = content, + location = location, + cardinal = cardinal, + code = code, + status = status, + start = start, + end = end, + ) + if (id != 0L) ReflectionTestUtils.setField(session, "id", id) + return session + } + + fun createOneDaySession( + date: LocalDate, + cardinal: Int, + code: Int, + title: String, + ): Session = + Session( + title = title, + location = "Test Location", + start = date.atTime(10, 0), + end = date.atTime(12, 0), + code = code, + cardinal = cardinal, + ) + + fun createInProgressSession( + cardinal: Int, + code: Int, + title: String, + ): Session = + Session( + title = title, + location = "Test Location", + start = LocalDateTime.now().minusMinutes(5), + end = LocalDateTime.now().plusMinutes(5), + code = code, + cardinal = cardinal, + ) +} diff --git a/src/test/kotlin/com/weeth/domain/user/application/usecase/command/AdminUserUseCaseTest.kt b/src/test/kotlin/com/weeth/domain/user/application/usecase/command/AdminUserUseCaseTest.kt index f4c88e7a..06c279cf 100644 --- a/src/test/kotlin/com/weeth/domain/user/application/usecase/command/AdminUserUseCaseTest.kt +++ b/src/test/kotlin/com/weeth/domain/user/application/usecase/command/AdminUserUseCaseTest.kt @@ -1,8 +1,9 @@ package com.weeth.domain.user.application.usecase.command -import com.weeth.domain.attendance.domain.service.AttendanceSaveService -import com.weeth.domain.schedule.domain.entity.Meeting -import com.weeth.domain.schedule.domain.service.MeetingGetService +import com.weeth.domain.attendance.domain.entity.Attendance +import com.weeth.domain.attendance.domain.repository.AttendanceRepository +import com.weeth.domain.session.domain.entity.Session +import com.weeth.domain.session.domain.repository.SessionReader import com.weeth.domain.user.application.dto.request.UserApplyObRequest import com.weeth.domain.user.application.dto.request.UserIdsRequest import com.weeth.domain.user.application.dto.request.UserRoleUpdateRequest @@ -25,8 +26,8 @@ import io.mockk.verify class AdminUserUseCaseTest : DescribeSpec({ val userReader = mockk() - val attendanceSaveService = mockk(relaxUnitFun = true) - val meetingGetService = mockk() + val sessionReader = mockk() + val attendanceRepository = mockk(relaxed = true) val cardinalRepository = mockk() val userCardinalRepository = mockk(relaxUnitFun = true) val userCardinalPolicy = mockk() @@ -34,8 +35,8 @@ class AdminUserUseCaseTest : val useCase = AdminUserUseCase( userReader, - attendanceSaveService, - meetingGetService, + sessionReader, + attendanceRepository, cardinalRepository, userCardinalRepository, userCardinalPolicy, @@ -44,8 +45,8 @@ class AdminUserUseCaseTest : beforeTest { clearMocks( userReader, - attendanceSaveService, - meetingGetService, + sessionReader, + attendanceRepository, cardinalRepository, userCardinalRepository, userCardinalPolicy, @@ -56,15 +57,15 @@ class AdminUserUseCaseTest : it("비활성 유저 승인 시 출석 초기화를 수행한다") { val user = UserTestFixture.createWaitingUser1(1L) val currentCardinal = CardinalTestFixture.createCardinal(id = 1L, cardinalNumber = 8, year = 2025, semester = 1) - val meetings = listOf(mockk()) + val sessions = listOf(mockk()) every { userReader.findAllByIds(listOf(1L)) } returns listOf(user) every { userCardinalPolicy.getCurrentCardinal(user) } returns currentCardinal - every { meetingGetService.find(8) } returns meetings + every { sessionReader.findAllByCardinal(8) } returns sessions useCase.accept(UserIdsRequest(listOf(1L))) - verify(exactly = 1) { attendanceSaveService.init(user, meetings) } + verify(exactly = 1) { attendanceRepository.saveAll(any>()) } user.status shouldBe Status.ACTIVE } } @@ -96,18 +97,19 @@ class AdminUserUseCaseTest : val user = UserTestFixture.createActiveUser1(1L) val currentCardinal = CardinalTestFixture.createCardinal(id = 10L, cardinalNumber = 3, year = 2024, semester = 2) val nextCardinal = CardinalTestFixture.createCardinal(id = 11L, cardinalNumber = 4, year = 2025, semester = 1) - val meetings = listOf(mockk()) + val session = mockk() + every { session.cardinal } returns 4 val request = listOf(UserApplyObRequest(1L, 4)) every { userReader.findAllByIds(listOf(1L)) } returns listOf(user) every { userCardinalRepository.findAllByUsers(listOf(user)) } returns listOf(UserCardinal(user, currentCardinal)) every { cardinalRepository.findAllByCardinalNumberIn(listOf(4)) } returns listOf(nextCardinal) - every { meetingGetService.findByCardinals(listOf(4)) } returns mapOf(4 to meetings) + every { sessionReader.findAllByCardinalIn(listOf(4)) } returns listOf(session) every { userCardinalRepository.save(any()) } answers { firstArg() } useCase.applyOb(request) - verify(exactly = 1) { attendanceSaveService.init(user, meetings) } + verify(exactly = 1) { attendanceRepository.saveAll(any>()) } verify(exactly = 1) { userCardinalRepository.save(match { it.user == user && it.cardinal == nextCardinal }) } } @@ -122,9 +124,9 @@ class AdminUserUseCaseTest : useCase.applyOb(request) - verify(exactly = 0) { meetingGetService.findByCardinals(any()) } + verify(exactly = 0) { sessionReader.findAllByCardinalIn(any()) } verify(exactly = 0) { userCardinalRepository.save(any()) } - verify(exactly = 0) { attendanceSaveService.init(any(), any()) } + verify(exactly = 0) { attendanceRepository.saveAll(any>()) } } it("요청 목록이 비어 있으면 아무 처리도 하지 않는다") { @@ -138,20 +140,21 @@ class AdminUserUseCaseTest : val user = UserTestFixture.createActiveUser1(1L) val currentCardinal = CardinalTestFixture.createCardinal(id = 10L, cardinalNumber = 3, year = 2024, semester = 2) val createdCardinal = CardinalTestFixture.createCardinal(id = 12L, cardinalNumber = 5, year = 2025, semester = 2) - val meetings = listOf(mockk()) + val session = mockk() + every { session.cardinal } returns 5 val request = listOf(UserApplyObRequest(1L, 5)) every { userReader.findAllByIds(listOf(1L)) } returns listOf(user) every { userCardinalRepository.findAllByUsers(listOf(user)) } returns listOf(UserCardinal(user, currentCardinal)) every { cardinalRepository.findAllByCardinalNumberIn(listOf(5)) } returns emptyList() every { cardinalRepository.save(any()) } returns createdCardinal - every { meetingGetService.findByCardinals(listOf(5)) } returns mapOf(5 to meetings) + every { sessionReader.findAllByCardinalIn(listOf(5)) } returns listOf(session) every { userCardinalRepository.save(any()) } answers { firstArg() } useCase.applyOb(request) verify(exactly = 1) { cardinalRepository.save(any()) } - verify(exactly = 1) { attendanceSaveService.init(user, meetings) } + verify(exactly = 1) { attendanceRepository.saveAll(any>()) } verify(exactly = 1) { userCardinalRepository.save(match { it.user == user && it.cardinal == createdCardinal }) } } }