NPC 감사에서 최근 기록 월을 기본 조회하고 실제 시나리오로 검증한다
This commit is contained in:
@@ -173,7 +173,35 @@ export const decisionHistory = auditProcedure
|
||||
.query(({ ctx, input }) =>
|
||||
readAudit(ctx, async (tx) => {
|
||||
const world = await readAuditWorld(tx);
|
||||
const month = input.month ?? { year: world.year, month: world.month };
|
||||
// 장수 턴은 월 경계와 동시에 실행되지 않는다. 현재 상태에서는 마지막으로
|
||||
// 실제 결정이 저장된 월을 열고, 명시한 과거 월은 빈 월이어도 그대로 보존한다.
|
||||
const latest =
|
||||
!input.month && world.serverId
|
||||
? await tx.playAuditDecision.findFirst({
|
||||
where: {
|
||||
serverId: world.serverId,
|
||||
generalId: input.generalId,
|
||||
phase: input.phase,
|
||||
AND: [
|
||||
{
|
||||
OR: [
|
||||
{ year: { gt: world.startYear } },
|
||||
{ year: world.startYear, month: { gte: world.startMonth } },
|
||||
],
|
||||
},
|
||||
{
|
||||
OR: [
|
||||
{ year: { lt: world.year } },
|
||||
{ year: world.year, month: { lte: world.month } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
orderBy: [{ year: 'desc' }, { month: 'desc' }, { tick: 'desc' }, { id: 'desc' }],
|
||||
select: { year: true, month: true },
|
||||
})
|
||||
: null;
|
||||
const month = input.month ?? latest ?? { year: world.year, month: world.month };
|
||||
const ordinal = monthOrdinal(month.year, month.month);
|
||||
if (
|
||||
ordinal < monthOrdinal(world.startYear, world.startMonth) ||
|
||||
@@ -206,6 +234,8 @@ export const decisionHistory = auditProcedure
|
||||
return {
|
||||
...world,
|
||||
month,
|
||||
currentMonth: { year: world.year, month: world.month },
|
||||
selection: input.month ? ('MONTH' as const) : ('LATEST' as const),
|
||||
coverage: world.serverId ? ('PROCEDURES_ONLY' as const) : ('IDENTITY_MISSING' as const),
|
||||
items: rows.slice(0, input.limit).map(project),
|
||||
nextCursor: rows.length > input.limit && last ? { tick: last.tick.toString(), id: last.id } : null,
|
||||
|
||||
@@ -2352,6 +2352,33 @@ integration('game API security over HTTP transport', () => {
|
||||
],
|
||||
});
|
||||
const decisionInput = { generalId: decisionGeneral, month: { year: 190, month: 1 }, limit: 1 };
|
||||
// 월 경계 뒤 장수의 첫 턴 전에도 직전 결정에 도달할 수 있어야 한다.
|
||||
expect((await get('decisionHistory', admin, { generalId: decisionGeneral })).body).toMatchObject({
|
||||
result: {
|
||||
data: {
|
||||
selection: 'LATEST',
|
||||
month: { year: 190, month: 1 },
|
||||
currentMonth: { year: 190, month: 2 },
|
||||
items: expect.arrayContaining([expect.objectContaining({ id: decisionIds[0] })]),
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(
|
||||
(await get('decisionHistory', admin, { generalId: decisionGeneral, month: { year: 190, month: 2 } }))
|
||||
.body
|
||||
).toMatchObject({
|
||||
result: { data: { selection: 'MONTH', month: { year: 190, month: 2 }, items: [] } },
|
||||
});
|
||||
expect(
|
||||
(await get('decisionHistory', admin, { generalId: decisionGeneral, phase: 'nation' })).body
|
||||
).toMatchObject({
|
||||
result: {
|
||||
data: { selection: 'LATEST', month: { year: 190, month: 1 }, items: [{ id: decisionIds[1] }] },
|
||||
},
|
||||
});
|
||||
expect((await get('decisionHistory', admin, { generalId: 2147483647 })).body).toMatchObject({
|
||||
result: { data: { selection: 'LATEST', month: { year: 190, month: 2 }, items: [] } },
|
||||
});
|
||||
expect((await get('decisionHistory', undefined, decisionInput)).status).toBe(401);
|
||||
expect((await get('decisionHistory', await token(['admin']), decisionInput)).status).toBe(403);
|
||||
const decisionList = await get('decisionHistory', admin, decisionInput);
|
||||
|
||||
@@ -147,7 +147,9 @@ const install = async (
|
||||
case 'playAudit.decisionHistory':
|
||||
return result({
|
||||
...world,
|
||||
month: input.month ?? { year: 190, month: 7 },
|
||||
month: input.month ?? { year: 190, month: 6 },
|
||||
currentMonth: { year: 190, month: 7 },
|
||||
selection: input.month ? 'MONTH' : 'LATEST',
|
||||
coverage: 'PROCEDURES_ONLY',
|
||||
items: [
|
||||
{
|
||||
@@ -1608,3 +1610,42 @@ for (const width of [390, 1280]) {
|
||||
await page.screenshot({ path: testInfo.outputPath('audit-gap.png'), fullPage: true });
|
||||
});
|
||||
}
|
||||
|
||||
for (const width of [390, 1280]) {
|
||||
test(`NPC decision month navigation shows latest recorded month at ${width}px`, async ({ page }) => {
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
const requests = await install(page);
|
||||
await page.goto(gamePath('/play-audit?tab=generals&general=1'));
|
||||
await page.getByRole('button', { name: 'NPC 결정 기록 조회', exact: true }).click();
|
||||
await expect(page.getByText('가장 최근 결정이 수집된 월입니다.', { exact: false })).toContainText('190년 7월');
|
||||
const panel = page.getByRole('region', { name: 'NPC 결정 기록', exact: true });
|
||||
await expect(panel).toContainText('190년 6월');
|
||||
await panel.getByRole('button', { name: '다음 월', exact: true }).click();
|
||||
await expect
|
||||
.poll(() => requests.filter((r) => r.operation === 'playAudit.decisionHistory').at(-1)?.input)
|
||||
.toMatchObject({ month: { year: 190, month: 7 } });
|
||||
await expect(panel.getByRole('button', { name: '다음 월', exact: true })).toBeDisabled();
|
||||
await panel.getByRole('button', { name: '이전 월', exact: true }).click();
|
||||
await expect
|
||||
.poll(() => requests.filter((r) => r.operation === 'playAudit.decisionHistory').at(-1)?.input)
|
||||
.toMatchObject({ month: { year: 190, month: 6 } });
|
||||
await panel.getByRole('button', { name: '최근 결정 조회', exact: true }).click();
|
||||
await expect(page.getByText('가장 최근 결정이 수집된 월입니다.', { exact: false })).toBeVisible();
|
||||
expect(requests.filter((r) => r.operation === 'playAudit.decisionHistory').at(-1)?.input).not.toHaveProperty(
|
||||
'month'
|
||||
);
|
||||
await capture(page, `npc-decision-latest-${width}`);
|
||||
});
|
||||
}
|
||||
|
||||
test('direct decision URL keeps the history panel open when changing month', async ({ page }) => {
|
||||
await install(page);
|
||||
await page.goto(gamePath(`/play-audit?tab=generals&general=1&decision=${decision.id}`));
|
||||
const panel = page.getByRole('region', { name: 'NPC 결정 기록', exact: true });
|
||||
await expect(panel.getByRole('list', { name: '판단 절차' })).toBeVisible();
|
||||
await panel.getByRole('button', { name: '이전 월', exact: true }).click();
|
||||
await expect(panel).toBeVisible();
|
||||
await expect(panel.getByRole('region', { name: '선택 결정 상세' })).toHaveCount(0);
|
||||
await panel.getByRole('button', { name: '최근 결정 조회', exact: true }).click();
|
||||
await expect(panel).toContainText('가장 최근 결정이 수집된 월입니다.');
|
||||
});
|
||||
|
||||
@@ -15,6 +15,12 @@ const detail = ref<Detail | null>(null);
|
||||
const error = ref('');
|
||||
const detailError = ref('');
|
||||
const loading = ref(false);
|
||||
const selectedMonth = ref<{ year: number; month: number }>();
|
||||
const ordinal = (value: { year: number; month: number }) => value.year * 12 + value.month - 1;
|
||||
const canPrevious = computed(
|
||||
() => history.value && ordinal(history.value.month) > history.value.startYear * 12 + history.value.startMonth - 1
|
||||
);
|
||||
const canNext = computed(() => history.value && ordinal(history.value.month) < ordinal(history.value.currentMonth));
|
||||
const detailLoading = ref(false);
|
||||
let generation = 0;
|
||||
let detailGeneration = 0;
|
||||
@@ -41,13 +47,14 @@ const load = async (more = false) => {
|
||||
try {
|
||||
const response = await trpc.playAudit.decisionHistory.query({
|
||||
generalId: props.generalId,
|
||||
month: more ? (history.value?.month ?? props.month) : props.month,
|
||||
month: more ? (history.value?.month ?? selectedMonth.value) : selectedMonth.value,
|
||||
limit: 50,
|
||||
cursor: more ? (history.value?.nextCursor ?? undefined) : undefined,
|
||||
});
|
||||
if (request === generation)
|
||||
history.value = {
|
||||
...response,
|
||||
selection: more ? (history.value?.selection ?? response.selection) : response.selection,
|
||||
items: more ? [...(history.value?.items ?? []), ...response.items] : response.items,
|
||||
};
|
||||
} catch (cause) {
|
||||
@@ -56,6 +63,18 @@ const load = async (more = false) => {
|
||||
if (request === generation) loading.value = false;
|
||||
}
|
||||
};
|
||||
const changeMonth = (offset: number) => {
|
||||
if (!history.value || loading.value) return;
|
||||
const value = ordinal(history.value.month) + offset;
|
||||
selectedMonth.value = { year: Math.floor(value / 12), month: (value % 12) + 1 };
|
||||
void select(null);
|
||||
void load();
|
||||
};
|
||||
const latest = () => {
|
||||
selectedMonth.value = undefined;
|
||||
void select(null);
|
||||
void load();
|
||||
};
|
||||
const loadDetail = async (more = false) => {
|
||||
if (!selected.value || detailLoading.value) return;
|
||||
const request = detailGeneration;
|
||||
@@ -130,6 +149,7 @@ watch(
|
||||
[() => props.generalId, () => props.month?.year, () => props.month?.month],
|
||||
() => {
|
||||
generation++;
|
||||
selectedMonth.value = props.month;
|
||||
history.value = null;
|
||||
error.value = '';
|
||||
loading.value = false;
|
||||
@@ -156,6 +176,16 @@ watch(
|
||||
{{ history ? `${history.month.year}년 ${history.month.month}월` : '선택 월' }} · NPC·유저 자동턴의 개인/수뇌
|
||||
판단
|
||||
</p>
|
||||
<p v-if="history?.selection === 'LATEST' && history.items.length">
|
||||
가장 최근 결정이 수집된 월입니다. 현재 게임은 {{ history.currentMonth.year }}년
|
||||
{{ history.currentMonth.month }}월입니다.
|
||||
</p>
|
||||
<p>각 장수의 턴 실행 후 저장됩니다. 월이 바뀌어도 해당 장수의 다음 턴 전까지는 이전 월 기록이 최신입니다.</p>
|
||||
<nav aria-label="결정 조회 월">
|
||||
<button class="legacy-button" :disabled="loading || !canPrevious" @click="changeMonth(-1)">이전 월</button>
|
||||
<button class="legacy-button" :disabled="loading || !canNext" @click="changeMonth(1)">다음 월</button>
|
||||
<button class="legacy-button" :disabled="loading" @click="latest">최근 결정 조회</button>
|
||||
</nav>
|
||||
<p>
|
||||
절차와 선택 결과를 수집한 기록입니다. 후보 내부 조건 전체는 아직 포함되지 않으며, 기록이 없다고 판단 시도가
|
||||
없었다는 뜻은 아닙니다.
|
||||
@@ -164,7 +194,13 @@ watch(
|
||||
<p v-if="error" role="alert">
|
||||
{{ error }} <button class="legacy-button" @click="load()">결정 목록 다시 조회</button>
|
||||
</p>
|
||||
<p v-if="history && !history.items.length">이 월에 수집된 결정 기록이 없습니다.</p>
|
||||
<p v-if="history && !history.items.length">
|
||||
{{
|
||||
history.selection === 'LATEST'
|
||||
? '이 장수의 현재 기수에 수집된 결정 기록이 없습니다.'
|
||||
: '선택한 월에 수집된 결정 기록이 없습니다. 최근 결정 조회로 마지막 기록을 확인할 수 있습니다.'
|
||||
}}
|
||||
</p>
|
||||
<div v-if="history?.items.length" class="table-scroll">
|
||||
<table>
|
||||
<thead>
|
||||
|
||||
@@ -22,6 +22,15 @@ const showLogs = ref(false);
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const showDecisions = ref(false);
|
||||
// 상세 URL로 들어온 경우에도 목록의 열림 상태를 유지한다. 월 이동으로
|
||||
// decision query만 지웠을 때 전체 결정 패널이 닫히면 안 된다.
|
||||
watch(
|
||||
() => route.query.decision,
|
||||
(decision) => {
|
||||
if (typeof decision === 'string') showDecisions.value = true;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
const decisionsOpen = computed(() => showDecisions.value || typeof route.query.decision === 'string');
|
||||
const toggleDecisions = async () => {
|
||||
if (decisionsOpen.value) {
|
||||
|
||||
Reference in New Issue
Block a user