
Supabase RBAC 역할 관리 - 13편. 고객과 관리자 권한 분리
RBAC(역할 기반 접근 제어)로 사용자 권한을 관리합니다.
이 글에서는 역할 데이터 설계, RLS 정책, Server Action 권한 체크, 역할별 UI 표시를 다룹니다.
🤔 역할이 필요한 이유
견적 플랫폼에는 두 종류의 사용자가 있습니다.
- 고객: 문의를 하고, 견적을 받는 사람
- 관리자: 문의를 확인하고, 견적을 보내는 사람
같은 데이터를 다르게 봐야 합니다.
고객: 내 문의만 볼 수 있음
관리자: 모든 문의를 볼 수 있음이걸 RBAC(Role-Based Access Control)라고 합니다.
Claude에게 물었습니다.
고객과 관리자 권한을 어떻게 나눌 수 있어?역할 기반으로 설계하면 된다고 했습니다.
🗄️ 역할 데이터 설계
users 테이블 수정
-- 기존 테이블에 role 컬럼 추가
alter table users
add column role text default 'customer'
check (role in ('customer', 'admin'));
-- 인덱스 추가 (조회 성능)
create index idx_users_role on users(role);
기본값은 customer입니다. 가입하면 자동으로 고객이 됩니다.
타입 정의
// lib/types/auth.ts
export type UserRole = 'customer' | 'admin'
export interface UserWithRole {
id: string
email: string
name: string | null
role: UserRole
}
TypeScript에서도 역할을 타입으로 정의했습니다.
🔐 회원가입 시 역할 부여
일반 회원가입
일반 회원가입은 자동으로 customer가 됩니다.
// app/actions/auth.ts
export async function signup(formData: FormData) {
const supabase = await createClient()
const { data: authData, error } = await supabase.auth.signUp({
email,
password,
options: {
data: { name, phone, company }
}
})
if (error) return { error: error.message }
// users 테이블에도 추가 (role은 기본값 'customer')
await supabase.from('users').insert({
id: authData.user!.id,
email,
name,
phone,
company,
// role: 'customer' ← 기본값이므로 생략
})
return { success: true }
}
관리자 계정 생성
관리자는 직접 DB에서 설정합니다.
-- 직접 업데이트
update users
set role = 'admin'
where email = 'admin@kmetaro.com';
보안상 코드로 관리자를 만들 수 있게 하지 않았습니다.
🎭 역할 기반 접근 제어
1. RLS 정책
DB 레벨에서 먼저 막습니다.
-- 고객은 자신의 데이터만 조회
create policy "Customers can view own data"
on inquiries for select
using (user_id = auth.uid());
-- 관리자는 모든 데이터 조회
create policy "Admins can view all data"
on inquiries for select
using (
exists (
select 1 from users
where id = auth.uid() and role = 'admin'
)
);
고객이 다른 사람의 문의를 보려고 해도 DB가 막습니다.
2. Server Action에서 체크
API 레벨에서도 한 번 더 체크합니다.
// app/actions/admin.ts
'use server'
import { createClient } from '@/lib/supabase/server'
async function requireAdmin() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) throw new Error('로그인이 필요합니다')
const { data: profile } = await supabase
.from('users')
.select('role')
.eq('id', user.id)
.single()
if (profile?.role !== 'admin') {
throw new Error('관리자 권한이 필요합니다')
}
return user
}
export async function deleteInquiry(inquiryId: string) {
await requireAdmin() // 관리자 체크
const supabase = await createClient()
await supabase.from('inquiries').delete().eq('id', inquiryId)
return { success: true }
}
requireAdmin() 함수를 만들어서 재사용합니다.
📱 역할별 UI 표시
네비게이션 메뉴
// components/layout/Navigation.tsx
import { useAuth } from '@/hooks/useAuth'
export function Navigation() {
const { user, isAdmin } = useAuth()
return (
<nav>
<Link href="/">홈</Link>
<Link href="/products">제품</Link>
{user ? (
<>
<Link href="/mypage">마이페이지</Link>
{/* 관리자만 보이는 메뉴 */}
{isAdmin && (
<Link href="/admin">관리자</Link>
)}
<LogoutButton />
</>
) : (
<>
<Link href="/login">로그인</Link>
<Link href="/signup">회원가입</Link>
</>
)}
</nav>
)
}
관리자에게만 "관리자" 메뉴가 보입니다.
조건부 버튼
function InquiryCard({ inquiry }: { inquiry: Inquiry }) {
const { isAdmin } = useAuth()
return (
<div>
<h3>{inquiry.title}</h3>
<p>{inquiry.content}</p>
{isAdmin && (
<button onClick={() => deleteInquiry(inquiry.id)}>
삭제
</button>
)}
</div>
)
}
삭제 버튼은 관리자만 볼 수 있습니다.
🔄 역할 변경 기능
관리자가 다른 사용자의 역할을 바꿀 수 있어야 합니다.
역할 변경 API
// app/actions/admin.ts
export async function updateUserRole(
userId: string,
role: UserRole
) {
await requireAdmin()
const supabase = await createClient()
const { error } = await supabase
.from('users')
.update({ role })
.eq('id', userId)
if (error) return { error: error.message }
return { success: true }
}
관리자 페이지 UI
// app/admin/users/[id]/page.tsx
'use client'
import { updateUserRole } from '@/app/actions/admin'
export default function UserDetailPage({ params }) {
const [role, setRole] = useState(user.role)
const handleRoleChange = async (newRole: UserRole) => {
const result = await updateUserRole(params.id, newRole)
if (result.success) {
setRole(newRole)
alert('역할이 변경되었습니다')
}
}
return (
<div>
<h1>{user.name}</h1>
<select
value={role}
onChange={(e) => handleRoleChange(e.target.value as UserRole)}
>
<option value="customer">고객</option>
<option value="admin">관리자</option>
</select>
</div>
)
}
🛡️ 보안 주의사항
Claude가 중요한 점을 알려줬습니다.
클라이언트에서만 체크하면 안 됨
// ❌ 나쁜 예: 프론트엔드에서만 숨김
{isAdmin && <DeleteButton />}
// 버튼이 안 보여도 API는 호출 가능!
// 개발자 도구로 얼마든지 호출할 수 있음
항상 서버에서 검증
// ✅ 좋은 예: Server Action에서 체크
export async function deleteUser(userId: string) {
const admin = await requireAdmin() // 필수!
// 삭제 로직
}
RLS도 필수
-- 관리자만 삭제 가능
create policy "Only admins can delete"
on users for delete
using (
exists (
select 1 from users
where id = auth.uid() and role = 'admin'
)
);
3중 체크입니다. 미들웨어, Server Action, RLS.
⚠️ 삽질했던 것들
역할이 업데이트 안 됨
역할을 바꾸려는데 아무 반응이 없었습니다.
RLS가 update를 막고 있었습니다.
-- 관리자만 역할 변경 가능
create policy "Admins can update user roles"
on users for update
using (
exists (
select 1 from users
where id = auth.uid() and role = 'admin'
)
);
자기 자신의 역할 변경
관리자가 실수로 자기 역할을 customer로 바꿔버렸습니다.
관리자 페이지에 접근 못하게 됐습니다.
export async function updateUserRole(userId: string, role: UserRole) {
const admin = await requireAdmin()
// 자기 자신은 변경 불가
if (admin.id === userId) {
return { error: '자신의 역할은 변경할 수 없습니다' }
}
// ...
}
자기 자신은 변경 못하게 막았습니다.
역할 캐싱 문제
역할을 바꿨는데 UI가 그대로였습니다.
useAuth 훅이 캐시된 값을 쓰고 있었습니다. router.refresh()로 해결했습니다.
💡 역할 확장
나중에 역할이 더 필요할 수 있습니다.
type UserRole = 'customer' | 'staff' | 'manager' | 'admin'
권한 기반으로 체크하는 방법도 있습니다.
const permissions = {
customer: ['read:own'],
staff: ['read:all', 'create:quotation'],
manager: ['read:all', 'create:quotation', 'delete:inquiry'],
admin: ['*'], // 모든 권한
}
function hasPermission(role: UserRole, permission: string) {
const rolePermissions = permissions[role]
return rolePermissions.includes('*') || rolePermissions.includes(permission)
}
지금은 고객/관리자로 충분해서 이것까지는 구현하지 않았습니다.
🎯 체크 위치 정리
| 체크 위치 | 용도 | 필수 여부 |
|---|---|---|
| 미들웨어 | 페이지 접근 제어 | 선택 |
| Server Action | API 권한 체크 | 필수 |
| RLS | DB 레벨 보호 | 필수 |
| 클라이언트 | UI 표시 제어 | UX용 |
백엔드에서 반드시 체크하세요!
📋 이번 편 요약
| 항목 | 내용 |
|---|---|
| 다룬 기능 | RBAC 역할 관리, 권한 체크 |
| 역할 종류 | customer, admin |
| 체크 위치 | 미들웨어, Server Action, RLS |
| 핵심 원칙 | 백엔드에서 반드시 검증 |
Part 3 인증 파트가 끝났습니다. 다음 Part 4에서는 고객용 기능을 구현합니다.
💬 역할 관리 관련 질문 있으면 댓글로 남겨주세요!
'바이브코딩' 카테고리의 다른 글
| 15. 목록과 상세 구현 - Next.js 제품 카탈로그 페이지 (0) | 2026.01.22 |
|---|---|
| 14. 스팸 방지 구현 - Cloudflare Turnstile CAPTCHA (0) | 2026.01.21 |
| 12. 인증과 권한 체크 구현 - Next.js 미들웨어 페이지 보호 (0) | 2026.01.19 |
| 11. 회원가입과 비밀번호 찾기 - Supabase 이메일 인증 구현 (0) | 2026.01.18 |
| 10. 회원가입부터 로그아웃까지 - Supabase Auth 로그인 구현 (0) | 2026.01.17 |