fix: 실제 행동 기준으로 접속 중 국가를 집계
This commit is contained in:
@@ -810,7 +810,7 @@ export const generalRouter = router({
|
||||
const [onlineAccess, ownNation, latestVote] = await Promise.all([
|
||||
ctx.db.generalAccessLog.findMany({
|
||||
where: {
|
||||
lastRefresh: {
|
||||
lastActionAt: {
|
||||
gte: scoreStartedAt,
|
||||
},
|
||||
},
|
||||
@@ -868,8 +868,9 @@ export const generalRouter = router({
|
||||
onlineByNation.set(general.nationId, bucket);
|
||||
}
|
||||
const onlineNations = [...onlineByNation.entries()]
|
||||
.filter(([nationId]) => nationId > 0)
|
||||
.sort((left, right) => right[1].length - left[1].length || left[0] - right[0])
|
||||
.map(([nationId]) => `【${nationId === 0 ? '재야' : (nationNames.get(nationId) ?? `세력 ${nationId}`)}】`)
|
||||
.map(([nationId]) => `【${nationNames.get(nationId) ?? `세력 ${nationId}`}】`)
|
||||
.join(', ');
|
||||
const myOnlineGenerals = onlineGenerals
|
||||
.filter((general) => general.nationId === me.nationId)
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { GamePrisma } from '@sammo-ts/infra';
|
||||
|
||||
import type { GameApiContext } from '../context.js';
|
||||
|
||||
const adminRoles = new Set(['superuser', 'admin', 'admin.superuser']);
|
||||
|
||||
/**
|
||||
* Records a completed, authenticated user mutation without changing the Ref
|
||||
* refresh counters. Page loads and read-model refreshes never call this path.
|
||||
*/
|
||||
export const recordGeneralActivity = async (
|
||||
ctx: Pick<GameApiContext, 'auth' | 'db'>,
|
||||
now = new Date()
|
||||
): Promise<boolean> => {
|
||||
const user = ctx.auth?.user;
|
||||
if (!user || user.roles.some((role) => adminRoles.has(role))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const written = await ctx.db.$executeRaw(
|
||||
GamePrisma.sql`
|
||||
INSERT INTO general_access_log (
|
||||
general_id,
|
||||
user_id,
|
||||
last_action_at
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
${user.id},
|
||||
${now}
|
||||
FROM "general"
|
||||
WHERE user_id = ${user.id}
|
||||
ORDER BY id ASC
|
||||
LIMIT 1
|
||||
ON CONFLICT (general_id) DO UPDATE SET
|
||||
user_id = EXCLUDED.user_id,
|
||||
last_action_at = GREATEST(
|
||||
general_access_log.last_action_at,
|
||||
EXCLUDED.last_action_at
|
||||
)
|
||||
`
|
||||
);
|
||||
return written > 0;
|
||||
};
|
||||
+54
-10
@@ -16,6 +16,7 @@ import {
|
||||
type GeneralAccessEndpoint,
|
||||
} from './services/generalAccess.js';
|
||||
import { getDeferredGeneralAccessLimit } from './services/deferredGeneralAccess.js';
|
||||
import { recordGeneralActivity } from './services/generalActivity.js';
|
||||
|
||||
const t = initTRPC.context<GameApiContext>().create();
|
||||
|
||||
@@ -41,6 +42,21 @@ const requireAuthMiddleware = t.middleware(({ ctx, next }) => {
|
||||
});
|
||||
});
|
||||
|
||||
const generalActivityMiddleware = t.middleware(async ({ ctx, type, next }) => {
|
||||
const result = await next();
|
||||
if (type !== 'mutation' || !result.ok || ctx.generalAccessTracking !== true) {
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
await recordGeneralActivity(ctx);
|
||||
} catch {
|
||||
// 활동 표시는 업무 transaction보다 약한 보조 기록이다. 배포 중 schema
|
||||
// 전환이나 일시 DB 오류가 이미 완료된 사용자 mutation을 실패시키지 않는다.
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
const inputEventMiddleware = t.middleware(async ({ ctx, type, path, next }) => {
|
||||
if (type !== 'mutation' || !ctx.db.$transaction) {
|
||||
return next();
|
||||
@@ -146,7 +162,10 @@ const deferredGeneralAccessLimitMiddleware = t.middleware(async ({ ctx, next })
|
||||
|
||||
export const router = t.router;
|
||||
export const procedure = t.procedure.use(inputEventMiddleware);
|
||||
export const authedProcedure: typeof procedure = procedure.use(requireAuthMiddleware);
|
||||
export const authedProcedure: typeof procedure = t.procedure
|
||||
.use(requireAuthMiddleware)
|
||||
.use(generalActivityMiddleware)
|
||||
.use(inputEventMiddleware);
|
||||
|
||||
// Ref의 increaseRefresh()는 로그인/제재 확인 뒤, 업무 validation과 mutation
|
||||
// transaction보다 먼저 별도 저장된다. access middleware를 input-event보다
|
||||
@@ -154,16 +173,20 @@ export const authedProcedure: typeof procedure = procedure.use(requireAuthMiddle
|
||||
export const accessAuthedProcedure: typeof procedure = t.procedure
|
||||
.use(requireAuthMiddleware)
|
||||
.use(generalAccessEndpointMiddleware)
|
||||
.use(generalActivityMiddleware)
|
||||
.use(inputEventMiddleware);
|
||||
|
||||
// 턴 데몬이 ENGINE input_event와 world/DB 변경을 자체 transaction으로
|
||||
// 커밋하는 mutation에 사용한다. API input-event transaction으로 한 번 더
|
||||
// 감싸면 daemon이 아직 commit되지 않은 command를 볼 수 없어 교착된다.
|
||||
export const engineAuthedProcedure: typeof procedure = t.procedure.use(requireAuthMiddleware);
|
||||
export const engineAuthedProcedure: typeof procedure = t.procedure
|
||||
.use(requireAuthMiddleware)
|
||||
.use(generalActivityMiddleware);
|
||||
export const engineProcedure: typeof procedure = t.procedure;
|
||||
export const accessEngineAuthedProcedure: typeof procedure = t.procedure
|
||||
.use(requireAuthMiddleware)
|
||||
.use(generalAccessEndpointMiddleware);
|
||||
.use(generalAccessEndpointMiddleware)
|
||||
.use(generalActivityMiddleware);
|
||||
|
||||
// 페이지 조회 계측처럼 game state/input-event 원장과 무관한 세션 보조
|
||||
// mutation에 사용한다. gameplay state 변경에는 사용하지 않는다.
|
||||
@@ -171,22 +194,43 @@ export const sessionActivityProcedure = t.procedure;
|
||||
|
||||
// 시뮬레이터처럼 게임 상태를 변경하지 않는 계산은 input-event transaction과
|
||||
// 이벤트 원장을 만들지 않는다. 인증은 유지하되 lifecycle DB 경계 밖에서 실행한다.
|
||||
export const readOnlyAuthedProcedure: typeof procedure = t.procedure.use(requireAuthMiddleware);
|
||||
export const readOnlyAuthedProcedure: typeof procedure = t.procedure
|
||||
.use(requireAuthMiddleware)
|
||||
.use(generalActivityMiddleware);
|
||||
export const accessLimitAuthedProcedure: typeof procedure = t.procedure
|
||||
.use(requireAuthMiddleware)
|
||||
.use(generalAccessLimitMiddleware);
|
||||
.use(generalAccessLimitMiddleware)
|
||||
.use(generalActivityMiddleware);
|
||||
export const deferredAccessLimitAuthedProcedure: typeof procedure = t.procedure
|
||||
.use(requireAuthMiddleware)
|
||||
.use(deferredGeneralAccessLimitMiddleware);
|
||||
.use(deferredGeneralAccessLimitMiddleware)
|
||||
.use(generalActivityMiddleware);
|
||||
// 입력이 있는 Ref handler는 request parsing을 마친 뒤 increaseRefresh()를
|
||||
// 호출한다. 이 factory들은 parser를 access/input-event middleware 앞에 둔다.
|
||||
export const accessInputProcedure: typeof procedure.input = (input) =>
|
||||
t.procedure.input(input).use(generalAccessEndpointMiddleware).use(inputEventMiddleware);
|
||||
export const accessAuthedInputProcedure: typeof procedure.input = (input) =>
|
||||
t.procedure.use(requireAuthMiddleware).input(input).use(generalAccessEndpointMiddleware).use(inputEventMiddleware);
|
||||
t.procedure
|
||||
.use(requireAuthMiddleware)
|
||||
.input(input)
|
||||
.use(generalAccessEndpointMiddleware)
|
||||
.use(generalActivityMiddleware)
|
||||
.use(inputEventMiddleware);
|
||||
export const accessEngineAuthedInputProcedure: typeof procedure.input = (input) =>
|
||||
t.procedure.use(requireAuthMiddleware).input(input).use(generalAccessEndpointMiddleware);
|
||||
t.procedure
|
||||
.use(requireAuthMiddleware)
|
||||
.input(input)
|
||||
.use(generalAccessEndpointMiddleware)
|
||||
.use(generalActivityMiddleware);
|
||||
export const accessReadOnlyAuthedInputProcedure: typeof procedure.input = (input) =>
|
||||
t.procedure.use(requireAuthMiddleware).input(input).use(generalAccessEndpointMiddleware);
|
||||
t.procedure
|
||||
.use(requireAuthMiddleware)
|
||||
.input(input)
|
||||
.use(generalAccessEndpointMiddleware)
|
||||
.use(generalActivityMiddleware);
|
||||
export const accessLimitAuthedInputProcedure: typeof procedure.input = (input) =>
|
||||
t.procedure.use(requireAuthMiddleware).input(input).use(generalAccessLimitMiddleware);
|
||||
t.procedure
|
||||
.use(requireAuthMiddleware)
|
||||
.input(input)
|
||||
.use(generalAccessLimitMiddleware)
|
||||
.use(generalActivityMiddleware);
|
||||
|
||||
Reference in New Issue
Block a user