개요 — 무엇을 하나
역할(Role) 과 팀(Team) 은 사람을 분류하는 정보성 라벨입니다. 화면·기능에 대한 접근을 여는 것은 오직 등급(user_level) 입니다. 즉 어떤 사용자가 무엇을 볼 수 있고 무엇을 저장·삭제할 수 있는지는 100% 등급으로 결정되고, 역할("DBA"·"DevOps"·"보안담당자")과 팀("인프라팀"·"보안팀")은 표시·조직 관리용 꼬리표일 뿐 어떤 권한도 부여하지 않습니다.
이 규칙 덕분에 권한 모델이 단순합니다. 새 사람에게 관리 권한을 주려면 역할을 바꾸는 것이 아니라 등급을 올립니다. "보안담당자" 라벨을 붙였다고 보안 화면이 열리는 것이 아니라, 등급이 스탭(5) 이상이어야 열립니다.
이 문서는 세 가지를 다룹니다.
- 역할(
infr_role) — 직무 라벨 6종(DBA·DevOps·보안담당자·관리자·일반직원·개발자). - 팀(
infr_team) — 조직 단위. 사용자 소속 + 자산 담당자 지정에 사용. - 담당자(Owner) 선택기 — 사용자와 팀을 하나의 드롭다운에서 고르는 통합 SOT(
App\Support\Owners). Instances·Hardware·Storage 등 자산 화면의 "담당자" 필드가 이걸 씁니다.
접근 통제 = 오직 등급(
user_level). 역할·팀은 프로그램적으로 어떤 권한도 열지 않습니다. 등급 체계는 등급(RBAC) 이해를 참고하세요.
언제 쓰나
- 신규 입사자를 등록하고 직무(역할)·소속(팀)을 붙일 때.
- 조직도가 바뀌어 팀을 만들거나 리더를 바꿀 때.
- 서버·하드웨어·스토리지 자산에 담당자(개인 또는 팀)를 지정해 "누가 이걸 책임지나"를 명확히 하고 싶을 때.
- 감사·인수인계에서 "이 자산의 오너가 누구인가"를 조회할 때.
화면 구성
/admin/users(Administration ▸ Users & Roles)는 두 개의 탭으로 구성됩니다.
| 탭 | 내용 |
|---|---|
| 사용자 | 사용자 목록(로그인ID·이름·이메일·역할·팀·등급·상태·마지막 접속) + 모달 CRUD |
| 팀 | 팀 목록(이름·색상·리더·설명·인원수) + 모달 CRUD |
사용자 편집 모달에는 등급·역할·소속 팀 선택기가 있고, 아래에 다음 안내가 붙습니다.
등급이 접근을 결정합니다 — 데모(읽기)·스탭(운영)·관리자(전체). 역할은 표시용입니다.
사용법
역할(Role) — 직무 라벨
역할 마스터는 시더(InfrRoleSeeder)가 key 기준으로 멱등 upsert 하는 6종입니다.
| key | 표시명 | 색 토큰 | seq |
|---|---|---|---|
dba |
DBA | info | 10 |
devops |
DevOps | accent | 20 |
security |
보안담당자 | crit | 30 |
admin |
관리자 | warn | 40 |
staff |
일반직원 | muted | 50 |
developer |
개발자 | good | 60 |
- 사용자는 역할을 최대 하나(
users.role_id, nullable) 가집니다. 미지정 가능. - 역할은 상단바 사용자 드롭다운·목록 배지에 라벨/색으로만 나타납니다.
- 표시 라벨은
App\Support\RolePolicy::roleLabel($roleId)가 60초 캐시로 해석합니다(변경 후RolePolicy::flush()).
역할
admin(라벨 "관리자")은 등급 관리자(10)와 무관합니다. 역할은 라벨일 뿐이므로, 역할이 "관리자"여도 등급이 데모(1)면 아무것도 저장할 수 없습니다. 반대로 등급이 관리자(10)면 역할이 비어 있어도 전체 관리가 됩니다.
팀(Team) — 조직 단위
팀은 [팀] 탭에서 자유롭게 CRUD 합니다. 시드 예시 4팀:
| 이름 | 색 | 설명 |
|---|---|---|
| 인프라팀 | accent | 서버·네트워크·스토리지 운영 |
| 보안팀 | crit | 탐지·대응·차단 관제 |
| 개발팀 | good | 서비스 개발·배포 |
| DBA팀 | info | 데이터베이스 운영·튜닝 |
팀 편집 필드:
| 필드 | 설명 |
|---|---|
이름(name) |
필수. 팀 이름 |
색상(color) |
배지 톤(accent/crit/good/info/muted …) |
팀 리더(lead_user_id) |
사용자 1명(선택) |
설명(description) |
자유 텍스트 |
활성(is_active) |
비활성 팀은 담당자 선택기에서 숨겨짐 |
- 사용자는 팀에 최대 하나 소속(
users.team_id, nullable)됩니다. - 팀 삭제 시: 그 팀에 소속된 사용자의
team_id는null로 정리됩니다(사용자는 남고 미지정 상태가 됨). 팀을 담당자로 지정했던 자산의owner_type='team'참조는 별도이므로 자산 자체는 삭제되지 않습니다.
담당자(Owner) 선택기 — 사용자 + 팀 통합
자산(서버 인스턴스·하드웨어·스토리지)에는 "담당자" 필드가 있고, 개인 또는 팀 어느 쪽이든 지정할 수 있습니다. 이 통합 선택기의 규약은 App\Support\Owners 가 SOT 입니다.
저장 형태 = 두 컬럼
자산 테이블은 담당자를 두 컬럼으로 저장합니다.
| 컬럼 | 의미 |
|---|---|
owner_id |
사용자 또는 팀의 정수 PK |
owner_type |
'user' 또는 'team' |
프런트 교환 형태 = 단일 ref 문자열
브라우저(폼·필터)는 owner_type 과 owner_id 를 한 문자열로 주고받습니다.
user:5 → 사용자 id 5
team:3 → 팀 id 3
부트스트랩 옵션(Owners::options())은 optgroup 렌더용으로 사용자·팀 두 그룹을 내려줍니다(20초 캐시). 닫힌 사용자(is_close=1)와 비활성 팀(is_active=0)은 제외됩니다.
{
"users": [ { "ref": "user:5", "id": 5, "name": "홍길동" } ],
"teams": [ { "ref": "team:3", "id": 3, "name": "인프라팀" } ]
}
서버측 분해·복원
| 헬퍼 | 동작 |
|---|---|
Owners::split($ref) |
"user:5" → ['user', 5]. 형식이 잘못되면 ['user', 0](= 미지정) |
Owners::ref($ownerId, $ownerType) |
(5, 'user') → "user:5". ownerId 가 비면 null |
Owners::resolveName($ownerId, $ownerType) |
카드·목록에 표시할 이름(없으면 null, 20초 캐시) |
Owners::flush() |
사용자/팀 변경 후 캐시 무효화 |
예를 들어 인스턴스 저장 컨트롤러는 다음과 같이 처리합니다.
// 담당자 ref("user:5"/"team:3") → owner_id + owner_type
[$ownerType, $ownerId] = Owners::split($request->input('owner_ref'));
$payload['owner_id'] = $ownerId ?: null;
$payload['owner_type'] = $ownerId ? $ownerType : 'user'; // 미지정이면 기본 'user'
담당자로 필터할 때도 같은 ref 를 씁니다 — 목록 쿼리는 owner 파라미터(user:5/team:3)를 split() 으로 분해해 WHERE owner_id = ? AND owner_type = ? 로 좁힙니다.
담당자 선택기는 Instances · Hardware · Storage & Files 자산 화면에서 동일하게 재사용됩니다. 팀 하나를 담당자로 지정하면 그 팀원 전체가 책임 라인에 들어옵니다(개인 이탈에 강함).
아바타 · 이니셜 · 색
라벨과 함께 표시되는 아바타는 RolePolicy 가 이름에서 자동 생성합니다.
- 이니셜(
initials): 한글/CJK 는 첫 글자 1자, 라틴 이름은 첫·끝 단어 첫 글자 2자. - 색조(
hue):crc32(이름) % 360으로 안정적 HSL 배경. - 등급 톤(
tierTone): 관리자(≥10)=crit · 스탭/운영(≥2)=accent · 데모=muted.
데이터 소스
| 테이블 | 용도 |
|---|---|
infr_role |
역할 마스터(6종, 정보성 라벨) — key·name·color·seq·is_active |
infr_team |
팀 마스터 — name·color·lead_user_id·description·is_active·seq |
users |
사용자 — user_level(접근 결정), role_id(nullable), team_id(nullable) |
| 자산 테이블 | owner_id + owner_type(user/team) 2컬럼으로 담당자 저장(Instances/Hardware/Storage 등) |
SOT 헬퍼:
App\Support\Owners— 담당자 통합 선택기(options/split/ref/resolveName/flush).App\Support\RolePolicy— 등급 라벨/톤, 역할 라벨(60초 캐시), 아바타 이니셜·색.
권한
- 화면 진입 자체가 관리자 전용입니다.
/admin/*는 그룹 미들웨어infr.level:10로 보호되고,UserController·TeamController는 자체적으로 세션user_level >= 10(RolePolicy::ADMIN)을 재확인합니다. 데모(1)·스탭(5)은 접근 시 403. - 등급 임계(참고): 쓰기 ≥ 2(
RolePolicy::WRITE, 데모 차단) · 관리 ≥ 10(ADMIN) · 개발/관측 ≥ 9(DEV, 관리자 10 포함). - 사용자 저장 시
user_level은1,5,10중 하나만 허용됩니다. - 모든 사용자·팀 변경은
infr_audit_log에 감사 기록됩니다(team.create/team.update/team.delete, 사용자 저장/삭제 등).
안전장치(마지막 관리자 보호)
| 상황 | 결과 |
|---|---|
| 마지막 활성 관리자를 강등·비활성·삭제 | 거부 — "마지막 활성 관리자는 강등·비활성·삭제할 수 없습니다." |
| 자기 자신의 관리자 등급을 낮추거나 비활성 | 거부 — "자기 자신의 관리자 권한을 낮추거나 비활성화할 수 없습니다." |
| 자기 자신 삭제 | 거부 — "자기 자신은 삭제할 수 없습니다." |
| 이미 쓰는 로그인 ID | 거부 — "이미 사용 중인 로그인 ID 입니다." |
문제해결 · FAQ
Q. "보안담당자" 역할을 줬는데 보안 화면이 안 열립니다. 역할은 라벨이라 권한을 열지 않습니다. 보안 화면(대부분 읽기)은 등급이 필요합니다 — 스탭(5) 이상으로 올려주세요. 등급(RBAC) 참고.
Q. 담당자 드롭다운에 어떤 사용자/팀이 안 보입니다.
닫힌 사용자(is_close=1)와 비활성 팀(is_active=0)은 선택기에서 제외됩니다. 사용자/팀을 활성으로 돌리거나, 목록이 오래됐다면 20초 캐시가 만료될 때까지 잠시 기다리세요(Owners::flush() 로 즉시 무효화됨).
Q. 팀을 삭제하면 그 팀 사람들이 사라지나요?
아니요. 사용자는 남고 team_id 만 null(미지정)이 됩니다. 자산의 owner_type='team' 참조는 별도라 자산도 삭제되지 않습니다(담당자 표시만 비게 됨).
Q. 담당자를 저장했는데 이름이 안 뜹니다.
resolveName() 은 20초 캐시입니다. 방금 이름을 바꿨다면 잠시 후 갱신됩니다. ref 형식이 user:5/team:3 인지도 확인하세요 — 형식이 깨지면 split() 이 미지정(user:0)으로 취급합니다.
Q. 사용자에게 역할을 두 개 줄 수 있나요?
아니요. role_id 는 단일(nullable)입니다. 조직 단위 표현이 필요하면 팀을 함께 쓰세요(팀도 하나).
관련 항목
- 등급(RBAC) 이해 — 접근을 실제로 결정하는 유일한 축
- 사용자 · 팀 관리 콘솔 —
/admin/usersCRUD 상세 - Instances(인스턴스) — 담당자 선택기가 쓰이는 대표 자산 화면
- Hardware(하드웨어) — 물리 자산 담당자
- Storage & Files — 스토리지 자산 담당자
- 감사 로그 — 사용자·팀·담당자 변경 이력
