fix: 빙의 전용 화면과 가오픈 갱신을 보정한다

This commit is contained in:
2026-09-01 02:38:02 +00:00
parent dd84e6d6b3
commit 9c82eb0cf5
16 changed files with 243 additions and 73 deletions
+10 -2
View File
@@ -2007,7 +2007,11 @@ test('장수 생성에서 등록 전콘을 골라 생성 요청에 전달한다'
accessPages: [],
createGeneralInputs: [],
joinConfig: {
rules: { stat: { total: 150, min: 30, max: 70 }, allowCustomName: true },
rules: {
stat: { total: 150, min: 30, max: 70 },
allowDirectCreation: true,
allowCustomName: true,
},
user: {
id: 'user-1',
displayName: '생성장수',
@@ -2056,7 +2060,11 @@ test('활성 전용 아이콘이 없으면 대표 preset을 장수 생성 요청
accessPages: [],
createGeneralInputs: [],
joinConfig: {
rules: { stat: { total: 150, min: 30, max: 70 }, allowCustomName: true },
rules: {
stat: { total: 150, min: 30, max: 70 },
allowDirectCreation: true,
allowCustomName: true,
},
user: {
id: 'user-1',
displayName: '생성장수',
+1
View File
@@ -41,6 +41,7 @@ const installFixture = async (page: Page, state: FixtureState): Promise<void> =>
return response({
rules: {
stat: { total: 165, min: 15, max: 80, bonusMin: 3, bonusMax: 5 },
allowDirectCreation: true,
allowCustomName: true,
},
user: {
+87 -1
View File
@@ -141,9 +141,16 @@ const installFixture = async (page: Page, state: FixtureState): Promise<void> =>
return response({
rules: {
stat: { total: 165, min: 15, max: 80, bonusMin: 3, bonusMax: 5 },
allowDirectCreation: false,
allowCustomName: true,
},
user: { id: 'npc-user', displayName: '빙의사용자', canCreateGeneral: true },
user: {
id: 'npc-user',
displayName: '빙의사용자',
canCreateGeneral: true,
icons: [],
preferredPicture: null,
},
personalities: [{ key: 'Random', name: '???', info: '' }],
warSpecials: [],
nations: [],
@@ -225,6 +232,83 @@ const installFixture = async (page: Page, state: FixtureState): Promise<void> =>
});
};
test('shows only possession and keeps every candidate reachable from the left on mobile', async ({
page,
}, testInfo) => {
const state: FixtureState = {
reservationCalls: 0,
reservationInputs: [],
rawBodies: [],
possessInputs: [],
hasGeneral: false,
injectTimeout: false,
};
await installFixture(page, state);
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('join');
await expect(page.getByRole('heading', { name: '장수 빙의', exact: true, level: 1 })).toBeVisible();
await expect(page.getByRole('button', { name: '장수 생성', exact: true })).toHaveCount(0);
await expect(page.locator('.npc-card')).toHaveCount(5);
const candidateGeometry = await page.locator('.npc-possession-section').evaluate((section) => {
const sectionRect = section.getBoundingClientRect();
const holder = section.querySelector<HTMLElement>('.npc-card-holder')!;
const holderRect = holder.getBoundingClientRect();
const cards = [...holder.querySelectorAll<HTMLElement>('.npc-card')];
return {
viewportWidth: window.innerWidth,
documentWidth: document.documentElement.scrollWidth,
sectionLeft: sectionRect.left,
sectionRight: sectionRect.right,
holderLeft: holderRect.left,
firstCardLeft: cards[0]!.getBoundingClientRect().left,
lastCardRight: cards.at(-1)!.getBoundingClientRect().right,
holderClientWidth: holder.clientWidth,
holderScrollWidth: holder.scrollWidth,
holderScrollLeft: holder.scrollLeft,
};
});
expect(candidateGeometry.viewportWidth).toBe(390);
expect(candidateGeometry.documentWidth).toBe(390);
expect(candidateGeometry.sectionLeft).toBeGreaterThanOrEqual(0);
expect(candidateGeometry.sectionRight).toBeLessThanOrEqual(390);
expect(candidateGeometry.firstCardLeft).toBeGreaterThanOrEqual(candidateGeometry.holderLeft);
expect(candidateGeometry.lastCardRight).toBeGreaterThan(candidateGeometry.sectionRight);
expect(candidateGeometry.holderScrollWidth).toBeGreaterThan(candidateGeometry.holderClientWidth);
expect(candidateGeometry.holderScrollLeft).toBe(0);
const refreshButton = page.locator('#btn-pick-more');
await expect(refreshButton).toBeEnabled();
const refreshStyle = await refreshButton.evaluate((element) => {
const style = getComputedStyle(element);
return {
backgroundColor: style.backgroundColor,
borderBottomWidth: style.borderBottomWidth,
opacity: style.opacity,
cursor: style.cursor,
};
});
expect(refreshStyle.backgroundColor).not.toBe('rgba(0, 0, 0, 0)');
expect(refreshStyle).toMatchObject({ borderBottomWidth: '4px', opacity: '1', cursor: 'pointer' });
await page.screenshot({ path: testInfo.outputPath('npc-possession-mobile-active.png'), fullPage: true });
const dialogs: string[] = [];
page.on('dialog', async (dialog) => {
dialogs.push(dialog.message());
await dialog.dismiss();
});
await page.locator('.npc-action').last().click();
expect(dialogs).toContain('빙의할까요? : 빙의후보5');
await expect
.poll(() => page.locator('.npc-card-holder').evaluate((holder) => (holder as HTMLElement).scrollLeft))
.toBeGreaterThan(0);
await refreshButton.click();
await expect.poll(() => state.reservationCalls).toBe(2);
expect(state.reservationInputs.at(-1)).toMatchObject({ refresh: true, keepIds: [] });
});
test('renders Ref-shaped token cards, preserves keep cooldown and retries possession with one ID', async ({
page,
}, testInfo) => {
@@ -239,6 +323,8 @@ test('renders Ref-shaped token cards, preserves keep cooldown and retries posses
await installFixture(page, state);
await page.setViewportSize({ width: 1024, height: 900 });
await page.goto('join?tab=possess');
await expect(page.getByRole('heading', { name: '장수 빙의', exact: true, level: 1 })).toBeVisible();
await expect(page.getByRole('button', { name: '장수 생성', exact: true })).toHaveCount(0);
await expect(page.getByRole('button', { name: 'NPC 빙의' })).toHaveClass(/active/);
await expect(page.locator('.npc-card')).toHaveCount(5);
+30 -4
View File
@@ -53,6 +53,8 @@ const submitting = ref(false);
const joinConfig = ref<JoinConfig | null>(null);
const accountIcons = computed(() => joinConfig.value?.user.icons ?? []);
const directCreationEnabled = computed(() => joinConfig.value?.rules.allowDirectCreation === true);
const possessionOnly = computed(() => !directCreationEnabled.value && joinConfig.value?.npcPossession.enabled === true);
const activeTab = ref<'create' | 'possess'>('create');
const contextTab = ref<'invitation' | 'map' | 'generals'>('invitation');
const inheritOpen = ref(false);
@@ -429,7 +431,11 @@ const loadConfig = async () => {
joinConfig.value = config;
const storedPossession = readPendingPossess();
pendingPossessAction.value = storedPossession?.ownerUserId === config.user.id ? storedPossession : null;
if (route.query.tab === 'possess' && config.npcPossession.enabled && config.user.canCreateGeneral) {
if (
config.npcPossession.enabled &&
config.user.canCreateGeneral &&
(!config.rules.allowDirectCreation || route.query.tab === 'possess')
) {
activeTab.value = 'possess';
}
const pending = readPendingJoin();
@@ -658,13 +664,14 @@ onUnmounted(() => {
<main class="join-page">
<header class="join-header">
<div>
<h1 class="join-title">장수 생성/빙의</h1>
<h1 class="join-title">{{ possessionOnly ? '장수 빙의' : '장수 생성/빙의' }}</h1>
<p class="join-subtitle">로그인 완료, 아직 장수가 없는 상태입니다.</p>
</div>
<div class="join-tabs">
<RouterLink class="simulator-link" to="/past-plays"> 지난 플레이</RouterLink>
<RouterLink class="simulator-link" to="/battle-simulator">전투 시뮬레이터</RouterLink>
<button
v-if="directCreationEnabled"
:class="{ active: activeTab === 'create' }"
:disabled="joinConfig?.user.canCreateGeneral === false"
@click="activeTab = 'create'"
@@ -1099,7 +1106,7 @@ onUnmounted(() => {
<div class="npc-footer">
<button
id="btn-pick-more"
class="ghost"
class="legacy-button legacy-button--secondary"
type="button"
:disabled="npcLoading || npcPickMoreSeconds > 0 || submitting || hasPendingPossession"
@click="loadNpcCandidates(true)"
@@ -1683,6 +1690,7 @@ onUnmounted(() => {
.npc-possession-section {
width: 1000px;
min-width: 0;
align-self: center;
}
@@ -1807,7 +1815,7 @@ onUnmounted(() => {
}
.npc-general-list-wrap {
width: 970px;
width: min(100%, 970px);
margin: 0 auto 20px;
overflow-x: auto;
}
@@ -1929,5 +1937,23 @@ onUnmounted(() => {
.context-general-table {
min-width: 520px;
}
.npc-possession-section {
width: 100%;
}
.npc-card-holder {
display: flex;
max-width: 100%;
justify-content: flex-start;
overflow-x: auto;
padding-bottom: 8px;
overscroll-behavior-x: contain;
-webkit-overflow-scrolling: touch;
}
.npc-card {
flex: 0 0 125px;
}
}
</style>