Merge remote-tracking branch 'origin/main' into fix/turn-selector-divider-20260813
This commit is contained in:
@@ -274,6 +274,8 @@ test('matches the ref meeting-room geometry, typography, textures, and controls'
|
||||
'src',
|
||||
'https://sam-image.hided.net/icons/22.jpg'
|
||||
);
|
||||
await expect(page.locator('.article-header .date')).toHaveText('07-26 19:20');
|
||||
await expect(page.locator('.comment-row .date')).toHaveText('07-26 19:25');
|
||||
if (artifactRoot) {
|
||||
await page.screenshot({
|
||||
path: resolve(artifactRoot, 'board-core-desktop.png'),
|
||||
@@ -392,7 +394,9 @@ test('uses the ref 500px responsive form widths', async ({ page }) => {
|
||||
await expect(page.getByRole('heading', { name: '기밀실' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('retains article and comment input after a failed mutation, then reloads after success', async ({ page }, testInfo) => {
|
||||
test('retains article and comment input after a failed mutation, then reloads after success', async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
const state: BoardFixture = {
|
||||
permission: 2,
|
||||
canMeeting: true,
|
||||
@@ -408,7 +412,9 @@ test('retains article and comment input after a failed mutation, then reloads af
|
||||
await page.locator('#board-title').fill('새 제목');
|
||||
await page.locator('#board-content').fill('새 내용');
|
||||
await page.locator('#submitArticle').click();
|
||||
const articleToast = page.getByTestId('game-toast').filter({ hasText: '게시물 등록에 실패했습니다: 접속 제한입니다.' });
|
||||
const articleToast = page
|
||||
.getByTestId('game-toast')
|
||||
.filter({ hasText: '게시물 등록에 실패했습니다: 접속 제한입니다.' });
|
||||
await expect(articleToast).toHaveAttribute('data-feedback-kind', 'error');
|
||||
await expect(articleToast).toHaveAttribute('role', 'alert');
|
||||
await expect(page.locator('#board-title')).toHaveValue('새 제목');
|
||||
@@ -416,7 +422,13 @@ test('retains article and comment input after a failed mutation, then reloads af
|
||||
|
||||
const desktopToastGeometry = await articleToast.evaluate((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return { left: rect.left, right: rect.right, top: rect.top, width: rect.width, viewportWidth: window.innerWidth };
|
||||
return {
|
||||
left: rect.left,
|
||||
right: rect.right,
|
||||
top: rect.top,
|
||||
width: rect.width,
|
||||
viewportWidth: window.innerWidth,
|
||||
};
|
||||
});
|
||||
expect(desktopToastGeometry.left).toBeGreaterThanOrEqual(0);
|
||||
expect(desktopToastGeometry.right).toBeLessThanOrEqual(desktopToastGeometry.viewportWidth);
|
||||
@@ -435,10 +447,14 @@ test('retains article and comment input after a failed mutation, then reloads af
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
const documentWidthBeforeToast = await page.evaluate(() => document.documentElement.scrollWidth);
|
||||
await commentInput.press('Enter');
|
||||
const commentToast = page.getByTestId('game-toast').filter({ hasText: '댓글 등록에 실패했습니다: 접속 제한입니다.' });
|
||||
const commentToast = page
|
||||
.getByTestId('game-toast')
|
||||
.filter({ hasText: '댓글 등록에 실패했습니다: 접속 제한입니다.' });
|
||||
await expect(commentToast).toBeVisible();
|
||||
await expect
|
||||
.poll(async () => commentToast.evaluate((element) => window.innerHeight - element.getBoundingClientRect().bottom))
|
||||
.poll(async () =>
|
||||
commentToast.evaluate((element) => window.innerHeight - element.getBoundingClientRect().bottom)
|
||||
)
|
||||
.toBeGreaterThanOrEqual(0);
|
||||
const mobileToastGeometry = await commentToast.evaluate((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
|
||||
@@ -1224,6 +1224,125 @@ test('the 939/940 boundary switches to the Ref-style 500px single document', asy
|
||||
await persistArtifact(page, `${basePath.slice(1)}-mobile-500`);
|
||||
});
|
||||
|
||||
test('real mobile devices initially fit the complete 500px game canvas', async ({ browser }, testInfo) => {
|
||||
test.setTimeout(60_000);
|
||||
const configuredBaseUrl = testInfo.project.use.baseURL;
|
||||
if (typeof configuredBaseUrl !== 'string') {
|
||||
throw new Error('Playwright baseURL is required for the mobile viewport contract');
|
||||
}
|
||||
|
||||
const deviceWidths = [360, 390, 480];
|
||||
const measurements: Record<string, unknown> = {};
|
||||
|
||||
for (const deviceWidth of deviceWidths) {
|
||||
const context = await browser.newContext({
|
||||
baseURL: configuredBaseUrl,
|
||||
viewport: { width: deviceWidth, height: 844 },
|
||||
screen: { width: deviceWidth, height: 844 },
|
||||
deviceScaleFactor: 1,
|
||||
isMobile: true,
|
||||
hasTouch: true,
|
||||
colorScheme: 'dark',
|
||||
});
|
||||
const mobilePage = await context.newPage();
|
||||
const state: NavigationFixture = {
|
||||
officerLevel: 5,
|
||||
permission: 2,
|
||||
nationLevel: 3,
|
||||
stage: 6,
|
||||
npcMode: 1,
|
||||
generalMeCalls: 0,
|
||||
operations: [],
|
||||
};
|
||||
await installFixture(mobilePage, state);
|
||||
await waitForMain(mobilePage);
|
||||
|
||||
const mainGeometry = await mobilePage.locator('.main-page').evaluate((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return {
|
||||
viewportMeta: document.querySelector<HTMLMetaElement>('meta[name="viewport"]')?.content,
|
||||
screenWidth: screen.availWidth,
|
||||
innerWidth: window.innerWidth,
|
||||
layoutViewportWidth: document.documentElement.clientWidth,
|
||||
visualViewportWidth: window.visualViewport?.width ?? null,
|
||||
visualViewportScale: window.visualViewport?.scale ?? null,
|
||||
documentScrollWidth: document.documentElement.scrollWidth,
|
||||
canvas: {
|
||||
left: rect.left,
|
||||
right: rect.right,
|
||||
width: rect.width,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
expect(mainGeometry.viewportMeta).toBe('width=500');
|
||||
expect(mainGeometry.screenWidth).toBe(deviceWidth);
|
||||
expect(mainGeometry.layoutViewportWidth).toBe(500);
|
||||
expect(mainGeometry.visualViewportWidth).toBeCloseTo(500, 2);
|
||||
expect(mainGeometry.visualViewportScale).toBeCloseTo(deviceWidth / 500, 2);
|
||||
expect(mainGeometry.documentScrollWidth).toBeLessThanOrEqual(mainGeometry.innerWidth);
|
||||
expect(mainGeometry.canvas).toEqual({ left: 0, right: 500, width: 500 });
|
||||
expect(mainGeometry.canvas.right).toBeLessThanOrEqual((mainGeometry.visualViewportWidth ?? 0) + 0.01);
|
||||
if (artifactRoot) {
|
||||
await mkdir(artifactRoot, { recursive: true });
|
||||
await mobilePage.screenshot({
|
||||
path: resolve(artifactRoot, `initial-mobile-fit-${deviceWidth}.png`),
|
||||
fullPage: true,
|
||||
});
|
||||
}
|
||||
|
||||
const routeGeometry: Record<string, unknown> = {};
|
||||
if (deviceWidth === 390) {
|
||||
for (const target of [
|
||||
'chief-center',
|
||||
'battle-center',
|
||||
'inherit',
|
||||
'nation-betting',
|
||||
]) {
|
||||
await mobilePage.goto(target);
|
||||
await expect
|
||||
.poll(() => mobilePage.locator('#app').evaluate((element) => getComputedStyle(element).minWidth))
|
||||
.toBe('500px');
|
||||
routeGeometry[target] = await mobilePage.locator('#app').evaluate((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return {
|
||||
viewportMeta: document.querySelector<HTMLMetaElement>('meta[name="viewport"]')?.content,
|
||||
layoutViewportWidth: document.documentElement.clientWidth,
|
||||
visualViewportWidth: window.visualViewport?.width ?? null,
|
||||
left: rect.left,
|
||||
right: rect.right,
|
||||
width: rect.width,
|
||||
};
|
||||
});
|
||||
const geometry = routeGeometry[target] as {
|
||||
viewportMeta: string;
|
||||
layoutViewportWidth: number;
|
||||
visualViewportWidth: number;
|
||||
left: number;
|
||||
right: number;
|
||||
width: number;
|
||||
};
|
||||
expect(geometry.viewportMeta).toBe('width=500');
|
||||
expect(geometry.layoutViewportWidth).toBe(500);
|
||||
expect(geometry.visualViewportWidth).toBeCloseTo(500, 2);
|
||||
expect(geometry.left).toBeCloseTo(0, 2);
|
||||
expect(geometry.right).toBeCloseTo(500, 2);
|
||||
expect(geometry.width).toBeCloseTo(500, 2);
|
||||
}
|
||||
}
|
||||
|
||||
measurements[String(deviceWidth)] = { main: mainGeometry, routes: routeGeometry };
|
||||
await context.close();
|
||||
}
|
||||
|
||||
if (artifactRoot) {
|
||||
await writeFile(
|
||||
resolve(artifactRoot, 'initial-mobile-fit-computed-dom.json'),
|
||||
`${JSON.stringify(measurements, null, 2)}\n`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('nation menu presentation follows the server-derived permission matrix', async ({ page }) => {
|
||||
const state: NavigationFixture = {
|
||||
officerLevel: 1,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="viewport" content="width=500" />
|
||||
<title>Sammo HiDCHe - Game</title>
|
||||
</head>
|
||||
<body class="bg-black text-white">
|
||||
|
||||
@@ -1,24 +1,6 @@
|
||||
const KOREA_TIME_OFFSET_MS = 9 * 60 * 60 * 1000;
|
||||
import { formatServerDateTime } from '@sammo-ts/common';
|
||||
|
||||
const pad = (value: number): string => String(value).padStart(2, '0');
|
||||
export const formatSeoulDateTime = (value: string | Date): string => formatServerDateTime(value);
|
||||
|
||||
export const formatSeoulDateTime = (value: string | Date): string => {
|
||||
if (
|
||||
typeof value === 'string' &&
|
||||
!/(?:Z|[+-]\d{2}:?\d{2})$/i.test(value.trim())
|
||||
) {
|
||||
return value.trim().replace('T', ' ').slice(0, 19);
|
||||
}
|
||||
const date = value instanceof Date ? value : new Date(value);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return typeof value === 'string' ? value.slice(0, 19) : '';
|
||||
}
|
||||
const koreaTime = new Date(date.getTime() + KOREA_TIME_OFFSET_MS);
|
||||
return `${koreaTime.getUTCFullYear()}-${pad(koreaTime.getUTCMonth() + 1)}-${pad(
|
||||
koreaTime.getUTCDate()
|
||||
)} ${pad(koreaTime.getUTCHours())}:${pad(koreaTime.getUTCMinutes())}:${pad(
|
||||
koreaTime.getUTCSeconds()
|
||||
)}`;
|
||||
};
|
||||
|
||||
export const formatSeoulHourMinute = (value: string | Date): string => formatSeoulDateTime(value).slice(11, 16);
|
||||
export const formatSeoulHourMinute = (value: string | Date): string =>
|
||||
formatServerDateTime(value, { format: 'hourMinute' });
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { formatServerDateTime } from '@sammo-ts/common';
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
@@ -41,24 +42,10 @@ const formatNumber = (value: number | null | undefined): string => (value ?? 0).
|
||||
const displayCode = (value: string | null | undefined): string =>
|
||||
!value || /^\d+$/u.test(value) ? '-' : value.replace(/^che_(?:event_)?/u, '');
|
||||
const cutDateTime = (value: string | null | undefined, showSecond = false): string => {
|
||||
if (!value) {
|
||||
return '-';
|
||||
}
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return value.slice(5, showSecond ? 19 : 16);
|
||||
}
|
||||
const parts = new Intl.DateTimeFormat('ko-KR', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
...(showSecond ? { second: '2-digit' } : {}),
|
||||
hour12: false,
|
||||
}).formatToParts(date);
|
||||
const part = (type: Intl.DateTimeFormatPartTypes): string =>
|
||||
parts.find((entry) => entry.type === type)?.value ?? '';
|
||||
return `${part('month')}-${part('day')} ${part('hour')}:${part('minute')}${showSecond ? `:${part('second')}` : ''}`;
|
||||
return formatServerDateTime(value, {
|
||||
format: showSecond ? 'monthDayTimeSeconds' : 'monthDayTime',
|
||||
fallback: '-',
|
||||
});
|
||||
};
|
||||
|
||||
const buyRice = computed(() =>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { formatServerDateTime } from '@sammo-ts/common';
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import PanelCard from '../components/ui/PanelCard.vue';
|
||||
@@ -126,9 +127,9 @@ const selectedGeneral = computed(() => {
|
||||
|
||||
const formatGeneralLabel = (general: GeneralEntry): string => {
|
||||
const name = general.officerLevel > 4 ? `*${general.name}*` : general.name;
|
||||
const time = general.turnTime ? general.turnTime.slice(-5) : '--:--';
|
||||
const time = formatServerDateTime(general.turnTime, { format: 'hourMinute', fallback: '--:--' });
|
||||
if (orderBy.value === 'recentWar') {
|
||||
return `${name} (${general.recentWar ? general.recentWar.slice(-5) : '--:--'})`;
|
||||
return `${name} (${formatServerDateTime(general.recentWar, { format: 'hourMinute', fallback: '--:--' })})`;
|
||||
}
|
||||
if (orderBy.value === 'warnum') {
|
||||
return `${name} (${general.warnum}회)`;
|
||||
@@ -156,7 +157,10 @@ const loadLogs = async (generalId: number) => {
|
||||
}
|
||||
for (const response of responses) {
|
||||
const formatted = response.logs.map((entry) => {
|
||||
const eventTime = response.type === 'generalAction' ? ` ${entry.createdAt.slice(-8, -3)}` : '';
|
||||
const eventTime =
|
||||
response.type === 'generalAction'
|
||||
? ` ${formatServerDateTime(entry.createdAt, { format: 'hourMinute' })}`
|
||||
: '';
|
||||
return {
|
||||
id: entry.id,
|
||||
html: formatLog(`${entry.text}${eventTime}`),
|
||||
@@ -308,7 +312,12 @@ onMounted(() => {
|
||||
<LegacyGeneralProgress :general="selectedGeneral" />
|
||||
</div>
|
||||
<div v-if="selectedGeneral" class="general-meta">
|
||||
<div>최근 턴: {{ selectedGeneral.turnTime ? selectedGeneral.turnTime.slice(-5) : '-' }}</div>
|
||||
<div>
|
||||
최근 턴:
|
||||
{{
|
||||
formatServerDateTime(selectedGeneral.turnTime, { format: 'hourMinute', fallback: '-' })
|
||||
}}
|
||||
</div>
|
||||
<div>최근 전투: {{ selectedGeneral.recentWar || '-' }}</div>
|
||||
<div>전투 횟수: {{ selectedGeneral.warnum }}</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { formatServerDateTime } from '@sammo-ts/common';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import TournamentBracket from '../components/tournament/TournamentBracket.vue';
|
||||
import { trpc } from '../utils/trpc';
|
||||
@@ -68,7 +69,9 @@ const ratio = (id: number) => {
|
||||
const amount = totals?.[id] ?? 0;
|
||||
return amount ? (totalAmount.value / amount).toFixed(2) : '0';
|
||||
};
|
||||
const openingTime = computed(() => snapshot.value?.state?.nextAt?.slice(11, 16) ?? '--:--');
|
||||
const openingTime = computed(() =>
|
||||
formatServerDateTime(snapshot.value?.state?.nextAt, { format: 'hourMinute', fallback: '--:--' })
|
||||
);
|
||||
const expected = (id: number) => {
|
||||
const myTotals = summary.value?.myTotals as Record<number, number> | undefined;
|
||||
const current = myTotals?.[id] ?? 0;
|
||||
@@ -248,8 +251,7 @@ const placeBet = async (targetId: number) => {
|
||||
<button class="close-button" type="button" @click="navigate">창 닫기</button>
|
||||
</RouterLink>
|
||||
<small>
|
||||
삼국지 모의전투 HiDCHe / KOEI의 이미지를 사용, 응용하였습니다 / 제작 :
|
||||
HideD(hided62@gmail.com) / Credit
|
||||
삼국지 모의전투 HiDCHe / KOEI의 이미지를 사용, 응용하였습니다 / 제작 : HideD(hided62@gmail.com) / Credit
|
||||
</small>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { formatServerDateTime } from '@sammo-ts/common';
|
||||
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
@@ -40,7 +41,7 @@ const resizeTextArea = (element: HTMLTextAreaElement | null) => {
|
||||
element.style.height = `${Math.max(element.scrollHeight, 42)}px`;
|
||||
};
|
||||
|
||||
const formatDate = (value: string): string => value.slice(5, 16).replace('T', ' ');
|
||||
const formatDate = (value: string): string => formatServerDateTime(value, { format: 'monthDayTime' });
|
||||
|
||||
const iconPath = (article: BoardArticle): string =>
|
||||
resolveGeneralIconUrl({
|
||||
@@ -160,7 +161,14 @@ onMounted(() => {
|
||||
</div>
|
||||
<div class="article-submit-row">
|
||||
<div></div>
|
||||
<button id="submitArticle" class="legacy-button legacy-button--secondary" type="button" @click="submitArticle">등록</button>
|
||||
<button
|
||||
id="submitArticle"
|
||||
class="legacy-button legacy-button--secondary"
|
||||
type="button"
|
||||
@click="submitArticle"
|
||||
>
|
||||
등록
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -244,7 +252,9 @@ onMounted(() => {
|
||||
padding: 8px;
|
||||
color: #000;
|
||||
background: #fff;
|
||||
font: 16px/normal 'Times New Roman', serif;
|
||||
font:
|
||||
16px/normal 'Times New Roman',
|
||||
serif;
|
||||
}
|
||||
|
||||
.legacy-board-page {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { formatServerDateTime } from '@sammo-ts/common';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
@@ -47,12 +48,7 @@ const loadDetail = async (): Promise<void> => {
|
||||
}
|
||||
};
|
||||
|
||||
const formatArchiveDate = (value: string): string =>
|
||||
new Intl.DateTimeFormat('sv-SE', {
|
||||
dateStyle: 'short',
|
||||
timeStyle: 'medium',
|
||||
timeZone: 'UTC',
|
||||
}).format(new Date(value));
|
||||
const formatArchiveDate = (value: string): string => formatServerDateTime(value);
|
||||
|
||||
watch(emperorId, loadDetail);
|
||||
onMounted(loadDetail);
|
||||
@@ -67,7 +63,9 @@ onMounted(loadDetail);
|
||||
역 대 왕 조<br />
|
||||
<button class="native-button" type="button" @click="closePage">창 닫기</button>
|
||||
<span class="all-link">
|
||||
<RouterLink to="/dynasty"><button class="native-button" type="button">전체보기</button></RouterLink>
|
||||
<RouterLink to="/dynasty"
|
||||
><button class="native-button" type="button">전체보기</button></RouterLink
|
||||
>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -202,7 +200,11 @@ onMounted(loadDetail);
|
||||
<td colspan="5">
|
||||
<!-- 레거시 색상 tag를 동일한 span 구조로 변환한다. -->
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-for="(entry, index) in data.emperor.history" :key="index" v-html="formatLog(entry)" />
|
||||
<div
|
||||
v-for="(entry, index) in data.emperor.history"
|
||||
:key="index"
|
||||
v-html="formatLog(entry)"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -283,14 +285,10 @@ onMounted(loadDetail);
|
||||
<table class="legacy-table legacy-bg0 footer-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<button class="native-button" type="button" @click="closePage">창 닫기</button><br />
|
||||
</td>
|
||||
<td><button class="native-button" type="button" @click="closePage">창 닫기</button><br /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="banner">
|
||||
삼국지 모의전투 HiDCHe / KOEI의 이미지를 사용, 응용하였습니다 / 제작 : HideD
|
||||
</td>
|
||||
<td class="banner">삼국지 모의전투 HiDCHe / KOEI의 이미지를 사용, 응용하였습니다 / 제작 : HideD</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { formatServerDateTime } from '@sammo-ts/common';
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { trpc } from '../utils/trpc';
|
||||
|
||||
@@ -169,11 +170,7 @@ const turnTimeLabel = computed(() => {
|
||||
if (!turnTimeResult.value) {
|
||||
return null;
|
||||
}
|
||||
const parsed = new Date(turnTimeResult.value);
|
||||
if (Number.isNaN(parsed.getTime())) {
|
||||
return turnTimeResult.value;
|
||||
}
|
||||
return parsed.toLocaleString();
|
||||
return formatServerDateTime(turnTimeResult.value);
|
||||
});
|
||||
|
||||
const isUnited = computed(() => status.value?.isUnited ?? false);
|
||||
@@ -735,7 +732,7 @@ onMounted(() => {
|
||||
<div v-if="logLoading && logs.length === 0" class="log-empty">불러오는 중...</div>
|
||||
<div v-else-if="logs.length === 0" class="log-empty">기록이 없습니다.</div>
|
||||
<div v-for="entry in logs" v-else :key="entry.id" class="log-row">
|
||||
<small>[{{ new Date(entry.createdAt).toLocaleString('ko-KR') }}]</small>
|
||||
<small>[{{ formatServerDateTime(entry.createdAt) }}]</small>
|
||||
<span>{{ entry.text }}</span>
|
||||
</div>
|
||||
<button
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { formatServerDateTime } from '@sammo-ts/common';
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useMediaQuery } from '@vueuse/core';
|
||||
@@ -68,14 +69,8 @@ const nationColor = computed(() => nation.value?.color ?? '#000000');
|
||||
const voteActive = computed(() => Boolean(frontStatus.value?.latestVote));
|
||||
const formatRecord = (entry: { text: string; createdAt?: string | Date }, appendTime = false): string => {
|
||||
if (!appendTime || /\d{2}:\d{2}\s*$/u.test(entry.text)) return formatLog(entry.text);
|
||||
const parsed = entry.createdAt ? new Date(entry.createdAt) : null;
|
||||
if (!parsed || Number.isNaN(parsed.getTime())) return formatLog(entry.text);
|
||||
const time = new Intl.DateTimeFormat('ko-KR', {
|
||||
timeZone: 'Asia/Seoul',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
}).format(parsed);
|
||||
const time = formatServerDateTime(entry.createdAt, { format: 'hourMinute', fallback: '' });
|
||||
if (!time) return formatLog(entry.text);
|
||||
return formatLog(`${entry.text} ${time}`);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { trpc } from '../utils/trpc';
|
||||
import { formatLog } from '../utils/formatLog';
|
||||
import { formatSeoulDateTime } from '../utils/legacyDateTime';
|
||||
import { formatSeoulDateTime, formatSeoulHourMinute } from '../utils/legacyDateTime';
|
||||
import { isDefenceTrainPenaltyWaivedByScenarioEffect } from '@sammo-ts/logic';
|
||||
import { useSessionStore } from '../stores/session';
|
||||
import { resolveGeneralIconUrl, useDefaultGeneralIcon } from '../utils/generalIcon';
|
||||
@@ -430,7 +430,10 @@ onMounted(() => {
|
||||
</div>
|
||||
<div>
|
||||
<dt>나이/다음턴</dt>
|
||||
<dd>{{ data.general.age ?? '-' }}세 / {{ data.general.turnTime?.slice(11, 16) ?? '-' }}</dd>
|
||||
<dd>
|
||||
{{ data.general.age ?? '-' }}세 /
|
||||
{{ data.general.turnTime ? formatSeoulHourMinute(data.general.turnTime) : '-' }}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { formatServerDateTime } from '@sammo-ts/common';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { trpc } from '../utils/trpc';
|
||||
type Result = Awaited<ReturnType<typeof trpc.nation.getSecretGeneralList.query>>;
|
||||
@@ -147,7 +148,7 @@ onMounted(load);
|
||||
>
|
||||
</td>
|
||||
<td>{{ general.killTurn }}</td>
|
||||
<td>{{ general.turnTime.slice(14, 19) }}</td>
|
||||
<td>{{ formatServerDateTime(general.turnTime, { format: 'minuteSecond' }) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -159,8 +160,8 @@ onMounted(load);
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="legacy-banner">
|
||||
삼국지 모의전투 HiDCHe / KOEI의 이미지를 사용, 응용하였습니다 / 제작 :
|
||||
HideD(hided62@gmail.com) /
|
||||
삼국지 모의전투 HiDCHe / KOEI의 이미지를 사용, 응용하였습니다 / 제작 : HideD(hided62@gmail.com)
|
||||
/
|
||||
<a href="https://github.com/hided/SamK" target="_blank" rel="noopener noreferrer">Credit</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { formatServerDateTime } from '@sammo-ts/common';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
@@ -67,16 +68,9 @@ const newVoteOptions = computed(() => newVoteOptionsText.value.split('\n').filte
|
||||
|
||||
const percentage = (count: number, total: number): string => ((count / Math.max(1, total)) * 100).toFixed(1);
|
||||
|
||||
const formatStartDate = (value: string): string => value.slice(0, 10);
|
||||
const formatStartDate = (value: string): string => formatServerDateTime(value, { format: 'date' });
|
||||
|
||||
const formatCommentDate = (value: string): string => {
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return value;
|
||||
}
|
||||
const pad = (part: number) => String(part).padStart(2, '0');
|
||||
return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||
};
|
||||
const formatCommentDate = (value: string): string => formatServerDateTime(value, { format: 'monthDayTime' });
|
||||
|
||||
const voteColor = (index: number): string =>
|
||||
['#ff0000', '#ffa500', '#ffff00', '#008000', '#0000ff', '#000080', '#800080'][index % 7]!;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { formatServerDateTime } from '@sammo-ts/common';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import TournamentBracket from '../components/tournament/TournamentBracket.vue';
|
||||
import { trpc } from '../utils/trpc';
|
||||
@@ -62,7 +63,9 @@ const matchesAt = (stage: number) =>
|
||||
.sort((a, b) => a.roundIndex - b.roundIndex);
|
||||
const nameOf = (id?: number) => (id ? (participantsById.value.get(id)?.name ?? `#${id}`) : '-');
|
||||
const totalBet = computed(() => betting.value?.totalAmount ?? 0);
|
||||
const openingTime = computed(() => snapshot.value?.state?.nextAt?.slice(11, 16) ?? '--:--');
|
||||
const openingTime = computed(() =>
|
||||
formatServerDateTime(snapshot.value?.state?.nextAt, { format: 'hourMinute', fallback: '--:--' })
|
||||
);
|
||||
const betTotals = computed(() => betting.value?.totals as Record<number, number> | undefined);
|
||||
const isParticipant = computed(() =>
|
||||
(snapshot.value?.participants ?? []).some((participant) => participant.id === myGeneralId.value)
|
||||
@@ -287,8 +290,7 @@ const start = async () => {
|
||||
<button class="close-button" type="button" @click="navigate">창 닫기</button>
|
||||
</RouterLink>
|
||||
<small>
|
||||
삼국지 모의전투 HiDCHe / KOEI의 이미지를 사용, 응용하였습니다 / 제작 :
|
||||
HideD(hided62@gmail.com) / Credit
|
||||
삼국지 모의전투 HiDCHe / KOEI의 이미지를 사용, 응용하였습니다 / 제작 : HideD(hided62@gmail.com) / Credit
|
||||
</small>
|
||||
</footer>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { formatServerDateTime } from '@sammo-ts/common';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
@@ -50,10 +51,7 @@ const onlineRows = computed(() =>
|
||||
}))
|
||||
);
|
||||
|
||||
const timeLabel = (value: string): string => {
|
||||
const timePart = value.includes('T') ? value.split('T')[1] : value.slice(11);
|
||||
return (timePart ?? '').slice(0, 5);
|
||||
};
|
||||
const timeLabel = (value: string): string => formatServerDateTime(value, { format: 'hourMinute' });
|
||||
|
||||
const trafficColor = (percentage: number): string => {
|
||||
const channel = (value: number): string =>
|
||||
@@ -204,8 +202,8 @@ onMounted(() => {
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="banner">
|
||||
삼국지 모의전투 HiDCHe / KOEI의 이미지를 사용, 응용하였습니다 / 제작 :
|
||||
HideD(hided62@gmail.com) /
|
||||
삼국지 모의전투 HiDCHe / KOEI의 이미지를 사용, 응용하였습니다 / 제작 : HideD(hided62@gmail.com)
|
||||
/
|
||||
<a href="https://sam.hided.net/wiki/hidche/credit" target="_blank" rel="noreferrer">Credit</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { formatServerDateTime } from '@sammo-ts/common';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
@@ -166,10 +167,7 @@ const hideMemberPopup = () => {
|
||||
const iconPath = (troop: Troop): string => resolveGeneralIconUrl(troop.leader ?? {});
|
||||
|
||||
const formatTurn = (turnTime: string | null): string => {
|
||||
if (!turnTime) {
|
||||
return '--:--';
|
||||
}
|
||||
return turnTime.slice(14, 19);
|
||||
return formatServerDateTime(turnTime, { format: 'minuteSecond', fallback: '--:--' });
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
Reference in New Issue
Block a user