import { ConflictException, Injectable, NotFoundException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Prisma } from '@prisma/launcher-client'; import { AuthPrismaService } from '../auth-prisma/auth-prisma.service'; import { LauncherPrismaService } from '../launcher-prisma/launcher-prisma.service'; import { getUserAvatarUrl } from '../users/users.service'; import { CreateTermDto, ListTermsQuery, UpdateTermDto } from './dto'; type AdminRequester = { id: string; name: string; email: string; }; @Injectable() export class TermsService { constructor( private readonly authPrisma: AuthPrismaService, private readonly configService: ConfigService, private readonly launcherPrisma: LauncherPrismaService, ) {} async list(query: ListTermsQuery) { const keyword = query.keyword?.trim(); const page = toPositiveInt(query.page, 1); const limit = Math.min(toPositiveInt(query.limit, 20), 100); const where = { key: query.key || undefined, OR: keyword ? [ { key: { contains: keyword, mode: 'insensitive' as const } }, { body: { contains: keyword, mode: 'insensitive' as const } }, ] : undefined, }; const [terms, total] = await this.launcherPrisma.$transaction([ this.launcherPrisma.launcherTerm.findMany({ where, orderBy: { createdAt: 'desc' }, skip: (page - 1) * limit, take: limit, }), this.launcherPrisma.launcherTerm.count({ where }), ]); return { terms: await this.withCreatorAvatars(terms.map((term, index) => ({ ...term, no: total - ((page - 1) * limit + index), }))), total, page, limit, }; } async get(id: string) { const term = await this.launcherPrisma.launcherTerm.findUnique({ where: { id }, }); if (!term) { throw new NotFoundException('약관을 찾을 수 없습니다.'); } return { term }; } async create(body: CreateTermDto, requester?: AdminRequester) { await this.ensureUniqueKey(body.key); const term = await this.launcherPrisma.launcherTerm.create({ data: { ...toCreateTermData(body), ...this.creatorData(requester), }, }); return { term }; } async update(id: string, body: UpdateTermDto) { await this.ensureExists(id); if (body.key) { await this.ensureUniqueKey(body.key, id); } const term = await this.launcherPrisma.launcherTerm.update({ where: { id }, data: toUpdateTermData(body), }); return { term }; } async remove(id: string) { await this.ensureExists(id); await this.launcherPrisma.launcherTerm.delete({ where: { id }, }); return { ok: true }; } private creatorData(requester?: AdminRequester) { return requester ? { createdById: requester.id, createdByName: requester.name, createdByEmail: requester.email, } : {}; } private async withCreatorAvatars(items: T[]) { const emails = [...new Set(items.map((item) => item.createdByEmail).filter((email): email is string => Boolean(email)))]; if (emails.length === 0) return items.map((item) => ({ ...item, createdByAvatarUrl: null })); const users = await this.authPrisma.user.findMany({ where: { email: { in: emails } }, select: { id: true, email: true, profileImageMime: true, }, }); const userByEmail = new Map(users.map((user) => [user.email, user])); const authBaseUrl = this.configService.get('AUTH_PUBLIC_URL') ?? this.configService.get('ATNMY_AUTH_URL') ?? this.configService.get('AUTH_EXTERNAL_URL') ?? 'https://auth.autonomystudio.net'; return items.map((item) => { const user = item.createdByEmail ? userByEmail.get(item.createdByEmail) : null; return { ...item, createdByAvatarUrl: getUserAvatarUrl(user?.id ?? '', user?.profileImageMime, authBaseUrl), }; }); } private async ensureExists(id: string) { const term = await this.launcherPrisma.launcherTerm.findUnique({ where: { id }, select: { id: true }, }); if (!term) { throw new NotFoundException('약관을 찾을 수 없습니다.'); } } private async ensureUniqueKey(key: string, excludeId?: string) { const term = await this.launcherPrisma.launcherTerm.findFirst({ where: { key, id: excludeId ? { not: excludeId } : undefined, }, select: { id: true }, }); if (term) { throw new ConflictException('이미 등록된 약관입니다.'); } } } function toCreateTermData(body: CreateTermDto): Prisma.LauncherTermCreateInput { return { key: body.key, body: body.body, isRequired: body.isRequired, }; } function toUpdateTermData(body: UpdateTermDto): Prisma.LauncherTermUpdateInput { return { key: body.key, body: body.body, isRequired: body.isRequired, }; } function toPositiveInt(value: string | number | undefined, fallback: number) { const parsed = Number(value); if (!Number.isFinite(parsed) || parsed < 1) return fallback; return Math.floor(parsed); }