Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: timetable frame v3 #1160

Merged
merged 9 commits into from
Jan 8, 2025
Original file line number Diff line number Diff line change
@@ -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<List<TimetableFrameResponseV3>> 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<List<TimetableFrameResponseV3>> 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<List<TimetableFrameResponseV3>> 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<List<TimetableFramesResponseV3>> 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<Void> deleteTimetablesFrames(
@RequestParam(name = "year") Integer year,
@RequestParam(name = "term") String term,
@Auth(permit = {STUDENT}) Integer userId
);
}
Original file line number Diff line number Diff line change
@@ -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<List<TimetableFrameResponseV3>> createTimetablesFrame(
@Valid @RequestBody TimetableFrameCreateRequestV3 request,
@Auth(permit = {STUDENT}) Integer userId
) {
List<TimetableFrameResponseV3> response = timetableFrameServiceV3.createTimetablesFrame(request, userId);
return ResponseEntity.ok(response);
}

@PutMapping("/v3/timetables/frame/{id}")
public ResponseEntity<List<TimetableFrameResponseV3>> updateTimetableFrame(
@Valid @RequestBody TimetableFrameUpdateRequestV3 request,
@PathVariable(value = "id") Integer timetableFrameId,
@Auth(permit = {STUDENT}) Integer userId
) {
List<TimetableFrameResponseV3> response = timetableFrameServiceV3.updateTimetableFrame(request,
timetableFrameId, userId);
return ResponseEntity.ok(response);
}

@GetMapping("/v3/timetables/frame")
public ResponseEntity<List<TimetableFrameResponseV3>> getTimetablesFrame(
@RequestParam(name = "year") Integer year,
@RequestParam(name = "term") String term,
@Auth(permit = {STUDENT}) Integer userId
) {
List<TimetableFrameResponseV3> response = timetableFrameServiceV3.getTimetablesFrame(year, term, userId);
return ResponseEntity.ok(response);
}

@GetMapping("/v3/timetables/frames")
public ResponseEntity<List<TimetableFramesResponseV3>> getTimetablesFrames(
@Auth(permit = {STUDENT}) Integer userId
) {
List<TimetableFramesResponseV3> response = timetableFrameServiceV3.getTimetablesFrames(userId);
return ResponseEntity.ok(response);
}

@DeleteMapping("/v3/timetables/frames")
public ResponseEntity<Void> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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
) {
}
Original file line number Diff line number Diff line change
@@ -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()
);
}
}
Original file line number Diff line number Diff line change
@@ -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<InnerTimetableFrameResponse> timetableFrames
) {
@JsonNaming(SnakeCaseStrategy.class)
public record InnerTimetableFrameResponse(
@Schema(description = "ํ•™๊ธฐ", example = "2ํ•™๊ธฐ", requiredMode = REQUIRED)
String term,

@Schema(description = "timetableFrame ๋ฆฌ์ŠคํŠธ", requiredMode = REQUIRED)
List<TimetableFrameResponseV3> frames
) {

}

public static List<TimetableFramesResponseV3> from(List<TimetableFrame> timetableFrameList) {
List<TimetableFramesResponseV3> responseList = new ArrayList<>();
Map<Integer, Map<Term, List<TimetableFrameResponseV3>>> 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<Term, List<TimetableFrameResponseV3>> 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<Integer, Map<Term, List<TimetableFrameResponseV3>>> yearEntry : groupedByYearAndTerm.entrySet()) {
List<InnerTimetableFrameResponse> termResponses = new ArrayList<>();
int year = yearEntry.getKey();
Map<Term, List<TimetableFrameResponseV3>> termMap = yearEntry.getValue();

for (Map.Entry<Term, List<TimetableFrameResponseV3>> termEntry : termMap.entrySet()) {
Term term = termEntry.getKey();
termResponses.add(new InnerTimetableFrameResponse(term.getDescription(), termEntry.getValue()));
}
responseList.add(new TimetableFramesResponseV3(year, termResponses));
}

return responseList;
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -24,6 +24,6 @@ public static Term fromDescription(String description) {
return term;
}
}
throw new KoinIllegalArgumentException("term ์–‘์‹์ด ์ž˜๋ชป๋์Šต๋‹ˆ๋‹ค.");
throw new InvalidTermFormatException("term : " + description);
}
}
Loading
Loading