Skip to content

[Feat/#146] 사물함 상태 일괄 변경 관리자 API 추가 - #148

Open
sangrae2325 wants to merge 12 commits into
feat/#132-admin-locker-sectionsfrom
feat/#146-admin-locker-status
Open

sangrae2325 wants to merge 12 commits into
feat/#132-admin-locker-sectionsfrom
feat/#146-admin-locker-status

Conversation

@sangrae2325

@sangrae2325 sangrae2325 commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

#️⃣연관된 이슈

🎯 해결하려는 문제가 무엇인가요?

운영진 콘솔에서 구역 안의 사물함 여러 개를 골라 물리 상태를 한 번에 바꾸는 관리자 API 1개를 추가합니다.

PATCH /v1/admin/lockers/sections/{sectionId}/lockers/status
{ "lockerIds": [106, 107], "lockerStatus": "DISABLED" }

#132(구역 조회)에 이어지는 사물함 관리자 API의 세 번째 작업입니다.

❓ 왜 해결해야 하나요?

사물함이 고장 나도 상태를 바꿀 방법이 DB 직접 수정뿐입니다. LockerRepository에 사물함 쓰기 경로가 아예 없습니다 — 회차는 #119에서 뚫었지만 사물함은 조회만 있습니다.

상태를 DISABLED로 내리면 Locker.isSelectable이 false가 되어 학생 화면의 선택 가능 수와 칸 표시에서 빠집니다. 그게 이 기능의 목적입니다.

⭐ 어떻게 해결했나요?

1. 도메인 — Locker.changeStatus와 LockerStatus.isUsable을 더합니다. 쓸 수 있는 상태를 허용 목록으로 두어, 상태가 늘어도 새 값이 자동으로 "쓸 수 없음"에 들어가게 했습니다. Locker.isSelectable과 이용 중 검사가 같은 메서드를 봅니다.

2. 저장소 — saveLockers(여러 건 저장), findLockersByIds, existsByPeriodIdsAndLockerIds를 추가합니다.

3. 서비스 — AdminLockerService.changeLockerStatus입니다.

List<Locker> lockers = getLockersInSection(sectionId, lockerIds);   // 구역·소속 확인
if (!status.isUsable()) {
    validateNotInUse(lockerIds);                                     // 이용 중 확인
}
lockers.forEach(locker -> locker.changeStatus(status));
lockerRepository.saveLockers(lockers);

검증을 전부 먼저 하고 바꿉니다. "하나라도 안 되면 전체를 안 바꾼다"를 @Transactional에만 기대지 않았습니다 — 예외로 롤백하는 것보다 사전 검증이 의도가 분명하고 실패 사유도 정확해집니다.

이용 중 검사는 쓸 수 없는 상태로 바꿀 때만 합니다. AVAILABLE로 되돌리는 건 막을 이유가 없고 쿼리 2개를 아낍니다.

이용 중 판정은 회차를 먼저 좁힙니다. 회차는 학기당 한두 건이라 전부 읽어도 되고, 그래야 신청 조회가 (회차, 사물함) 유니크 인덱스를 탑니다. 사물함 식별자만으로 신청을 찾으면 탈 인덱스가 없어 이력 전체를 훑습니다.

4. API — 기존 AdminLockerSectionController에 @PatchMapping("/{sectionId}/lockers/status")를 더합니다. 관리자 "구역 관리" 탭 한 화면이 쓰는 API가 전부 /sections/{sectionId} 아래라 컨트롤러를 탭 단위로 맞췄습니다. 조회 컨트롤러에 쓰기가 섞이는 건 AdminNoticeController·AdminLockerPeriodController 선례가 있습니다.

응답 본문은 없습니다. 바뀐 결과는 화면이 구역 상세를 다시 조회해 받습니다.

🧩 이 PR의 한계 & 트레이드오프

  • 이용 중 검사와 학생 신청이 겹치면 막으려던 상태가 생깁니다. 검사가 "신청 없음"을 본 직후 학생이 같은 칸을 신청해 커밋하면, 두 트랜잭션이 서로 다른 테이블에 써서 충돌 없이 둘 다 성공합니다. 유니크 제약은 중복 배정만 막아 이 경우를 못 잡습니다. 완전히 막으려면 사물함 행을 비관적 락으로 잡아야 하는데 저장소에 선례가 없어, Swagger 설명에 적어두고 감수했습니다
  • 배정된 칸은 이 API로 사용 중지할 수 없습니다. 관리자 화면도 배정된 칸을 고를 수 없게 하고 있어 둘이 맞습니다. 배정된 칸이 실제로 고장 나면 #149의 신청 취소가 생긴 뒤 취소 → 사용 중지 로 처리합니다
  • 쿼리가 하나 늘었습니다. 없는 구역을 404로 내보내려고 구역 존재를 따로 확인합니다. 대신 사물함은 구역 전체가 아니라 요청한 것만 읽어 읽는 행은 줄었습니다
  • 테스트 코드는 없습니다. AGENTS.md 원칙에 따랐습니다

⛓️ 기존 기능에 미치는 영향

  • 학생 조회 경로는 그대로입니다. LockerStatus.isUsable 도입으로 Locker.isSelectable 내부 표현만 바뀌고 결과는 같습니다
  • 마이그레이션이 없습니다. lockers.status 컬럼이 이미 있습니다
  • DISABLED로 내리면 학생 화면의 선택 가능 수와 칸 표시에서 빠집니다 — 의도한 동작입니다
  • LockerJpaEntity에 @DynamicUpdate를 붙여 상태 변경이 is_deleted 같은 다른 컬럼을 덮어쓰지 않습니다

🔀 Edge Case & 실패 시나리오

상황 동작
lockerIds가 빈 배열 INVALID_INPUT (400) + "사물함을 하나 이상 선택해 주세요."
lockerStatus가 enum에 없는 값 INVALID_INPUT (400) — Jackson이 못 읽어 HttpMessageNotReadableException
없는 sectionId LOCKER_SECTION_NOT_FOUND (404) — 같은 경로의 구역 상세 조회와 같습니다
요청한 사물함 중 하나가 다른 구역 소속 LOCKER_NOT_IN_SECTION (400), 아무것도 안 바뀜
없는 사물함 ID 위와 같음 (구분하지 않음)
lockerIds에 같은 id가 여러 번 중복을 걷어내고 처리 — 상한이 그 구역의 칸 수가 됩니다
종료되지 않은 회차에 배정된 칸을 DISABLED로 LOCKER_IN_USE (409), 아무것도 안 바뀜
이미 끝난 회차에만 배정된 칸을 DISABLED로 통과 — 이용 중인 학생이 없음
배정된 칸을 AVAILABLE로 통과 — 되돌리는 건 막지 않음
이미 그 상태인 칸 그대로 둡니다. 값이 바뀐 행만 UPDATE됩니다
삭제된 구역·사물함 조회가 is_deleted = false로 걸러 LOCKER_NOT_IN_SECTION

📋 검토한 대안과 선택 이유

  • "이용 중인가"를 JPQL 한 방으로: 처음엔 그렇게 짜다가, 도메인과 쿼리 양쪽에 판정 기준이 생겨 사용 종료일 당일 포함 여부가 어긋나면 조용히 다르게 동작해서 걷어냈습니다. architecture.md:74도 infrastructure:*가 비즈니스 규칙을 갖지 않는다고 합니다. 판정은 내 신청 내역 화면이 쓰는 LockerApplicationStatus.from(period, today)를 그대로 재사용해서, "배정 중"의 기준이 바뀌어도 한 곳만 고치면 됩니다
  • 신청을 사물함 식별자로 먼저 찾기: 그렇게 짰다가 되돌렸습니다. locker_applications에 locker_id로 시작하는 인덱스가 없어 이력 전체를 훑습니다. 회차를 먼저 좁히면 (회차, 사물함) 유니크 인덱스를 타고 쿼리 수는 그대로 둘입니다
  • 응답으로 바뀐 사물함을 돌려주기: 처음엔 그렇게 했는데, 이 응답이 주는 LockerStatus(물리 상태)와 구역 상세가 주는 표시 상태(회차 기준)가 서로 다른 값이라 응답으로 칸을 다시 그리면 어긋납니다. 배정된 칸을 AVAILABLE로 바꾸면 응답엔 AVAILABLE이 오는데 화면은 ASSIGNED로 그려야 합니다. Void로 두고 상세를 다시 조회하게 했습니다
  • changeStatus가 바뀐 여부를 돌려주기: 바뀐 것만 저장하려 했는데, filter 안에서 상태를 바꾸게 되어 스트림 규칙에 어긋납니다. 같은 트랜잭션에서 이미 영속성 컨텍스트에 올라와 있어 전부 넘겨도 값이 바뀐 행만 UPDATE됩니다
  • LOCKER_NOT_IN_SECTION을 404로: "없는 사물함"으로 보면 404지만, 구역과 사물함의 조합이 틀린 요청이라 400으로 뒀습니다. 없는 구역은 404입니다
  • LOCKER_NOT_FOUND를 따로 두기: 만들었다가 지웠습니다. 없는 사물함과 다른 구역 사물함을 운영 화면에서 가를 이유가 없어 던질 곳이 없습니다
  • 요청 길이에 @Size(max = N): 구역마다 칸 수가 달라 숫자를 박으면 언젠가 틀립니다. 중복만 걷어내면 상한이 자연히 그 구역의 칸 수가 됩니다
  • 별도 컨트롤러(AdminLockerStatusController): 화면 단위로 묶는 쪽이 /periods·/applications와 나란히 가서 기존 컨트롤러에 넣었습니다
  • PUT: 전체 교체가 아니라 상태 한 필드만 바꿔서 PATCH입니다

💬 리뷰 포인트

  • [r] 배정된 칸의 사용 중지를 막는 기준이 맞는지 봐주세요. 관리자 화면이 배정된 칸을 고를 수 없게 하고 있어 그에 맞췄고, 고장 난 칸은 #149의 신청 취소가 생긴 뒤 취소 → 사용 중지로 처리할 생각입니다
  • [c] 이용 중 검사와 학생 신청의 경합은 막지 않았습니다. 비관적 락이 저장소에 없는 패턴이라 Swagger에 적어두는 선에서 멈췄는데, 이 선택이 괜찮은지 봐주세요
  • [a] 사용 종료일 당일까지는 사용 중으로 봅니다(isUsageEnded가 today.isAfter(usageEndAt)). 운영 기준이 이게 맞는지

@coderabbitai

coderabbitai Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sangrae2325
sangrae2325 force-pushed the feat/#146-admin-locker-status branch from 5e96754 to 4eeecd7 Compare October 27, 2026 10:01
@sangrae2325
sangrae2325 force-pushed the feat/#146-admin-locker-status branch from 4eeecd7 to 20cc543 Compare October 27, 2026 10:36
@sangrae2325
sangrae2325 force-pushed the feat/#146-admin-locker-status branch from 20cc543 to 9654f0c Compare October 27, 2026 11:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

사물함 상태 일괄 변경 관리자 API 추가

2 participants