From e356d215b334a8373662224f9f1cc04874af1619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EA=B4=80=EA=B7=9C?= Date: Sun, 5 Jan 2025 05:57:34 +0900 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20timetableFrame=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/TimetableFrameApiV3.java | 41 +++++++++++++ .../TimetableFrameControllerV3.java | 33 +++++++++++ .../TimetableFrameCreateRequestV3.java | 32 +++++++++++ .../response/TimetableFrameResponseV3.java | 29 ++++++++++ .../koin/domain/timetableV3/model/Term.java | 10 ++++ .../repository/SemesterRepositoryV3.java | 11 ++++ .../TimetableFrameRepositoryV3.java | 40 ++++++++++++- .../service/TimetableFrameServiceV3.java | 57 +++++++++++++++++++ .../config/swagger/SwaggerGroupConfig.java | 3 +- 9 files changed, 253 insertions(+), 3 deletions(-) create mode 100644 src/main/java/in/koreatech/koin/domain/timetableV3/controller/TimetableFrameApiV3.java create mode 100644 src/main/java/in/koreatech/koin/domain/timetableV3/controller/TimetableFrameControllerV3.java create mode 100644 src/main/java/in/koreatech/koin/domain/timetableV3/dto/request/TimetableFrameCreateRequestV3.java create mode 100644 src/main/java/in/koreatech/koin/domain/timetableV3/dto/response/TimetableFrameResponseV3.java create mode 100644 src/main/java/in/koreatech/koin/domain/timetableV3/service/TimetableFrameServiceV3.java 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 000000000..ad71dbfab --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/timetableV3/controller/TimetableFrameApiV3.java @@ -0,0 +1,41 @@ +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.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +import in.koreatech.koin.domain.timetableV3.dto.request.TimetableFrameCreateRequestV3; +import in.koreatech.koin.domain.timetableV3.dto.response.TimetableFrameResponseV3; +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 + ); +} 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 000000000..8cd3cd95a --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/timetableV3/controller/TimetableFrameControllerV3.java @@ -0,0 +1,33 @@ +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.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import in.koreatech.koin.domain.timetableV3.dto.request.TimetableFrameCreateRequestV3; +import in.koreatech.koin.domain.timetableV3.dto.response.TimetableFrameResponseV3; +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); + } +} 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 000000000..650a6006c --- /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/response/TimetableFrameResponseV3.java b/src/main/java/in/koreatech/koin/domain/timetableV3/dto/response/TimetableFrameResponseV3.java new file mode 100644 index 000000000..9cd8250d6 --- /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 timetableName, + + @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/model/Term.java b/src/main/java/in/koreatech/koin/domain/timetableV3/model/Term.java index 6f65b2280..76eb3a07a 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,5 +1,6 @@ package in.koreatech.koin.domain.timetableV3.model; +import in.koreatech.koin.global.exception.KoinIllegalArgumentException; import lombok.Getter; @Getter @@ -16,4 +17,13 @@ public enum Term { this.description = description; this.priority = priority; } + + public static Term fromDescription(String description) { + for (Term term : Term.values()) { + if (term.description.equals(description)) { + return term; + } + } + throw new KoinIllegalArgumentException("term 양식이 잘못됐습니다."); + } } diff --git a/src/main/java/in/koreatech/koin/domain/timetableV3/repository/SemesterRepositoryV3.java b/src/main/java/in/koreatech/koin/domain/timetableV3/repository/SemesterRepositoryV3.java index dd8454dae..7a167c8a4 100644 --- a/src/main/java/in/koreatech/koin/domain/timetableV3/repository/SemesterRepositoryV3.java +++ b/src/main/java/in/koreatech/koin/domain/timetableV3/repository/SemesterRepositoryV3.java @@ -1,14 +1,25 @@ package in.koreatech.koin.domain.timetableV3.repository; import java.util.List; +import java.util.Optional; import org.springframework.data.repository.Repository; +import in.koreatech.koin.domain.timetable.exception.SemesterNotFoundException; import in.koreatech.koin.domain.timetable.model.Semester; +import in.koreatech.koin.domain.timetableV3.model.Term; public interface SemesterRepositoryV3 extends Repository { List findAll(); Semester save(Semester semester); + + Optional findByYearAndTerm(Integer year, Term term); + + default Semester getByYearAndTerm(Integer year, Term term) { + return findByYearAndTerm(year, term) + .orElseThrow( + () -> SemesterNotFoundException.withDetail("year : " + year + "term : " + term.getDescription())); + } } 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 c860bd18f..2be436660 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 @@ -1,12 +1,48 @@ package in.koreatech.koin.domain.timetableV3.repository; -import org.springframework.data.repository.Repository; - import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.query.Param; +import in.koreatech.koin.domain.timetable.exception.TimetableNotFoundException; +import in.koreatech.koin.domain.timetable.model.Semester; +import in.koreatech.koin.domain.timetableV2.exception.TimetableFrameNotFoundException; import in.koreatech.koin.domain.timetableV2.model.TimetableFrame; +import in.koreatech.koin.domain.user.model.User; public interface TimetableFrameRepositoryV3 extends Repository { + Optional findById(Integer id); + + default TimetableFrame getById(Integer id) { + return findById(id) + .orElseThrow(() -> TimetableNotFoundException.withDetail("id: " + id)); + } + List findByUserIdAndIsMainTrue(Integer userId); + + @Query(value = "SELECT * FROM timetable_frame WHERE id = :id", nativeQuery = true) + Optional findByIdWithDeleted(@Param("id") Integer id); + + default TimetableFrame getByIdWithDeleted(Integer id) { + return findByIdWithDeleted(id) + .orElseThrow(() -> TimetableFrameNotFoundException.withDetail("id: " + 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); } 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 000000000..527077ea9 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/timetableV3/service/TimetableFrameServiceV3.java @@ -0,0 +1,57 @@ +package in.koreatech.koin.domain.timetableV3.service; + +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.response.TimetableFrameResponseV3; +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(), Term.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); + } +} diff --git a/src/main/java/in/koreatech/koin/global/config/swagger/SwaggerGroupConfig.java b/src/main/java/in/koreatech/koin/global/config/swagger/SwaggerGroupConfig.java index de048156e..117d18626 100644 --- a/src/main/java/in/koreatech/koin/global/config/swagger/SwaggerGroupConfig.java +++ b/src/main/java/in/koreatech/koin/global/config/swagger/SwaggerGroupConfig.java @@ -60,7 +60,8 @@ public GroupedOpenApi userApi() { "in.koreatech.koin.domain.user", "in.koreatech.koin.domain.student", "in.koreatech.koin.domain.timetable", - "in.koreatech.koin.domain.timetableV2" + "in.koreatech.koin.domain.timetableV2", + "in.koreatech.koin.domain.timetableV3" }; return createGroupedOpenApi("4. User API", packagesPath); From 4947a6c70eae365a66e991258bda403901a5ab17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EA=B4=80=EA=B7=9C?= Date: Sun, 5 Jan 2025 06:30:02 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20timetableFrame=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/TimetableFrameApiV3.java | 20 +++++++++++++ .../TimetableFrameControllerV3.java | 16 ++++++++++ .../TimetableFrameUpdateRequestV3.java | 24 +++++++++++++++ .../TimetableFrameRepositoryV3.java | 8 +++++ .../service/TimetableFrameServiceV3.java | 29 +++++++++++++++++++ 5 files changed, 97 insertions(+) create mode 100644 src/main/java/in/koreatech/koin/domain/timetableV3/dto/request/TimetableFrameUpdateRequestV3.java 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 index ad71dbfab..73d79b30b 100644 --- a/src/main/java/in/koreatech/koin/domain/timetableV3/controller/TimetableFrameApiV3.java +++ b/src/main/java/in/koreatech/koin/domain/timetableV3/controller/TimetableFrameApiV3.java @@ -5,10 +5,13 @@ import java.util.List; import org.springframework.http.ResponseEntity; +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 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.global.auth.Auth; import io.swagger.v3.oas.annotations.Operation; @@ -38,4 +41,21 @@ 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 + ); } 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 index 8cd3cd95a..55831d725 100644 --- a/src/main/java/in/koreatech/koin/domain/timetableV3/controller/TimetableFrameControllerV3.java +++ b/src/main/java/in/koreatech/koin/domain/timetableV3/controller/TimetableFrameControllerV3.java @@ -5,11 +5,14 @@ import java.util.List; import org.springframework.http.ResponseEntity; +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.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.service.TimetableFrameServiceV3; import in.koreatech.koin.global.auth.Auth; @@ -30,4 +33,17 @@ public ResponseEntity> createTimetablesFrame( 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); + } + + } 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 000000000..6211a2d4c --- /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 timetableName, + + @Schema(description = "메인 시간표 여부", example = "false", requiredMode = REQUIRED) + @NotNull(message = "시간표 메인 여부를 입력해주세요.") + Boolean isMain +) { +} 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 2be436660..6694a5c6d 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 @@ -45,4 +45,12 @@ SELECT COUNT(t) FROM TimetableFrame t 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)); + } } 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 index 527077ea9..7d2b28a62 100644 --- a/src/main/java/in/koreatech/koin/domain/timetableV3/service/TimetableFrameServiceV3.java +++ b/src/main/java/in/koreatech/koin/domain/timetableV3/service/TimetableFrameServiceV3.java @@ -1,5 +1,7 @@ package in.koreatech.koin.domain.timetableV3.service; +import static in.koreatech.koin.domain.timetableV2.validation.TimetableFrameValidate.validateTimetableFrameUpdate; + import java.util.Comparator; import java.util.List; @@ -9,6 +11,7 @@ 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.model.Term; import in.koreatech.koin.domain.timetableV3.repository.SemesterRepositoryV3; @@ -54,4 +57,30 @@ private boolean determineIfMain(int currentFrameCount) { 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.timetableName(), 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); + } + } } From d2629e26515f991f7242db72cdb17b7b4ef68302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EA=B4=80=EA=B7=9C?= Date: Sun, 5 Jan 2025 09:01:55 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feat:=20timetableFrame=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/TimetableFrameApiV3.java | 35 +++++++++ .../TimetableFrameControllerV3.java | 19 +++++ .../response/TimetableFramesResponseV3.java | 76 +++++++++++++++++++ .../TimetableFrameRepositoryV3.java | 4 + .../service/TimetableFrameServiceV3.java | 29 +++++-- 5 files changed, 155 insertions(+), 8 deletions(-) create mode 100644 src/main/java/in/koreatech/koin/domain/timetableV3/dto/response/TimetableFramesResponseV3.java 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 index 73d79b30b..2b03785d0 100644 --- a/src/main/java/in/koreatech/koin/domain/timetableV3/controller/TimetableFrameApiV3.java +++ b/src/main/java/in/koreatech/koin/domain/timetableV3/controller/TimetableFrameApiV3.java @@ -5,14 +5,17 @@ import java.util.List; import org.springframework.http.ResponseEntity; +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; @@ -58,4 +61,36 @@ ResponseEntity> updateTimetableFrame( @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 + ); } 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 index 55831d725..36c7aa2b6 100644 --- a/src/main/java/in/koreatech/koin/domain/timetableV3/controller/TimetableFrameControllerV3.java +++ b/src/main/java/in/koreatech/koin/domain/timetableV3/controller/TimetableFrameControllerV3.java @@ -5,15 +5,18 @@ import java.util.List; import org.springframework.http.ResponseEntity; +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; @@ -45,5 +48,21 @@ public ResponseEntity> updateTimetableFrame( 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); + } } 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 000000000..2934f8cf7 --- /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.dto.response.TimetableFrameResponse; +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 termName, + + @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(TimetableFrameResponse.from(timetableFrame)); + } + + // 메인 시간표가 멘 위로 그 다음은 id로 정렬 + groupedByYearAndTerm.values().forEach(termMap -> + termMap.values().forEach(frameList -> + frameList.sort(Comparator.comparing(TimetableFrameResponse::isMain).reversed() + .thenComparing(TimetableFrameResponse::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/repository/TimetableFrameRepositoryV3.java b/src/main/java/in/koreatech/koin/domain/timetableV3/repository/TimetableFrameRepositoryV3.java index 6694a5c6d..54d8bb1bc 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 @@ -53,4 +53,8 @@ default TimetableFrame getMainTimetableByUserIdAndSemesterId(Integer userId, Int .orElseThrow( () -> TimetableFrameNotFoundException.withDetail("userId: " + userId + ", semesterId: " + semesterId)); } + + List findAllByUserIdAndSemesterId(Integer userId, Integer semesterId); + + List findAllByUserId(Integer userId); } 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 index 7d2b28a62..cb2e9b60b 100644 --- a/src/main/java/in/koreatech/koin/domain/timetableV3/service/TimetableFrameServiceV3.java +++ b/src/main/java/in/koreatech/koin/domain/timetableV3/service/TimetableFrameServiceV3.java @@ -1,6 +1,7 @@ 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; @@ -13,6 +14,7 @@ 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; @@ -33,7 +35,7 @@ public class TimetableFrameServiceV3 { @Transactional public List createTimetablesFrame(TimetableFrameCreateRequestV3 request, Integer userId) { - Semester semester = semesterRepositoryV3.getByYearAndTerm(request.year(), Term.fromDescription(request.term())); + Semester semester = semesterRepositoryV3.getByYearAndTerm(request.year(), fromDescription(request.term())); User user = userRepository.getById(userId); int currentFrameCount = timetableFrameRepositoryV3.countByUserIdAndSemesterId(userId, semester.getId()); @@ -42,9 +44,7 @@ public List createTimetablesFrame(TimetableFrameCreate timetableFrameRepositoryV3.save(frame); List frames = timetableFrameRepositoryV3.findByUserAndSemester(user, semester); - frames.sort(Comparator.comparing(TimetableFrame::isMain).reversed() - .thenComparing(TimetableFrame::getId)); - + frames.sort(Comparator.comparing(TimetableFrame::isMain).reversed().thenComparing(TimetableFrame::getId)); return frames.stream() .map(TimetableFrameResponseV3::from) .toList(); @@ -67,10 +67,9 @@ public List updateTimetableFrame( cancelIfMainTimetable(userId, frame.getSemester().getId(), request.isMain()); frame.updateTimetableFrame(frame.getSemester(), request.timetableName(), request.isMain()); - List frames = timetableFrameRepositoryV3.findByUserAndSemester(frame.getUser(), frame.getSemester()); - frames.sort(Comparator.comparing(TimetableFrame::isMain).reversed() - .thenComparing(TimetableFrame::getId)); - + 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(); @@ -83,4 +82,18 @@ private void cancelIfMainTimetable(Integer userId, Integer semesterId, boolean i 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); + } } From 450f8108b4a6fb8571ab5f28d842292e0240d224 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EA=B4=80=EA=B7=9C?= Date: Sun, 5 Jan 2025 09:11:57 +0900 Subject: [PATCH 4/8] =?UTF-8?q?feat:=20timetableFrame=20=EB=AA=A8=EB=91=90?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/TimetableFrameApiV3.java | 17 +++++++++++++++++ .../controller/TimetableFrameControllerV3.java | 11 +++++++++++ .../repository/TimetableFrameRepositoryV3.java | 2 ++ .../service/TimetableFrameServiceV3.java | 8 ++++++++ 4 files changed, 38 insertions(+) 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 index 2b03785d0..d543355cc 100644 --- a/src/main/java/in/koreatech/koin/domain/timetableV3/controller/TimetableFrameApiV3.java +++ b/src/main/java/in/koreatech/koin/domain/timetableV3/controller/TimetableFrameApiV3.java @@ -5,6 +5,7 @@ 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; @@ -93,4 +94,20 @@ ResponseEntity> getTimetablesFrame( 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 index 36c7aa2b6..dedd5a18b 100644 --- a/src/main/java/in/koreatech/koin/domain/timetableV3/controller/TimetableFrameControllerV3.java +++ b/src/main/java/in/koreatech/koin/domain/timetableV3/controller/TimetableFrameControllerV3.java @@ -5,6 +5,7 @@ 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; @@ -65,4 +66,14 @@ public ResponseEntity> getTimetablesFrames( 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/repository/TimetableFrameRepositoryV3.java b/src/main/java/in/koreatech/koin/domain/timetableV3/repository/TimetableFrameRepositoryV3.java index 54d8bb1bc..21111be6b 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 @@ -57,4 +57,6 @@ default TimetableFrame getMainTimetableByUserIdAndSemesterId(Integer userId, Int 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 index cb2e9b60b..6cba1ac95 100644 --- a/src/main/java/in/koreatech/koin/domain/timetableV3/service/TimetableFrameServiceV3.java +++ b/src/main/java/in/koreatech/koin/domain/timetableV3/service/TimetableFrameServiceV3.java @@ -96,4 +96,12 @@ 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); + } } From ff23b9e193e64920d9b745eeb58e09f239b2a085 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EA=B4=80=EA=B7=9C?= Date: Sun, 5 Jan 2025 09:39:44 +0900 Subject: [PATCH 5/8] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../response/TimetableFramesResponseV3.java | 2 +- .../acceptance/TimetableFrameApiV3Test.java | 225 ++++++++++++++++++ 2 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 src/test/java/in/koreatech/koin/acceptance/TimetableFrameApiV3Test.java 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 index 2934f8cf7..fcacb89d7 100644 --- 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 @@ -27,7 +27,7 @@ public record TimetableFramesResponseV3( @JsonNaming(SnakeCaseStrategy.class) public record InnerTimetableFrameResponse( @Schema(description = "학기", example = "2학기", requiredMode = REQUIRED) - String termName, + String term, @Schema(description = "timetableFrame 리스트", requiredMode = REQUIRED) List frames 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 000000000..d70b5e990 --- /dev/null +++ b/src/test/java/in/koreatech/koin/acceptance/TimetableFrameApiV3Test.java @@ -0,0 +1,225 @@ +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, + "timetable_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(""" + { + "timetable_name": "새로운 이름", + "is_main": true + } + """ + ) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" + [ + { + "id": 1, + "timetable_name": "새로운 이름", + "is_main": true + } + ] + """)); + } + + @Test + void 모든_시간표_frame을_조회한다() throws Exception { + timetableV2Fixture.시간표1(user, semester); + timetableV2Fixture.시간표2(user, semester); + + mockMvc.perform( + get("/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().isOk()) + .andExpect(content().json(""" + [ + { + "year": 2019, + "timetable_frames": [ + { + "term": "2학기", + "frames": [ + { + "id": 1, + "timetable_name": "시간표1", + "is_main": true + }, + { + "id": 2, + "timetable_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, + "timetable_name": "시간표1", + "is_main": true + }, + { + "id": 4, + "timetable_name": "시간표2", + "is_main": false + }, + { + "id": 5, + "timetable_name": "시간표3", + "is_main": false + } + ] + }, + { + "term": "1학기", + "frames": [ + { + "id": 1, + "timetable_name": "시간표1", + "is_main": true + }, + { + "id": 2, + "timetable_name": "시간표2", + "is_main": false + } + ] + } + ] + } + ] + """)); + } +} From 7ce29fd9bb11aa2a6f6f65ac6dc0edef6713bdc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EA=B4=80=EA=B7=9C?= Date: Mon, 6 Jan 2025 14:00:49 +0900 Subject: [PATCH 6/8] =?UTF-8?q?chore:=20=EB=A6=AC=EB=B7=B0=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TimetableFrameCreateRequestV3.java | 4 ++-- .../TimetableFrameUpdateRequestV3.java | 2 +- .../response/TimetableFrameResponseV3.java | 2 +- .../exception/InvalidTermFormatException.java | 20 +++++++++++++++++++ .../koin/domain/timetableV3/model/Term.java | 4 ++-- .../acceptance/TimetableFrameApiV3Test.java | 20 +++++++++---------- 6 files changed, 36 insertions(+), 16 deletions(-) create mode 100644 src/main/java/in/koreatech/koin/domain/timetableV3/exception/InvalidTermFormatException.java 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 index 650a6006c..e2a13e6eb 100644 --- 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 @@ -13,8 +13,8 @@ @JsonNaming(value = SnakeCaseStrategy.class) public record TimetableFrameCreateRequestV3( - @Schema(description = "학기 년도", example = "2019", requiredMode = REQUIRED) - @NotNull(message = "학기 년도를 입력해주세요") + @Schema(description = "년도", example = "2019", requiredMode = REQUIRED) + @NotNull(message = "년도를 입력해주세요") Integer year, @Schema(description = "년도", example = "2학기", requiredMode = REQUIRED) 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 index 6211a2d4c..f2c7df024 100644 --- 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 @@ -15,7 +15,7 @@ public record TimetableFrameUpdateRequestV3( @Schema(description = "시간표 이름", example = "시간표1", requiredMode = REQUIRED) @Size(max = 255, message = "시간표 이름의 최대 길이는 255자입니다.") @NotBlank(message = "시간표 이름을 입력해주세요.") - String timetableName, + String name, @Schema(description = "메인 시간표 여부", example = "false", requiredMode = REQUIRED) @NotNull(message = "시간표 메인 여부를 입력해주세요.") 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 index 9cd8250d6..fe0eea4a1 100644 --- 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 @@ -14,7 +14,7 @@ public record TimetableFrameResponseV3( Integer id, @Schema(description = "시간표 이름", example = "시간표1", requiredMode = REQUIRED) - String timetableName, + String name, @Schema(description = "메인 시간표 여부", example = "false", requiredMode = REQUIRED) Boolean isMain 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 000000000..bba49e5b0 --- /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 76eb3a07a..08ab181e8 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/test/java/in/koreatech/koin/acceptance/TimetableFrameApiV3Test.java b/src/test/java/in/koreatech/koin/acceptance/TimetableFrameApiV3Test.java index d70b5e990..3100c0347 100644 --- a/src/test/java/in/koreatech/koin/acceptance/TimetableFrameApiV3Test.java +++ b/src/test/java/in/koreatech/koin/acceptance/TimetableFrameApiV3Test.java @@ -64,7 +64,7 @@ void setup() { [ { "id": 1, - "timetable_name": "시간표1", + "name": "시간표1", "is_main": true } ] @@ -81,7 +81,7 @@ void setup() { .header("Authorization", "Bearer " + token) .content(""" { - "timetable_name": "새로운 이름", + "name": "새로운 이름", "is_main": true } """ @@ -93,7 +93,7 @@ void setup() { [ { "id": 1, - "timetable_name": "새로운 이름", + "name": "새로운 이름", "is_main": true } ] @@ -123,12 +123,12 @@ void setup() { "frames": [ { "id": 1, - "timetable_name": "시간표1", + "name": "시간표1", "is_main": true }, { "id": 2, - "timetable_name": "시간표2", + "name": "시간표2", "is_main": false } ] @@ -187,17 +187,17 @@ void setup() { "frames": [ { "id": 3, - "timetable_name": "시간표1", + "name": "시간표1", "is_main": true }, { "id": 4, - "timetable_name": "시간표2", + "name": "시간표2", "is_main": false }, { "id": 5, - "timetable_name": "시간표3", + "name": "시간표3", "is_main": false } ] @@ -207,12 +207,12 @@ void setup() { "frames": [ { "id": 1, - "timetable_name": "시간표1", + "name": "시간표1", "is_main": true }, { "id": 2, - "timetable_name": "시간표2", + "name": "시간표2", "is_main": false } ] From 263439c67ced78e7833b879f0fec042695d7da00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EA=B4=80=EA=B7=9C?= Date: Mon, 6 Jan 2025 14:10:25 +0900 Subject: [PATCH 7/8] =?UTF-8?q?chore:=20=EB=A6=AC=EB=B7=B0=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/timetableV3/service/TimetableFrameServiceV3.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 6cba1ac95..bd83a63e1 100644 --- a/src/main/java/in/koreatech/koin/domain/timetableV3/service/TimetableFrameServiceV3.java +++ b/src/main/java/in/koreatech/koin/domain/timetableV3/service/TimetableFrameServiceV3.java @@ -65,7 +65,7 @@ public List updateTimetableFrame( TimetableFrame frame = timetableFrameRepositoryV3.getById(timetableFrameId); validateTimetableFrameUpdate(frame, request.isMain()); cancelIfMainTimetable(userId, frame.getSemester().getId(), request.isMain()); - frame.updateTimetableFrame(frame.getSemester(), request.timetableName(), request.isMain()); + frame.updateTimetableFrame(frame.getSemester(), request.name(), request.isMain()); List frames = timetableFrameRepositoryV3.findByUserAndSemester(frame.getUser(), frame.getSemester()); From 1a69ff765e0e7a483be422736191ced24449ed0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EA=B4=80=EA=B7=9C?= Date: Mon, 6 Jan 2025 14:54:50 +0900 Subject: [PATCH 8/8] =?UTF-8?q?chore:=20dto=20=EC=B0=B8=EC=A1=B0=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../response/TimetableFramesResponseV3.java | 22 ++--- .../acceptance/TimetableFrameApiV3Test.java | 92 +++++++++---------- 2 files changed, 52 insertions(+), 62 deletions(-) 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 index fcacb89d7..c66bc10d6 100644 --- 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 @@ -11,7 +11,6 @@ import com.fasterxml.jackson.databind.annotation.JsonNaming; -import in.koreatech.koin.domain.timetableV2.dto.response.TimetableFrameResponse; import in.koreatech.koin.domain.timetableV2.model.TimetableFrame; import in.koreatech.koin.domain.timetableV3.model.Term; import io.swagger.v3.oas.annotations.media.Schema; @@ -30,14 +29,15 @@ public record InnerTimetableFrameResponse( String term, @Schema(description = "timetableFrame 리스트", requiredMode = REQUIRED) - List frames + List frames ) { - + } public static List from(List timetableFrameList) { List responseList = new ArrayList<>(); - Map>> groupedByYearAndTerm = new TreeMap<>(Comparator.reverseOrder()); + Map>> groupedByYearAndTerm = new TreeMap<>( + Comparator.reverseOrder()); // 일차로 Term으로 그룹핑, 이후 year로 그룹핑 for (TimetableFrame timetableFrame : timetableFrameList) { @@ -46,25 +46,25 @@ public static List from(List timetabl groupedByYearAndTerm.computeIfAbsent(year, k -> new TreeMap<>(Comparator.comparing(Term::getPriority))); - Map> termMap = groupedByYearAndTerm.get(year); + Map> termMap = groupedByYearAndTerm.get(year); termMap.computeIfAbsent(term, k -> new ArrayList<>()); - termMap.get(term).add(TimetableFrameResponse.from(timetableFrame)); + termMap.get(term).add(TimetableFrameResponseV3.from(timetableFrame)); } // 메인 시간표가 멘 위로 그 다음은 id로 정렬 groupedByYearAndTerm.values().forEach(termMap -> termMap.values().forEach(frameList -> - frameList.sort(Comparator.comparing(TimetableFrameResponse::isMain).reversed() - .thenComparing(TimetableFrameResponse::id)) + frameList.sort(Comparator.comparing(TimetableFrameResponseV3::isMain).reversed() + .thenComparing(TimetableFrameResponseV3::id)) ) ); - for (Map.Entry>> yearEntry : groupedByYearAndTerm.entrySet()) { + for (Map.Entry>> yearEntry : groupedByYearAndTerm.entrySet()) { List termResponses = new ArrayList<>(); int year = yearEntry.getKey(); - Map> termMap = yearEntry.getValue(); + Map> termMap = yearEntry.getValue(); - for (Map.Entry> termEntry : termMap.entrySet()) { + for (Map.Entry> termEntry : termMap.entrySet()) { Term term = termEntry.getKey(); termResponses.add(new InnerTimetableFrameResponse(term.getDescription(), termEntry.getValue())); } diff --git a/src/test/java/in/koreatech/koin/acceptance/TimetableFrameApiV3Test.java b/src/test/java/in/koreatech/koin/acceptance/TimetableFrameApiV3Test.java index 3100c0347..a0c19ac0d 100644 --- a/src/test/java/in/koreatech/koin/acceptance/TimetableFrameApiV3Test.java +++ b/src/test/java/in/koreatech/koin/acceptance/TimetableFrameApiV3Test.java @@ -51,24 +51,24 @@ void setup() { post("/v3/timetables/frame") .header("Authorization", "Bearer " + token) .content(String.format(""" - { - "year": "%d", - "term": "%s" - } - """, semester.getYear(), semester.getTerm().getDescription() + { + "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 - } - ] - """)); + [ + { + "id": 1, + "name": "시간표1", + "is_main": true + } + ] + """)); } @Test @@ -80,33 +80,33 @@ void setup() { put("/v3/timetables/frame/{id}", frameId) .header("Authorization", "Bearer " + token) .content(""" - { - "name": "새로운 이름", - "is_main": true - } - """ + { + "name": "새로운 이름", + "is_main": true + } + """ ) .contentType(MediaType.APPLICATION_JSON) ) .andExpect(status().isOk()) .andExpect(content().json(""" - [ - { - "id": 1, - "name": "새로운 이름", - "is_main": true - } - ] - """)); + [ + { + "id": 1, + "name": "새로운 이름", + "is_main": true + } + ] + """)); } @Test - void 모든_시간표_frame을_조회한다() throws Exception { + void 특정_학기의_모든_시간표_frame을_조회한다() throws Exception { timetableV2Fixture.시간표1(user, semester); timetableV2Fixture.시간표2(user, semester); mockMvc.perform( - get("/v3/timetables/frames") + get("/v3/timetables/frame") .header("Authorization", "Bearer " + token) .param("year", String.valueOf(semester.getYear())) .param("term", String.valueOf(semester.getTerm().getDescription())) @@ -114,29 +114,19 @@ void setup() { ) .andExpect(status().isOk()) .andExpect(content().json(""" - [ - { - "year": 2019, - "timetable_frames": [ - { - "term": "2학기", - "frames": [ - { - "id": 1, - "name": "시간표1", - "is_main": true - }, - { - "id": 2, - "name": "시간표2", - "is_main": false - } - ] - } - ] - } - ] - """)); + [ + { + "id": 1, + "name": "시간표1", + "is_main": true + }, + { + "id": 2, + "name": "시간표2", + "is_main": false + } + ] + """)); } @Test