Repository navigation
[Feat/#146] 사물함 상태 일괄 변경 관리자 API 추가 #148
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package kr.ac.kookmin.stream.api.admin.event.locker.request; | ||
|
|
||
| import jakarta.validation.constraints.NotEmpty; | ||
| import jakarta.validation.constraints.NotNull; | ||
| import java.util.List; | ||
| import kr.ac.kookmin.stream.event.domain.locker.domain.LockerStatus; | ||
|
|
||
| /** | ||
| * 구역 안의 사물함 여러 개를 같은 상태로 바꾼다. | ||
| * <p> | ||
| * 구역 상세가 내려주는 {@code ASSIGNED}는 신청으로 생기는 표시용 값이라 이 경로로는 바꿀 수 없다. | ||
| */ | ||
| public record LockerStatusChangeRequest( | ||
| @NotEmpty(message = "사물함을 하나 이상 선택해 주세요.") | ||
| List<@NotNull(message = "사물함 ID는 비워 둘 수 없습니다.") Long> lockerIds, | ||
|
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.
|
||
|
|
||
| @NotNull(message = "변경할 상태를 입력해 주세요.") | ||
| LockerStatus lockerStatus | ||
| ) { | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| package kr.ac.kookmin.stream.api.admin.event.locker.response; | ||
|
|
||
| import java.util.List; | ||
| import kr.ac.kookmin.stream.event.domain.locker.domain.Locker; | ||
| import kr.ac.kookmin.stream.event.domain.locker.domain.LockerStatus; | ||
|
|
||
| /** | ||
| * 요청한 사물함 전체를 바뀐 뒤 상태로 돌려준다. 이미 그 상태였던 사물함도 담긴다. | ||
| */ | ||
| public record AdminLockerStatusChangeResponse(List<LockerItem> lockers) { | ||
|
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.
|
||
|
|
||
| public static AdminLockerStatusChangeResponse from(List<Locker> lockers) { | ||
| return new AdminLockerStatusChangeResponse(lockers.stream() | ||
| .map(LockerItem::from) | ||
| .toList()); | ||
| } | ||
|
|
||
| /** 도메인 {@link Locker}와 이름이 겹치지 않도록 {@code LockerItem}으로 둔다. */ | ||
| public record LockerItem(Long lockerId, int lockerNumber, LockerStatus lockerStatus) { | ||
|
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.
회차 관리 쓰기 API처럼 |
||
|
|
||
| public static LockerItem from(Locker locker) { | ||
| return new LockerItem(locker.getId(), locker.getLockerNumber(), locker.getStatus()); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -42,4 +42,17 @@ public static Locker of( | |
| public boolean isSelectable(boolean applied) { | ||
| return status == LockerStatus.AVAILABLE && !applied; | ||
| } | ||
|
|
||
| /** | ||
| * 사물함 자체 상태를 바꾼다. 바뀐 값이 있으면 true를 돌려준다. | ||
| * <p> | ||
| * 운영진이 여러 칸을 한 번에 바꾸므로, 이미 그 상태인 사물함은 저장하지 않도록 바뀌었는지 알려준다. | ||
| */ | ||
| public boolean changeStatus(LockerStatus status) { | ||
|
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.
public void changeStatus(LockerStatus status) {
this.status = status;
}lockers.forEach(locker -> locker.changeStatus(status));
lockerRepository.saveLockers(lockers); |
||
| if (this.status == status) { | ||
| return false; | ||
| } | ||
| this.status = status; | ||
| return true; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,11 @@ | ||
| package kr.ac.kookmin.stream.event.domain.locker.service; | ||
|
|
||
| import java.util.List; | ||
| import kr.ac.kookmin.stream.event.domain.locker.domain.Locker; | ||
| import kr.ac.kookmin.stream.event.domain.locker.domain.LockerPeriodCreateCommand; | ||
| import kr.ac.kookmin.stream.event.domain.locker.domain.LockerPeriodSummary; | ||
| import kr.ac.kookmin.stream.event.domain.locker.domain.LockerPeriodUpdateCommand; | ||
| import kr.ac.kookmin.stream.event.domain.locker.domain.LockerStatus; | ||
|
|
||
| /** | ||
| * 운영진 사물함 API가 쓰는 서비스. | ||
|
|
@@ -36,4 +38,14 @@ public interface AdminLockerService { | |
| * 운영 회차를 소프트 삭제한다. 신청이 한 건이라도 있으면 거부한다. | ||
| */ | ||
| void deletePeriod(Long lockerPeriodId); | ||
|
|
||
| /** | ||
| * 구역 안의 사물함 여러 개의 상태를 한 번에 바꾸고, 바뀐 사물함을 돌려준다. | ||
|
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.
|
||
| * <p> | ||
| * 하나라도 바꿀 수 없으면 아무것도 바꾸지 않는다. 사용 기간이 끝나지 않은 회차에 배정된 사물함은 | ||
| * 이용 중인 학생이 있으므로 {@code DISABLED}로 바꿀 수 없다. | ||
| * | ||
| * @return 요청한 사물함 전체. 이미 그 상태였던 사물함도 포함한다 | ||
| */ | ||
| List<Locker> changeLockerStatus(Long sectionId, List<Long> lockerIds, LockerStatus status); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,22 @@ | ||
| package kr.ac.kookmin.stream.event.domain.locker.service.impl; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.time.LocalDateTime; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Set; | ||
| import java.util.function.Function; | ||
| import java.util.stream.Collectors; | ||
| import kr.ac.kookmin.stream.common.BusinessException; | ||
| import kr.ac.kookmin.stream.event.domain.locker.domain.Locker; | ||
| import kr.ac.kookmin.stream.event.domain.locker.domain.LockerApplication; | ||
| import kr.ac.kookmin.stream.event.domain.locker.domain.LockerApplicationStatus; | ||
| import kr.ac.kookmin.stream.event.domain.locker.domain.LockerErrorCode; | ||
| import kr.ac.kookmin.stream.event.domain.locker.domain.LockerPeriod; | ||
| import kr.ac.kookmin.stream.event.domain.locker.domain.LockerPeriodCreateCommand; | ||
| import kr.ac.kookmin.stream.event.domain.locker.domain.LockerPeriodSummary; | ||
| import kr.ac.kookmin.stream.event.domain.locker.domain.LockerPeriodUpdateCommand; | ||
| import kr.ac.kookmin.stream.event.domain.locker.domain.LockerStatus; | ||
| import kr.ac.kookmin.stream.event.domain.locker.repository.LockerApplicationRepository; | ||
| import kr.ac.kookmin.stream.event.domain.locker.repository.LockerRepository; | ||
| import kr.ac.kookmin.stream.event.domain.locker.service.AdminLockerService; | ||
|
|
@@ -66,6 +75,66 @@ public void deletePeriod(Long lockerPeriodId) { | |
| } | ||
| } | ||
|
|
||
| @Override | ||
| @Transactional | ||
| public List<Locker> changeLockerStatus(Long sectionId, List<Long> lockerIds, LockerStatus status) { | ||
| List<Locker> lockers = findLockersInSection(sectionId, lockerIds); | ||
| if (status == LockerStatus.DISABLED) { | ||
|
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.
public enum LockerStatus {
AVAILABLE,
DISABLED;
public boolean isUsable() {
return this == AVAILABLE;
}
}
// Locker.isSelectable: status.isUsable() && !applied
// 여기: if (!status.isUsable()) { validateNotInUse(lockerIds); } |
||
| requireNotInUse(lockerIds); | ||
|
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.
두 방향 중 하나로 맞추면 좋을 것 같아요.
운영 기준은 팀에서 한번 같이 정하면 좋겠습니다! |
||
| } | ||
|
|
||
| // 이미 그 상태인 사물함은 저장하지 않는다. 36칸 구역에서 두 칸만 바꿔도 전체를 쓰지 않도록 한다 | ||
| lockerRepository.saveLockers(lockers.stream() | ||
| .filter(locker -> locker.changeStatus(status)) | ||
| .toList()); | ||
| return lockers; | ||
| } | ||
|
|
||
| /** | ||
| * 요청한 사물함을 구역 소속으로 확인하고 돌려준다. 하나라도 그 구역에 없으면 아무것도 바꾸지 않도록 여기서 끊는다. | ||
| */ | ||
| private List<Locker> findLockersInSection(Long sectionId, List<Long> lockerIds) { | ||
|
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.
|
||
| Map<Long, Locker> lockersInSection = lockerRepository.findLockersBySectionId(sectionId).stream() | ||
| .collect(Collectors.toMap(Locker::getId, Function.identity())); | ||
|
|
||
| return lockerIds.stream() | ||
| .map(lockerId -> { | ||
| Locker locker = lockersInSection.get(lockerId); | ||
| if (locker == null) { | ||
| throw new BusinessException(LockerErrorCode.LOCKER_NOT_IN_SECTION); | ||
|
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.
그리고 없는 |
||
| } | ||
| return locker; | ||
| }) | ||
| .toList(); | ||
| } | ||
|
|
||
| /** | ||
| * 아직 배정 상태인 신청이 걸린 사물함이 하나라도 있으면 끊는다. | ||
| * <p> | ||
| * 배정인지 만료인지는 내 신청 내역 화면과 같은 기준을 써야 하므로 {@link LockerApplicationStatus}에 맡긴다. | ||
| * 저장소는 신청과 회차를 읽어 오기만 한다. | ||
| */ | ||
| private void requireNotInUse(List<Long> lockerIds) { | ||
|
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.
막으려면 사물함 행을 비관적 락으로 같이 잡아야 하는데 저장소에 선례가 없는 패턴이라, 위 |
||
| List<LockerApplication> applications = lockerApplicationRepository.findByLockerIds(lockerIds); | ||
| if (applications.isEmpty()) { | ||
| return; | ||
| } | ||
|
|
||
| LocalDate today = LocalDate.now(); | ||
| Set<Long> assignedPeriodIds = lockerRepository.findPublishedPeriodsByIds(applications.stream() | ||
|
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.
|
||
| .map(LockerApplication::getLockerPeriodId) | ||
| .collect(Collectors.toSet())).stream() | ||
| .filter(period -> LockerApplicationStatus.from(period, today) == LockerApplicationStatus.ASSIGNED) | ||
| .map(LockerPeriod::getId) | ||
| .collect(Collectors.toSet()); | ||
|
|
||
| boolean inUse = applications.stream() | ||
| .anyMatch(application -> assignedPeriodIds.contains(application.getLockerPeriodId())); | ||
| if (inUse) { | ||
| throw new BusinessException(LockerErrorCode.LOCKER_IN_USE); | ||
| } | ||
| } | ||
|
|
||
| private LockerPeriod getPeriod(Long lockerPeriodId) { | ||
| return lockerRepository.findPeriodById(lockerPeriodId) | ||
| .orElseThrow(() -> new BusinessException(LockerErrorCode.LOCKER_PERIOD_NOT_FOUND)); | ||
|
|
||
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]질문 하나 드려요. 학생 쪽 구역 상태(SectionAvailabilityStatus.from)는 사용 중지 칸도 분모에 넣고 있어서, 이 API로DISABLED가 흔해지면 예를 들어 36칸 중 30칸 사용 중지·6칸 빈 구역이 6/36(약 17%)으로 계산되어 학생에게 "거의 마감"으로 보일 것 같아요. 의도하신 동작인지 궁금합니다!