← 개발 로그 목록

website: 최고관리자/관리자 2단계 권한을 단일 관리자로 통합

/ 6분 분량 / 개발 로그

어드민 페이지의 SUPER_ADMIN/ADMIN 2단계 권한 구분을 없애고 단일 관리자 권한으로 합친 PR이다.

발단은 우진님 피드백이었다. "최고관리자-관리자-멤버 이거 너무 복잡해요. 어드민페이지 방금 들어갔는데 머리아파서 쓰기 싫었어요." 라는 말을 듣고 나서, 이 3단 구조가 실제로 어떤 가치를 주고 있는지부터 다시 생각해봤다. 스태프 CRUD, 어드민 초대, 다른 어드민 삭제 — 전부 SUPER_ADMIN만 할 수 있게 막아뒀는데, 정작 그 구분이 필요한 이유를 딱히 설명하기 어려웠다. 그래서 권한 자체를 하나로 합치기로 했다.

바꾸기로 한 이상 어중간하게 남겨두지 않는 쪽으로 밀었다. AdminRole enum과 역할변경 API(PATCH /api/admin/admins/:id/role)를 통째로 지웠다. 역할이 하나뿐이면 승급/강등이라는 개념 자체가 성립하지 않으니, 어설프게 "역할은 남기고 체크만 느슨하게" 하는 것보다 애초에 역할 구분을 코드에서 지우는 게 맞다고 판단했다. 컨트롤러 곳곳에 흩어져 있던 hasRole('SUPER_ADMIN') / hasAnyRole('ADMIN','SUPER_ADMIN') 체크는 전부 hasRole('ADMIN')으로 통일했고, 프론트에서도 역할 배지·역할변경 드롭다운·초대 버튼 노출 조건(isSuperAdmin)을 다 걷어냈다.

다만 지울 수 없는 것도 있었다. "마지막 남은 관리자는 삭제할 수 없다"는 안전장치는 그대로 남겨야 했는데, 기존엔 이게 LastSuperAdminException으로 SUPER_ADMIN 기준이었다. 이걸 LastAdminException(에러 코드도 LAST_SUPER_ADMIN → LAST_ADMIN)으로 바꿔서, 역할과 무관하게 전체 어드민 수를 기준으로 판단하도록 일반화했다. 역할 구분이 없어졌다고 이 가드까지 같이 없애면 관리자가 0명이 되는 사고가 날 수 있어서, 이 부분만큼은 의미를 유지한 채 조건만 바꾸는 방식으로 갔다.

DB 쪽이 제일 신경 쓰였다. admins.role 컬럼과 그 CHECK 제약을 지우는 마이그레이션인데, SQLite라 컬럼 드롭이 테이블 재생성 패턴(새 테이블 생성 → 데이터 복사 → 기존 테이블 DROP → RENAME)으로 들어간다. 이 패턴은 복사할 컬럼 목록을 코드에 나열해야 하는데, 하나라도 빠뜨리면 그 값이 조용히 유실된다. 그래서 stage.db 사본을 떠서 실제로 마이그레이션을 적용해보고 4개 계정 데이터가 다 남아있는지, integrity_check까지 통과하는지 수동으로 확인했다.

문제는 여기서 끝나지 않았다. 첫 커밋을 올리고 나니 migration-guard CI가 걸렸다. 이유는 정확했다 — 수동으로 stage.db 사본에 적용해서 확인한 건 맞지만, 그걸 커밋 가능한 자동 테스트로는 남기지 않았던 것이다. 그래서 AdminRoleDropUpgradeTest를 추가했다. V7 상태(role 컬럼이 있는 상태)로 관리자 행을 하나 심어두고 최신 버전까지 마이그레이션을 올린 뒤, role 컬럼 조회 자체가 실패하는지(컬럼이 진짜 사라졌는지), 그리고 이메일·비밀번호 해시·잠금 상태·생성일 같은 나머지 값들이 전부 보존되는지 검증하는 테스트다.

같은 커밋에서 마이그레이션 파일명도 바꿨다. 원래 V8__drop_admin_role.sql로 순번을 매겨뒀는데, 이 브랜치가 갈라진 뒤 db-man 스킬 쪽 컨벤션이 순번 대신 타임스탬프 버전(V20260730015219__...)을 쓰도록 바뀌어 있었다. 동시에 개발 중인 다른 PR과 버전 번호가 충돌하는 걸 막기 위한 변경이었다. 다행히 아직 머지 전이라 체크섬 문제 없이 안전하게 리네임할 수 있었다.

테스트 코드 정리량도 꽤 됐다. AdminManagementControllerTest에서 역할변경(changeRole_*) 테스트들과 "일반 어드민은 403" 류의 테스트를 지웠고, StaffControllerTest에서도 createStaff_NotSuperAdmin_Returns403, deleteStaff_NotSuperAdmin_Returns403 같은 것들을 걷어내고 나머지는 SUPER_ADMIN → ADMIN으로 역할 인자만 바꿨다. AdminAuthControllerTest에는 로그인 응답의 role 필드를 검증하던 테스트가 있었는데(#132에서 SUPER_ADMIN 계정으로만 로그인해서 role이 우연히 항상 SUPER_ADMIN으로 검증되던 문제를 잡았던 테스트였다), role 자체가 응답에서 빠지면서 이 테스트도 함께 삭제됐다.

영향 범위를 확실히 하기 위해 로그인, 초대 수락, 비밀번호 재설정, 계정 잠금 같은 나머지 인증 흐름은 건드리지 않았다는 것도 확인해뒀다. e2e 테스트용 고정 비밀번호 계정(#291)도 이메일·비밀번호는 그대로 두고 내부적으로 역할 인자만 빼는 선에서 정리했다.

백엔드는 관련 테스트 전체가 통과했고(무관한 Mailpit Docker 통합테스트 3건은 제외), 프론트는 tsc --noEmit으로 타입을 확인했다(사전에 존재하던 무관한 react-markdown 모듈 에러 제외). 생성부터 병합까지 25분 정도로 짧게 끝난 PR인데, 실제 변경 범위는 백엔드 컨트롤러·서비스·엔티티부터 프론트 대시보드, shared 타입 계약, DB 마이그레이션까지 전체 레이어에 걸쳐 있었다. 권한 체계처럼 여러 레이어에 얇게 퍼져 있는 개념을 걷어낼 때는, 코드 grep으로 참조를 다 찾아내는 것도 중요하지만 DB 마이그레이션처럼 되돌리기 어려운 부분은 반드시 실제 데이터로 한 번 검증해보는 게 필요하다는 걸 다시 확인한 작업이었다.