feat: Ref 호환 접속 제한과 벌점 초기화 구현

실행 중인 프로필에서만 접속 벌점을 누적하고 제한 임계값과 대상 경로를 Ref 순서에 맞춘다. 자기 턴 명령 성공 시 순간 점수를 같은 flush에서 초기화하며 월간 누적 감쇠는 유지한다. 제한 중 메인 자동 갱신과 실시간 구독을 중지하고 수동 갱신 성공 시 복구한다.
This commit is contained in:
2026-08-15 18:49:41 +00:00
parent a99f8166eb
commit 48d94e5ff2
29 changed files with 982 additions and 232 deletions
@@ -3,7 +3,7 @@ import { z } from 'zod';
import { LogCategory, LogScope } from '@sammo-ts/logic';
import { authedProcedure } from '../../../trpc.js';
import { accessLimitAuthedInputProcedure } from '../../../trpc.js';
import { getMyGeneral } from '../../shared/general.js';
import {
assertNationAccess,
@@ -13,77 +13,75 @@ import {
type GeneralLogType,
} from '../shared.js';
export const getGeneralLog = authedProcedure
.input(
z.object({
generalId: z.number().int().positive(),
type: zGeneralLogType,
beforeId: z.number().int().positive().optional(),
})
)
.query(async ({ ctx, input }) => {
const me = await getMyGeneral(ctx);
assertNationAccess(me);
export const getGeneralLog = accessLimitAuthedInputProcedure(
z.object({
generalId: z.number().int().positive(),
type: zGeneralLogType,
beforeId: z.number().int().positive().optional(),
})
).query(async ({ ctx, input }) => {
const me = await getMyGeneral(ctx);
assertNationAccess(me);
const [nation, target] = await Promise.all([
ctx.db.nation.findUnique({
where: { id: me.nationId },
select: { meta: true },
}),
ctx.db.general.findUnique({
where: { id: input.generalId },
select: { id: true, nationId: true, npcState: true },
}),
]);
const [nation, target] = await Promise.all([
ctx.db.nation.findUnique({
where: { id: me.nationId },
select: { meta: true },
}),
ctx.db.general.findUnique({
where: { id: input.generalId },
select: { id: true, nationId: true, npcState: true },
}),
]);
if (!nation) {
throw new TRPCError({ code: 'NOT_FOUND', message: 'Nation not found' });
}
if (!target) {
throw new TRPCError({ code: 'NOT_FOUND', message: 'General not found' });
}
if (!nation) {
throw new TRPCError({ code: 'NOT_FOUND', message: 'Nation not found' });
}
if (!target) {
throw new TRPCError({ code: 'NOT_FOUND', message: 'General not found' });
}
const permissionLevel = resolveNationPermission(me, nation.meta, true);
if (permissionLevel < 1) {
throw new TRPCError({ code: 'FORBIDDEN', message: '권한이 부족합니다.' });
}
if (target.nationId !== me.nationId) {
throw new TRPCError({ code: 'FORBIDDEN', message: '같은 나라의 장수가 아닙니다.' });
}
if (input.type === 'generalAction' && target.npcState < 2 && target.id !== me.id && permissionLevel < 2) {
throw new TRPCError({
code: 'FORBIDDEN',
message: '권한이 부족합니다. 유저 장수의 개인 기록은 수뇌만 열람 가능합니다.',
});
}
const categoryMap: Record<GeneralLogType, LogCategory> = {
generalHistory: LogCategory.HISTORY,
generalAction: LogCategory.ACTION,
battleResult: LogCategory.BATTLE_BRIEF,
battleDetail: LogCategory.BATTLE_DETAIL,
};
const logs = await ctx.db.logEntry.findMany({
where: {
generalId: target.id,
scope: LogScope.GENERAL,
category: categoryMap[input.type],
...(input.type !== 'generalHistory' && input.beforeId ? { id: { lt: input.beforeId } } : {}),
},
orderBy: { id: 'desc' },
...(input.type === 'generalHistory' ? {} : { take: 30 }),
const permissionLevel = resolveNationPermission(me, nation.meta, true);
if (permissionLevel < 1) {
throw new TRPCError({ code: 'FORBIDDEN', message: '권한이 부족합니다.' });
}
if (target.nationId !== me.nationId) {
throw new TRPCError({ code: 'FORBIDDEN', message: '같은 나라의 장수가 아닙니다.' });
}
if (input.type === 'generalAction' && target.npcState < 2 && target.id !== me.id && permissionLevel < 2) {
throw new TRPCError({
code: 'FORBIDDEN',
message: '권한이 부족합니다. 유저 장수의 개인 기록은 수뇌만 열람 가능합니다.',
});
}
return {
type: input.type,
const categoryMap: Record<GeneralLogType, LogCategory> = {
generalHistory: LogCategory.HISTORY,
generalAction: LogCategory.ACTION,
battleResult: LogCategory.BATTLE_BRIEF,
battleDetail: LogCategory.BATTLE_DETAIL,
};
const logs = await ctx.db.logEntry.findMany({
where: {
generalId: target.id,
logs: logs.map((entry) => ({
id: entry.id,
text: entry.text,
year: entry.year,
month: entry.month,
createdAt: formatDateTime(entry.createdAt),
})),
};
scope: LogScope.GENERAL,
category: categoryMap[input.type],
...(input.type !== 'generalHistory' && input.beforeId ? { id: { lt: input.beforeId } } : {}),
},
orderBy: { id: 'desc' },
...(input.type === 'generalHistory' ? {} : { take: 30 }),
});
return {
type: input.type,
generalId: target.id,
logs: logs.map((entry) => ({
id: entry.id,
text: entry.text,
year: entry.year,
month: entry.month,
createdAt: formatDateTime(entry.createdAt),
})),
};
});