This commit is contained in:
@@ -45,6 +45,20 @@ model LauncherTerm {
|
||||
@@map("launcher_terms")
|
||||
}
|
||||
|
||||
model LauncherManualDocument {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
appKey String @map("app_key")
|
||||
title String
|
||||
body String
|
||||
isPublished Boolean @default(true) @map("is_published")
|
||||
sortOrder Int @default(0) @map("sort_order")
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
|
||||
@@index([appKey, isPublished, sortOrder])
|
||||
@@map("launcher_manual_documents")
|
||||
}
|
||||
|
||||
model ProvisionedDevice {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
accountEmail String @map("account_email")
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
CREATE TABLE "launcher_manual_documents" (
|
||||
"id" UUID NOT NULL,
|
||||
"key" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"body" TEXT NOT NULL,
|
||||
"is_published" BOOLEAN NOT NULL DEFAULT true,
|
||||
"sort_order" INTEGER NOT NULL DEFAULT 0,
|
||||
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "launcher_manual_documents_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "launcher_manual_documents_key_key" ON "launcher_manual_documents"("key");
|
||||
CREATE INDEX "launcher_manual_documents_is_published_sort_order_idx" ON "launcher_manual_documents"("is_published", "sort_order");
|
||||
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE "launcher_manual_documents"
|
||||
ADD COLUMN "app_key" TEXT NOT NULL DEFAULT 'Launcher';
|
||||
|
||||
DROP INDEX IF EXISTS "launcher_manual_documents_is_published_sort_order_idx";
|
||||
CREATE INDEX "launcher_manual_documents_app_key_is_published_sort_order_idx"
|
||||
ON "launcher_manual_documents"("app_key", "is_published", "sort_order");
|
||||
@@ -0,0 +1,2 @@
|
||||
DROP INDEX IF EXISTS "launcher_manual_documents_key_key";
|
||||
ALTER TABLE "launcher_manual_documents" DROP COLUMN IF EXISTS "key";
|
||||
@@ -11,6 +11,7 @@ import { GiteaModule } from './gitea/gitea.module';
|
||||
import { HealthModule } from './health/health.module';
|
||||
import { LauncherModule } from './launcher/launcher.module';
|
||||
import { LicensesModule } from './licenses/licenses.module';
|
||||
import { ManualsModule } from './manuals/manuals.module';
|
||||
import { NoticesModule } from './notices/notices.module';
|
||||
import { OrganizationsModule } from './organizations/organizations.module';
|
||||
import { PageAgentModule } from './page-agent/page-agent.module';
|
||||
@@ -40,6 +41,7 @@ import { WoodpeckerModule } from './woodpecker/woodpecker.module';
|
||||
HealthModule,
|
||||
LauncherModule,
|
||||
LicensesModule,
|
||||
ManualsModule,
|
||||
NoticesModule,
|
||||
OrganizationsModule,
|
||||
PageAgentModule,
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { IsBoolean, IsInt, IsOptional, IsString, MaxLength, Min } from 'class-validator';
|
||||
|
||||
export class ListManualDocumentsQuery {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
appKey?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
page?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
limit?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
keyword?: string;
|
||||
}
|
||||
|
||||
export class CreateManualDocumentDto {
|
||||
@IsString()
|
||||
@MaxLength(120)
|
||||
appKey!: string;
|
||||
|
||||
@IsString()
|
||||
@MaxLength(160)
|
||||
title!: string;
|
||||
|
||||
@IsString()
|
||||
body!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isPublished?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
sortOrder?: number;
|
||||
}
|
||||
|
||||
export class UpdateManualDocumentDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(120)
|
||||
appKey?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(160)
|
||||
title?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
body?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isPublished?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
sortOrder?: number;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common';
|
||||
|
||||
import { Public } from '../admin-auth/decorators/admin-auth.decorators';
|
||||
import { CreateManualDocumentDto, ListManualDocumentsQuery, UpdateManualDocumentDto } from './dto';
|
||||
import { ManualsService } from './manuals.service';
|
||||
|
||||
@Controller('manuals')
|
||||
export class ManualsController {
|
||||
constructor(private readonly manualsService: ManualsService) {}
|
||||
|
||||
@Public()
|
||||
@Get('public')
|
||||
publicList(@Query('appKey') appKey?: string) {
|
||||
return this.manualsService.publicList(appKey);
|
||||
}
|
||||
|
||||
@Get()
|
||||
list(@Query() query: ListManualDocumentsQuery) {
|
||||
return this.manualsService.list(query);
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@Body() body: CreateManualDocumentDto) {
|
||||
return this.manualsService.create(body);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
update(@Param('id') id: string, @Body() body: UpdateManualDocumentDto) {
|
||||
return this.manualsService.update(id, body);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
remove(@Param('id') id: string) {
|
||||
return this.manualsService.remove(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { LauncherPrismaModule } from '../launcher-prisma/launcher-prisma.module';
|
||||
import { ManualsController } from './manuals.controller';
|
||||
import { ManualsService } from './manuals.service';
|
||||
|
||||
@Module({
|
||||
imports: [LauncherPrismaModule],
|
||||
controllers: [ManualsController],
|
||||
providers: [ManualsService],
|
||||
})
|
||||
export class ManualsModule {}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/launcher-client';
|
||||
|
||||
import { LauncherPrismaService } from '../launcher-prisma/launcher-prisma.service';
|
||||
import { CreateManualDocumentDto, ListManualDocumentsQuery, UpdateManualDocumentDto } from './dto';
|
||||
|
||||
@Injectable()
|
||||
export class ManualsService {
|
||||
constructor(private readonly launcherPrisma: LauncherPrismaService) {}
|
||||
|
||||
async list(query: ListManualDocumentsQuery) {
|
||||
const page = toPositiveInt(query.page, 1);
|
||||
const limit = Math.min(toPositiveInt(query.limit, 20), 100);
|
||||
const keyword = query.keyword?.trim();
|
||||
const where = {
|
||||
...(query.appKey?.trim() ? { appKey: query.appKey.trim() } : {}),
|
||||
...(keyword
|
||||
? {
|
||||
OR: [
|
||||
{ title: { contains: keyword, mode: 'insensitive' as const } },
|
||||
{ body: { contains: keyword, mode: 'insensitive' as const } },
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
const [documents, total] = await this.launcherPrisma.$transaction([
|
||||
this.launcherPrisma.launcherManualDocument.findMany({
|
||||
where,
|
||||
orderBy: [{ sortOrder: 'asc' }, { createdAt: 'desc' }],
|
||||
skip: (page - 1) * limit,
|
||||
take: limit,
|
||||
}),
|
||||
this.launcherPrisma.launcherManualDocument.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
documents: documents.map((document, index) => ({
|
||||
...document,
|
||||
no: total - ((page - 1) * limit + index),
|
||||
})),
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
};
|
||||
}
|
||||
|
||||
async publicList(appKey?: string) {
|
||||
const documents = await this.launcherPrisma.launcherManualDocument.findMany({
|
||||
where: { isPublished: true, ...(appKey?.trim() ? { appKey: appKey.trim() } : {}) },
|
||||
orderBy: [{ sortOrder: 'asc' }, { createdAt: 'desc' }],
|
||||
select: {
|
||||
id: true,
|
||||
appKey: true,
|
||||
title: true,
|
||||
body: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
return { documents };
|
||||
}
|
||||
|
||||
async create(body: CreateManualDocumentDto) {
|
||||
const document = await this.launcherPrisma.launcherManualDocument.create({
|
||||
data: {
|
||||
appKey: body.appKey.trim(),
|
||||
title: body.title.trim(),
|
||||
body: body.body.trim(),
|
||||
isPublished: body.isPublished ?? true,
|
||||
sortOrder: body.sortOrder ?? 0,
|
||||
},
|
||||
});
|
||||
|
||||
return { document };
|
||||
}
|
||||
|
||||
async update(id: string, body: UpdateManualDocumentDto) {
|
||||
await this.ensureExists(id);
|
||||
|
||||
const document = await this.launcherPrisma.launcherManualDocument.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(body.appKey === undefined ? {} : { appKey: body.appKey.trim() }),
|
||||
...(body.title === undefined ? {} : { title: body.title.trim() }),
|
||||
...(body.body === undefined ? {} : { body: body.body.trim() }),
|
||||
...(body.isPublished === undefined ? {} : { isPublished: body.isPublished }),
|
||||
...(body.sortOrder === undefined ? {} : { sortOrder: body.sortOrder }),
|
||||
} as Prisma.LauncherManualDocumentUpdateInput,
|
||||
});
|
||||
|
||||
return { document };
|
||||
}
|
||||
|
||||
async remove(id: string) {
|
||||
await this.ensureExists(id);
|
||||
await this.launcherPrisma.launcherManualDocument.delete({ where: { id } });
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
private async ensureExists(id: string) {
|
||||
const document = await this.launcherPrisma.launcherManualDocument.findUnique({ where: { id }, select: { id: true } });
|
||||
if (!document) throw new NotFoundException('매뉴얼 문서를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function toPositiveInt(value: string | undefined, fallback: number) {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
|
||||
}
|
||||
Reference in New Issue
Block a user