feat: 로그 시스템 구현 및 관련 클래스 추가

This commit is contained in:
2025-12-29 08:40:01 +00:00
parent e0eb768e2e
commit 27cc016804
13 changed files with 630 additions and 11 deletions
+147
View File
@@ -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));
}
}
+62
View File
@@ -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;
};
+32
View File
@@ -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;
}
};
+5
View File
@@ -0,0 +1,5 @@
export * from './actionLogger.js';
export * from './entries.js';
export * from './formatter.js';
export * from './types.js';
export * from './userLogger.js';
+72
View File
@@ -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,
}
+48
View File
@@ -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,
});
}
}