feat: 로그 시스템 구현 및 관련 클래스 추가
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import type { Prisma } from '@prisma/client';
|
import type { Prisma } from '@prisma/client';
|
||||||
|
|
||||||
import { createPostgresConnector } from '@sammo-ts/infra';
|
import { createPostgresConnector } from '@sammo-ts/infra';
|
||||||
|
import { finalizeLogEntry, type LogEntryDraft } from '@sammo-ts/logic';
|
||||||
|
|
||||||
import type { TurnDaemonHooks } from '../lifecycle/types.js';
|
import type { TurnDaemonHooks } from '../lifecycle/types.js';
|
||||||
import type { InMemoryTurnWorld } from './inMemoryWorld.js';
|
import type { InMemoryTurnWorld } from './inMemoryWorld.js';
|
||||||
@@ -85,6 +86,34 @@ const buildNationUpdate = (
|
|||||||
meta: asJson(nation.meta),
|
meta: asJson(nation.meta),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const buildLogCreateData = (
|
||||||
|
entry: LogEntryDraft,
|
||||||
|
context: { year: number; month: number; at: Date }
|
||||||
|
): Prisma.LogEntryCreateManyInput | null => {
|
||||||
|
const record = finalizeLogEntry(entry, {
|
||||||
|
year: context.year,
|
||||||
|
month: context.month,
|
||||||
|
at: context.at,
|
||||||
|
});
|
||||||
|
if (!record) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
scope: record.scope,
|
||||||
|
category: record.category,
|
||||||
|
subType: record.subType ?? null,
|
||||||
|
year: record.year,
|
||||||
|
month: record.month,
|
||||||
|
text: record.text,
|
||||||
|
generalId: record.generalId ?? null,
|
||||||
|
nationId: record.nationId ?? null,
|
||||||
|
userId: record.userId ?? null,
|
||||||
|
meta: asJson(record.meta ?? {}),
|
||||||
|
createdAt: record.createdAt,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const createDatabaseTurnHooks = async (
|
export const createDatabaseTurnHooks = async (
|
||||||
databaseUrl: string,
|
databaseUrl: string,
|
||||||
world: InMemoryTurnWorld
|
world: InMemoryTurnWorld
|
||||||
@@ -130,7 +159,22 @@ export const createDatabaseTurnHooks = async (
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if (logs.length > 0) {
|
if (logs.length > 0) {
|
||||||
// TODO: API 서버 연동 전까지는 로그를 별도 처리하지 않는다.
|
const logContext = {
|
||||||
|
year: state.currentYear,
|
||||||
|
month: state.currentMonth,
|
||||||
|
at: state.lastTurnTime,
|
||||||
|
};
|
||||||
|
const payload = logs
|
||||||
|
.map((entry) => buildLogCreateData(entry, logContext))
|
||||||
|
.filter(
|
||||||
|
(entry): entry is Prisma.LogEntryCreateManyInput =>
|
||||||
|
Boolean(entry)
|
||||||
|
);
|
||||||
|
if (payload.length > 0) {
|
||||||
|
await connector.prisma.logEntry.createMany({
|
||||||
|
data: payload,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { City, Nation, TurnSchedule } from '@sammo-ts/logic';
|
import type { City, LogEntryDraft, Nation, TurnSchedule } from '@sammo-ts/logic';
|
||||||
import { getNextTurnAt } from '@sammo-ts/logic';
|
import { getNextTurnAt } from '@sammo-ts/logic';
|
||||||
|
|
||||||
import type { TurnCheckpoint } from '../lifecycle/types.js';
|
import type { TurnCheckpoint } from '../lifecycle/types.js';
|
||||||
@@ -17,7 +17,7 @@ export interface GeneralTurnResult {
|
|||||||
city?: City;
|
city?: City;
|
||||||
nation?: Nation | null;
|
nation?: Nation | null;
|
||||||
nextTurnAt?: Date;
|
nextTurnAt?: Date;
|
||||||
logs?: string[];
|
logs?: LogEntryDraft[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GeneralTurnHandler {
|
export interface GeneralTurnHandler {
|
||||||
@@ -85,7 +85,7 @@ export class InMemoryTurnWorld {
|
|||||||
private readonly dirtyGeneralIds = new Set<number>();
|
private readonly dirtyGeneralIds = new Set<number>();
|
||||||
private readonly dirtyCityIds = new Set<number>();
|
private readonly dirtyCityIds = new Set<number>();
|
||||||
private readonly dirtyNationIds = new Set<number>();
|
private readonly dirtyNationIds = new Set<number>();
|
||||||
private readonly logs: string[] = [];
|
private readonly logs: LogEntryDraft[] = [];
|
||||||
private checkpoint?: TurnCheckpoint;
|
private checkpoint?: TurnCheckpoint;
|
||||||
private state: TurnWorldState;
|
private state: TurnWorldState;
|
||||||
|
|
||||||
@@ -242,7 +242,7 @@ export class InMemoryTurnWorld {
|
|||||||
generals: TurnGeneral[];
|
generals: TurnGeneral[];
|
||||||
cities: City[];
|
cities: City[];
|
||||||
nations: Nation[];
|
nations: Nation[];
|
||||||
logs: string[];
|
logs: LogEntryDraft[];
|
||||||
} {
|
} {
|
||||||
const generals = Array.from(this.dirtyGeneralIds)
|
const generals = Array.from(this.dirtyGeneralIds)
|
||||||
.map((id) => this.generals.get(id))
|
.map((id) => this.generals.get(id))
|
||||||
|
|||||||
@@ -6,6 +6,22 @@ datasource db {
|
|||||||
provider = "postgresql"
|
provider = "postgresql"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum LogScope {
|
||||||
|
SYSTEM
|
||||||
|
NATION
|
||||||
|
GENERAL
|
||||||
|
USER
|
||||||
|
}
|
||||||
|
|
||||||
|
enum LogCategory {
|
||||||
|
HISTORY
|
||||||
|
SUMMARY
|
||||||
|
ACTION
|
||||||
|
BATTLE_BRIEF
|
||||||
|
BATTLE_DETAIL
|
||||||
|
USER
|
||||||
|
}
|
||||||
|
|
||||||
model WorldState {
|
model WorldState {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
scenarioCode String @map("scenario_code")
|
scenarioCode String @map("scenario_code")
|
||||||
@@ -133,3 +149,24 @@ model Event {
|
|||||||
|
|
||||||
@@map("event")
|
@@map("event")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model LogEntry {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
scope LogScope
|
||||||
|
category LogCategory
|
||||||
|
subType String? @map("sub_type")
|
||||||
|
year Int
|
||||||
|
month Int
|
||||||
|
text String
|
||||||
|
generalId Int? @map("general_id")
|
||||||
|
nationId Int? @map("nation_id")
|
||||||
|
userId Int? @map("user_id")
|
||||||
|
meta Json @default(dbgenerated("'{}'::jsonb"))
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
|
||||||
|
@@index([scope, category, id])
|
||||||
|
@@index([generalId, category, id])
|
||||||
|
@@index([nationId, category, id])
|
||||||
|
@@index([userId, category, id])
|
||||||
|
@@map("log_entry")
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from './postgres.js';
|
export * from './postgres.js';
|
||||||
|
export * from './logRepository.js';
|
||||||
export * from './redis.js';
|
export * from './redis.js';
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import { LogCategory, LogScope } from '@prisma/client';
|
||||||
|
import type { Prisma, PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
export interface LogQueryOptions {
|
||||||
|
limit?: number;
|
||||||
|
beforeId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LogEntryView {
|
||||||
|
id: number;
|
||||||
|
scope: LogScope;
|
||||||
|
category: LogCategory;
|
||||||
|
subType: string | null;
|
||||||
|
text: string;
|
||||||
|
year: number;
|
||||||
|
month: number;
|
||||||
|
createdAt: Date;
|
||||||
|
generalId: number | null;
|
||||||
|
nationId: number | null;
|
||||||
|
userId: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildPaginationWhere = (
|
||||||
|
base: Prisma.LogEntryWhereInput,
|
||||||
|
options: LogQueryOptions
|
||||||
|
): Prisma.LogEntryWhereInput => {
|
||||||
|
if (options.beforeId) {
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
id: { lt: options.beforeId },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildFindArgs = (
|
||||||
|
where: Prisma.LogEntryWhereInput,
|
||||||
|
options: LogQueryOptions
|
||||||
|
): Prisma.LogEntryFindManyArgs => ({
|
||||||
|
where: buildPaginationWhere(where, options),
|
||||||
|
orderBy: { id: 'desc' },
|
||||||
|
take: options.limit ?? 50,
|
||||||
|
});
|
||||||
|
|
||||||
|
export class LogRepository {
|
||||||
|
constructor(private readonly prisma: PrismaClient) {}
|
||||||
|
|
||||||
|
// 전역(시스템) 로그 조회
|
||||||
|
async listSystemLogs(
|
||||||
|
category: LogCategory,
|
||||||
|
options: LogQueryOptions = {}
|
||||||
|
): Promise<LogEntryView[]> {
|
||||||
|
return this.prisma.logEntry.findMany(
|
||||||
|
buildFindArgs(
|
||||||
|
{
|
||||||
|
scope: LogScope.SYSTEM,
|
||||||
|
category,
|
||||||
|
},
|
||||||
|
options
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 국가 로그 조회
|
||||||
|
async listNationLogs(
|
||||||
|
nationId: number,
|
||||||
|
category: LogCategory,
|
||||||
|
options: LogQueryOptions = {}
|
||||||
|
): Promise<LogEntryView[]> {
|
||||||
|
return this.prisma.logEntry.findMany(
|
||||||
|
buildFindArgs(
|
||||||
|
{
|
||||||
|
scope: LogScope.NATION,
|
||||||
|
category,
|
||||||
|
nationId,
|
||||||
|
},
|
||||||
|
options
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 장수 로그 조회
|
||||||
|
async listGeneralLogs(
|
||||||
|
generalId: number,
|
||||||
|
category: LogCategory,
|
||||||
|
options: LogQueryOptions = {}
|
||||||
|
): Promise<LogEntryView[]> {
|
||||||
|
return this.prisma.logEntry.findMany(
|
||||||
|
buildFindArgs(
|
||||||
|
{
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category,
|
||||||
|
generalId,
|
||||||
|
},
|
||||||
|
options
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 유저 로그 조회
|
||||||
|
async listUserLogs(
|
||||||
|
userId: number,
|
||||||
|
options: LogQueryOptions & { subType?: string } = {}
|
||||||
|
): Promise<LogEntryView[]> {
|
||||||
|
return this.prisma.logEntry.findMany(
|
||||||
|
buildFindArgs(
|
||||||
|
{
|
||||||
|
scope: LogScope.USER,
|
||||||
|
category: LogCategory.USER,
|
||||||
|
userId,
|
||||||
|
subType: options.subType,
|
||||||
|
},
|
||||||
|
options
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,12 @@ import type {
|
|||||||
} from '../domain/entities.js';
|
} from '../domain/entities.js';
|
||||||
import type { GeneralActionContext } from '../triggers/general.js';
|
import type { GeneralActionContext } from '../triggers/general.js';
|
||||||
import { getNextTurnAt, type TurnSchedule } from '../turn/calendar.js';
|
import { getNextTurnAt, type TurnSchedule } from '../turn/calendar.js';
|
||||||
|
import {
|
||||||
|
LogCategory,
|
||||||
|
type LogEntryDraft,
|
||||||
|
LogFormat,
|
||||||
|
LogScope,
|
||||||
|
} from '../logging/types.js';
|
||||||
|
|
||||||
export interface GeneralActionResolveContext<
|
export interface GeneralActionResolveContext<
|
||||||
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
@@ -45,7 +51,7 @@ export interface NationPatchEffect {
|
|||||||
|
|
||||||
export interface LogEffect {
|
export interface LogEffect {
|
||||||
type: 'log';
|
type: 'log';
|
||||||
message: string;
|
entry: LogEntryDraft;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NextTurnOverrideEffect {
|
export interface NextTurnOverrideEffect {
|
||||||
@@ -82,7 +88,7 @@ export interface GeneralActionResolution {
|
|||||||
city?: City;
|
city?: City;
|
||||||
nation?: Nation | null;
|
nation?: Nation | null;
|
||||||
nextTurnAt: Date;
|
nextTurnAt: Date;
|
||||||
logs: string[];
|
logs: LogEntryDraft[];
|
||||||
effects: GeneralActionEffect[];
|
effects: GeneralActionEffect[];
|
||||||
dirty?: {
|
dirty?: {
|
||||||
general: boolean;
|
general: boolean;
|
||||||
@@ -171,9 +177,26 @@ export const createNationPatchEffect = (
|
|||||||
patch,
|
patch,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createLogEffect = (message: string): LogEffect => ({
|
export const createLogEffect = (
|
||||||
|
message: string,
|
||||||
|
options: Partial<Omit<LogEntryDraft, 'text'>> = {}
|
||||||
|
): LogEffect => ({
|
||||||
type: 'log',
|
type: 'log',
|
||||||
message,
|
entry: {
|
||||||
|
scope: options.scope ?? LogScope.GENERAL,
|
||||||
|
category: options.category ?? LogCategory.ACTION,
|
||||||
|
text: message,
|
||||||
|
...(options.generalId !== undefined
|
||||||
|
? { generalId: options.generalId }
|
||||||
|
: {}),
|
||||||
|
...(options.nationId !== undefined
|
||||||
|
? { nationId: options.nationId }
|
||||||
|
: {}),
|
||||||
|
...(options.userId !== undefined ? { userId: options.userId } : {}),
|
||||||
|
...(options.subType !== undefined ? { subType: options.subType } : {}),
|
||||||
|
...(options.meta !== undefined ? { meta: options.meta } : {}),
|
||||||
|
format: options.format ?? LogFormat.MONTH,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createNextTurnOverrideEffect = (
|
export const createNextTurnOverrideEffect = (
|
||||||
@@ -192,7 +215,7 @@ export const resolveGeneralAction = <
|
|||||||
scheduleContext: TurnScheduleContext
|
scheduleContext: TurnScheduleContext
|
||||||
): GeneralActionResolution => {
|
): GeneralActionResolution => {
|
||||||
const outcome = resolver.resolve(context);
|
const outcome = resolver.resolve(context);
|
||||||
const logs: string[] = [];
|
const logs: LogEntryDraft[] = [];
|
||||||
let nextGeneral = context.general;
|
let nextGeneral = context.general;
|
||||||
let nextCity = context.city;
|
let nextCity = context.city;
|
||||||
let nextNation = context.nation ?? null;
|
let nextNation = context.nation ?? null;
|
||||||
@@ -229,7 +252,37 @@ export const resolveGeneralAction = <
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'log':
|
case 'log':
|
||||||
logs.push(effect.message);
|
// 로그 대상이 비어 있으면 현재 장수/국가 기준으로 보정한다.
|
||||||
|
switch (effect.entry.scope) {
|
||||||
|
case LogScope.GENERAL:
|
||||||
|
logs.push({
|
||||||
|
...effect.entry,
|
||||||
|
generalId:
|
||||||
|
effect.entry.generalId ?? context.general.id,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case LogScope.NATION:
|
||||||
|
if (effect.entry.nationId !== undefined) {
|
||||||
|
logs.push(effect.entry);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (context.nation?.id !== undefined) {
|
||||||
|
logs.push({
|
||||||
|
...effect.entry,
|
||||||
|
nationId: context.nation.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case LogScope.USER:
|
||||||
|
if (effect.entry.userId) {
|
||||||
|
logs.push(effect.entry);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case LogScope.SYSTEM:
|
||||||
|
default:
|
||||||
|
logs.push(effect.entry);
|
||||||
|
break;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'schedule:override':
|
case 'schedule:override':
|
||||||
nextTurnAtOverride = effect.nextTurnAt;
|
nextTurnAtOverride = effect.nextTurnAt;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export * from './domain/entities.js';
|
export * from './domain/entities.js';
|
||||||
export type { RandomGenerator } from '@sammo-ts/common';
|
export type { RandomGenerator } from '@sammo-ts/common';
|
||||||
export * from './actions/index.js';
|
export * from './actions/index.js';
|
||||||
|
export * from './logging/index.js';
|
||||||
export * from './ports/world.js';
|
export * from './ports/world.js';
|
||||||
export * from './ports/worldSnapshot.js';
|
export * from './ports/worldSnapshot.js';
|
||||||
export * from './scenario/index.js';
|
export * from './scenario/index.js';
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
import {
|
||||||
|
LogCategory,
|
||||||
|
type LogEntryDraft,
|
||||||
|
LogFormat,
|
||||||
|
LogScope,
|
||||||
|
} from './types.js';
|
||||||
|
|
||||||
|
export class ActionLogger {
|
||||||
|
private readonly generalId: number | undefined;
|
||||||
|
private readonly nationId: number | undefined;
|
||||||
|
private readonly logs: LogEntryDraft[] = [];
|
||||||
|
|
||||||
|
constructor(options: { generalId?: number; nationId?: number } = {}) {
|
||||||
|
this.generalId = options.generalId;
|
||||||
|
this.nationId = options.nationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 장수/국가/전역 로그를 한번에 모아두고 외부에서 저장한다.
|
||||||
|
public flush(): LogEntryDraft[] {
|
||||||
|
const items = this.logs.splice(0, this.logs.length);
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
public rollback(): LogEntryDraft[] {
|
||||||
|
const backup = this.logs.splice(0, this.logs.length);
|
||||||
|
return backup;
|
||||||
|
}
|
||||||
|
|
||||||
|
public pushGeneralHistoryLog(
|
||||||
|
text: string | string[],
|
||||||
|
format: LogFormat = LogFormat.YEAR_MONTH
|
||||||
|
): void {
|
||||||
|
this.pushBatch(text, (message) => ({
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.HISTORY,
|
||||||
|
text: message,
|
||||||
|
...(this.generalId !== undefined
|
||||||
|
? { generalId: this.generalId }
|
||||||
|
: {}),
|
||||||
|
format,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public pushGeneralActionLog(
|
||||||
|
text: string | string[],
|
||||||
|
format: LogFormat = LogFormat.MONTH
|
||||||
|
): void {
|
||||||
|
this.pushBatch(text, (message) => ({
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
text: message,
|
||||||
|
...(this.generalId !== undefined
|
||||||
|
? { generalId: this.generalId }
|
||||||
|
: {}),
|
||||||
|
format,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public pushGeneralBattleResultLog(
|
||||||
|
text: string | string[],
|
||||||
|
format: LogFormat = LogFormat.RAWTEXT
|
||||||
|
): void {
|
||||||
|
this.pushBatch(text, (message) => ({
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.BATTLE_BRIEF,
|
||||||
|
text: message,
|
||||||
|
...(this.generalId !== undefined
|
||||||
|
? { generalId: this.generalId }
|
||||||
|
: {}),
|
||||||
|
format,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public pushGeneralBattleDetailLog(
|
||||||
|
text: string | string[],
|
||||||
|
format: LogFormat = LogFormat.PLAIN
|
||||||
|
): void {
|
||||||
|
this.pushBatch(text, (message) => ({
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.BATTLE_DETAIL,
|
||||||
|
text: message,
|
||||||
|
...(this.generalId !== undefined
|
||||||
|
? { generalId: this.generalId }
|
||||||
|
: {}),
|
||||||
|
format,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public pushNationHistoryLog(
|
||||||
|
text: string | string[],
|
||||||
|
format: LogFormat = LogFormat.YEAR_MONTH,
|
||||||
|
nationId: number | undefined = this.nationId
|
||||||
|
): void {
|
||||||
|
if (!nationId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.pushBatch(text, (message) => ({
|
||||||
|
scope: LogScope.NATION,
|
||||||
|
category: LogCategory.HISTORY,
|
||||||
|
text: message,
|
||||||
|
nationId,
|
||||||
|
format,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public pushGlobalHistoryLog(
|
||||||
|
text: string | string[],
|
||||||
|
format: LogFormat = LogFormat.YEAR_MONTH
|
||||||
|
): void {
|
||||||
|
this.pushBatch(text, (message) => ({
|
||||||
|
scope: LogScope.SYSTEM,
|
||||||
|
category: LogCategory.HISTORY,
|
||||||
|
text: message,
|
||||||
|
format,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public pushGlobalActionLog(
|
||||||
|
text: string | string[],
|
||||||
|
format: LogFormat = LogFormat.MONTH
|
||||||
|
): void {
|
||||||
|
this.pushBatch(text, (message) => ({
|
||||||
|
scope: LogScope.SYSTEM,
|
||||||
|
category: LogCategory.SUMMARY,
|
||||||
|
text: message,
|
||||||
|
format,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private pushBatch(
|
||||||
|
text: string | string[],
|
||||||
|
builder: (message: string) => LogEntryDraft
|
||||||
|
): void {
|
||||||
|
if (Array.isArray(text)) {
|
||||||
|
for (const item of text) {
|
||||||
|
if (item) {
|
||||||
|
this.logs.push(builder(item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!text) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.logs.push(builder(text));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { formatLogText } from './formatter.js';
|
||||||
|
import {
|
||||||
|
type LogContext,
|
||||||
|
type LogEntryDraft,
|
||||||
|
type LogEntryRecord,
|
||||||
|
LogFormat,
|
||||||
|
LogScope,
|
||||||
|
} from './types.js';
|
||||||
|
|
||||||
|
const shouldDropEntry = (entry: LogEntryDraft): boolean => {
|
||||||
|
if (entry.scope === LogScope.GENERAL && !entry.generalId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (entry.scope === LogScope.NATION && !entry.nationId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (entry.scope === LogScope.USER && !entry.userId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const finalizeLogEntry = (
|
||||||
|
entry: LogEntryDraft,
|
||||||
|
context: LogContext
|
||||||
|
): LogEntryRecord | null => {
|
||||||
|
if (shouldDropEntry(entry)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const format = entry.format ?? LogFormat.RAWTEXT;
|
||||||
|
const text = formatLogText(entry.text, format, context.year, context.month);
|
||||||
|
|
||||||
|
const record: LogEntryRecord = {
|
||||||
|
scope: entry.scope,
|
||||||
|
category: entry.category,
|
||||||
|
text,
|
||||||
|
year: context.year,
|
||||||
|
month: context.month,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (entry.generalId !== undefined) {
|
||||||
|
record.generalId = entry.generalId;
|
||||||
|
}
|
||||||
|
if (entry.nationId !== undefined) {
|
||||||
|
record.nationId = entry.nationId;
|
||||||
|
}
|
||||||
|
if (entry.userId !== undefined) {
|
||||||
|
record.userId = entry.userId;
|
||||||
|
}
|
||||||
|
if (entry.subType !== undefined) {
|
||||||
|
record.subType = entry.subType;
|
||||||
|
}
|
||||||
|
if (entry.meta !== undefined) {
|
||||||
|
record.meta = entry.meta;
|
||||||
|
}
|
||||||
|
if (context.at !== undefined) {
|
||||||
|
record.createdAt = context.at;
|
||||||
|
}
|
||||||
|
|
||||||
|
return record;
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { LogFormat } from './types.js';
|
||||||
|
|
||||||
|
// 로그 포맷은 기존 표시 규칙(<C>/<S>/<R> + 기호)을 그대로 유지한다.
|
||||||
|
export const formatLogText = (
|
||||||
|
text: string,
|
||||||
|
format: LogFormat,
|
||||||
|
year: number,
|
||||||
|
month: number
|
||||||
|
): string => {
|
||||||
|
switch (format) {
|
||||||
|
case LogFormat.RAWTEXT:
|
||||||
|
return text;
|
||||||
|
case LogFormat.PLAIN:
|
||||||
|
return `<C>●</>${text}`;
|
||||||
|
case LogFormat.YEAR_MONTH:
|
||||||
|
return `<C>●</>${year}년 ${month}월:${text}`;
|
||||||
|
case LogFormat.YEAR:
|
||||||
|
return `<C>●</>${year}년:${text}`;
|
||||||
|
case LogFormat.MONTH:
|
||||||
|
return `<C>●</>${month}월:${text}`;
|
||||||
|
case LogFormat.EVENT_PLAIN:
|
||||||
|
return `<S>◆</>${text}`;
|
||||||
|
case LogFormat.EVENT_YEAR_MONTH:
|
||||||
|
return `<S>◆</>${year}년 ${month}월:${text}`;
|
||||||
|
case LogFormat.NOTICE:
|
||||||
|
return `<R>★</>${text}`;
|
||||||
|
case LogFormat.NOTICE_YEAR_MONTH:
|
||||||
|
return `<R>★</>${year}년 ${month}월:${text}`;
|
||||||
|
default:
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export * from './actionLogger.js';
|
||||||
|
export * from './entries.js';
|
||||||
|
export * from './formatter.js';
|
||||||
|
export * from './types.js';
|
||||||
|
export * from './userLogger.js';
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
export const LogScope = {
|
||||||
|
SYSTEM: 'SYSTEM',
|
||||||
|
NATION: 'NATION',
|
||||||
|
GENERAL: 'GENERAL',
|
||||||
|
USER: 'USER',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type LogScope = (typeof LogScope)[keyof typeof LogScope];
|
||||||
|
|
||||||
|
export const LogCategory = {
|
||||||
|
HISTORY: 'HISTORY',
|
||||||
|
SUMMARY: 'SUMMARY',
|
||||||
|
ACTION: 'ACTION',
|
||||||
|
BATTLE_BRIEF: 'BATTLE_BRIEF',
|
||||||
|
BATTLE_DETAIL: 'BATTLE_DETAIL',
|
||||||
|
USER: 'USER',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type LogCategory =
|
||||||
|
(typeof LogCategory)[keyof typeof LogCategory];
|
||||||
|
|
||||||
|
export interface LogEntryDraft {
|
||||||
|
scope: LogScope;
|
||||||
|
category: LogCategory;
|
||||||
|
text: string;
|
||||||
|
generalId?: number;
|
||||||
|
nationId?: number;
|
||||||
|
userId?: number;
|
||||||
|
subType?: string;
|
||||||
|
meta?: Record<string, unknown>;
|
||||||
|
format?: LogFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LogEntryRecord {
|
||||||
|
scope: LogScope;
|
||||||
|
category: LogCategory;
|
||||||
|
text: string;
|
||||||
|
year: number;
|
||||||
|
month: number;
|
||||||
|
generalId?: number;
|
||||||
|
nationId?: number;
|
||||||
|
userId?: number;
|
||||||
|
subType?: string;
|
||||||
|
meta?: Record<string, unknown>;
|
||||||
|
createdAt?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LogContext {
|
||||||
|
year: number;
|
||||||
|
month: number;
|
||||||
|
at?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum LogFormat {
|
||||||
|
RAWTEXT = 0,
|
||||||
|
/** <C>●</> */
|
||||||
|
PLAIN = 1,
|
||||||
|
/** <C>●</>{$year}년 {$month}월: */
|
||||||
|
YEAR_MONTH = 2,
|
||||||
|
/** <C>●</>{$year}년: */
|
||||||
|
YEAR = 3,
|
||||||
|
/** <C>●</>{$month}월: */
|
||||||
|
MONTH = 4,
|
||||||
|
/** <S>◆</> */
|
||||||
|
EVENT_PLAIN = 5,
|
||||||
|
/** <S>◆</>{$year}년 {$month}월: */
|
||||||
|
EVENT_YEAR_MONTH = 6,
|
||||||
|
/** <R>★</> */
|
||||||
|
NOTICE = 7,
|
||||||
|
/** <R>★</>{$year}년 {$month}월: */
|
||||||
|
NOTICE_YEAR_MONTH = 8,
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { LogCategory, type LogEntryDraft, LogScope } from './types.js';
|
||||||
|
|
||||||
|
export class UserLogger {
|
||||||
|
private readonly userId: number;
|
||||||
|
private readonly logs: LogEntryDraft[] = [];
|
||||||
|
|
||||||
|
constructor(userId: number) {
|
||||||
|
this.userId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 유저 단위의 기록을 모아두었다가 외부에서 저장한다.
|
||||||
|
public flush(): LogEntryDraft[] {
|
||||||
|
const items = this.logs.splice(0, this.logs.length);
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
public rollback(): LogEntryDraft[] {
|
||||||
|
const backup = this.logs.splice(0, this.logs.length);
|
||||||
|
return backup;
|
||||||
|
}
|
||||||
|
|
||||||
|
public push(text: string | string[], subType: string): void {
|
||||||
|
if (Array.isArray(text)) {
|
||||||
|
for (const item of text) {
|
||||||
|
if (item) {
|
||||||
|
this.logs.push({
|
||||||
|
scope: LogScope.USER,
|
||||||
|
category: LogCategory.USER,
|
||||||
|
text: item,
|
||||||
|
userId: this.userId,
|
||||||
|
subType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!text) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.logs.push({
|
||||||
|
scope: LogScope.USER,
|
||||||
|
category: LogCategory.USER,
|
||||||
|
text,
|
||||||
|
userId: this.userId,
|
||||||
|
subType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user