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; }