Skip to content

[Feat/#178] 사물함 구역 배치·사진 수정 관리자 API 추가 - #184

Open
sangrae2325 wants to merge 4 commits into
feat/#149-admin-locker-applicationsfrom
feat/#178-admin-locker-section-layout
Open

sangrae2325 wants to merge 4 commits into
feat/#149-admin-locker-applicationsfrom
feat/#178-admin-locker-section-layout

Conversation

@sangrae2325

Copy link
Copy Markdown
Collaborator

#️⃣연관된 이슈

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

운영진 콘솔에서 구역의 칸 배치 구조와 구역 사진을 고치는 관리자 API 2개를 추가합니다.

PUT /v1/admin/lockers/sections/{sectionId}/layout    { root }
PUT /v1/admin/lockers/sections/{sectionId}/photos    { photoFileIds }

사물함 관리자 API의 마지막 작업입니다.

조회는 이미 있습니다. #141의 구역 상세(GET /v1/admin/lockers/sections/{sectionId})가 layout.root·photos·lockers를 한 번에 내려주고, photos에 fileId까지 담아 수정 화면이 쓸 수 있게 해뒀습니다. 이 PR은 쓰기만 더합니다.

❓ 왜 해결해야 하나요?

locker_section_layouts는 V15에서 만들면서 "관리자 등록 API가 아직 없어 행은 DB에 직접 넣는다" 고 적어둔 테이블입니다. 배치를 고치거나 사진을 바꾸려면 지금도 DB를 직접 건드려야 합니다.

V21도 숙제를 하나 남겼습니다 — 사진을 여러 장으로 바꾸면서 photo_file_ids를 JSON NULL로 뒀고, 주석에 "빈 배열은 NULL이 아니라서 NOT NULL을 통과한다 — 최소 한 장 검증은 구역 쓰기 API에서 한다" 고 미뤄뒀습니다. 그 쓰기 API가 이 PR입니다.

⭐ 어떻게 해결했나요?

커밋 3개로 나눴고 각각 단독으로 컴파일됩니다. 마이그레이션은 없습니다 — V15·V21이 이미 쓰기에 필요한 모양을 만들어 뒀습니다.

1. 저장소 경로 — LockerSectionLayout에 createEmpty·changeLayout·changePhotos와 CURRENT_VERSION을 더하고 saveLayout을 연결합니다. 엔티티의 from이 이미 있어 저장 경로만 이었습니다.

2. 서비스 — AdminLockerService.changeSectionLayout·changeSectionPhotos. 두 메서드가 구역 존재 확인을 같이 쓰게 되어 getLockersInSection에 있던 검사를 validateSectionExists로 뽑았습니다.

3. API 계층 — 사진은 AdminLockerPhotoUpdateUseCase가 파일 존재를 확인하고(AdminNoticeWriteUseCase 선례), 배치는 사물함 도메인만 보므로 서비스를 바로 부릅니다. core:domain:event는 파일을 모릅니다.

배치는 해석하지 않고 그대로 저장합니다

V15 주석의 "서버는 layout 내용을 해석하지 않고 그대로 내려준다" 를 쓰기에서도 지킵니다. 칸 번호가 lockers와 맞는지 보지 않습니다 — 블록 종류도 필드 이름도 서버 코드에 없고, 대조하려면 서버가 운영진 콘솔의 블록 형식에 묶여 블록이 바뀔 때마다 따라 고쳐야 합니다.

요청은 ObjectNode로 받습니다. String으로 받으면 깨진 JSON이 MySQL까지 내려가 500이 되고, 블록 타입으로 받으면 서버가 형식을 알아야 합니다. ObjectNode는 Jackson이 파싱해 유효한 JSON 객체임만 보장하고 내용은 모른 채 넘깁니다.

version은 받지 않고 서버가 박습니다. 형식 버전은 서버와 앱 사이의 약속이라 운영진이 화면에서 고를 값이 아니고, 잘못 올리면 앱이 멀쩡한 배치를 "모르는 형식"으로 보고 그리지 않습니다.

사진은 전체 교체입니다

화면이 X로 지우고 "추가"로 넣은 뒤 저장 버튼 하나로 올리는 구조라, 개별 추가·삭제가 아니라 최종 목록을 통째로 받습니다. 배열 순서가 노출 순서입니다. 빠진 사진은 연결만 끊기고 파일 자체는 지우지 않습니다.

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

  • 칸 번호가 어긋난 배치가 그대로 저장됩니다. lockers에 없는 번호를 넣으면 앱이 그 칸을 그리지만 상태를 못 칠하고 눌러도 신청할 게 없습니다. 반대로 번호가 빠지면 있는 사물함이 화면에서 사라집니다. 저장한 운영진이 화면에서 바로 보는 대신, 서버가 블록 형식을 모르는 쪽을 택했습니다
  • 빈 배치 검증은 {}까지만 걸러냅니다. @NotNull이 빈 객체를 막지 못하고 @NotEmpty는 ObjectNode를 지원하지 않아 @AssertTrue로 "필드가 하나라도 있는지"를 봅니다. {"type":"column","children":[]}처럼 비어 있지 않으면서 아무것도 그리지 않는 배치는 통과합니다 — 그 이상은 블록 형식을 알아야 합니다
  • 배치가 없는 구역에는 사진만 올릴 수 없습니다. layout 컬럼이 NOT NULL이라 사진은 배치 행에 얹혀 있습니다. 지금 8개 구역은 전부 배치가 있어 당장 걸리지 않지만, 새 구역은 배치부터 넣어야 합니다
  • 배치 저장과 사진 저장이 겹치면 한쪽이 상대를 덮어씁니다. 둘 다 같은 행을 읽어 고치고 저장하므로 나중 커밋이 앞의 변경을 옛 값으로 되돌립니다. 막으려면 낙관적 락이 필요한데, 탭이 다르고 운영진 혼자 쓰는 화면이라 감수했습니다
  • 구역·사물함 등록·삭제는 없습니다. 화면에 등록 수단이 없어 범위에서 뺐습니다. 칸을 늘려야 할 때 별도 이슈로 봅니다 — 배치 JSON만 고쳐서는 칸이 늘지 않습니다. 학생이 누를 수 있는 칸은 lockers 행이 있어야 합니다
  • 테스트 코드는 없습니다. AGENTS.md 원칙에 따랐습니다

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

  • 학생 앱 조회는 그대로입니다. 저장 형식이 바뀌지 않아 LockerSectionDetailResponse도, #73도 건드리지 않습니다
  • getLockersInSection의 구역 확인이 validateSectionExists로 옮겨졌습니다. 동작은 같습니다 — #148의 상태 일괄 변경은 없는 구역에 여전히 LOCKER_SECTION_NOT_FOUND(404)를 냅니다
  • LockerErrorCode에 LOCKER_SECTION_LAYOUT_NOT_FOUND 하나가 늘고, 기존 코드는 그대로입니다
  • 배치를 저장하면 그 행의 version이 CURRENT_VERSION(1)로 맞춰집니다. V15가 손으로 넣는 테이블이라 저장된 값을 레포에서 확인할 방법이 없는데, 형식이 V15 이후 바뀐 적이 없어 1로 봤습니다. dev DB의 값이 1이 아니면 첫 저장에서 1이 됩니다

🔀 Edge Case & 실패 시나리오

상황 동작
없는 구역에 배치·사진 저장 LOCKER_SECTION_NOT_FOUND (404) — 같은 경로의 구역 상세 조회와 같다
배치가 없는 구역에 배치 저장 통과 — 이 요청이 첫 등록이다
배치가 없는 구역에 사진 저장 LOCKER_SECTION_LAYOUT_NOT_FOUND (404)
root에 배열·숫자·문자열 INVALID_INPUT (400) — ObjectNode 바인딩이 거부한다
root가 깨진 JSON INVALID_INPUT (400) — DB까지 내려가지 않는다
root: {} INVALID_INPUT (400)
root 누락 INVALID_INPUT (400)
칸 번호가 lockers와 어긋남 통과 — 대조하지 않는다
photoFileIds: [] INVALID_INPUT (400) — V21이 미뤄둔 "최소 한 장"
photoFileIds에 없는 파일 FILE_NOT_FOUND (404)
photoFileIds에 같은 파일 두 번 통과 — 중복을 걷어내고 처음 나온 자리를 지킨다
확인 직후 파일이 삭제됨 구역 상세 조회가 목록에서 조용히 제외한다 (공지와 같은 판단)

📋 검토한 대안과 선택 이유

  • 칸 번호를 lockers와 대조하기: 처음엔 검증하는 쪽으로 적었다가 접었습니다. 서버가 블록 포맷을 알아야 하고, V15가 명시한 "해석하지 않는다"를 깨며, 블록이 바뀔 때마다 서버도 따라가야 합니다. 어긋난 번호는 저장한 운영진이 화면에서 바로 봅니다
  • 배치 JSON을 String으로 받기: 깨진 JSON이 MySQL까지 내려가 500이 됩니다. JsonNode로 받고 객체인지 따로 검증하는 방법도 봤는데, ObjectNode로 받으면 Jackson이 바인딩 단계에서 거부해 @AssertTrue 보조 메서드가 필요 없습니다
  • 사진을 개별 추가·삭제 엔드포인트로: 화면에 저장 버튼이 하나뿐이라 부분 변경을 쓸 데가 없습니다. 전체 교체가 화면과 모양이 같습니다
  • version을 요청에서 받기: 프론트가 잘못 올리면 앱이 멀쩡한 배치를 안 그립니다. 형식 버전은 서버·앱 사이의 약속이라 서버가 박습니다
  • 사진 저장 시 빈 배치로 행을 만들기: layout이 NOT NULL이라 가능하긴 한데, 배치 없이 사진만 있는 구역이 생깁니다. 배치부터 넣는 순서를 그대로 두고 404를 냈습니다
  • LOCKER_SECTION_PHOTO_REQUIRED 에러 코드 신설: "최소 한 장"은 @NotEmpty가 400으로 막아주는 형식 검증입니다. #129에서 INVALID_LOCKER_PERIOD_SEMESTER를 "DTO @Min/@Max가 커버한다"며 지운 것과 같은 판단입니다

💬 리뷰 포인트

  • [r] 칸 번호를 대조하지 않는 게 맞는지 봐주세요. 어긋나면 학생 화면에서 칸이 비거나 사라지는데, 서버가 블록 포맷을 모르는 쪽을 택했습니다. 팀에서 "배치 JSON은 서버가 해석하지 않는다"로 합의한 부분이라 그대로 갔습니다
  • [c] 빈 배치를 막은 범위가 애매합니다. {}만 걸러지고 children이 빈 블록은 통과합니다. 아예 안 막는 것(운영진의 선택으로 보기)과 비교해 봐주세요
  • [a] 사진 저장이 배치 행에 의존합니다. 새 구역을 만들 때 배치 → 사진 순서를 지켜야 하는데, 지금 화면 흐름과 어긋나지 않는지 확인 부탁드립니다

@sangrae2325 sangrae2325 self-assigned this Oct 29, 2026
@coderabbitai

coderabbitai Bot commented Oct 29, 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.

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