diff --git a/src/main/java/in/koreatech/koin/domain/timetableV3/controller/TimetableFrameApiV3.java b/src/main/java/in/koreatech/koin/domain/timetableV3/controller/TimetableFrameApiV3.java new file mode 100644 index 0000000000..d543355cce --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/timetableV3/controller/TimetableFrameApiV3.java @@ -0,0 +1,113 @@ +package in.koreatech.koin.domain.timetableV3.controller; + +import static in.koreatech.koin.domain.user.model.UserType.STUDENT; + +import java.util.List; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +import in.koreatech.koin.domain.timetableV3.dto.request.TimetableFrameCreateRequestV3; +import in.koreatech.koin.domain.timetableV3.dto.request.TimetableFrameUpdateRequestV3; +import in.koreatech.koin.domain.timetableV3.dto.response.TimetableFrameResponseV3; +import in.koreatech.koin.domain.timetableV3.dto.response.TimetableFramesResponseV3; +import in.koreatech.koin.global.auth.Auth; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; + +@Tag(name = "(Normal) Timetable: V3-시간표", description = "시간표 프레임을 관리한다") +public interface TimetableFrameApiV3 { + + @ApiResponses( + value = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))) + } + ) + @Operation(summary = "시간표 프레임 생성") + @SecurityRequirement(name = "Jwt Authentication") + @PostMapping("/v3/timetables/frame") + ResponseEntity> createTimetablesFrame( + @Valid @RequestBody TimetableFrameCreateRequestV3 request, + @Auth(permit = {STUDENT}) Integer userId + ); + + @ApiResponses( + value = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))) + } + ) + @Operation(summary = "시간표 프레임 수정") + @SecurityRequirement(name = "Jwt Authentication") + @PutMapping("/v3/timetables/frame/{id}") + ResponseEntity> updateTimetableFrame( + @Valid @RequestBody TimetableFrameUpdateRequestV3 request, + @PathVariable(value = "id") Integer timetableFrameId, + @Auth(permit = {STUDENT}) Integer userId + ); + + @ApiResponses( + value = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))) + } + ) + @Operation(summary = "시간표 프레임 조회") + @SecurityRequirement(name = "Jwt Authentication") + @GetMapping("/v3/timetables/frame") + ResponseEntity> getTimetablesFrame( + @RequestParam(name = "year") Integer year, + @RequestParam(name = "term") String term, + @Auth(permit = {STUDENT}) Integer userId + ); + + @ApiResponses( + value = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))) + } + ) + @Operation(summary = "시간표 프레임 모두 조회") + @SecurityRequirement(name = "Jwt Authentication") + @GetMapping("/v3/timetables/frames") + ResponseEntity> getTimetablesFrames( + @Auth(permit = {STUDENT}) Integer userId + ); + + @ApiResponses( + value = { + @ApiResponse(responseCode = "204"), + @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))) + } + ) + @Operation(summary = "시간표 프레임 모두 삭제") + @DeleteMapping("/v3/timetables/frames") + ResponseEntity deleteTimetablesFrames( + @RequestParam(name = "year") Integer year, + @RequestParam(name = "term") String term, + @Auth(permit = {STUDENT}) Integer userId + ); +} diff --git a/src/main/java/in/koreatech/koin/domain/timetableV3/controller/TimetableFrameControllerV3.java b/src/main/java/in/koreatech/koin/domain/timetableV3/controller/TimetableFrameControllerV3.java new file mode 100644 index 0000000000..dedd5a18bd --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/timetableV3/controller/TimetableFrameControllerV3.java @@ -0,0 +1,79 @@ +package in.koreatech.koin.domain.timetableV3.controller; + +import static in.koreatech.koin.domain.user.model.UserType.STUDENT; + +import java.util.List; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import in.koreatech.koin.domain.timetableV3.dto.request.TimetableFrameCreateRequestV3; +import in.koreatech.koin.domain.timetableV3.dto.request.TimetableFrameUpdateRequestV3; +import in.koreatech.koin.domain.timetableV3.dto.response.TimetableFrameResponseV3; +import in.koreatech.koin.domain.timetableV3.dto.response.TimetableFramesResponseV3; +import in.koreatech.koin.domain.timetableV3.service.TimetableFrameServiceV3; +import in.koreatech.koin.global.auth.Auth; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +public class TimetableFrameControllerV3 implements TimetableFrameApiV3 { + + private final TimetableFrameServiceV3 timetableFrameServiceV3; + + @PostMapping("/v3/timetables/frame") + public ResponseEntity> createTimetablesFrame( + @Valid @RequestBody TimetableFrameCreateRequestV3 request, + @Auth(permit = {STUDENT}) Integer userId + ) { + List response = timetableFrameServiceV3.createTimetablesFrame(request, userId); + return ResponseEntity.ok(response); + } + + @PutMapping("/v3/timetables/frame/{id}") + public ResponseEntity> updateTimetableFrame( + @Valid @RequestBody TimetableFrameUpdateRequestV3 request, + @PathVariable(value = "id") Integer timetableFrameId, + @Auth(permit = {STUDENT}) Integer userId + ) { + List response = timetableFrameServiceV3.updateTimetableFrame(request, + timetableFrameId, userId); + return ResponseEntity.ok(response); + } + + @GetMapping("/v3/timetables/frame") + public ResponseEntity> getTimetablesFrame( + @RequestParam(name = "year") Integer year, + @RequestParam(name = "term") String term, + @Auth(permit = {STUDENT}) Integer userId + ) { + List response = timetableFrameServiceV3.getTimetablesFrame(year, term, userId); + return ResponseEntity.ok(response); + } + + @GetMapping("/v3/timetables/frames") + public ResponseEntity> getTimetablesFrames( + @Auth(permit = {STUDENT}) Integer userId + ) { + List response = timetableFrameServiceV3.getTimetablesFrames(userId); + return ResponseEntity.ok(response); + } + + @DeleteMapping("/v3/timetables/frames") + public ResponseEntity deleteTimetablesFrames( + @RequestParam(name = "year") Integer year, + @RequestParam(name = "term") String term, + @Auth(permit = {STUDENT}) Integer userId + ) { + timetableFrameServiceV3.deleteTimetablesFrames(year, term, userId); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/timetableV3/dto/request/TimetableFrameCreateRequestV3.java b/src/main/java/in/koreatech/koin/domain/timetableV3/dto/request/TimetableFrameCreateRequestV3.java new file mode 100644 index 0000000000..e2a13e6eb8 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/timetableV3/dto/request/TimetableFrameCreateRequestV3.java @@ -0,0 +1,32 @@ +package in.koreatech.koin.domain.timetableV3.dto.request; + +import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import in.koreatech.koin.domain.timetable.model.Semester; +import in.koreatech.koin.domain.timetableV2.model.TimetableFrame; +import in.koreatech.koin.domain.user.model.User; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +@JsonNaming(value = SnakeCaseStrategy.class) +public record TimetableFrameCreateRequestV3( + @Schema(description = "년도", example = "2019", requiredMode = REQUIRED) + @NotNull(message = "년도를 입력해주세요") + Integer year, + + @Schema(description = "년도", example = "2학기", requiredMode = REQUIRED) + @NotNull(message = "학기를 입력해주세요") + String term +) { + public TimetableFrame toTimetablesFrame(User user, Semester semester, String name, boolean isMain) { + return TimetableFrame.builder() + .user(user) + .semester(semester) + .name(name) + .isMain(isMain) + .build(); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/timetableV3/dto/request/TimetableFrameUpdateRequestV3.java b/src/main/java/in/koreatech/koin/domain/timetableV3/dto/request/TimetableFrameUpdateRequestV3.java new file mode 100644 index 0000000000..f2c7df024c --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/timetableV3/dto/request/TimetableFrameUpdateRequestV3.java @@ -0,0 +1,24 @@ +package in.koreatech.koin.domain.timetableV3.dto.request; + +import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +@JsonNaming(value = SnakeCaseStrategy.class) +public record TimetableFrameUpdateRequestV3( + @Schema(description = "시간표 이름", example = "시간표1", requiredMode = REQUIRED) + @Size(max = 255, message = "시간표 이름의 최대 길이는 255자입니다.") + @NotBlank(message = "시간표 이름을 입력해주세요.") + String name, + + @Schema(description = "메인 시간표 여부", example = "false", requiredMode = REQUIRED) + @NotNull(message = "시간표 메인 여부를 입력해주세요.") + Boolean isMain +) { +} diff --git a/src/main/java/in/koreatech/koin/domain/timetableV3/dto/response/TimetableFrameResponseV3.java b/src/main/java/in/koreatech/koin/domain/timetableV3/dto/response/TimetableFrameResponseV3.java new file mode 100644 index 0000000000..fe0eea4a15 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/timetableV3/dto/response/TimetableFrameResponseV3.java @@ -0,0 +1,29 @@ +package in.koreatech.koin.domain.timetableV3.dto.response; + +import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import in.koreatech.koin.domain.timetableV2.model.TimetableFrame; +import io.swagger.v3.oas.annotations.media.Schema; + +@JsonNaming(value = SnakeCaseStrategy.class) +public record TimetableFrameResponseV3( + @Schema(description = "id", example = "1", requiredMode = REQUIRED) + Integer id, + + @Schema(description = "시간표 이름", example = "시간표1", requiredMode = REQUIRED) + String name, + + @Schema(description = "메인 시간표 여부", example = "false", requiredMode = REQUIRED) + Boolean isMain +) { + public static TimetableFrameResponseV3 from(TimetableFrame timetableFrames) { + return new TimetableFrameResponseV3( + timetableFrames.getId(), + timetableFrames.getName(), + timetableFrames.isMain() + ); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/timetableV3/dto/response/TimetableFramesResponseV3.java b/src/main/java/in/koreatech/koin/domain/timetableV3/dto/response/TimetableFramesResponseV3.java new file mode 100644 index 0000000000..c66bc10d6c --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/timetableV3/dto/response/TimetableFramesResponseV3.java @@ -0,0 +1,76 @@ +package in.koreatech.koin.domain.timetableV3.dto.response; + +import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import in.koreatech.koin.domain.timetableV2.model.TimetableFrame; +import in.koreatech.koin.domain.timetableV3.model.Term; +import io.swagger.v3.oas.annotations.media.Schema; + +@JsonNaming(SnakeCaseStrategy.class) +public record TimetableFramesResponseV3( + @Schema(description = "년도", example = "2024", requiredMode = REQUIRED) + Integer year, + + @Schema(description = "학기 별 timetableFrames", requiredMode = REQUIRED) + List timetableFrames +) { + @JsonNaming(SnakeCaseStrategy.class) + public record InnerTimetableFrameResponse( + @Schema(description = "학기", example = "2학기", requiredMode = REQUIRED) + String term, + + @Schema(description = "timetableFrame 리스트", requiredMode = REQUIRED) + List frames + ) { + + } + + public static List from(List timetableFrameList) { + List responseList = new ArrayList<>(); + Map>> groupedByYearAndTerm = new TreeMap<>( + Comparator.reverseOrder()); + + // 일차로 Term으로 그룹핑, 이후 year로 그룹핑 + for (TimetableFrame timetableFrame : timetableFrameList) { + int year = timetableFrame.getSemester().getYear(); + Term term = timetableFrame.getSemester().getTerm(); + + groupedByYearAndTerm.computeIfAbsent(year, k -> new TreeMap<>(Comparator.comparing(Term::getPriority))); + + Map> termMap = groupedByYearAndTerm.get(year); + termMap.computeIfAbsent(term, k -> new ArrayList<>()); + termMap.get(term).add(TimetableFrameResponseV3.from(timetableFrame)); + } + + // 메인 시간표가 멘 위로 그 다음은 id로 정렬 + groupedByYearAndTerm.values().forEach(termMap -> + termMap.values().forEach(frameList -> + frameList.sort(Comparator.comparing(TimetableFrameResponseV3::isMain).reversed() + .thenComparing(TimetableFrameResponseV3::id)) + ) + ); + + for (Map.Entry>> yearEntry : groupedByYearAndTerm.entrySet()) { + List termResponses = new ArrayList<>(); + int year = yearEntry.getKey(); + Map> termMap = yearEntry.getValue(); + + for (Map.Entry> termEntry : termMap.entrySet()) { + Term term = termEntry.getKey(); + termResponses.add(new InnerTimetableFrameResponse(term.getDescription(), termEntry.getValue())); + } + responseList.add(new TimetableFramesResponseV3(year, termResponses)); + } + + return responseList; + } +} diff --git a/src/main/java/in/koreatech/koin/domain/timetableV3/exception/InvalidTermFormatException.java b/src/main/java/in/koreatech/koin/domain/timetableV3/exception/InvalidTermFormatException.java new file mode 100644 index 0000000000..bba49e5b07 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/timetableV3/exception/InvalidTermFormatException.java @@ -0,0 +1,20 @@ +package in.koreatech.koin.domain.timetableV3.exception; + +import in.koreatech.koin.global.exception.KoinIllegalArgumentException; + +public class InvalidTermFormatException extends KoinIllegalArgumentException { + + private static final String DEFAULT_MESSAGE = "term 양식이 잘못됐습니다."; + + public InvalidTermFormatException(String message) { + super(message); + } + + public InvalidTermFormatException(String message, String detail) { + super(message, detail); + } + + public static InvalidTermFormatException withDetail(String detail) { + return new InvalidTermFormatException(DEFAULT_MESSAGE, detail); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/timetableV3/model/Term.java b/src/main/java/in/koreatech/koin/domain/timetableV3/model/Term.java index 76eb3a07a2..08ab181e80 100644 --- a/src/main/java/in/koreatech/koin/domain/timetableV3/model/Term.java +++ b/src/main/java/in/koreatech/koin/domain/timetableV3/model/Term.java @@ -1,6 +1,6 @@ package in.koreatech.koin.domain.timetableV3.model; -import in.koreatech.koin.global.exception.KoinIllegalArgumentException; +import in.koreatech.koin.domain.timetableV3.exception.InvalidTermFormatException; import lombok.Getter; @Getter @@ -24,6 +24,6 @@ public static Term fromDescription(String description) { return term; } } - throw new KoinIllegalArgumentException("term 양식이 잘못됐습니다."); + throw new InvalidTermFormatException("term : " + description); } } diff --git a/src/main/java/in/koreatech/koin/domain/timetableV3/repository/TimetableFrameRepositoryV3.java b/src/main/java/in/koreatech/koin/domain/timetableV3/repository/TimetableFrameRepositoryV3.java index 7f8cef0282..21111be6b6 100644 --- a/src/main/java/in/koreatech/koin/domain/timetableV3/repository/TimetableFrameRepositoryV3.java +++ b/src/main/java/in/koreatech/koin/domain/timetableV3/repository/TimetableFrameRepositoryV3.java @@ -33,4 +33,30 @@ default TimetableFrame getByIdWithDeleted(Integer id) { } boolean existsByUserAndSemester(User user, Semester semester); + + @Query( + """ + SELECT COUNT(t) FROM TimetableFrame t + WHERE t.user.id = :userId + AND t.semester.id = :semesterId + """) + int countByUserIdAndSemesterId(@Param("userId") Integer userId, @Param("semesterId") Integer semesterId); + + void save(TimetableFrame timetableFrame); + + List findByUserAndSemester(User user, Semester semester); + + Optional findByUserIdAndSemesterIdAndIsMainTrue(Integer userId, Integer semesterId); + + default TimetableFrame getMainTimetableByUserIdAndSemesterId(Integer userId, Integer semesterId) { + return findByUserIdAndSemesterIdAndIsMainTrue(userId, semesterId) + .orElseThrow( + () -> TimetableFrameNotFoundException.withDetail("userId: " + userId + ", semesterId: " + semesterId)); + } + + List findAllByUserIdAndSemesterId(Integer userId, Integer semesterId); + + List findAllByUserId(Integer userId); + + List findAllByUserAndSemester(User user, Semester semester); } diff --git a/src/main/java/in/koreatech/koin/domain/timetableV3/service/TimetableFrameServiceV3.java b/src/main/java/in/koreatech/koin/domain/timetableV3/service/TimetableFrameServiceV3.java new file mode 100644 index 0000000000..bd83a63e1c --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/timetableV3/service/TimetableFrameServiceV3.java @@ -0,0 +1,107 @@ +package in.koreatech.koin.domain.timetableV3.service; + +import static in.koreatech.koin.domain.timetableV2.validation.TimetableFrameValidate.validateTimetableFrameUpdate; +import static in.koreatech.koin.domain.timetableV3.model.Term.fromDescription; + +import java.util.Comparator; +import java.util.List; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import in.koreatech.koin.domain.timetable.model.Semester; +import in.koreatech.koin.domain.timetableV2.model.TimetableFrame; +import in.koreatech.koin.domain.timetableV3.dto.request.TimetableFrameCreateRequestV3; +import in.koreatech.koin.domain.timetableV3.dto.request.TimetableFrameUpdateRequestV3; +import in.koreatech.koin.domain.timetableV3.dto.response.TimetableFrameResponseV3; +import in.koreatech.koin.domain.timetableV3.dto.response.TimetableFramesResponseV3; +import in.koreatech.koin.domain.timetableV3.model.Term; +import in.koreatech.koin.domain.timetableV3.repository.SemesterRepositoryV3; +import in.koreatech.koin.domain.timetableV3.repository.TimetableFrameRepositoryV3; +import in.koreatech.koin.domain.user.model.User; +import in.koreatech.koin.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class TimetableFrameServiceV3 { + + private static final String DEFAULT_TIMETABLE_FRAME_NAME = "시간표"; + + private final TimetableFrameRepositoryV3 timetableFrameRepositoryV3; + private final SemesterRepositoryV3 semesterRepositoryV3; + private final UserRepository userRepository; + + @Transactional + public List createTimetablesFrame(TimetableFrameCreateRequestV3 request, Integer userId) { + Semester semester = semesterRepositoryV3.getByYearAndTerm(request.year(), fromDescription(request.term())); + User user = userRepository.getById(userId); + int currentFrameCount = timetableFrameRepositoryV3.countByUserIdAndSemesterId(userId, semester.getId()); + + TimetableFrame frame = request.toTimetablesFrame(user, semester, + getDefaultTimetableFrameName(currentFrameCount), determineIfMain(currentFrameCount)); + timetableFrameRepositoryV3.save(frame); + + List frames = timetableFrameRepositoryV3.findByUserAndSemester(user, semester); + frames.sort(Comparator.comparing(TimetableFrame::isMain).reversed().thenComparing(TimetableFrame::getId)); + return frames.stream() + .map(TimetableFrameResponseV3::from) + .toList(); + } + + private boolean determineIfMain(int currentFrameCount) { + return currentFrameCount == 0; + } + + private String getDefaultTimetableFrameName(int currentFrameCount) { + return DEFAULT_TIMETABLE_FRAME_NAME + (currentFrameCount + 1); + } + + @Transactional + public List updateTimetableFrame( + TimetableFrameUpdateRequestV3 request, Integer timetableFrameId, Integer userId + ) { + TimetableFrame frame = timetableFrameRepositoryV3.getById(timetableFrameId); + validateTimetableFrameUpdate(frame, request.isMain()); + cancelIfMainTimetable(userId, frame.getSemester().getId(), request.isMain()); + frame.updateTimetableFrame(frame.getSemester(), request.name(), request.isMain()); + + List frames = timetableFrameRepositoryV3.findByUserAndSemester(frame.getUser(), + frame.getSemester()); + frames.sort(Comparator.comparing(TimetableFrame::isMain).reversed().thenComparing(TimetableFrame::getId)); + return frames.stream() + .map(TimetableFrameResponseV3::from) + .toList(); + } + + private void cancelIfMainTimetable(Integer userId, Integer semesterId, boolean isMain) { + if (isMain) { + TimetableFrame mainTimetableFrame = timetableFrameRepositoryV3.getMainTimetableByUserIdAndSemesterId(userId, + semesterId); + mainTimetableFrame.updateMainFlag(false); + } + } + + public List getTimetablesFrame(Integer year, String term, Integer userId) { + Semester semester = semesterRepositoryV3.getByYearAndTerm(year, Term.fromDescription(term)); + List frames = timetableFrameRepositoryV3.findAllByUserIdAndSemesterId(userId, semester.getId()); + frames.sort(Comparator.comparing(TimetableFrame::isMain).reversed().thenComparing(TimetableFrame::getId)); + return frames.stream() + .map(TimetableFrameResponseV3::from) + .toList(); + } + + public List getTimetablesFrames(Integer userId) { + List timetableFrames = timetableFrameRepositoryV3.findAllByUserId(userId); + return TimetableFramesResponseV3.from(timetableFrames); + } + + @Transactional + public void deleteTimetablesFrames(Integer year, String term, Integer userId) { + User user = userRepository.getById(userId); + Semester timetableSemester = semesterRepositoryV3.getByYearAndTerm(year, Term.fromDescription(term)); + timetableFrameRepositoryV3.findAllByUserAndSemester(user, timetableSemester) + .forEach(TimetableFrame::delete); + } +} diff --git a/src/test/java/in/koreatech/koin/acceptance/TimetableFrameApiV3Test.java b/src/test/java/in/koreatech/koin/acceptance/TimetableFrameApiV3Test.java new file mode 100644 index 0000000000..a0c19ac0d1 --- /dev/null +++ b/src/test/java/in/koreatech/koin/acceptance/TimetableFrameApiV3Test.java @@ -0,0 +1,215 @@ +package in.koreatech.koin.acceptance; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.transaction.annotation.Transactional; + +import in.koreatech.koin.AcceptanceTest; +import in.koreatech.koin.domain.timetable.model.Semester; +import in.koreatech.koin.domain.timetableV2.model.TimetableFrame; +import in.koreatech.koin.domain.user.model.User; +import in.koreatech.koin.fixture.SemesterFixture; +import in.koreatech.koin.fixture.TimeTableV2Fixture; +import in.koreatech.koin.fixture.UserFixture; + +@SuppressWarnings("NonAsciiCharacters") +@Transactional +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class TimetableFrameApiV3Test extends AcceptanceTest { + @Autowired + private TimeTableV2Fixture timetableV2Fixture; + + @Autowired + private UserFixture userFixture; + + @Autowired + private SemesterFixture semesterFixture; + + private User user; + private String token; + private Semester semester; + + @BeforeAll + void setup() { + clear(); + user = userFixture.준호_학생().getUser(); + token = userFixture.getToken(user); + semester = semesterFixture.semester_2019년도_2학기(); + } + + @Test + void 특정_시간표_frame을_생성한다() throws Exception { + mockMvc.perform( + post("/v3/timetables/frame") + .header("Authorization", "Bearer " + token) + .content(String.format(""" + { + "year": "%d", + "term": "%s" + } + """, semester.getYear(), semester.getTerm().getDescription() + )) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" + [ + { + "id": 1, + "name": "시간표1", + "is_main": true + } + ] + """)); + } + + @Test + void 특정_시간표_frame을_수정한다() throws Exception { + TimetableFrame frame = timetableV2Fixture.시간표1(user, semester); + Integer frameId = frame.getId(); + + mockMvc.perform( + put("/v3/timetables/frame/{id}", frameId) + .header("Authorization", "Bearer " + token) + .content(""" + { + "name": "새로운 이름", + "is_main": true + } + """ + ) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" + [ + { + "id": 1, + "name": "새로운 이름", + "is_main": true + } + ] + """)); + } + + @Test + void 특정_학기의_모든_시간표_frame을_조회한다() throws Exception { + timetableV2Fixture.시간표1(user, semester); + timetableV2Fixture.시간표2(user, semester); + + mockMvc.perform( + get("/v3/timetables/frame") + .header("Authorization", "Bearer " + token) + .param("year", String.valueOf(semester.getYear())) + .param("term", String.valueOf(semester.getTerm().getDescription())) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" + [ + { + "id": 1, + "name": "시간표1", + "is_main": true + }, + { + "id": 2, + "name": "시간표2", + "is_main": false + } + ] + """)); + } + + @Test + void 모든_시간표_프레임을_삭제한다() throws Exception { + TimetableFrame frame1 = timetableV2Fixture.시간표1(user, semester); + TimetableFrame frame2 = timetableV2Fixture.시간표1(user, semester); + TimetableFrame frame3 = timetableV2Fixture.시간표1(user, semester); + + mockMvc.perform( + delete("/v3/timetables/frames") + .header("Authorization", "Bearer " + token) + .param("year", String.valueOf(semester.getYear())) + .param("term", String.valueOf(semester.getTerm().getDescription())) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isNoContent()); + + assertThat(frame1.isDeleted()).isTrue(); + assertThat(frame2.isDeleted()).isTrue(); + assertThat(frame3.isDeleted()).isTrue(); + } + + @Test + void 모든_학기의_시간표_프레임을_조회한다() throws Exception { + Semester semester1 = semesterFixture.semester_2024년도_1학기(); + Semester semester2 = semesterFixture.semester_2024년도_2학기(); + + timetableV2Fixture.시간표1(user, semester1); + timetableV2Fixture.시간표2(user, semester1); + + timetableV2Fixture.시간표1(user, semester2); + timetableV2Fixture.시간표2(user, semester2); + timetableV2Fixture.시간표3(user, semester2); + + mockMvc.perform( + get("/v3/timetables/frames") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" + [ + { + "year": 2024, + "timetable_frames": [ + { + "term": "2학기", + "frames": [ + { + "id": 3, + "name": "시간표1", + "is_main": true + }, + { + "id": 4, + "name": "시간표2", + "is_main": false + }, + { + "id": 5, + "name": "시간표3", + "is_main": false + } + ] + }, + { + "term": "1학기", + "frames": [ + { + "id": 1, + "name": "시간표1", + "is_main": true + }, + { + "id": 2, + "name": "시간표2", + "is_main": false + } + ] + } + ] + } + ] + """)); + } +}