stack-analysis
CHAPTER 63 / 90
읽기 약 2분
FUNCTION
사용자 관리: 초대/역할/팀
핵심 개념
초대 토큰·역할(RBAC)·팀 멤버십 — 협업 SaaS 표준 패턴.
본문
데이터 모델
CREATE TABLE tenants (
id UUID PRIMARY KEY,
name VARCHAR NOT NULL
);
CREATE TABLE users (
id UUID PRIMARY KEY,
email VARCHAR UNIQUE NOT NULL,
name VARCHAR
);
-- N:M 관계 (한 사용자가 여러 tenant 가입 가능)
CREATE TABLE tenant_members (
tenant_id UUID NOT NULL REFERENCES tenants(id),
user_id UUID NOT NULL REFERENCES users(id),
role VARCHAR NOT NULL, -- owner, admin, member, viewer
joined_at TIMESTAMP DEFAULT NOW(),
PRIMARY KEY (tenant_id, user_id)
);
CREATE TABLE invitations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
email VARCHAR NOT NULL,
role VARCHAR NOT NULL DEFAULT 'member',
token VARCHAR UNIQUE NOT NULL, -- 한 번만 사용 가능
invited_by UUID REFERENCES users(id),
expires_at TIMESTAMP NOT NULL,
accepted_at TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW()
);초대 흐름
// 1. 초대 발송
async function inviteUser(tenantId: string, email: string, role: string, inviterId: string) {
// 이미 멤버인가?
const existing = await db.tenantMember.findFirst({
where: { tenantId, user: { email } },
});
if (existing) throw new ConflictError('Already a member');
// 진행 중 초대 있는가?
const pending = await db.invitation.findFirst({
where: { tenantId, email, acceptedAt: null, expiresAt: { gt: new Date() } },
});
if (pending) {
// 재발송만
return resendInvitation(pending.id);
}
const token = crypto.randomBytes(32).toString('base64url');
const invitation = await db.invitation.create({
data: {
tenantId, email, role, token,
invitedBy: inviterId,
expiresAt: new Date(Date.now() + 7 * 86400 * 1000),
},
});
await emailQueue.add('invitation', {
email,
inviterName: (await db.user.findUnique({ where: { id: inviterId } }))?.name,
tenantName: (await db.tenant.findUnique({ where: { id: tenantId } }))?.name,
acceptUrl: `${APP_URL}/invitations/${token}`,
});
return invitation;
}
// 2. 초대 수락
async function acceptInvitation(token: string, userId: string) {
const invitation = await db.invitation.findUnique({ where: { token } });
if (!invitation) throw new NotFoundError('Invitation');
if (invitation.acceptedAt) throw new ConflictError('Already accepted');
if (invitation.expiresAt < new Date()) throw new BadRequestError('Expired');
const user = await db.user.findUnique({ where: { id: userId } });
if (user!.email !== invitation.email) {
throw new ForbiddenError('Email mismatch');
}
return db.$transaction([
db.tenantMember.create({
data: {
tenantId: invitation.tenantId,
userId,
role: invitation.role,
},
}),
db.invitation.update({
where: { id: invitation.id },
data: { acceptedAt: new Date() },
}),
]);
}역할 기반 권한 (RBAC)
const PERMISSIONS = {
owner: ['*'], // 모든 권한
admin: [
'project:read', 'project:write', 'project:delete',
'member:read', 'member:invite', 'member:remove',
'billing:read',
],
member: [
'project:read', 'project:write',
'member:read',
],
viewer: [
'project:read',
'member:read',
],
} as const;
function hasPermission(role: string, permission: string): boolean {
const perms = PERMISSIONS[role as keyof typeof PERMISSIONS];
if (!perms) return false;
return perms.includes('*') || perms.includes(permission);
}
// 미들웨어
function requirePermission(permission: string) {
return (req: Request, res: Response, next: NextFunction) => {
if (!hasPermission(req.tenantRole!, permission)) {
return res.status(403).json({
error: 'forbidden',
required: permission,
});
}
next();
};
}
// 사용
router.post('/projects',
tenantMiddleware,
requirePermission('project:write'),
createProject,
);조직 전환 (multi-tenant)
// 사용자가 여러 tenant 소속 시
// 헤더 X-Tenant-Id로 현재 활성 tenant 지정
app.get('/api/me/tenants', async (req, res) => {
const tenants = await db.tenantMember.findMany({
where: { userId: req.user!.id },
include: { tenant: { select: { id: true, name: true, slug: true } } },
});
res.json(tenants);
});
// UI — 좌측 사이드바에 tenant 전환
// Slack/Discord 스타일다음 챕터
CH.64 "알림 시스템: 이메일/푸시/인앱".
AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude
무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6
내 프로젝트의 사용자 관리 부분을 분석해서 실전 분석 + 개선 우선순위를 알려줘.
ChatGPT
무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro
사용자 관리 관련 실제 서비스 5개를 비교 분석해서 패턴 추출를 알려줘.
Gemini
무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro
내 코드베이스에서 사용자 관리 최적화 가능 위치를 보고해줘.
Grok
무료: Grok 4.1 / SuperGrok $30/mo
2026년 한국 풀스택 시장의 사용자 관리 트렌드를 솔직히 알려줘.
⭐ 이것만 기억하세요
사용자 관리: 초대/역할/팀은 이 3가지만 확실히 잡으세요
1.초대 토큰 = 1회용·만료 시간 7일 — 보안 + UX
2.RBAC = 역할별 권한 매트릭스 — owner/admin/member/viewer 4단
3.한 사용자가 여러 tenant 소속 — N:M 관계 + 활성 tenant 헤더
공유하기
진행도 63 / 90