refactor: tRPC 조회 입력을 JSON 본문으로 전송

브라우저의 Gateway·Game tRPC 클라이언트를 POST JSON 전송으로 통일하고 서버의 query method override를 허용한다. GET mutation 거부 계약과 production Chromium 요청 형태를 회귀 테스트로 고정한다.
This commit is contained in:
2026-08-17 11:12:02 +00:00
parent e3931602cc
commit 2d302a5f91
16 changed files with 135 additions and 20 deletions
+8 -4
View File
@@ -131,11 +131,15 @@ const generals = [
];
const parseSort = (route: Route): number => {
const raw = new URL(route.request().url()).searchParams.get('input');
if (!raw) return 9;
try {
const input = JSON.parse(raw) as { 0?: { sort?: number }; json?: { sort?: number } };
return input[0]?.sort ?? input.json?.sort ?? 9;
const request = route.request();
const queryInput = new URL(request.url()).searchParams.get('input');
const input = (request.postData()
? request.postDataJSON()
: queryInput
? JSON.parse(queryInput)
: {}) as { 0?: { json?: { sort?: number }; sort?: number }; json?: { sort?: number } };
return input[0]?.json?.sort ?? input[0]?.sort ?? input.json?.sort ?? 9;
} catch {
return 9;
}
+27 -3
View File
@@ -63,6 +63,12 @@ type NavigationFixture = {
boardAccessKind: string | null;
}>;
dashboardRequests?: DashboardBundleInput[];
trpcRequests?: Array<{
operations: string[];
method: string;
url: string;
body: unknown;
}>;
};
type JsonPatchOperation = {
@@ -79,9 +85,13 @@ type DashboardBundleInput = {
};
const operationInput = (route: Route, index: number): DashboardBundleInput => {
const input = new URL(route.request().url()).searchParams.get('input');
if (!input) return {};
const parsed = JSON.parse(input) as Record<string, unknown>;
const request = route.request();
const queryInput = new URL(request.url()).searchParams.get('input');
const parsed = (request.postData()
? request.postDataJSON()
: queryInput
? JSON.parse(queryInput)
: {}) as Record<string, unknown>;
const entry = (parsed[String(index)] ?? parsed) as { json?: DashboardBundleInput };
return entry.json ?? (entry as DashboardBundleInput);
};
@@ -361,6 +371,12 @@ const installFixture = async (page: Page, state: NavigationFixture) => {
});
await page.route(`**${basePath}/api/trpc/**`, async (route) => {
const operations = operationNames(route);
(state.trpcRequests ??= []).push({
operations,
method: route.request().method(),
url: route.request().url(),
body: route.request().postDataJSON(),
});
state.operations.push(...operations);
if (
operations.some((operation) => ['general.me', 'dashboard.getContextBundleDelta'].includes(operation)) &&
@@ -2706,6 +2722,14 @@ test('realtime read-model events skip clock-only work, merge bursts, patch in pl
boardAccessKind: 'unchanged',
});
expect(realtimeBundle?.bytes).toBeLessThan(1_000);
const dashboardTransport = state.trpcRequests?.find(
({ operations, body }) =>
operations.includes('dashboard.getContextBundleDelta') && JSON.stringify(body).includes('knownSource')
);
expect(dashboardTransport).toMatchObject({ method: 'POST' });
expect(new URL(dashboardTransport?.url ?? '').searchParams.has('input')).toBe(false);
expect(dashboardTransport?.body).toBeTruthy();
expect(state.trpcRequests?.every(({ method }) => method === 'POST')).toBe(true);
expect(
state.dashboardRequests?.find(
(request) =>
@@ -1,3 +1,4 @@
import { trpcJsonBodyHttpClientOptions } from '@sammo-ts/common';
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from '@sammo-ts/gateway-api';
@@ -13,6 +14,7 @@ export const gatewayTrpc = createTRPCProxyClient<AppRouter>({
links: [
httpBatchLink({
url: import.meta.env.VITE_GATEWAY_API_URL ?? '/api/trpc',
...trpcJsonBodyHttpClientOptions,
headers() {
const token = getSessionToken();
return token ? { 'x-session-token': token } : {};
+2
View File
@@ -1,3 +1,4 @@
import { trpcJsonBodyHttpClientOptions } from '@sammo-ts/common';
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from '@sammo-ts/game-api';
@@ -13,6 +14,7 @@ export const trpc = createTRPCProxyClient<AppRouter>({
links: [
httpBatchLink({
url: import.meta.env.VITE_GAME_API_URL ?? '/api/trpc',
...trpcJsonBodyHttpClientOptions,
headers() {
const token = getGameToken();
return token ? { authorization: `Bearer ${token}` } : {};