merge: 최신 main을 Profile 포괄 권한 제거에 통합

This commit is contained in:
2026-08-17 16:11:07 +00:00
58 changed files with 4438 additions and 963 deletions
@@ -1,3 +1,5 @@
import { generateKeyPairSync } from 'node:crypto';
import { expect, test, type Page, type Route } from '@playwright/test';
const response = (data: unknown) => ({ result: { data } });
@@ -6,6 +8,43 @@ const operationNames = (route: Route): string[] => {
return decodeURIComponent(url.pathname.slice(url.pathname.lastIndexOf('/trpc/') + 6)).split(',');
};
const { publicKey } = generateKeyPairSync('rsa', { modulusLength: 2048 });
const publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' }).toString();
const installPasswordSetupFixture = async (page: Page) => {
const calls: string[] = [];
await page.route('**/gateway/api/trpc/**', async (route) => {
const results = operationNames(route).map((operation) => {
calls.push(operation);
if (operation === 'me') return response(null);
if (operation === 'lobby.notice') return response('');
if (operation === 'lobby.profiles') return response([]);
if (operation === 'auth.passwordKey') {
return response({ keyId: 'password-setup-key', publicKeyPem, algorithm: 'RSA-OAEP-256' });
}
if (operation === 'auth.kakaoExchange') {
return response({
status: 'password_setup',
oauthSessionId: '11111111-1111-4111-8111-111111111112',
email: 'migrated@example.test',
successStatus: 'login',
});
}
if (operation === 'auth.kakaoSetPassword') {
return response({
status: 'otp',
challengeId: '11111111-1111-4111-8111-111111111111',
expiresAt: '2026-08-17T12:03:00.000Z',
attemptsRemaining: 3,
});
}
throw new Error(`Unhandled password setup fixture operation: ${operation}`);
});
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(results) });
});
return calls;
};
const installFixture = async (page: Page, action: 'link_existing' | 'rejoin') => {
const calls: string[] = [];
await page.route('**/gateway/api/trpc/**', async (route) => {
@@ -108,4 +147,27 @@ for (const viewport of [
expect(calls.filter((operation) => operation === 'auth.kakaoResolveAccount')).toHaveLength(1);
expect(geometry.width).toBe(viewport.name === 'desktop' ? 698 : 372);
});
test(`sets a migrated password before opening the OTP dialog on ${viewport.name}`, async ({ page }) => {
const calls = await installPasswordSetupFixture(page);
await page.setViewportSize(viewport);
await page.goto('/gateway/oauth/callback?code=oauth-code&state=oauth-state');
const form = page.getByRole('form', { name: '새 비밀번호 설정' });
await expect(form).toBeVisible();
await expect(form).toContainText('카카오 인증으로 기존 계정을 확인했습니다.');
await expect(form.getByLabel('카카오 이메일')).toHaveValue('migrated@example.test');
const geometry = await form.evaluate((element) => {
const rect = element.getBoundingClientRect();
return { width: rect.width, right: rect.right };
});
await form.getByLabel('새 비밀번호').fill('new-password-value');
await form.getByLabel('비밀번호 확인').fill('new-password-value');
await form.getByRole('button', { name: '새 비밀번호 설정' }).click();
await expect(page.getByRole('dialog', { name: '인증 코드 필요' })).toBeVisible();
expect(calls.filter((operation) => operation === 'auth.kakaoSetPassword')).toHaveLength(1);
expect(geometry.width).toBeGreaterThan(300);
expect(geometry.right).toBeLessThanOrEqual(viewport.width);
});
}
@@ -219,7 +219,13 @@ test('shows one public map panel and switches it by hover, click, and keyboard',
await expect(tablist.getByRole('tab', { name: '퀘섭' })).toHaveCount(0);
await expect(panel).toHaveCount(1);
await expect(cheTab).toHaveAttribute('aria-selected', 'true');
await expect(panel).toContainText('유저 11 / 500');
await expect(panel.getByText('체섭', { exact: true })).toHaveCount(0);
await expect(panel.locator('.map-preview-title')).toHaveCount(0);
const dateBar = panel.locator('.map-preview-date-bar');
await expect(dateBar).toHaveText('200년 1월');
const summary = panel.getByTestId('public-map-preview-summary');
await expect(summary).toContainText('RUNNING');
await expect(summary).toContainText('유저 11 / 500');
const roadLayer = panel.getByTestId('map-preview-road');
const castles = panel.getByTestId('map-preview-castle');
@@ -251,6 +257,32 @@ test('shows one public map panel and switches it by hover, click, and keyboard',
largeCastle: { width: 32, height: 24 },
largeNationBackground: { width: 96, height: 72 },
});
const dateGeometry = await dateBar.evaluate((element) => {
const rect = element.getBoundingClientRect();
const bodyRect = element.nextElementSibling?.getBoundingClientRect();
const style = getComputedStyle(element);
const textStyle = getComputedStyle(element.firstElementChild!);
return {
width: rect.width,
height: rect.height,
bottom: rect.bottom,
bodyTop: bodyRect?.top,
backgroundColor: style.backgroundColor,
justifyContent: style.justifyContent,
fontSize: textStyle.fontSize,
lineHeight: textStyle.lineHeight,
};
});
expect(dateGeometry).toEqual({
width: 700,
height: 20,
bottom: dateGeometry.bodyTop,
bodyTop: dateGeometry.bodyTop,
backgroundColor: 'rgb(0, 0, 0)',
justifyContent: 'center',
fontSize: '14px',
lineHeight: '20px',
});
if (requestedAssets) {
await expect.poll(() => requestedAssets.has('/game/map/che/che_road.png')).toBe(true);
await expect.poll(() => requestedAssets.has('/game/cast_8.gif')).toBe(true);
@@ -297,7 +329,9 @@ test('shows one public map panel and switches it by hover, click, and keyboard',
await hweTab.hover();
await expect(hweTab).toHaveAttribute('aria-selected', 'true');
await expect(panel).toContainText('유저 22 / 500');
await expect(panel.getByText('훼섭', { exact: true })).toHaveCount(0);
await expect(summary).toContainText('PAUSED');
await expect(summary).toContainText('유저 22 / 500');
await cheTab.click();
await expect(cheTab).toHaveAttribute('aria-selected', 'true');
@@ -322,7 +356,7 @@ test('shows one public map panel and switches it by hover, click, and keyboard',
await page.screenshot({ path: testInfo.outputPath('public-map-tabs-desktop.png'), fullPage: true });
await testInfo.attach('public-map-tabs-desktop-geometry', {
body: Buffer.from(
`${JSON.stringify({ panel: geometry, map: mapGeometry, tooltip: tooltipGeometry }, null, 2)}\n`
`${JSON.stringify({ panel: geometry, date: dateGeometry, map: mapGeometry, tooltip: tooltipGeometry }, null, 2)}\n`
),
contentType: 'application/json',
});
@@ -344,13 +378,20 @@ test.describe('touch navigation', () => {
await hweTab.tap();
await expect(hweTab).toHaveAttribute('aria-selected', 'true');
const panel = page.getByTestId('public-map-preview-panel');
await expect(panel).toContainText('유저 22 / 500');
await expect(panel.getByText('훼섭', { exact: true })).toHaveCount(0);
await expect(panel.locator('.map-preview-title')).toHaveCount(0);
await expect(panel.locator('.map-preview-date-bar')).toHaveText('200년 1월');
const summary = panel.getByTestId('public-map-preview-summary');
await expect(summary).toContainText('PAUSED');
await expect(summary).toContainText('유저 22 / 500');
await expect(panel.getByTestId('map-preview-road')).toBeVisible();
await expect(panel.getByTestId('map-preview-castle')).toHaveCount(2);
const mapBox = await panel.locator('.map-preview-body').boundingBox();
expect(mapBox?.width).toBeGreaterThan(280);
expect(mapBox?.width).toBeLessThanOrEqual(366);
expect(mapBox?.height).toBeCloseTo((mapBox?.width ?? 0) * (5 / 7), 0);
const horizontalOverflow = await panel.evaluate((element) => element.scrollWidth - element.clientWidth);
expect(horizontalOverflow).toBe(0);
const rightCity = panel.getByTestId('map-preview-city').nth(1);
await rightCity.hover();
const tooltip = panel.getByTestId('map-preview-city-tooltip');