Skip to content

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

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

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

Conversation

@sangrae2325

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가 되어 학생 화면의 선택 가능 수와 칸 표시에서 빠집니다. 그게 이 기능의 목적입니다.

⭐ 어떻게 해결했나요?

커밋 4개로 나눴고 각각 단독으로 컴파일됩니다.

1. 도메인 — Locker에 changeStatus를 더합니다. 바뀐 값이 있으면 true를 돌려줍니다. 36칸 구역에서 두 칸만 바꿔도 전체를 저장하지 않으려고 서비스가 이 값으로 거릅니다.

2. 저장소 — saveLockers(여러 건 저장)와 findByLockerIds를 추가합니다. 빈 목록이 들어오면 쿼리를 타지 않습니다. 회차 조회는 기존 findPublishedPeriodsByIds를 그대로 씁니다.

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

List<Locker> lockers = findLockersInSection(sectionId, lockerIds);   // 소속 확인
if (status == LockerStatus.DISABLED) {
    requireNotInUse(lockerIds);                                       // 이용 중 확인
}
lockerRepository.saveLockers(lockers.stream()
    .filter(locker -> locker.changeStatus(status))                    // 바뀐 것만 저장
    .toList());
return lockers;

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

이용 중 검사는 DISABLED일 때만 합니다. AVAILABLE로 되돌리는 건 막을 이유가 없고 쿼리 2개를 아낍니다.

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

조회는 LockerService, 변경은 AdminLockerService로 서비스 2개를 주입받습니다. "운영진 전용 동작은 Admin 서비스에 둔다"는 팀 규칙 그대로입니다.

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

  • 같은 사물함을 두 운영진이 동시에 바꾸면 나중 저장이 이깁니다. @DynamicUpdate가 아니라 LockerJpaEntity.from(domain) + merge로 전체 행을 저장해서 다른 컬럼까지 덮어씁니다. 다만 사물함은 상태 말고 바뀌는 값이 없고 운영진 전용이라 감수했습니다
  • lockerIds에 중복이 와도 막지 않습니다. 저장은 changeStatus가 두 번째에 false를 돌려줘서 한 번만 되고, 응답에만 같은 항목이 두 번 담깁니다. 칸 그리드에서 선택 집합을 보내는 화면에선 일어나지 않아 요청 DTO에 distinct를 두지 않았습니다
  • 없는 사물함과 다른 구역 사물함을 구분하지 않습니다. 둘 다 LOCKER_NOT_IN_SECTION(400)입니다. 구역 조회 결과에 없다는 사실만으로는 둘을 가를 수 없어서, 가르려면 조회를 한 번 더 해야 합니다
  • DISABLED 변경은 쿼리가 2개 더 듭니다(신청 조회 → 회차 조회). 한 번에 바꾸는 칸이 수십 개 수준이라 묶음 조회 2개로 끝냈습니다
  • 테스트 코드는 없습니다. AGENTS.md 원칙에 따랐습니다

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

  • 전부 추가입니다. 학생 조회 경로와 LockerService를 하나도 고치지 않았습니다
  • 마이그레이션이 없습니다. lockers.status 컬럼이 이미 있습니다
  • DISABLED로 내리면 학생 화면의 선택 가능 수와 칸 표시에서 빠집니다 — 의도한 동작입니다
  • LockerErrorCode에 2개(LOCKER_NOT_IN_SECTION, LOCKER_IN_USE)가 늘지만 새 경로에서만 던집니다

🔀 Edge Case & 실패 시나리오

상황 동작
lockerIds가 빈 배열 INVALID_INPUT (400) + "사물함을 하나 이상 선택해 주세요."
lockerStatus가 enum에 없는 값 INVALID_INPUT (400) — Jackson이 못 읽어 HttpMessageNotReadableException
요청한 사물함 중 하나가 다른 구역 소속 LOCKER_NOT_IN_SECTION (400), 아무것도 안 바뀜
없는 사물함 ID 위와 같음 (구분하지 않음)
종료되지 않은 회차에 배정된 칸을 DISABLED로 LOCKER_IN_USE (409), 아무것도 안 바뀜
이미 끝난 회차에만 배정된 칸을 DISABLED로 통과 — 이용 중인 학생이 없음
배정된 칸을 AVAILABLE로 통과 — 되돌리는 건 막지 않음
이미 그 상태인 칸 저장하지 않지만 응답에는 담김 (화면이 누른 칸을 전부 갱신해야 함)
삭제된 구역·사물함 조회가 is_deleted = false로 걸러 LOCKER_NOT_IN_SECTION

📋 검토한 대안과 선택 이유

  • "이용 중인가"를 JPQL 한 방으로: 처음엔 그렇게 짜다가, 도메인과 쿼리 양쪽에 판정 기준이 생겨 사용 종료일 당일 포함 여부가 어긋나면 조용히 다르게 동작해서 걷어냈습니다. architecture.md:74도 infrastructure:*가 비즈니스 규칙을 갖지 않는다고 합니다. 저장소는 신청·회차를 읽어 오기만 합니다. (EventRepositoryImpl에 SQL CASE로 상태를 계산하는 선례가 있지만, 그건 그 값으로 목록을 거르느라 쿼리에서 해야 하는 경우라 여기엔 안 맞습니다)
  • 이용 중 판정을 이 서비스에서 !period.isUsageEnded(today)로: 같은 판정에 이름이 두 개 생겨서, 내 신청 내역 화면이 쓰는 LockerApplicationStatus.from(period, today)를 그대로 재사용했습니다. "배정 중"의 기준이 나중에 바뀌어도 한 곳만 고치면 됩니다
  • 게시 여부를 보지 않는 회차 조회 포트를 새로: findPeriodsByIds를 만들었다가 지웠습니다. 게시를 내리는 기능이 없고(AdminLockerService에 공개 토글이 없습니다), 신청은 게시된 회차에만 들어와서 기존 findPublishedPeriodsByIds와 결과가 같습니다. 비슷한 이름의 포트를 늘리지 않았습니다
  • LOCKER_NOT_IN_SECTION을 404로: "없는 사물함"으로 보면 404지만, 구역과 사물함의 조합이 틀린 요청이라 400으로 뒀습니다
  • LOCKER_NOT_FOUND를 따로 두기: 1단계에서 만들었다가 지웠습니다. 위 이유로 던질 곳이 없어 죽은 코드가 됩니다. 사물함 등록·삭제가 붙을 때 필요하면 그때 넣습니다
  • 별도 컨트롤러(AdminLockerStatusController): 화면 단위로 묶는 쪽이 /periods·/applications와 나란히 가서 기존 컨트롤러에 넣었습니다
  • lockerStatus를 String으로 받고 from()으로 변환(AdminFeeController 방식): enum을 그대로 받는 NoticeCreateRequest 쪽을 따랐습니다. 잘못된 값은 어차피 400 INVALID_INPUT입니다
  • 응답을 ApiResponse<Void>로: 화면이 누른 칸을 다시 그려야 해서 바뀐 뒤 상태를 돌려줍니다
  • PUT: 전체 교체가 아니라 상태 한 필드만 바꿔서 PATCH입니다

💬 리뷰 포인트

  • [r] 이미 끝난 회차에만 배정된 칸은 DISABLED로 바꿀 수 있게 했습니다. 이용 중 판정을 LockerApplicationStatus.from(period, today) == ASSIGNED로 두어 내 신청 내역 화면과 기준을 한 곳으로 묶었습니다. 사용 종료일 당일까지는 사용 중입니다(isUsageEnded가 today.isAfter(usageEndAt)). 운영 기준이 이게 맞는지 봐주세요
  • [r] 응답에 이미 그 상태였던 칸도 담습니다. 저장은 안 하고 반환만 합니다. 화면이 "실제로 바뀐 것"만 필요하다면 거르는 쪽으로 바꾸겠습니다
  • [c] 없는 사물함과 다른 구역 사물함을 LOCKER_NOT_IN_SECTION 하나로 묶었습니다. 운영진이 원인을 알기 어려우면 나눠야 할지
  • [c] 응답 레코드 이름이 AdminLockerStatusChangeResponse입니다. 이 패키지 응답 4개가 전부 Admin 접두사를 써서 맞췄는데, 요청은 접두사 없는 LockerStatusChangeRequest라 짝이 안 맞습니다
  • [a] 응답에 lockerNumber를 넣었습니다(명세대로). 요청이 lockerId로 들어오고 화면도 번호를 이미 알아서 빼도 됩니다

@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
  • Configuration used: Repository: billilge/stream-server/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 9d6eab86-ef97-4d3d-ac0d-021f4bb45c7b
  • 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.

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 추가

1 participant