Files
studio-admin-be/src/licenses/licenses.service.ts
T
2026-07-01 17:30:04 +09:00

221 lines
6.0 KiB
TypeScript

import { ConflictException, Injectable, NotFoundException } from '@nestjs/common';
import { AuthPrismaService } from '../auth-prisma/auth-prisma.service';
import { AssignLicenseDto, ListUserLicensesQuery } from './dto';
@Injectable()
export class LicensesService {
constructor(private readonly authPrisma: AuthPrismaService) {}
async listPlans() {
const now = new Date();
const plans = await this.authPrisma.license.findMany({
orderBy: { code: 'asc' },
include: {
userLicenses: {
select: {
id: true,
startsAt: true,
expiresAt: true,
},
},
},
});
return {
plans: plans.map((plan) => ({
id: plan.id,
code: plan.code,
name: plan.name,
description: plan.description,
defaultDurationDays: plan.defaultDurationDays,
isActive: plan.isActive,
assignedCount: plan.userLicenses.length,
activeUserCount: plan.userLicenses.filter((license) =>
isActiveLicense(license.startsAt, license.expiresAt, now),
).length,
expiringSoonCount: plan.userLicenses.filter((license) =>
expiresSoon(license.expiresAt, now),
).length,
createdAt: plan.createdAt,
updatedAt: plan.updatedAt,
})),
};
}
async listUserLicenses(query: ListUserLicensesQuery) {
const keyword = query.keyword?.trim();
const users = await this.authPrisma.user.findMany({
where: {
OR: keyword
? [
{ email: { contains: keyword, mode: 'insensitive' } },
{ name: { contains: keyword, mode: 'insensitive' } },
]
: undefined,
userLicenses: query.licenseCode
? { some: { license: { code: query.licenseCode } } }
: undefined,
},
orderBy: { createdAt: 'desc' },
include: {
userLicenses: {
orderBy: { startsAt: 'desc' },
include: { license: true },
},
},
});
const now = new Date();
return {
users: users.map((user) => ({
id: user.id,
email: user.email,
name: user.name,
role: user.role,
isActive: user.isActive,
currentLicense:
user.userLicenses.find((userLicense) =>
isActiveLicense(userLicense.startsAt, userLicense.expiresAt, now),
) ?? null,
licenses: user.userLicenses.map((userLicense) => ({
...userLicense,
status: isActiveLicense(userLicense.startsAt, userLicense.expiresAt, now)
? '활성'
: '만료',
})),
})),
};
}
async assign(userId: string, body: AssignLicenseDto) {
const user = await this.authPrisma.user.findUnique({
where: { id: userId },
select: { id: true },
});
if (!user) {
throw new NotFoundException('사용자를 찾을 수 없습니다.');
}
const activeLicense = await this.authPrisma.userLicense.findFirst({
where: {
userId,
startsAt: { lte: new Date() },
OR: [{ expiresAt: null }, { expiresAt: { gte: new Date() } }],
},
select: { id: true },
});
if (activeLicense) {
throw new ConflictException('이미 활성 라이센스가 있습니다. 기존 이력을 수정하세요.');
}
const license = await this.authPrisma.license.findUnique({
where: { code: body.licenseCode },
});
if (!license) {
throw new NotFoundException('라이센스 플랜을 찾을 수 없습니다.');
}
const startsAt = body.startsAt ? new Date(body.startsAt) : new Date();
const expiresAt = body.expiresAt
? new Date(body.expiresAt)
: calculateExpiresAt(startsAt, license.defaultDurationDays);
const userLicense = await this.authPrisma.userLicense.create({
data: {
userId,
licenseId: license.id,
startsAt,
expiresAt,
assignedById: body.assignedById,
},
include: { license: true, user: true },
});
return { userLicense };
}
async update(id: string, body: AssignLicenseDto) {
const current = await this.authPrisma.userLicense.findUnique({
where: { id },
select: { id: true },
});
if (!current) {
throw new NotFoundException('사용자 라이센스를 찾을 수 없습니다.');
}
const license = await this.authPrisma.license.findUnique({
where: { code: body.licenseCode },
});
if (!license) {
throw new NotFoundException('라이센스 플랜을 찾을 수 없습니다.');
}
const startsAt = body.startsAt ? new Date(body.startsAt) : new Date();
const expiresAt = body.expiresAt
? new Date(body.expiresAt)
: calculateExpiresAt(startsAt, license.defaultDurationDays);
const userLicense = await this.authPrisma.userLicense.update({
where: { id },
data: {
licenseId: license.id,
startsAt,
expiresAt,
assignedById: body.assignedById,
},
include: { license: true, user: true },
});
return { userLicense };
}
async revoke(id: string) {
const userLicense = await this.authPrisma.userLicense.findUnique({
where: { id },
select: { id: true },
});
if (!userLicense) {
throw new NotFoundException('사용자 라이센스를 찾을 수 없습니다.');
}
await this.authPrisma.userLicense.delete({
where: { id },
});
return { ok: true };
}
}
function calculateExpiresAt(startsAt: Date, durationDays?: number | null) {
if (!durationDays) {
return null;
}
const expiresAt = new Date(startsAt);
expiresAt.setDate(expiresAt.getDate() + durationDays);
return expiresAt;
}
function isActiveLicense(startsAt: Date, expiresAt: Date | null, now: Date) {
return startsAt <= now && (!expiresAt || expiresAt >= now);
}
function expiresSoon(expiresAt: Date | null, now: Date) {
if (!expiresAt) {
return false;
}
const diffDays = (expiresAt.getTime() - now.getTime()) / 1000 / 60 / 60 / 24;
return diffDays >= 0 && diffDays <= 30;
}