Repository navigation
[Feat/#119] 사물함 운영 회차 관리자 API 추가 #129
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
Open
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
effad0e
feat: 사물함 운영 회차 생성·수정 도메인 동작 추가
sangrae2325 9588b30
feat: 사물함 운영 회차 관리자 조회·쓰기 저장소 추가
sangrae2325 ad93469
feat: 사물함 운영 회차 관리자 서비스 추가
sangrae2325 c13867f
feat: 사물함 운영 회차 관리자 API 추가
sangrae2325 fb1552b
refactor: 사물함 회차 목록 응답 항목을 중첩 레코드로 변경
sangrae2325 cc11e4d
refactor: 사물함 운영진 동작을 AdminLockerService로 분리
sangrae2325 9fe8749
refactor: LockerService에서 운영진 전용 메서드 제거
sangrae2325 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
66 changes: 66 additions & 0 deletions
66
...n-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/locker/AdminLockerPeriodApi.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| package kr.ac.kookmin.stream.api.admin.event.locker; | ||
|
|
||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||
| import kr.ac.kookmin.stream.api.admin.AdminApiUser; | ||
| import kr.ac.kookmin.stream.api.admin.event.locker.request.LockerPeriodCreateRequest; | ||
| import kr.ac.kookmin.stream.api.admin.event.locker.request.LockerPeriodUpdateRequest; | ||
| import kr.ac.kookmin.stream.api.admin.event.locker.response.AdminLockerPeriodDetailResponse; | ||
| import kr.ac.kookmin.stream.api.admin.event.locker.response.AdminLockerPeriodListResponse; | ||
| import kr.ac.kookmin.stream.api.common.dto.ApiResponse; | ||
| import kr.ac.kookmin.stream.api.common.openapi.ApiErrorCode; | ||
| import kr.ac.kookmin.stream.common.CommonErrorCode; | ||
| import kr.ac.kookmin.stream.event.domain.locker.domain.LockerErrorCode; | ||
|
|
||
| /** | ||
| * 운영진 사물함 운영 회차 API의 문서 명세. 구현은 {@link AdminLockerPeriodController}가 맡는다. | ||
| * <p> | ||
| * 접근은 {@code ADMIN} role이면 된다. 부서 제한은 두지 않았다. | ||
| * <p> | ||
| * 회차 상태(`periodStatus`)는 저장하지 않고 서버 현재 시각으로 계산한다 — 신청 시작 전이면 {@code UPCOMING}, | ||
| * 사용 종료일이 지났으면 {@code ENDED}, 그 사이는 {@code IN_PROGRESS}다. | ||
| */ | ||
| @Tag(name = "사물함 회차 관리", description = "운영진 사물함 운영 회차 조회·등록·수정·삭제") | ||
| public interface AdminLockerPeriodApi { | ||
|
|
||
| /** 회차 목록과 진행 중인 회차. 학생 앱 조회와 달리 아직 게시하지 않은 회차도 함께 보여준다. */ | ||
| @Operation(summary = "사물함 운영 회차 목록 조회", | ||
| description = "운영 회차를 신청 시작 일시 최신순으로 모두 조회한다. 진행 중인 회차는 `currentPeriod`에도 따로 담기며 " | ||
| + "없으면 null이다. 삭제한 회차는 빠진다.") | ||
| ApiResponse<AdminLockerPeriodListResponse> getLockerPeriods(AdminApiUser apiUser); | ||
|
|
||
| /** 회차 상세. 수정 화면의 기존 값을 채우는 데 쓴다. */ | ||
| @Operation(summary = "사물함 운영 회차 상세 조회", | ||
| description = "운영 회차 한 건을 조회한다. 연도·학기는 수정할 수 없지만 화면 표시를 위해 따로 내려준다. " | ||
| + "삭제한 회차는 찾을 수 없는 것으로 응답한다.") | ||
| @ApiErrorCode(type = LockerErrorCode.class, codes = {"LOCKER_PERIOD_NOT_FOUND"}) | ||
| ApiResponse<AdminLockerPeriodDetailResponse> getLockerPeriod(AdminApiUser apiUser, Long lockerPeriodId); | ||
|
|
||
| /** 회차 등록. 등록한 회차는 바로 학생 앱에서 신청할 수 있는 상태가 된다. */ | ||
| @Operation(summary = "사물함 운영 회차 등록", | ||
| description = "운영 회차를 등록한다. 회차 이름은 연도와 학기로 서버가 만든다(예: 2027-1학기). 신청 기간은 " | ||
| + "시작일 00:00:00부터 마감일 23:59:59까지로 저장한다. 같은 학기는 하나만 등록할 수 있고, 회차 기간" | ||
| + "(신청 시작일 ~ 사용 종료일)은 다른 회차와 겹칠 수 없다. " | ||
| + "신청 마감일과 사용 시작일의 선후는 보지 않는다 — 사용을 시작한 뒤 남은 자리를 추가 모집할 수 있다.") | ||
| @ApiErrorCode(type = CommonErrorCode.class, codes = {"INVALID_INPUT"}) | ||
| @ApiErrorCode(type = LockerErrorCode.class, | ||
| codes = {"INVALID_LOCKER_PERIOD_RANGE", "LOCKER_PERIOD_SEMESTER_DUPLICATED", "LOCKER_PERIOD_OVERLAPPED"}) | ||
| ApiResponse<Void> createLockerPeriod(AdminApiUser apiUser, LockerPeriodCreateRequest request); | ||
|
|
||
| /** 회차 수정. 전체 교체이므로 바꾸지 않는 값도 원래 값 그대로 보낸다. */ | ||
| @Operation(summary = "사물함 운영 회차 수정", | ||
| description = "운영 회차의 신청·사용 기간을 통째로 바꾼다. 연도·학기와 게시 여부는 바뀌지 않는다. " | ||
| + "종료된 회차는 수정할 수 없고, 진행 중인 회차는 신청 시작일을 다른 값으로 바꿀 수 없다(원래 값을 그대로 보내면 된다). " | ||
| + "바꾼 기간이 다른 회차와 겹쳐도 거부한다.") | ||
| @ApiErrorCode(type = CommonErrorCode.class, codes = {"INVALID_INPUT"}) | ||
| @ApiErrorCode(type = LockerErrorCode.class, | ||
| codes = {"LOCKER_PERIOD_NOT_FOUND", "INVALID_LOCKER_PERIOD_RANGE", "LOCKER_PERIOD_ALREADY_ENDED", | ||
| "LOCKER_PERIOD_APPLY_ALREADY_STARTED", "LOCKER_PERIOD_OVERLAPPED"}) | ||
| ApiResponse<Void> updateLockerPeriod(AdminApiUser apiUser, Long lockerPeriodId, LockerPeriodUpdateRequest request); | ||
|
|
||
| /** 회차 삭제. 소프트 삭제라 행은 남고 조회에서만 빠진다. */ | ||
| @Operation(summary = "사물함 운영 회차 삭제", | ||
| description = "운영 회차를 삭제한다. 신청이 한 건이라도 있으면 삭제하지 않는다. 데이터는 남기고 조회에서만 빠진다.") | ||
| @ApiErrorCode(type = LockerErrorCode.class, codes = {"LOCKER_PERIOD_NOT_FOUND", "LOCKER_PERIOD_HAS_APPLICATIONS"}) | ||
| ApiResponse<Void> deleteLockerPeriod(AdminApiUser apiUser, Long lockerPeriodId); | ||
| } |
71 changes: 71 additions & 0 deletions
71
...rc/main/java/kr/ac/kookmin/stream/api/admin/event/locker/AdminLockerPeriodController.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| package kr.ac.kookmin.stream.api.admin.event.locker; | ||
|
|
||
| import jakarta.validation.Valid; | ||
| import kr.ac.kookmin.stream.api.admin.AdminApiUser; | ||
| import kr.ac.kookmin.stream.api.admin.event.locker.request.LockerPeriodCreateRequest; | ||
| import kr.ac.kookmin.stream.api.admin.event.locker.request.LockerPeriodUpdateRequest; | ||
| import kr.ac.kookmin.stream.api.admin.event.locker.response.AdminLockerPeriodDetailResponse; | ||
| import kr.ac.kookmin.stream.api.admin.event.locker.response.AdminLockerPeriodListResponse; | ||
| import kr.ac.kookmin.stream.api.common.dto.ApiResponse; | ||
| import kr.ac.kookmin.stream.event.domain.locker.service.AdminLockerService; | ||
| import lombok.RequiredArgsConstructor; | ||
| 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.RequestMapping; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| @RestController | ||
| @RequestMapping("/v1/admin/lockers/periods") | ||
| @RequiredArgsConstructor | ||
| public class AdminLockerPeriodController implements AdminLockerPeriodApi { | ||
|
|
||
| private final AdminLockerService adminLockerService; | ||
|
|
||
| @Override | ||
| @GetMapping | ||
| public ApiResponse<AdminLockerPeriodListResponse> getLockerPeriods(AdminApiUser apiUser) { | ||
| return ApiResponse.success(AdminLockerPeriodListResponse.from(adminLockerService.getAllPeriods())); | ||
| } | ||
|
|
||
| @Override | ||
| @GetMapping("/{lockerPeriodId}") | ||
| public ApiResponse<AdminLockerPeriodDetailResponse> getLockerPeriod( | ||
| AdminApiUser apiUser, | ||
| @PathVariable Long lockerPeriodId | ||
| ) { | ||
| return ApiResponse.success( | ||
| AdminLockerPeriodDetailResponse.from(adminLockerService.getPeriodById(lockerPeriodId))); | ||
| } | ||
|
|
||
| @Override | ||
| @PostMapping | ||
| public ApiResponse<Void> createLockerPeriod( | ||
| AdminApiUser apiUser, | ||
| @Valid @RequestBody LockerPeriodCreateRequest request | ||
| ) { | ||
| adminLockerService.createPeriod(request.toCommand()); | ||
| return ApiResponse.success(); | ||
| } | ||
|
|
||
| @Override | ||
| @PutMapping("/{lockerPeriodId}") | ||
| public ApiResponse<Void> updateLockerPeriod( | ||
| AdminApiUser apiUser, | ||
| @PathVariable Long lockerPeriodId, | ||
| @Valid @RequestBody LockerPeriodUpdateRequest request | ||
| ) { | ||
| adminLockerService.updatePeriod(lockerPeriodId, request.toCommand()); | ||
| return ApiResponse.success(); | ||
| } | ||
|
|
||
| @Override | ||
| @DeleteMapping("/{lockerPeriodId}") | ||
| public ApiResponse<Void> deleteLockerPeriod(AdminApiUser apiUser, @PathVariable Long lockerPeriodId) { | ||
| adminLockerService.deletePeriod(lockerPeriodId); | ||
| return ApiResponse.success(); | ||
| } | ||
| } |
43 changes: 43 additions & 0 deletions
43
...n/java/kr/ac/kookmin/stream/api/admin/event/locker/request/LockerPeriodCreateRequest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| package kr.ac.kookmin.stream.api.admin.event.locker.request; | ||
|
|
||
| import jakarta.validation.constraints.Max; | ||
| import jakarta.validation.constraints.Min; | ||
| import jakarta.validation.constraints.NotNull; | ||
| import java.time.LocalDate; | ||
| import kr.ac.kookmin.stream.event.domain.locker.domain.LockerPeriodCreateCommand; | ||
|
|
||
| /** | ||
| * 회차 이름은 연도와 학기로 서버가 만든다. 게시 여부도 받지 않는다 — 학생에게 회차를 노출하는 경로는 홈 화면 배너뿐이고 | ||
| * 그쪽에서 따로 설정한다. | ||
| * <p> | ||
| * 신청 기간은 날짜만 받고 시작일 00:00:00 ~ 마감일 23:59:59로 저장한다. | ||
| */ | ||
| public record LockerPeriodCreateRequest( | ||
| @NotNull(message = "연도를 입력해 주세요.") | ||
| @Min(value = 2000, message = "연도가 올바르지 않습니다.") | ||
| @Max(value = 2100, message = "연도가 올바르지 않습니다.") | ||
| Integer year, | ||
|
|
||
| @NotNull(message = "학기를 입력해 주세요.") | ||
| @Min(value = 1, message = "학기는 1 또는 2여야 합니다.") | ||
| @Max(value = 2, message = "학기는 1 또는 2여야 합니다.") | ||
| Integer semester, | ||
|
|
||
| @NotNull(message = "신청 시작일을 입력해 주세요.") | ||
| LocalDate applyStartDate, | ||
|
|
||
| @NotNull(message = "신청 마감일을 입력해 주세요.") | ||
| LocalDate applyEndDate, | ||
|
|
||
| @NotNull(message = "사용 시작일을 입력해 주세요.") | ||
| LocalDate usageStartDate, | ||
|
|
||
| @NotNull(message = "사용 종료일을 입력해 주세요.") | ||
| LocalDate usageEndDate | ||
| ) { | ||
|
|
||
| public LockerPeriodCreateCommand toCommand() { | ||
| return new LockerPeriodCreateCommand( | ||
| year, semester, applyStartDate, applyEndDate, usageStartDate, usageEndDate); | ||
| } | ||
| } | ||
29 changes: 29 additions & 0 deletions
29
...n/java/kr/ac/kookmin/stream/api/admin/event/locker/request/LockerPeriodUpdateRequest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| package kr.ac.kookmin.stream.api.admin.event.locker.request; | ||
|
|
||
| import jakarta.validation.constraints.NotNull; | ||
| import java.time.LocalDate; | ||
| import kr.ac.kookmin.stream.event.domain.locker.domain.LockerPeriodUpdateCommand; | ||
|
|
||
| /** | ||
| * 전체 교체다. 바꾸지 않는 값도 원래 값 그대로 보낸다. 연도·학기와 게시 여부는 이 경로로 바뀌지 않는다. | ||
| * <p> | ||
| * 진행 중인 회차는 신청 시작일을 원래 값 그대로 보내야 한다. | ||
| */ | ||
| public record LockerPeriodUpdateRequest( | ||
| @NotNull(message = "신청 시작일을 입력해 주세요.") | ||
| LocalDate applyStartDate, | ||
|
|
||
| @NotNull(message = "신청 마감일을 입력해 주세요.") | ||
| LocalDate applyEndDate, | ||
|
|
||
| @NotNull(message = "사용 시작일을 입력해 주세요.") | ||
| LocalDate usageStartDate, | ||
|
|
||
| @NotNull(message = "사용 종료일을 입력해 주세요.") | ||
| LocalDate usageEndDate | ||
| ) { | ||
|
|
||
| public LockerPeriodUpdateCommand toCommand() { | ||
| return new LockerPeriodUpdateCommand(applyStartDate, applyEndDate, usageStartDate, usageEndDate); | ||
| } | ||
| } |
38 changes: 38 additions & 0 deletions
38
...kr/ac/kookmin/stream/api/admin/event/locker/response/AdminLockerPeriodDetailResponse.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| package kr.ac.kookmin.stream.api.admin.event.locker.response; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.time.LocalDateTime; | ||
| import kr.ac.kookmin.stream.event.domain.locker.domain.LockerPeriod; | ||
| import kr.ac.kookmin.stream.event.domain.locker.domain.LockerPeriodStatus; | ||
| import kr.ac.kookmin.stream.event.domain.locker.domain.LockerPeriodSummary; | ||
|
|
||
| /** | ||
| * 수정 화면의 기존 값을 채우는 데 쓴다. 연도·학기는 수정할 수 없지만 화면에 보여줘야 해서 따로 내려준다. | ||
| */ | ||
| public record AdminLockerPeriodDetailResponse( | ||
| Long lockerPeriodId, | ||
| String lockerPeriodName, | ||
| int year, | ||
| int semester, | ||
| LocalDateTime applyStartAt, | ||
| LocalDateTime applyEndAt, | ||
| LocalDate usageStartDate, | ||
| LocalDate usageEndDate, | ||
| LockerPeriodStatus periodStatus | ||
| ) { | ||
|
|
||
| public static AdminLockerPeriodDetailResponse from(LockerPeriodSummary summary) { | ||
| LockerPeriod period = summary.period(); | ||
| return new AdminLockerPeriodDetailResponse( | ||
| period.getId(), | ||
| period.getName(), | ||
| period.getYear(), | ||
| period.getSemester(), | ||
| period.getApplyStartAt(), | ||
| period.getApplyEndAt(), | ||
| period.getUsageStartAt(), | ||
| period.getUsageEndAt(), | ||
| summary.status() | ||
| ); | ||
| } | ||
| } |
55 changes: 55 additions & 0 deletions
55
...a/kr/ac/kookmin/stream/api/admin/event/locker/response/AdminLockerPeriodListResponse.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| package kr.ac.kookmin.stream.api.admin.event.locker.response; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.time.LocalDateTime; | ||
| import java.util.List; | ||
| import kr.ac.kookmin.stream.event.domain.locker.domain.LockerPeriod; | ||
| import kr.ac.kookmin.stream.event.domain.locker.domain.LockerPeriodStatus; | ||
| import kr.ac.kookmin.stream.event.domain.locker.domain.LockerPeriodSummary; | ||
|
|
||
| /** | ||
| * @param currentPeriod 진행 중인 회차. 없으면 {@code null}. 회차 기간은 서로 겹치지 않으므로 최대 한 건이다 | ||
| * @param periods 전체 회차. 진행 중인 회차도 함께 들어 있다 | ||
| */ | ||
| public record AdminLockerPeriodListResponse( | ||
| Period currentPeriod, | ||
| List<Period> periods | ||
| ) { | ||
|
|
||
| public static AdminLockerPeriodListResponse from(List<LockerPeriodSummary> summaries) { | ||
| return new AdminLockerPeriodListResponse( | ||
| summaries.stream() | ||
| .filter(summary -> summary.status() == LockerPeriodStatus.IN_PROGRESS) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| .findFirst() | ||
| .map(Period::from) | ||
| .orElse(null), | ||
| summaries.stream() | ||
| .map(Period::from) | ||
| .toList() | ||
| ); | ||
| } | ||
|
|
||
| public record Period( | ||
| Long lockerPeriodId, | ||
| String lockerPeriodName, | ||
| LocalDateTime applyStartAt, | ||
| LocalDateTime applyEndAt, | ||
| LocalDate usageStartDate, | ||
| LocalDate usageEndDate, | ||
| LockerPeriodStatus periodStatus | ||
| ) { | ||
|
|
||
| public static Period from(LockerPeriodSummary summary) { | ||
| LockerPeriod period = summary.period(); | ||
| return new Period( | ||
| period.getId(), | ||
| period.getName(), | ||
| period.getApplyStartAt(), | ||
| period.getApplyEndAt(), | ||
| period.getUsageStartAt(), | ||
| period.getUsageEndAt(), | ||
| summary.status() | ||
| ); | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[a]아주 사소한 건데, 요청은LockerPeriodCreateRequest(접두사 없음), 응답은AdminLockerPeriodDetailResponse(접두사 있음)라 같은 PR 안에서 이름 규칙이 갈려서요. 공지 관리자 API(NoticeCreateRequest/NoticeCreateResponse)처럼 한쪽으로 맞추면 어떨까요?