fix: 예약 공지 본문과 서버 행 구분을 바로잡는다

선공개와 일반 가오픈 공지를 공유 컴포넌트로 렌더링해 드래그 문자열을 일치시킨다. 예약 공지 배지에는 실제 빌드 시작 시각 툴팁만 제공하고 서버별 행 경계를 강화한다.
This commit is contained in:
2026-08-24 03:15:00 +00:00
parent 47cfbb64fa
commit 15cb8cc481
3 changed files with 336 additions and 214 deletions
@@ -502,7 +502,7 @@ test('does not contact a STOPPED game runtime and labels it inaccessible', async
await page.screenshot({ path: testInfo.outputPath('gateway-stopped-profile-lobby.png'), fullPage: true });
});
test('shows a compact upcoming reset announcement without contacting the stopped runtime', async ({
test('matches the normal preopen copy text and labels an immediate announcement with its build time', async ({
page,
}, testInfo) => {
const gameOperations = await installFixture(page, {
@@ -525,15 +525,15 @@ test('shows a compact upcoming reset announcement without contacting the stopped
options: ['develop', 'battle'],
},
},
includeStoppedProfile: true,
});
await page.setViewportSize({ width: 1200, height: 900 });
await page.goto('lobby');
const row = page.locator('tbody tr').filter({ hasText: '체섭' });
const announcement = row.getByTestId('upcoming-reset-announcement');
const marker = announcement.getByTestId('upcoming-reset-options-marker');
const swatch = announcement.getByTestId('upcoming-reset-options-swatch');
const tooltip = announcement.getByTestId('upcoming-reset-options-tooltip');
const badge = announcement.getByTestId('reserved-announcement-badge');
const tooltip = announcement.getByTestId('reserved-announcement-build-tooltip');
await expect(row.getByTestId('upcoming-reset-phase')).toHaveCount(0);
await expect(row.getByTestId('upcoming-reset-scheduled-at')).toHaveCount(0);
await expect(row.getByTestId('upcoming-reset-preopen-at')).toHaveText('- 가오픈 일시 : 2026-08-27 14:30:00 -');
@@ -542,14 +542,15 @@ test('shows a compact upcoming reset announcement without contacting the stopped
await expect(
row.getByTestId('upcoming-reset-scenario-announcement').locator(':scope > .text-green-400')
).toHaveText('60분 턴 서버');
expect(
(await announcement.evaluate((element) => (element as HTMLElement).innerText)).replace(/\s+/g, ' ').trim()
).toBe(
'- 가오픈 일시 : 2026-08-27 14:30:00 - - 오픈 일시 : 2026-08-27 20:00:00 - 【가상】황건적의 난 60분 턴 서버'
await expect(announcement.locator('.profile-announcement-settings')).toHaveText(
'(상성 설정:가상), (빙의 여부:가능), (최대 스탯:70), ' +
'(기타 설정:랜덤 임관, 자율행동[내정, 출병, 24시간 유효])'
);
await expect(tooltip).toBeHidden();
await expect(marker).toHaveAttribute('aria-label', '초기화 옵션 보기');
await expect(swatch).toHaveCSS('background-color', 'rgb(217, 119, 6)');
await expect(badge).toHaveText(/예약 공지/);
await expect(badge).toHaveAttribute('aria-label', '예약 공지의 실제 빌드 시작 시각 보기');
await expect(badge).toHaveCSS('user-select', 'none');
await expect(badge).toHaveCSS('border-top-color', 'rgb(217, 119, 6)');
await expect(row).not.toContainText('서버 중지 · 접근 불가');
await expect(row).not.toContainText('정보를 불러오는 중');
await expect(row.getByRole('button', { name: '입장' })).toHaveCount(0);
@@ -557,39 +558,63 @@ test('shows a compact upcoming reset announcement without contacting the stopped
expect(gameOperations).toEqual([]);
const desktopGeometry = await announcement.evaluate((element) => {
const marker = element.querySelector<HTMLElement>('[data-testid="upcoming-reset-options-marker"]');
const swatch = element.querySelector<HTMLElement>('[data-testid="upcoming-reset-options-swatch"]');
if (!marker || !swatch) throw new Error('expected upcoming reset option marker');
const badge = element.querySelector<HTMLElement>('[data-testid="reserved-announcement-badge"]');
if (!badge) throw new Error('expected reserved announcement badge');
return {
announcement: element.getBoundingClientRect().toJSON(),
cell: element.parentElement?.getBoundingClientRect().toJSON(),
marker: marker.getBoundingClientRect().toJSON(),
swatch: swatch.getBoundingClientRect().toJSON(),
badge: badge.getBoundingClientRect().toJSON(),
documentWidth: document.documentElement.scrollWidth,
viewportWidth: window.innerWidth,
};
});
expect(desktopGeometry.announcement.left).toBeGreaterThanOrEqual(desktopGeometry.cell?.left ?? 0);
expect(desktopGeometry.announcement.right).toBeLessThanOrEqual(desktopGeometry.cell?.right ?? 0);
expect(desktopGeometry.marker.width).toBe(18);
expect(desktopGeometry.marker.height).toBe(18);
expect(desktopGeometry.swatch.width).toBe(8);
expect(desktopGeometry.swatch.height).toBe(8);
await marker.hover();
expect(desktopGeometry.badge.width).toBeGreaterThan(40);
expect(desktopGeometry.badge.height).toBe(18);
const desktopRowDivider = await page
.locator('tbody tr')
.first()
.evaluate((element) => {
const style = getComputedStyle(element);
return { color: style.borderBottomColor, width: style.borderBottomWidth };
});
expect(desktopRowDivider).toEqual({ color: 'rgb(82, 82, 91)', width: '1px' });
await badge.hover();
await expect(tooltip).toBeVisible();
await expect(tooltip).toContainText('초기화 옵션');
await expect(tooltip).toContainText('상성 설정: 가상 · 빙의 여부: 가능 · 최대 스탯: 70');
await expect(tooltip).toContainText('기타 설정: 랜덤 임관 · 자율행동: 내정, 출병, 24시간 유효');
await expect(tooltip).not.toContainText('초기화 시작');
await expect(tooltip).not.toContainText('빌드 대기');
await expect(tooltip).toHaveText('실제 빌드 시작 : 2026-08-27 14:00:00');
await expect(tooltip).not.toContainText('초기화 옵션');
const desktopTooltipGeometry = await tooltip.evaluate((element) => element.getBoundingClientRect().toJSON());
expect(desktopTooltipGeometry.left).toBeGreaterThanOrEqual(8);
expect(desktopTooltipGeometry.right).toBeLessThanOrEqual(desktopGeometry.viewportWidth - 8);
expect(desktopTooltipGeometry.top).toBeGreaterThanOrEqual(desktopGeometry.cell?.top ?? 0);
expect(desktopTooltipGeometry.bottom).toBeLessThanOrEqual(desktopGeometry.cell?.bottom ?? 0);
await marker.focus();
await expect(marker).toBeFocused();
await expect(marker).toHaveCSS('outline-width', '2px');
await badge.focus();
await expect(badge).toBeFocused();
await expect(badge).toHaveCSS('outline-width', '2px');
await page.mouse.click(8, 8);
const selectionStart = await announcement.getByTestId('upcoming-reset-preopen-at').boundingBox();
const selectionEnd = await announcement.locator('.profile-announcement-settings').boundingBox();
if (!selectionStart || !selectionEnd) throw new Error('expected upcoming announcement selection geometry');
await page.mouse.move(selectionStart.x + 1, selectionStart.y + selectionStart.height / 2);
await page.mouse.down();
await page.mouse.move(selectionEnd.x + selectionEnd.width - 1, selectionEnd.y + selectionEnd.height / 2, {
steps: 24,
});
await page.mouse.up();
const compactSelection = (await page.evaluate(() => window.getSelection()?.toString() ?? ''))
.replace(/\s+/g, ' ')
.trim();
expect(compactSelection).toBe(
'- 가오픈 일시 : 2026-08-27 14:30:00 - - 오픈 일시 : 2026-08-27 20:00:00 - ' +
'【가상】황건적의 난 60분 턴 서버 ' +
'(상성 설정:가상), (빙의 여부:가능), (최대 스탯:70), ' +
'(기타 설정:랜덤 임관, 자율행동[내정, 출병, 24시간 유효])'
);
expect(compactSelection).not.toContain('예약 공지');
expect(compactSelection).not.toContain('실제 빌드 시작');
await page.evaluate(() => window.getSelection()?.removeAllRanges());
await page.screenshot({ path: testInfo.outputPath('gateway-upcoming-reset-desktop.png'), fullPage: true });
await page.setViewportSize({ width: 390, height: 844 });
@@ -606,7 +631,7 @@ test('shows a compact upcoming reset announcement without contacting the stopped
expect(mobileGeometry.left).toBeGreaterThanOrEqual(0);
expect(mobileGeometry.right).toBeLessThanOrEqual(mobileGeometry.viewportWidth);
expect(mobileGeometry.documentWidth).toBe(mobileGeometry.viewportWidth);
await marker.hover();
await badge.hover();
await expect(tooltip).toBeVisible();
const mobileTooltipGeometry = await tooltip.evaluate((element) => element.getBoundingClientRect().toJSON());
expect(mobileTooltipGeometry.left).toBeGreaterThanOrEqual(8);
@@ -636,12 +661,14 @@ test('keeps the announcement through the RESERVED handoff after the build comple
await page.goto('lobby');
const row = page.locator('tbody tr').filter({ hasText: 'hwe섭' });
const marker = row.getByTestId('upcoming-reset-options-marker');
const badge = row.getByTestId('reserved-announcement-badge');
await expect(row.getByTestId('upcoming-reset-phase')).toHaveCount(0);
await expect(row.getByTestId('upcoming-reset-scenario-title')).toHaveText('【가상】황건적의 난');
await marker.hover();
await expect(row.getByTestId('upcoming-reset-options-tooltip')).toContainText('초기화 옵션');
await expect(row.getByTestId('upcoming-reset-options-tooltip')).not.toContainText('오픈 준비 완료');
await badge.hover();
await expect(row.getByTestId('reserved-announcement-build-tooltip')).toHaveText(
'실제 빌드 시작 : 2026-08-27 14:00:00'
);
await expect(row.getByTestId('reserved-announcement-build-tooltip')).not.toContainText('오픈 준비 완료');
await expect(row).not.toContainText('준 비 중 · 접근 불가');
await expect(row.getByRole('button', { name: '입장' })).toHaveCount(0);
expect(gameOperations).toEqual([]);
@@ -690,6 +717,8 @@ test('uses two-row mobile server cards without horizontal scrolling and keeps re
const rowRect = row.getBoundingClientRect();
const buttonRect = button.getBoundingClientRect();
const style = getComputedStyle(button);
const bodyStyle = getComputedStyle(scrollElement.querySelector('tbody')!);
const firstRowStyle = getComputedStyle(rows[0]!);
const rect = (element: Element) => {
const value = element.getBoundingClientRect();
return { top: value.top, right: value.right, bottom: value.bottom, left: value.left, width: value.width };
@@ -715,6 +744,10 @@ test('uses two-row mobile server cards without horizontal scrolling and keeps re
viewportWidth: window.innerWidth,
outlineStyle: style.outlineStyle,
outlineWidth: style.outlineWidth,
rowGap: bodyStyle.rowGap,
dividerBackground: bodyStyle.backgroundColor,
firstRowBorderColor: firstRowStyle.borderBottomColor,
firstRowBorderWidth: firstRowStyle.borderBottomWidth,
};
});
expect(geometry.pageScrollWidth).toBe(geometry.viewportWidth);
@@ -724,6 +757,10 @@ test('uses two-row mobile server cards without horizontal scrolling and keeps re
expect(geometry.rows).toHaveLength(2);
expect(geometry.rows[0]?.bottom).toBeLessThanOrEqual(geometry.rows[1]?.top ?? 0);
expect(geometry.rows.every((item) => item.width === geometry.scroll.clientWidth)).toBe(true);
expect(geometry.rowGap).toBe('2px');
expect(geometry.dividerBackground).toBe('rgb(63, 63, 70)');
expect(geometry.firstRowBorderColor).toBe('rgb(82, 82, 91)');
expect(geometry.firstRowBorderWidth).toBe('1px');
expect(geometry.button.left).toBeGreaterThanOrEqual(geometry.scroll.left);
expect(geometry.button.right).toBeLessThanOrEqual(geometry.scroll.right);
expect(geometry.cells.server.top).toBeCloseTo(geometry.cells.info.top, 0);
@@ -0,0 +1,216 @@
<script setup lang="ts">
import { formatServerDateTime } from '@sammo-ts/common/time/ServerDateTime';
import { computed } from 'vue';
type AutorunUser = {
limitMinutes: number;
options: string[];
};
const props = defineProps<{
profileName: string;
preopenAt: string | null | undefined;
openAt: string | null | undefined;
scenarioTitle: string;
turnTermMinutes: number;
fictionMode: string;
npcMode: number;
defaultStatTotal: number;
otherTextInfo: string;
autorunUser: AutorunUser | null;
scheduledAt?: string | null;
testIdPrefix: 'profile' | 'upcoming-reset';
}>();
const formatAnnouncementDate = (value: string | null | undefined): string =>
formatServerDateTime(value, { fallback: '-' });
const npcModeText = (mode: number): string => ['불가', '가능', '선택 생성'][mode] ?? '불가';
const autorunDetailText = (autorun: AutorunUser): string => {
const enabled = new Set(autorun.options);
const labels: string[] = [];
if (enabled.has('develop')) labels.push('내정');
if (enabled.has('warp')) labels.push('순간이동');
if (enabled.has('recruit_high')) labels.push('모병');
else if (enabled.has('recruit')) labels.push('징병');
if (enabled.has('train')) labels.push('훈련/사기진작');
if (enabled.has('battle')) labels.push('출병');
if (enabled.has('chief')) labels.push('사령턴');
const limit =
autorun.limitMinutes >= 43_200
? '항상 유효'
: autorun.limitMinutes % 60 === 0
? `${autorun.limitMinutes / 60}시간 유효`
: `${autorun.limitMinutes}분 유효`;
labels.push(limit);
return labels.join(', ');
};
const safeProfileName = computed(() => props.profileName.replaceAll(/[^a-zA-Z0-9_-]/g, '-'));
const autorunTooltipId = computed(() => `profile-autorun-${props.testIdPrefix}-${safeProfileName.value}`);
const buildTimeTooltipId = computed(() => `reserved-announcement-build-time-${safeProfileName.value}`);
</script>
<template>
<div class="preopen-announcement" :data-testid="`${testIdPrefix}-announcement`">
<div v-if="preopenAt" :data-testid="`${testIdPrefix}-preopen-at`">
- 가오픈 일시 : {{ formatAnnouncementDate(preopenAt) }} -
</div>
<div :data-testid="`${testIdPrefix}-open-at`">- 오픈 일시 : {{ formatAnnouncementDate(openAt) }} -</div>
<div :data-testid="`${testIdPrefix}-scenario-announcement`">
<span class="text-orange-400" :data-testid="`${testIdPrefix}-scenario-title`">{{ scenarioTitle }}</span
>{{ ' ' }}
<span class="text-green-400">{{ turnTermMinutes }} 서버</span>
<span
v-if="scheduledAt"
class="reserved-announcement-badge"
tabindex="0"
aria-label="예약 공지의 실제 빌드 시작 시각 보기"
:aria-describedby="buildTimeTooltipId"
data-testid="reserved-announcement-badge"
>
예약 공지
<span
:id="buildTimeTooltipId"
class="reserved-announcement-build-tooltip"
role="tooltip"
data-testid="reserved-announcement-build-tooltip"
>실제 빌드 시작 : {{ formatAnnouncementDate(scheduledAt) }}</span
>
</span>
</div>
<div class="profile-announcement-settings text-xs text-zinc-500">
(상성 설정:{{ fictionMode }}), (빙의 여부:{{ npcModeText(npcMode) }}), (최대 스탯:{{ defaultStatTotal }}),
(기타 설정:<template v-if="otherTextInfo"
>{{ otherTextInfo }}<template v-if="autorunUser">, </template></template
><span v-if="autorunUser" class="copyable-autorun" tabindex="0" :aria-describedby="autorunTooltipId"
>자율행동<span :id="autorunTooltipId" class="copyable-autorun-detail" role="tooltip"
><span class="copyable-autorun-bracket">[</span><span>{{ autorunDetailText(autorunUser) }}</span
><span class="copyable-autorun-bracket">]</span></span
></span
>)
</div>
</div>
</template>
<style scoped>
.preopen-announcement {
line-height: 1.5;
}
.reserved-announcement-badge {
position: relative;
display: inline-flex;
align-items: center;
margin-left: 6px;
padding: 1px 4px;
border: 1px solid #d97706;
border-radius: 2px;
background: rgb(120 53 15 / 35%);
color: #fbbf24;
cursor: help;
font-size: 10px;
font-weight: 700;
line-height: 14px;
user-select: none;
vertical-align: text-bottom;
}
.reserved-announcement-build-tooltip {
position: absolute;
z-index: 30;
bottom: calc(100% + 6px);
left: 50%;
visibility: hidden;
box-sizing: border-box;
width: max-content;
max-width: min(320px, calc(100vw - 32px));
padding: 6px 8px;
border: 1px solid #52525b;
border-radius: 4px;
background: #18181b;
box-shadow: 0 4px 12px rgb(0 0 0 / 45%);
color: #f4f4f5;
font-size: 12px;
font-weight: 400;
line-height: 1.4;
opacity: 0;
pointer-events: none;
text-align: left;
transform: translateX(-50%);
white-space: nowrap;
}
.reserved-announcement-badge:hover .reserved-announcement-build-tooltip,
.reserved-announcement-badge:focus-visible .reserved-announcement-build-tooltip {
visibility: visible;
opacity: 1;
}
.reserved-announcement-badge:focus-visible {
outline: 2px solid #fdba74;
outline-offset: 2px;
}
.copyable-autorun {
position: relative;
cursor: help;
text-decoration: underline;
text-underline-offset: 2px;
}
.copyable-autorun-detail {
display: inline;
color: transparent;
font-size: 0;
}
.copyable-autorun-bracket {
color: transparent;
font-size: 0;
}
.copyable-autorun:hover .copyable-autorun-detail,
.copyable-autorun:focus-visible .copyable-autorun-detail {
position: absolute;
z-index: 30;
right: 0;
bottom: calc(100% + 6px);
display: block;
box-sizing: border-box;
width: max-content;
max-width: min(520px, calc(100vw - 32px));
padding: 6px 8px;
border: 1px solid #52525b;
border-radius: 4px;
background: #18181b;
box-shadow: 0 4px 12px rgb(0 0 0 / 45%);
color: #f4f4f5;
font-size: 12px;
line-height: 1.4;
text-align: left;
white-space: normal;
}
.copyable-autorun:focus-visible {
border-radius: 2px;
outline: 2px solid #fdba74;
outline-offset: 2px;
}
@media (max-width: 640px) {
.reserved-announcement-badge:hover .reserved-announcement-build-tooltip,
.reserved-announcement-badge:focus-visible .reserved-announcement-build-tooltip,
.copyable-autorun:hover .copyable-autorun-detail,
.copyable-autorun:focus-visible .copyable-autorun-detail {
position: fixed;
top: auto;
right: 16px;
bottom: 16px;
left: 16px;
width: auto;
max-width: none;
transform: none;
white-space: normal;
}
}
</style>
+50 -181
View File
@@ -7,6 +7,7 @@ import type { AppRouter } from '@sammo-ts/gateway-api';
import { writeGameSessionTransfer } from '@sammo-ts/common/auth/gameSessionTransfer';
import DefaultLayout from '../layouts/DefaultLayout.vue';
import MapPreview from '../components/MapPreview.vue';
import ProfilePreopenAnnouncement from '../components/ProfilePreopenAnnouncement.vue';
import { useToast } from '../composables/useToast';
import { trpc } from '../utils/trpc';
import { createGameTrpc } from '../utils/gameTrpc';
@@ -106,11 +107,8 @@ const handleMapTabKeydown = (event: KeyboardEvent, profileName: string): void =>
const formatGraceEndsAt = (value: string | null | undefined): string => formatServerDateTime(value);
const serverSeasonStatus = (info: LobbyInfo) => resolveServerSeasonStatus(info);
const formatAnnouncementDate = (value: string | null | undefined): string =>
formatServerDateTime(value, { fallback: '-' });
const profileScenarioTitle = (profileName: string): string =>
profileDetails.value[profileName]?.scenarioTitle.trim() || '-';
const npcModeText = (mode: number): string => ['불가', '가능', '선택 생성'][mode] ?? '불가';
const autorunDetailText = (autorun: LobbyInfo['autorunUser']): string => {
if (!autorun) return '';
@@ -133,10 +131,8 @@ const autorunDetailText = (autorun: LobbyInfo['autorunUser']): string => {
labels.push(limit);
return labels.join(', ');
};
const autorunTooltipId = (profileName: string, scope = 'current'): string =>
'profile-autorun-' + scope + '-' + profileName.replaceAll(/[^a-zA-Z0-9_-]/g, '-');
const upcomingResetOptionsTooltipId = (profileName: string): string =>
'upcoming-reset-options-' + profileName.replaceAll(/[^a-zA-Z0-9_-]/g, '-');
const autorunTooltipId = (profileName: string): string =>
'profile-autorun-current-' + profileName.replaceAll(/[^a-zA-Z0-9_-]/g, '-');
const isProfileRuntimeAvailable = (profile: LobbyProfile): boolean => profile.lifecycle.userAccessible;
const unavailableProfileText = (profile: LobbyProfile): string => {
if (!profile.lifecycle.dataInitialized) return '- DB 초기화 전 · 접근 불가 -';
@@ -453,7 +449,7 @@ const handleEnter = async (profile: LobbyProfile, targetPath: string) => {
<th class="px-4 py-3 border-b border-zinc-700 w-32 text-center"> </th>
</tr>
</thead>
<tbody class="divide-y divide-zinc-800">
<tbody class="profile-table-body">
<tr
v-for="profile in profiles"
:key="profile.profileName"
@@ -504,100 +500,47 @@ const handleEnter = async (profile: LobbyProfile, targetPath: string) => {
<!-- Server Info -->
<td class="profile-info-cell px-4 py-4 border-r border-zinc-800">
<div
<ProfilePreopenAnnouncement
v-if="profile.upcomingReset"
class="upcoming-reset-announcement"
data-testid="upcoming-reset-announcement"
>
<div data-testid="upcoming-reset-preopen-at">
- 가오픈 일시 :
{{ formatAnnouncementDate(profile.upcomingReset.preopenAt) }} -
</div>
<div data-testid="upcoming-reset-open-at">
- 오픈 일시 : {{ formatAnnouncementDate(profile.upcomingReset.openAt) }} -
</div>
<div data-testid="upcoming-reset-scenario-announcement">
<span class="text-orange-400" data-testid="upcoming-reset-scenario-title">
{{ profile.upcomingReset.scenarioTitle }} </span
>{{ ' ' }}
<span class="text-green-400">
{{ profile.upcomingReset.turnTermMinutes }}분 턴 서버
</span>
<span
class="upcoming-reset-options-marker"
tabindex="0"
aria-label="초기화 옵션 보기"
:aria-describedby="upcomingResetOptionsTooltipId(profile.profileName)"
data-testid="upcoming-reset-options-marker"
>
<span
class="upcoming-reset-options-swatch"
data-testid="upcoming-reset-options-swatch"
aria-hidden="true"
></span>
<span
:id="upcomingResetOptionsTooltipId(profile.profileName)"
class="upcoming-reset-options-tooltip"
role="tooltip"
data-testid="upcoming-reset-options-tooltip"
>
<strong>초기화 옵션</strong>
<span
>상성 설정: {{ profile.upcomingReset.fictionMode }} · 빙의 여부:
{{ npcModeText(profile.upcomingReset.npcMode) }} · 최대 스탯:
{{ profile.upcomingReset.defaultStatTotal }}</span
>
<span
>기타 설정: {{ profile.upcomingReset.otherTextInfo || '없음'
}}<template v-if="profile.upcomingReset.autorunUser">
· 자율행동:
{{
autorunDetailText(profile.upcomingReset.autorunUser)
}}</template
></span
>
</span>
</span>
</div>
</div>
:profile-name="profile.profileName"
:preopen-at="profile.upcomingReset.preopenAt"
:open-at="profile.upcomingReset.openAt"
:scenario-title="profile.upcomingReset.scenarioTitle"
:turn-term-minutes="profile.upcomingReset.turnTermMinutes"
:fiction-mode="profile.upcomingReset.fictionMode"
:npc-mode="profile.upcomingReset.npcMode"
:default-stat-total="profile.upcomingReset.defaultStatTotal"
:other-text-info="profile.upcomingReset.otherTextInfo"
:autorun-user="profile.upcomingReset.autorunUser"
:scheduled-at="profile.upcomingReset.scheduledAt"
test-id-prefix="upcoming-reset"
/>
<template v-if="profileDetails[profile.profileName]">
<div
class="space-y-1"
:class="{ 'mt-3 border-t border-zinc-800 pt-3': profile.upcomingReset }"
>
<template v-if="profile.status === 'PREOPEN'">
<div
v-if="profileDetails[profile.profileName]?.preopenAt"
data-testid="profile-preopen-at"
>
- 가오픈 일시 :
{{
formatAnnouncementDate(
profileDetails[profile.profileName]?.preopenAt
)
}}
-
</div>
<div data-testid="profile-open-at">
- 오픈 일시 :
{{
formatAnnouncementDate(
profileDetails[profile.profileName]?.opentime ||
profileDetails[profile.profileName]?.starttime
)
}}
-
</div>
<div data-testid="profile-scenario-announcement">
<span
class="text-orange-400"
data-testid="profile-scenario-title"
>{{ profileScenarioTitle(profile.profileName) }}</span
>{{ ' ' }}
<span class="text-green-400">
{{ profileDetails[profile.profileName]?.turnTerm }}분 턴 서버
</span>
</div>
<ProfilePreopenAnnouncement
:profile-name="profile.profileName"
:preopen-at="profileDetails[profile.profileName]!.preopenAt"
:open-at="
profileDetails[profile.profileName]!.opentime ||
profileDetails[profile.profileName]!.starttime
"
:scenario-title="profileScenarioTitle(profile.profileName)"
:turn-term-minutes="profileDetails[profile.profileName]!.turnTerm"
:fiction-mode="profileDetails[profile.profileName]!.fictionMode"
:npc-mode="profileDetails[profile.profileName]!.npcMode"
:default-stat-total="
profileDetails[profile.profileName]!.defaultStatTotal
"
:other-text-info="
profileDetails[profile.profileName]!.otherTextInfo
"
:autorun-user="profileDetails[profile.profileName]!.autorunUser"
test-id-prefix="profile"
/>
</template>
<template v-else>
<div>
@@ -620,15 +563,11 @@ const handleEnter = async (profile: LobbyProfile, targetPath: string) => {
>
</div>
</template>
<div class="profile-announcement-settings text-xs text-zinc-500">
<div
v-if="profile.status !== 'PREOPEN'"
class="profile-announcement-settings text-xs text-zinc-500"
>
(상성 설정:{{ profileDetails[profile.profileName]?.fictionMode }}),
<template v-if="profile.status === 'PREOPEN'">
(빙의 여부:{{
npcModeText(profileDetails[profile.profileName]?.npcMode ?? 0)
}}), (최대 스탯:{{
profileDetails[profile.profileName]?.defaultStatTotal
}}),
</template>
(기타 설정:<template
v-if="profileDetails[profile.profileName]?.otherTextInfo"
>{{ profileDetails[profile.profileName]?.otherTextInfo
@@ -987,73 +926,14 @@ const handleEnter = async (profile: LobbyProfile, targetPath: string) => {
min-width: 760px;
}
.profile-table-body > tr:not(:last-child) {
border-bottom: 1px solid #52525b;
}
.season-status {
user-select: none;
}
.upcoming-reset-announcement {
line-height: 1.5;
}
.upcoming-reset-options-marker {
position: relative;
display: inline-flex;
width: 18px;
height: 18px;
align-items: center;
justify-content: center;
margin-left: 3px;
border-radius: 2px;
cursor: help;
vertical-align: text-bottom;
}
.upcoming-reset-options-swatch {
width: 8px;
height: 8px;
border: 1px solid #fbbf24;
border-radius: 1px;
background: #d97706;
box-shadow: 0 0 0 1px rgb(24 24 27 / 70%);
}
.upcoming-reset-options-tooltip {
position: absolute;
z-index: 30;
bottom: -16px;
left: calc(100% + 6px);
display: flex;
visibility: hidden;
box-sizing: border-box;
width: max-content;
max-width: min(400px, calc(100vw - 32px));
flex-direction: column;
gap: 2px;
padding: 7px 9px;
border: 1px solid #52525b;
border-radius: 4px;
background: #18181b;
box-shadow: 0 4px 12px rgb(0 0 0 / 45%);
color: #f4f4f5;
font-size: 12px;
line-height: 1.4;
opacity: 0;
pointer-events: none;
text-align: left;
white-space: normal;
}
.upcoming-reset-options-marker:hover .upcoming-reset-options-tooltip,
.upcoming-reset-options-marker:focus-visible .upcoming-reset-options-tooltip {
visibility: visible;
opacity: 1;
}
.upcoming-reset-options-marker:focus-visible {
outline: 2px solid #fdba74;
outline-offset: 1px;
}
.copyable-autorun {
position: relative;
cursor: help;
@@ -1101,17 +981,6 @@ const handleEnter = async (profile: LobbyProfile, targetPath: string) => {
}
@media (max-width: 640px) {
.upcoming-reset-options-marker:hover .upcoming-reset-options-tooltip,
.upcoming-reset-options-marker:focus-visible .upcoming-reset-options-tooltip {
position: fixed;
top: auto;
right: 16px;
bottom: 16px;
left: 16px;
width: auto;
max-width: none;
}
.copyable-autorun:hover .copyable-autorun-detail,
.copyable-autorun:focus-visible .copyable-autorun-detail {
position: fixed;
@@ -1148,8 +1017,8 @@ const handleEnter = async (profile: LobbyProfile, targetPath: string) => {
.profile-table tbody {
display: grid;
gap: 1px;
background: #27272a;
gap: 2px;
background: #3f3f46;
}
.profile-table tbody tr {
@@ -1171,7 +1040,7 @@ const handleEnter = async (profile: LobbyProfile, targetPath: string) => {
.profile-server-cell {
grid-area: server;
border-bottom: 1px solid #27272a;
border-bottom: 1px solid #3f3f46;
}
.profile-server-cell > div {
@@ -1182,7 +1051,7 @@ const handleEnter = async (profile: LobbyProfile, targetPath: string) => {
.profile-info-cell {
grid-area: info;
border-right: 0;
border-bottom: 1px solid #27272a;
border-bottom: 1px solid #3f3f46;
}
.profile-portrait-cell {