feat: 감사 사건에서 요청 처리 상태를 조회한다
This commit is contained in:
@@ -107,6 +107,32 @@ const install = async (
|
||||
},
|
||||
}
|
||||
: result({ profileName: gameProfile, read: true, accounts: false });
|
||||
case 'playAudit.requestState':
|
||||
return result({
|
||||
...world,
|
||||
coverage: 'CURRENT_JOURNAL_STATE',
|
||||
status: input.kind === 'POLICY' && input.id === 'a'.repeat(64) ? 'NOT_LINKED' : 'AVAILABLE',
|
||||
request:
|
||||
input.kind === 'POLICY' && input.id === 'a'.repeat(64)
|
||||
? null
|
||||
: {
|
||||
sequence: '9007199254740993',
|
||||
requestId: 'fixture-request',
|
||||
target: 'ENGINE',
|
||||
eventType: 'setNpcPolicy',
|
||||
status: 'SUCCEEDED',
|
||||
attempts: 2,
|
||||
acceptedGameTick: null,
|
||||
processingGameTick: '4320000000',
|
||||
acceptedClockRevision: null,
|
||||
processingClockRevision: '3',
|
||||
createdAt: world.asOf,
|
||||
processingAt: world.asOf,
|
||||
completedAt: world.asOf,
|
||||
resultRecorded: true,
|
||||
errorRecorded: false,
|
||||
},
|
||||
});
|
||||
case 'playAudit.decisionHistory':
|
||||
return result({
|
||||
...world,
|
||||
@@ -1228,3 +1254,65 @@ for (const [status, label] of [
|
||||
await expect(region).not.toContainText('실행 실패');
|
||||
});
|
||||
}
|
||||
|
||||
test('policy request state is explicit, retries independently and resets on version change', async ({ page }) => {
|
||||
const requests = await install(page);
|
||||
let fail = true;
|
||||
await page.route(gameTrpcRoute, async (route) => {
|
||||
if (fail && route.request().url().includes('playAudit.requestState')) {
|
||||
fail = false;
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify([
|
||||
{
|
||||
error: {
|
||||
message: '요청 조회 일시 오류',
|
||||
code: -32603,
|
||||
data: { code: 'INTERNAL_SERVER_ERROR', httpStatus: 500 },
|
||||
},
|
||||
},
|
||||
]),
|
||||
});
|
||||
} else await route.fallback();
|
||||
});
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.goto(
|
||||
gamePath('/play-audit?tab=policies&nation=2&policyArea=DEFENCE&fromYear=190&fromMonth=1&year=190&month=6')
|
||||
);
|
||||
await page.getByRole('button', { name: '버전 2', exact: true }).click();
|
||||
await expect(page.getByRole('button', { name: '요청 처리 조회', exact: true })).toBeVisible();
|
||||
expect(requests.some((r) => r.operation === 'playAudit.requestState')).toBe(false);
|
||||
const before = requests.length;
|
||||
await page.getByRole('button', { name: '요청 처리 조회', exact: true }).click();
|
||||
await expect(page.getByRole('alert')).toContainText('요청 조회 일시 오류');
|
||||
await page.getByRole('button', { name: '요청 처리 다시 조회', exact: true }).click();
|
||||
const region = page.getByRole('region', { name: '요청 처리 기록', exact: true });
|
||||
await expect(region).toContainText('9007199254740993');
|
||||
await expect(region).toContainText('처리 성공');
|
||||
await expect(region).toContainText('시도별 전체 이력이나 실제 변경 횟수는 아닙니다');
|
||||
expect(requests.slice(before).map((r) => r.operation)).toEqual(['playAudit.requestState']);
|
||||
expect(requests.at(-1)?.input).toEqual({ kind: 'POLICY', id: 'b'.repeat(64) });
|
||||
await capture(page, 'mobile-policy-request');
|
||||
const count = requests.filter((r) => r.operation === 'playAudit.requestState').length;
|
||||
await page.getByRole('button', { name: '이전 정책 버전', exact: true }).click();
|
||||
await expect(region).not.toContainText('9007199254740993');
|
||||
expect(requests.filter((r) => r.operation === 'playAudit.requestState')).toHaveLength(count);
|
||||
await page.getByRole('button', { name: '요청 처리 조회', exact: true }).click();
|
||||
await expect(region).toContainText('연결된 요청이 없습니다');
|
||||
});
|
||||
|
||||
test('diplomacy request state does not reload documents or lists', async ({ page }) => {
|
||||
const requests = await install(page);
|
||||
await page.goto(
|
||||
gamePath('/play-audit?tab=diplomacy&nation=2&otherNation=3&fromYear=190&fromMonth=1&year=190&month=6')
|
||||
);
|
||||
await page.getByRole('button', { name: '문서 승인', exact: true }).click();
|
||||
await expect(page.getByRole('button', { name: '요청 처리 조회', exact: true })).toBeVisible();
|
||||
const before = requests.length;
|
||||
await page.getByRole('button', { name: '요청 처리 조회', exact: true }).click();
|
||||
await expect(page.getByRole('region', { name: '요청 처리 기록', exact: true })).toContainText('처리 성공');
|
||||
expect(requests.slice(before).map((r) => r.operation)).toEqual(['playAudit.requestState']);
|
||||
expect(requests.at(-1)?.input.kind).toBe('DIPLOMACY');
|
||||
await capture(page, 'desktop-diplomacy-request');
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { trpc } from '../../utils/trpc';
|
||||
import AuditRequestState from './AuditRequestState.vue';
|
||||
const props = defineProps<{
|
||||
nationId: number;
|
||||
otherNationId: number;
|
||||
@@ -261,6 +262,7 @@ watch(
|
||||
{{ detail.event.inputSequence ?? '해당 없음' }}
|
||||
</p>
|
||||
</details>
|
||||
<AuditRequestState :id="detail.event.id" kind="DIPLOMACY" />
|
||||
</template>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { trpc } from '../../utils/trpc';
|
||||
import AuditRequestState from './AuditRequestState.vue';
|
||||
const props = withDefaults(defineProps<{ id: string; allowPrevious?: boolean }>(), { allowPrevious: true });
|
||||
const emit = defineEmits<{ select: [id: string | null] }>();
|
||||
type Detail = Awaited<ReturnType<typeof trpc.playAudit.policyVersion.query>>;
|
||||
@@ -128,6 +129,7 @@ watch(
|
||||
<p>요청 {{ detail.version.requestId ?? '해당 없음' }}</p>
|
||||
<p>입력 순번 {{ detail.version.inputSequence ?? '해당 없음' }}</p>
|
||||
</details>
|
||||
<AuditRequestState :id="detail.version.id" kind="POLICY" />
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { trpc } from '../../utils/trpc';
|
||||
const props = defineProps<{ kind: 'POLICY' | 'DIPLOMACY'; id: string }>();
|
||||
type Data = Awaited<ReturnType<typeof trpc.playAudit.requestState.query>>;
|
||||
const data = ref<Data | null>(null);
|
||||
const opened = ref(false);
|
||||
const loading = ref(false);
|
||||
const error = ref('');
|
||||
let generation = 0;
|
||||
const labels = { PENDING: '처리 대기', PROCESSING: '처리 중', SUCCEEDED: '처리 성공', FAILED: '처리 실패' };
|
||||
const missing = {
|
||||
NOT_LINKED: '이 사건에는 연결된 요청이 없습니다. 자동 처리나 최초 관측일 수 있습니다.',
|
||||
INCOMPLETE_REFERENCE: '요청 순번이 없어 현재 요청 기록과의 일치를 확인할 수 없습니다.',
|
||||
MISSING_REQUEST: '연결된 요청 기록이 보존되어 있지 않습니다. 원인을 이 자료만으로 판단할 수 없습니다.',
|
||||
REFERENCE_MISMATCH: '요청 ID와 저장된 입력 순번이 일치하지 않아 내용을 표시하지 않습니다.',
|
||||
};
|
||||
const load = async () => {
|
||||
if (loading.value) return;
|
||||
opened.value = true;
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
const request = ++generation;
|
||||
try {
|
||||
const response = await trpc.playAudit.requestState.query({ kind: props.kind, id: props.id });
|
||||
if (request === generation) data.value = response;
|
||||
} catch (cause) {
|
||||
if (request === generation)
|
||||
error.value = cause instanceof Error ? cause.message : '요청 기록을 조회하지 못했습니다.';
|
||||
} finally {
|
||||
if (request === generation) loading.value = false;
|
||||
}
|
||||
};
|
||||
watch([() => props.kind, () => props.id], () => {
|
||||
generation++;
|
||||
data.value = null;
|
||||
opened.value = false;
|
||||
loading.value = false;
|
||||
error.value = '';
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<section class="audit-request" aria-label="요청 처리 기록">
|
||||
<button class="legacy-button" :disabled="loading" @click="load">
|
||||
{{ opened ? '요청 처리 다시 조회' : '요청 처리 조회' }}
|
||||
</button>
|
||||
<p v-if="loading" role="status">요청 처리 기록 조회 중…</p>
|
||||
<p v-if="error" role="alert">{{ error }}</p>
|
||||
<template v-if="data">
|
||||
<p v-if="data.status !== 'AVAILABLE'">{{ missing[data.status] }}</p>
|
||||
<template v-else-if="data.request">
|
||||
<p>조회 시각 {{ data.asOf }}</p>
|
||||
<p>조회 시점에 보존된 요청 상태입니다. 처리 시도별 전체 이력이나 실제 변경 횟수는 아닙니다.</p>
|
||||
<dl>
|
||||
<dt>요청</dt>
|
||||
<dd>{{ data.request.requestId }}</dd>
|
||||
<dt>입력 순번</dt>
|
||||
<dd>{{ data.request.sequence }}</dd>
|
||||
<dt>처리 대상 · 종류</dt>
|
||||
<dd>{{ data.request.target }} · {{ data.request.eventType }}</dd>
|
||||
<dt>현재 상태</dt>
|
||||
<dd>{{ labels[data.request.status] }}</dd>
|
||||
<dt>처리 시도 횟수</dt>
|
||||
<dd>{{ data.request.attempts }}</dd>
|
||||
<dt>접수</dt>
|
||||
<dd>
|
||||
{{ data.request.createdAt }} · tick {{ data.request.acceptedGameTick ?? '미관측' }} · 시계 버전
|
||||
{{ data.request.acceptedClockRevision ?? '미관측' }}
|
||||
</dd>
|
||||
<dt>마지막 처리 시작</dt>
|
||||
<dd>
|
||||
{{ data.request.processingAt ?? '미관측' }} · tick
|
||||
{{ data.request.processingGameTick ?? '미관측' }} · 시계 버전
|
||||
{{ data.request.processingClockRevision ?? '미관측' }}
|
||||
</dd>
|
||||
<dt>완료 기록 시각</dt>
|
||||
<dd>{{ data.request.completedAt ?? '미관측' }}</dd>
|
||||
<dt>결과 · 오류 기록</dt>
|
||||
<dd>
|
||||
{{ data.request.resultRecorded ? '결과 있음' : '결과 없음' }} ·
|
||||
{{ data.request.errorRecorded ? '오류 있음' : '오류 없음' }}
|
||||
</dd>
|
||||
</dl>
|
||||
</template>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
<style scoped>
|
||||
.audit-request {
|
||||
margin-top: 8px;
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
dl {
|
||||
margin: 8px 0;
|
||||
}
|
||||
dt {
|
||||
font-weight: bold;
|
||||
margin-top: 6px;
|
||||
}
|
||||
dd {
|
||||
margin-left: 12px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user