Merge remote-tracking branch 'origin/main' into fix/command-editor-followup-20260812
This commit is contained in:
@@ -35,6 +35,7 @@ const history = ref<HistoryData | null>(null);
|
||||
const selectedYearMonth = ref<number | null>(null);
|
||||
const settingsOpen = ref(false);
|
||||
const rankingBottom = ref(localStorage.getItem('yearbook-ranking-bottom') === 'true');
|
||||
let historyRequestId = 0;
|
||||
const serverID = computed(() => {
|
||||
const value = route.query.serverID;
|
||||
const raw = Array.isArray(value) ? value[0] : value;
|
||||
@@ -71,19 +72,27 @@ const loadHistory = async (): Promise<void> => {
|
||||
if (selectedYearMonth.value === null) {
|
||||
return;
|
||||
}
|
||||
const requestId = ++historyRequestId;
|
||||
loading.value = true;
|
||||
errorMessage.value = '';
|
||||
try {
|
||||
const { year, month } = parseYearMonth(selectedYearMonth.value);
|
||||
const result = await trpc.yearbook.getHistory.query({ year, month, serverID: serverID.value });
|
||||
if (requestId !== historyRequestId) {
|
||||
return;
|
||||
}
|
||||
if ('data' in result) {
|
||||
history.value = result.data;
|
||||
}
|
||||
} catch (error) {
|
||||
history.value = null;
|
||||
if (requestId !== historyRequestId) {
|
||||
return;
|
||||
}
|
||||
errorMessage.value = error instanceof Error ? error.message : '연감 데이터를 불러오지 못했습니다.';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
if (requestId === historyRequestId) {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -129,7 +138,13 @@ onMounted(async () => {
|
||||
<strong>연 감</strong>
|
||||
<button class="legacy-button close-button" type="button" @click="closePage">창 닫기</button>
|
||||
<span class="settings-menu">
|
||||
<button class="legacy-button legacy-button--navigation" type="button" @click="settingsOpen = !settingsOpen">⚙ 설정⌄</button>
|
||||
<button
|
||||
class="legacy-button legacy-button--navigation"
|
||||
type="button"
|
||||
@click="settingsOpen = !settingsOpen"
|
||||
>
|
||||
⚙ 설정⌄
|
||||
</button>
|
||||
<button v-if="settingsOpen" class="settings-item" type="button" @click="toggleRankingPosition">
|
||||
국가 순서 위치 변경(모바일 전용)
|
||||
</button>
|
||||
@@ -164,9 +179,9 @@ onMounted(async () => {
|
||||
<div v-if="errorMessage" class="yearbook-message error" role="alert">{{ errorMessage }}</div>
|
||||
<div v-else-if="loading && !history" class="yearbook-message">불러오는 중...</div>
|
||||
|
||||
<section v-if="history" :class="['history-grid', { 'ranking-bottom': rankingBottom }]">
|
||||
<section v-if="history" :class="['history-grid', { 'ranking-bottom': rankingBottom }]" :aria-busy="loading">
|
||||
<div class="map-position">
|
||||
<MapViewer :map-data="history.map" :map-layout="mapLayout" :loading="loading" />
|
||||
<MapViewer :map-data="history.map" :map-layout="mapLayout" :loading="loading && !history" />
|
||||
</div>
|
||||
<aside class="nation-position">
|
||||
<table>
|
||||
|
||||
@@ -28,7 +28,16 @@ const fulfillTrpc = async (route: Route, results: unknown[]): Promise<void> => {
|
||||
|
||||
type LobbyFixtureOptions = {
|
||||
authenticated?: boolean;
|
||||
roles?: string[];
|
||||
kakaoVerified?: boolean;
|
||||
canCreateGeneral?: boolean;
|
||||
requiresKakaoVerification?: boolean;
|
||||
specialAccess?: {
|
||||
kind: 'OPERATOR' | 'TESTER' | 'RECOVERY' | 'OTHER';
|
||||
grantId: string | null;
|
||||
expiresAt: string | null;
|
||||
allowsGeneralCreation: boolean;
|
||||
} | null;
|
||||
myGeneral?: {
|
||||
name: string;
|
||||
picture: string;
|
||||
@@ -48,7 +57,11 @@ type LobbyFixtureOptions = {
|
||||
const installFixture = async (page: Page, options: LobbyFixtureOptions = {}) => {
|
||||
const {
|
||||
authenticated = true,
|
||||
roles = ['user'],
|
||||
kakaoVerified = true,
|
||||
canCreateGeneral = true,
|
||||
requiresKakaoVerification = false,
|
||||
specialAccess = null,
|
||||
myGeneral = {
|
||||
name: '선택장수',
|
||||
picture: 'users/core2026/account-hash.png',
|
||||
@@ -79,8 +92,8 @@ const installFixture = async (page: Page, options: LobbyFixtureOptions = {}) =>
|
||||
id: 'lobby-user',
|
||||
username: 'lobby-user',
|
||||
displayName: '로비사용자',
|
||||
roles: ['user'],
|
||||
kakaoVerified: true,
|
||||
roles,
|
||||
kakaoVerified,
|
||||
createdAt: '2026-07-30T00:00:00.000Z',
|
||||
}
|
||||
: null
|
||||
@@ -109,8 +122,9 @@ const installFixture = async (page: Page, options: LobbyFixtureOptions = {}) =>
|
||||
localAccountPolicy: {
|
||||
accessAllowed: true,
|
||||
canCreateGeneral,
|
||||
requiresKakaoVerification: false,
|
||||
requiresKakaoVerification,
|
||||
graceEndsAt: null,
|
||||
specialAccess,
|
||||
},
|
||||
},
|
||||
]);
|
||||
@@ -216,6 +230,51 @@ test('applies the signed general-acquisition policy to both create and possessio
|
||||
await expect(row.getByRole('button', { name: '장수빙의' })).toBeDisabled();
|
||||
});
|
||||
|
||||
test('shows the Kakao verification banner when a profile still requires verification', async ({ page }) => {
|
||||
await installFixture(page, {
|
||||
kakaoVerified: false,
|
||||
requiresKakaoVerification: true,
|
||||
});
|
||||
|
||||
await page.goto('lobby');
|
||||
await expect(page.getByText('카카오 인증이 필요합니다.')).toBeVisible();
|
||||
});
|
||||
|
||||
test('hides the Kakao verification banner for operator special access', async ({ page }) => {
|
||||
await installFixture(page, {
|
||||
roles: ['superuser'],
|
||||
kakaoVerified: false,
|
||||
specialAccess: {
|
||||
kind: 'OPERATOR',
|
||||
grantId: null,
|
||||
expiresAt: null,
|
||||
allowsGeneralCreation: true,
|
||||
},
|
||||
});
|
||||
|
||||
await page.goto('lobby');
|
||||
await expect(page.getByText('특수 접근 · OPERATOR')).toBeVisible();
|
||||
await expect(page.getByText('카카오 인증이 필요합니다.')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('hides the Kakao verification banner when a grant removes the remaining verification requirement', async ({
|
||||
page,
|
||||
}) => {
|
||||
await installFixture(page, {
|
||||
kakaoVerified: false,
|
||||
specialAccess: {
|
||||
kind: 'RECOVERY',
|
||||
grantId: '11111111-1111-4111-8111-111111111111',
|
||||
expiresAt: '2026-08-20T00:00:00.000Z',
|
||||
allowsGeneralCreation: true,
|
||||
},
|
||||
});
|
||||
|
||||
await page.goto('lobby');
|
||||
await expect(page.getByText('특수 접근 · RECOVERY')).toBeVisible();
|
||||
await expect(page.getByText('카카오 인증이 필요합니다.')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('opens the profile root without profile or game token query parameters', async ({ page }) => {
|
||||
await installFixture(page);
|
||||
await page.route('**/hwe/', async (route) => {
|
||||
|
||||
@@ -46,7 +46,12 @@ const canAccessAdmin = computed(
|
||||
role === 'superuser' || role === 'admin' || role === 'admin.superuser' || role.startsWith('admin.')
|
||||
) ?? false
|
||||
);
|
||||
const needsKakaoVerification = computed(() => me.value !== null && !me.value.kakaoVerified);
|
||||
const needsKakaoVerification = computed(
|
||||
() =>
|
||||
me.value !== null &&
|
||||
!me.value.kakaoVerified &&
|
||||
profiles.value.some((profile) => profile.localAccountPolicy?.requiresKakaoVerification === true)
|
||||
);
|
||||
const userIconBaseUrl = configuredUserIconPublicUrl();
|
||||
const sharedIconBaseUrl = configuredSharedIconPublicUrl();
|
||||
const publicMapProfiles = computed(() =>
|
||||
|
||||
Reference in New Issue
Block a user