Merge branch 'main' into feature/traffic-period-projection

This commit is contained in:
2026-07-27 11:32:59 +00:00
38 changed files with 466 additions and 366 deletions
+16 -2
View File
@@ -220,7 +220,21 @@ const installApi = async (page: Page, state: BoardFixture) => {
} }
if (operation === 'turns.getCommandTable') return response({ general: [], nation: [] }); if (operation === 'turns.getCommandTable') return response({ general: [], nation: [] });
if (operation === 'messages.getRecent') return response(emptyMessages); if (operation === 'messages.getRecent') return response(emptyMessages);
if (operation === 'turns.reserved.getGeneral') return response([]); if (operation === 'messages.getContacts') return response({ nation: [] });
if (operation === 'turns.reserved.getGeneral') return response({ turns: [], revision: 0 });
if (operation === 'turns.reserved.getNation') return response({ turns: [], revision: 0 });
if (operation === 'general.getRecentRecords')
return response({ global: [], general: [], history: [] });
if (operation === 'general.getFrontStatus')
return response({
onlineUserCount: 1,
onlineNations: '아국(1)',
onlineGenerals: '유비',
nationNotice: '',
lastExecuted: null,
latestVote: null,
});
if (operation === 'tournament.getState') return response({ stage: 0 });
return errorResponse(operation, `Unhandled board fixture operation: ${operation}`); return errorResponse(operation, `Unhandled board fixture operation: ${operation}`);
}); });
await route.fulfill({ await route.fulfill({
@@ -426,7 +440,7 @@ test('denies direct secret-room rendering and disables its in-game menu for an o
state.requests.length = 0; state.requests.length = 0;
await page.goto(''); await page.goto('');
await expect(page.getByRole('link', { name: '회의실', exact: true })).toBeVisible(); await expect(page.getByRole('link', { name: '회의실', exact: true })).toBeVisible();
await expect(page.locator('.header-actions [aria-disabled="true"]').filter({ hasText: '기밀실' })).toBeVisible(); await expect(page.locator('.game-shell__actions [aria-disabled="true"]').filter({ hasText: '기밀실' })).toBeVisible();
await expect(page.getByRole('link', { name: '기밀실', exact: true })).toHaveCount(0); await expect(page.getByRole('link', { name: '기밀실', exact: true })).toHaveCount(0);
}); });
+95 -3
View File
@@ -85,6 +85,22 @@ const generalContext = {
}; };
const turns = (count: number) => const turns = (count: number) =>
Array.from({ length: count }, (_, index) => ({ index, action: '휴식', args: {} })); Array.from({ length: count }, (_, index) => ({ index, action: '휴식', args: {} }));
const chiefCenter = {
me: { id: 1, officerLevel: 5, nationId: 1 },
nation: { id: 1, name: '아국', level: 1 },
currentYear: 200,
currentMonth: 1,
turnTermMinutes: 10,
maxTurns: 12,
chiefs: [12, 10, 8, 6, 11, 9, 7, 5].map((officerLevel) => ({
officerLevel,
name: officerLevel === 5 ? '장수' : null,
npcState: officerLevel === 5 ? 0 : null,
turnTime: null,
revision: 0,
turns: turns(12),
})),
};
const install = async (page: Page, rejectGeneral = false) => { const install = async (page: Page, rejectGeneral = false) => {
const requests: unknown[] = []; const requests: unknown[] = [];
@@ -115,8 +131,20 @@ const install = async (page: Page, rejectGeneral = false) => {
spyList: {}, shownByGeneralList: [], myCity: 1, myNation: 1, spyList: {}, shownByGeneralList: [], myCity: 1, myNation: 1,
}); });
if (name === 'turns.getCommandTable') return response(commandTable); if (name === 'turns.getCommandTable') return response(commandTable);
if (name === 'turns.reserved.getGeneral') return response(turns(30)); if (name === 'nation.getChiefCenter') return response(chiefCenter);
if (name === 'turns.reserved.getNation') return response(turns(12)); if (name === 'turns.reserved.getGeneral') return response({ turns: turns(30), revision: 0 });
if (name === 'turns.reserved.getNation') return response({ turns: turns(12), revision: 0 });
if (name === 'general.getRecentRecords')
return response({ global: [], general: [], history: [] });
if (name === 'general.getFrontStatus')
return response({
onlineUserCount: 1,
onlineNations: '아국(1)',
onlineGenerals: '장수',
nationNotice: '',
lastExecuted: null,
latestVote: null,
});
if (name === 'messages.getRecent') return response({ if (name === 'messages.getRecent') return response({
private: [], national: [], public: [], diplomacy: [], sequence: -1, private: [], national: [], public: [], diplomacy: [], sequence: -1,
hasMore: { private: false, national: false, public: false, diplomacy: false }, hasMore: { private: false, national: false, public: false, diplomacy: false },
@@ -197,6 +225,70 @@ test('keeps the entered command visible and reports a server validation error',
await page.locator('.reserved-section').filter({ hasText: '일반 예턴' }) await page.locator('.reserved-section').filter({ hasText: '일반 예턴' })
.getByRole('button', { name: '배치' }).first().click(); .getByRole('button', { name: '배치' }).first().click();
await expect(page.locator('.error')).toContainText('대상 도시를 선택할 수 없습니다.'); await expect(page.getByRole('alert')).toContainText('대상 도시를 선택할 수 없습니다.');
await expect(page.getByTestId('command-argument-form').locator('select')).toHaveValue('2'); await expect(page.getByTestId('command-argument-form').locator('select')).toHaveValue('2');
}); });
test('keeps the shared main and chief shell geometry and interaction states', async ({ page }) => {
await install(page);
await page.setViewportSize({ width: 1000, height: 900 });
await page.goto('/');
await expect(page.getByRole('heading', { name: '전장 현황' })).toBeVisible();
const mainGeometry = await page.locator('.main-page').evaluate((element) => {
const header = element.querySelector<HTMLElement>('.game-shell__header')!;
const title = element.querySelector<HTMLElement>('.game-shell__title')!;
const subtitle = element.querySelector<HTMLElement>('.game-shell__subtitle')!;
const action = element.querySelector<HTMLElement>('.game-shell__action')!;
return {
width: element.getBoundingClientRect().width,
padding: getComputedStyle(element).padding,
gap: getComputedStyle(element).gap,
headerWidth: header.getBoundingClientRect().width,
headerGap: getComputedStyle(header).gap,
headerBorder: getComputedStyle(header).borderBottomWidth,
headerPadding: getComputedStyle(header).paddingBottom,
titleFontSize: getComputedStyle(title).fontSize,
subtitleFontSize: getComputedStyle(subtitle).fontSize,
actionPadding: getComputedStyle(action).padding,
actionFontSize: getComputedStyle(action).fontSize,
};
});
expect(mainGeometry).toEqual({
width: 1000,
padding: '24px',
gap: '16px',
headerWidth: 952,
headerGap: '12px',
headerBorder: '1px',
headerPadding: '12px',
titleFontSize: '22.4px',
subtitleFontSize: '11.9px',
actionPadding: '6px 12px',
actionFontSize: '11.2px',
});
const mainAction = page.getByRole('link', { name: '세력 정보' });
await mainAction.hover();
expect(await mainAction.evaluate((element) => getComputedStyle(element).cursor)).toBe('pointer');
await mainAction.focus();
expect(await mainAction.evaluate((element) => document.activeElement === element)).toBe(true);
await page.setViewportSize({ width: 1200, height: 900 });
await page.goto('chief-center');
await expect(page.getByRole('heading', { name: '사령부', exact: true })).toBeVisible();
const chiefDesktop = await page.locator('.chief-page').evaluate((element) => ({
width: element.getBoundingClientRect().width,
padding: getComputedStyle(element).padding,
headerWidth: element.querySelector<HTMLElement>('.game-shell__header')!.getBoundingClientRect().width,
}));
expect(chiefDesktop).toEqual({ width: 1200, padding: '24px', headerWidth: 1152 });
await page.setViewportSize({ width: 500, height: 900 });
const chiefMobile = await page.locator('.chief-page').evaluate((element) => ({
width: element.getBoundingClientRect().width,
padding: getComputedStyle(element).padding,
headerWidth: element.querySelector<HTMLElement>('.game-shell__header')!.getBoundingClientRect().width,
}));
expect(chiefMobile).toEqual({ width: 500, padding: '16px', headerWidth: 468 });
});
+1 -1
View File
@@ -392,5 +392,5 @@ test('감찰부 keeps the selector interaction and shows the permission error pa
const member: FixtureState = { permission: 'member', myset: 3, settingMutations: [], accessPages: [] }; const member: FixtureState = { permission: 'member', myset: 3, settingMutations: [], accessPages: [] };
await install(page, member); await install(page, member);
await page.reload(); await page.reload();
await expect(page.locator('.error')).toContainText('권한이 부족합니다.'); await expect(page.getByRole('alert')).toContainText('권한이 부족합니다.');
}); });
+7 -4
View File
@@ -1,4 +1,7 @@
@import 'tailwindcss'; @import 'tailwindcss';
@import './styles/tokens.css';
@import './styles/game-shell.css';
@import './styles/ref-shell.css';
@theme { @theme {
--color-sammo-ink: #000; --color-sammo-ink: #000;
@@ -15,7 +18,7 @@ body {
margin: 0; margin: 0;
background: #000; background: #000;
color: #fff; color: #fff;
font-family: Pretendard, 'Apple SD Gothic Neo', 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-family: var(--sammo-font-sans);
font-size: 14px; font-size: 14px;
line-height: 1.3; line-height: 1.3;
} }
@@ -29,17 +32,17 @@ textarea {
.legacy-bg0 { .legacy-bg0 {
background-color: #302016; background-color: #302016;
background-image: url('/image/game/back_walnut.jpg'); background-image: var(--sammo-texture-walnut);
} }
.legacy-bg1 { .legacy-bg1 {
background-color: #14241b; background-color: #14241b;
background-image: url('/image/game/back_green.jpg'); background-image: var(--sammo-texture-green);
} }
.legacy-bg2 { .legacy-bg2 {
background-color: #172a52; background-color: #172a52;
background-image: url('/image/game/back_blue.jpg'); background-image: var(--sammo-texture-blue);
} }
.legacy-button { .legacy-button {
@@ -0,0 +1,54 @@
/*
* Shared shell for the main dashboard family.
*
* Keep geometry here limited to declarations that are identical on the main,
* chief-center, and public screens. Page-specific grids remain scoped in their
* owning SFC so a generic class cannot silently change a ref measurement.
*/
.game-shell {
min-height: 100vh;
padding: 24px;
display: flex;
flex-direction: column;
gap: 16px;
}
.game-shell__header {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 12px;
border-bottom: 1px solid var(--sammo-color-border);
padding-bottom: 12px;
}
.game-shell__title {
font-size: 1.6rem;
font-weight: 600;
}
.game-shell__subtitle {
font-size: 0.85rem;
color: var(--sammo-color-text-muted);
}
.game-shell__actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.game-shell__action {
border: 1px solid var(--sammo-color-border);
padding: 6px 12px;
background: var(--sammo-color-action-bg);
color: inherit;
text-decoration: none;
font-size: 0.8rem;
cursor: pointer;
}
.game-feedback--error {
color: var(--sammo-color-error);
font-size: 0.85rem;
}
@@ -0,0 +1,77 @@
/*
* Fixed ref geometry. This namespace is intentionally separate from
* .game-shell: its 1000/500px contract must win over generic responsive styles.
*/
.ref-shell {
width: 100%;
min-width: 500px;
max-width: 1000px;
min-height: 100vh;
margin: 0 auto;
padding: 0;
display: flex;
flex-direction: column;
gap: 0;
background-color: #302016;
background-image: var(--sammo-texture-walnut);
color: var(--sammo-color-text);
font-family: var(--sammo-font-sans);
font-size: 14px;
line-height: 1.5;
}
.ref-shell__topbar {
position: relative;
min-height: 32px;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
flex-wrap: wrap;
padding: 0 8px;
border: 1px solid #666;
background-color: #302016;
background-image: var(--sammo-texture-walnut);
}
.ref-shell__title {
font-size: 17px;
font-weight: 500;
}
.ref-shell__subtitle {
display: none;
}
.ref-shell__actions {
position: absolute;
left: 0;
top: 0;
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.ref-shell__control {
min-height: 32px;
border: 1px solid #777;
border-radius: 0;
background: #303030;
color: inherit;
padding: 4px 8px;
font: inherit;
cursor: pointer;
}
.ref-feedback--error {
padding: 5px 8px;
border: 1px solid #a33;
color: #ff7777;
text-align: center;
}
@media (max-width: 991px) {
.ref-shell {
width: 500px;
}
}
@@ -0,0 +1,12 @@
:root {
--sammo-font-sans: Pretendard, 'Apple SD Gothic Neo', 'Noto Sans KR', 'Malgun Gothic', sans-serif;
--sammo-color-text: #fff;
--sammo-color-text-muted: rgba(232, 221, 196, 0.7);
--sammo-color-accent: #f39c12;
--sammo-color-border: rgba(201, 164, 90, 0.4);
--sammo-color-action-bg: rgba(16, 16, 16, 0.6);
--sammo-color-error: #f5b7b1;
--sammo-texture-walnut: url('/image/game/back_walnut.jpg');
--sammo-texture-green: url('/image/game/back_green.jpg');
--sammo-texture-blue: url('/image/game/back_blue.jpg');
}
@@ -39,7 +39,7 @@ defineProps<{
width: calc(100% + 48px); width: calc(100% + 48px);
margin-left: -24px; margin-left: -24px;
background-color: #302016; background-color: #302016;
background-image: url('/image/game/back_walnut.jpg'); background-image: var(--sammo-texture-walnut);
color: #fff; color: #fff;
font-size: 14px; font-size: 14px;
font-weight: 400; font-weight: 400;
@@ -266,7 +266,7 @@ const forwardResponse = (messageId: number, response: boolean) => {
grid-template-columns: minmax(0, 1fr) minmax(0, 4fr) minmax(0, 1fr); grid-template-columns: minmax(0, 1fr) minmax(0, 4fr) minmax(0, 1fr);
grid-template-areas: 'mailbox input submit'; grid-template-areas: 'mailbox input submit';
background-color: #302016; background-color: #302016;
background-image: url('/image/game/back_walnut.jpg'); background-image: var(--sammo-texture-walnut);
} }
#mailbox_list-col { #mailbox_list-col {
@@ -347,7 +347,7 @@ const forwardResponse = (messageId: number, response: boolean) => {
align-items: center; align-items: center;
outline: 1px solid gray; outline: 1px solid gray;
background-color: #302016; background-color: #302016;
background-image: url('/image/game/back_walnut.jpg'); background-image: var(--sammo-texture-walnut);
color: #fff; color: #fff;
} }
@@ -29,7 +29,7 @@ defineProps<{
border-top: 1px solid gray; border-top: 1px solid gray;
border-bottom: 1px solid gray; border-bottom: 1px solid gray;
background-color: #14241b; background-color: #14241b;
background-image: url('/image/game/back_green.jpg'); background-image: var(--sammo-texture-green);
color: #fff; color: #fff;
font: inherit; font: inherit;
text-align: center; text-align: center;
@@ -28,7 +28,7 @@ defineProps<Props>();
.panel-card { .panel-card {
border: 1px solid gray; border: 1px solid gray;
background-color: #302016; background-color: #302016;
background-image: url('/image/game/back_walnut.jpg'); background-image: var(--sammo-texture-walnut);
} }
.panel-header { .panel-header {
@@ -38,7 +38,7 @@ defineProps<Props>();
gap: 8px; gap: 8px;
border-bottom: 1px solid gray; border-bottom: 1px solid gray;
background-color: #14241b; background-color: #14241b;
background-image: url('/image/game/back_green.jpg'); background-image: var(--sammo-texture-green);
padding: 3px 6px; padding: 3px 6px;
} }
+4 -4
View File
@@ -478,21 +478,21 @@ onMounted(() => {
box-sizing: border-box; box-sizing: border-box;
margin: 0 auto; margin: 0 auto;
color: #fff; color: #fff;
font-family: Pretendard, 'Apple SD Gothic Neo', 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-family: var(--sammo-font-sans);
font-size: 14px; font-size: 14px;
line-height: 21px; line-height: 21px;
} }
.bg0 { .bg0 {
background-color: #302016; background-color: #302016;
background-image: url('/image/game/back_walnut.jpg'); background-image: var(--sammo-texture-walnut);
} }
.bg1 { .bg1 {
background-color: #14241b; background-color: #14241b;
background-image: url('/image/game/back_green.jpg'); background-image: var(--sammo-texture-green);
} }
.bg2 { .bg2 {
background-color: #172a52; background-color: #172a52;
background-image: url('/image/game/back_blue.jpg'); background-image: var(--sammo-texture-blue);
} }
.top-back-bar { .top-back-bar {
width: 100%; width: 100%;
@@ -228,26 +228,26 @@ onMounted(() => {
</script> </script>
<template> <template>
<main class="battle-page"> <main class="ref-shell battle-page">
<header class="page-header"> <header class="ref-shell__topbar">
<div> <div>
<h1 class="page-title">감찰부</h1> <h1 class="ref-shell__title">감찰부</h1>
<p class="page-subtitle">{{ statusLine }}</p> <p class="ref-shell__subtitle">{{ statusLine }}</p>
</div> </div>
<div class="header-actions"> <div class="ref-shell__actions">
<RouterLink class="ghost" to="/">메인</RouterLink> <RouterLink class="ref-shell__control" to="/">메인</RouterLink>
<RouterLink class="ghost" to="/nation/finance">내무부</RouterLink> <RouterLink class="ref-shell__control" to="/nation/finance">내무부</RouterLink>
<button class="ghost" @click="loadBattleCenter">새로고침</button> <button class="ref-shell__control" @click="loadBattleCenter">새로고침</button>
</div> </div>
</header> </header>
<div v-if="error" class="error">{{ error }}</div> <div v-if="error" class="ref-feedback ref-feedback--error" role="alert">{{ error }}</div>
<section class="layout-grid"> <section class="layout-grid">
<div class="stack"> <div class="stack">
<PanelCard title="대상 선택" subtitle="정렬 기준과 장수를 선택합니다."> <PanelCard title="대상 선택" subtitle="정렬 기준과 장수를 선택합니다.">
<div class="selector-row"> <div class="selector-row">
<button class="ghost" @click="changeTargetByOffset(-1)"> 이전</button> <button class="ref-shell__control" @click="changeTargetByOffset(-1)"> 이전</button>
<select v-model="orderBy" class="select-input"> <select v-model="orderBy" class="select-input">
<option v-for="option in orderOptions" :key="option.key" :value="option.key"> <option v-for="option in orderOptions" :key="option.key" :value="option.key">
{{ option.label }} {{ option.label }}
@@ -263,7 +263,7 @@ onMounted(() => {
{{ formatGeneralLabel(general) }} {{ formatGeneralLabel(general) }}
</option> </option>
</select> </select>
<button class="ghost" @click="changeTargetByOffset(1)">다음 </button> <button class="ref-shell__control" @click="changeTargetByOffset(1)">다음 </button>
</div> </div>
</PanelCard> </PanelCard>
@@ -315,56 +315,6 @@ onMounted(() => {
</template> </template>
<style scoped> <style scoped>
.battle-page {
width: 100%;
min-width: 500px;
max-width: 1000px;
min-height: 100vh;
margin: 0 auto;
padding: 0;
display: flex;
flex-direction: column;
gap: 0;
color: #fff;
background-color: #302016;
background-image: url('/image/game/back_walnut.jpg');
font-family: Pretendard, 'Apple SD Gothic Neo', 'Noto Sans KR', 'Malgun Gothic', sans-serif;
font-size: 14px;
line-height: 1.5;
}
.page-header {
position: relative;
min-height: 32px;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
flex-wrap: wrap;
padding: 0 8px;
border: 1px solid #666;
background-color: #302016;
background-image: url('/image/game/back_walnut.jpg');
}
.page-title {
font-size: 17px;
font-weight: 500;
}
.page-subtitle {
display: none;
}
.header-actions {
position: absolute;
left: 0;
top: 0;
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.layout-grid { .layout-grid {
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -393,17 +343,6 @@ onMounted(() => {
font: inherit; font: inherit;
} }
.ghost {
min-height: 32px;
border: 1px solid #777;
border-radius: 0;
background: #303030;
color: inherit;
padding: 4px 8px;
font: inherit;
cursor: pointer;
}
.general-meta { .general-meta {
margin: 0; margin: 0;
padding: 6px 8px; padding: 6px 8px;
@@ -415,7 +354,7 @@ onMounted(() => {
.battle-general-card { .battle-general-card {
min-height: 292px; min-height: 292px;
background-color: #172a52; background-color: #172a52;
background-image: url('/image/game/back_blue.jpg'); background-image: var(--sammo-texture-blue);
} }
.battle-general-name { .battle-general-name {
@@ -485,13 +424,6 @@ onMounted(() => {
color: #999; color: #999;
} }
.error {
padding: 5px 8px;
color: #ff7777;
border: 1px solid #a33;
text-align: center;
}
/* PanelCard is retained as a data wrapper, but its presentation follows the /* PanelCard is retained as a data wrapper, but its presentation follows the
flat bootstrap rows used by the reference page. */ flat bootstrap rows used by the reference page. */
:deep(.panel-card) { :deep(.panel-card) {
@@ -499,7 +431,7 @@ onMounted(() => {
border: 1px solid #666; border: 1px solid #666;
border-radius: 0; border-radius: 0;
background-color: #302016; background-color: #302016;
background-image: url('/image/game/back_walnut.jpg'); background-image: var(--sammo-texture-walnut);
box-shadow: none; box-shadow: none;
} }
.stack:first-child :deep(.panel-card:first-child) { .stack:first-child :deep(.panel-card:first-child) {
@@ -531,13 +463,10 @@ onMounted(() => {
} }
:deep(.panel-header), :deep(.panel-header),
.log-title { .log-title {
background-image: url('/image/game/back_green.jpg'); background-image: var(--sammo-texture-green);
} }
@media (max-width: 991px) { @media (max-width: 991px) {
.battle-page {
width: 500px;
}
.layout-grid { .layout-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@@ -187,7 +187,7 @@ watch(viewMode, () => {
min-height: 100vh; min-height: 100vh;
margin: 0 auto 100px; margin: 0 auto 100px;
color: #fff; color: #fff;
font-family: Pretendard, 'Apple SD Gothic Neo', 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-family: var(--sammo-font-sans);
font-size: 14px; font-size: 14px;
} }
+3 -3
View File
@@ -218,7 +218,7 @@ const placeBet = async (targetId: number) => {
min-height: 100vh; min-height: 100vh;
margin: 0 auto; margin: 0 auto;
color: #fff; color: #fff;
font-family: Pretendard, 'Apple SD Gothic Neo', 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-family: var(--sammo-font-sans);
font-size: 14px; font-size: 14px;
line-height: 1.3; line-height: 1.3;
text-align: center; text-align: center;
@@ -228,10 +228,10 @@ const placeBet = async (targetId: number) => {
box-sizing: border-box; box-sizing: border-box;
} }
.bg0 { .bg0 {
background: #3a2118 url('/image/game/back_walnut.jpg'); background: #3a2118 var(--sammo-texture-walnut);
} }
.bg2 { .bg2 {
background: #142b42 url('/image/game/back_blue.jpg'); background: #142b42 var(--sammo-texture-blue);
} }
.title { .title {
height: 55.6875px; height: 55.6875px;
+4 -4
View File
@@ -233,21 +233,21 @@ onMounted(() => {
margin: 0 auto; margin: 0 auto;
color: #fff; color: #fff;
background: #000; background: #000;
font-family: Pretendard, 'Apple SD Gothic Neo', 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-family: var(--sammo-font-sans);
font-size: 14px; font-size: 14px;
line-height: 1.3; line-height: 1.3;
} }
.bg0 { .bg0 {
background-image: url('/image/game/back_walnut.jpg'); background-image: var(--sammo-texture-walnut);
} }
.bg1 { .bg1 {
background-image: url('/image/game/back_green.jpg'); background-image: var(--sammo-texture-green);
} }
.bg2 { .bg2 {
background-image: url('/image/game/back_blue.jpg'); background-image: var(--sammo-texture-blue);
} }
.center { .center {
@@ -388,19 +388,19 @@ const shiftTurns = async (amount: number) => {
</script> </script>
<template> <template>
<main class="chief-page"> <main class="game-shell chief-page">
<header class="page-header"> <header class="game-shell__header">
<div> <div>
<h1 class="page-title">사령부</h1> <h1 class="game-shell__title">사령부</h1>
<p class="page-subtitle">{{ statusLine }}</p> <p class="game-shell__subtitle">{{ statusLine }}</p>
</div> </div>
<div class="header-actions"> <div class="game-shell__actions">
<RouterLink class="ghost" to="/">메인</RouterLink> <RouterLink class="game-shell__action" to="/">메인</RouterLink>
<button class="ghost" @click="loadChiefCenter">새로고침</button> <button class="game-shell__action" @click="loadChiefCenter">새로고침</button>
</div> </div>
</header> </header>
<div v-if="error" class="error">{{ error }}</div> <div v-if="error" class="game-feedback game-feedback--error" role="alert">{{ error }}</div>
<section v-if="loading && !data" class="loading-panel"> <section v-if="loading && !data" class="loading-panel">
<PanelCard title="사령부 로딩"> <PanelCard title="사령부 로딩">
@@ -558,53 +558,6 @@ const shiftTurns = async (amount: number) => {
</template> </template>
<style scoped> <style scoped>
.chief-page {
min-height: 100vh;
padding: 24px;
display: flex;
flex-direction: column;
gap: 16px;
}
.page-header {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 12px;
border-bottom: 1px solid rgba(201, 164, 90, 0.4);
padding-bottom: 12px;
}
.page-title {
font-size: 1.6rem;
font-weight: 600;
}
.page-subtitle {
font-size: 0.85rem;
color: rgba(232, 221, 196, 0.7);
}
.header-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.ghost {
border: 1px solid rgba(201, 164, 90, 0.4);
padding: 6px 12px;
font-size: 0.8rem;
cursor: pointer;
background: rgba(16, 16, 16, 0.6);
color: inherit;
}
.error {
color: #f5b7b1;
font-size: 0.85rem;
}
.layout-desktop { .layout-desktop {
display: grid; display: grid;
grid-template-columns: minmax(0, 2.2fr) minmax(280px, 1fr); grid-template-columns: minmax(0, 2.2fr) minmax(280px, 1fr);
@@ -379,7 +379,7 @@ const generalImage = (general: General) => {
.stats th, .stats th,
.generals th { .generals th {
background-color: #14241b; background-color: #14241b;
background-image: url('/image/game/back_green.jpg'); background-image: var(--sammo-texture-green);
} }
.city-title { .city-title {
text-align: center; text-align: center;
@@ -239,7 +239,7 @@ onMounted(() => {
height: 18px; height: 18px;
text-align: center; text-align: center;
background-color: #14241b; background-color: #14241b;
background-image: url('/image/game/back_green.jpg'); background-image: var(--sammo-texture-green);
} }
.general-icon { .general-icon {
display: inline; display: inline;
@@ -189,7 +189,7 @@ onMounted(async () => {
min-height: 100vh; min-height: 100vh;
margin: 0 auto 100px; margin: 0 auto 100px;
color: #fff; color: #fff;
font-family: Pretendard, 'Apple SD Gothic Neo', 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-family: var(--sammo-font-sans);
font-size: 14px; font-size: 14px;
} }
+46 -96
View File
@@ -113,67 +113,67 @@ watch(
</script> </script>
<template> <template>
<main class="main-page"> <main class="game-shell main-page">
<header class="page-header"> <header class="game-shell__header">
<div> <div>
<h1 class="page-title">전장 현황</h1> <h1 class="game-shell__title">전장 현황</h1>
<p class="page-subtitle">{{ statusLine }}</p> <p class="game-shell__subtitle">{{ statusLine }}</p>
</div> </div>
<div class="header-actions"> <div class="game-shell__actions">
<RouterLink v-if="boardAccess?.canMeeting" class="ghost" to="/board">회의실</RouterLink> <RouterLink v-if="boardAccess?.canMeeting" class="game-shell__action" to="/board">회의실</RouterLink>
<span v-else class="ghost disabled" aria-disabled="true">회의실</span> <span v-else class="game-shell__action disabled" aria-disabled="true">회의실</span>
<RouterLink v-if="boardAccess?.canSecret" class="ghost" to="/board/secret">기밀실</RouterLink> <RouterLink v-if="boardAccess?.canSecret" class="game-shell__action" to="/board/secret">기밀실</RouterLink>
<span v-else class="ghost disabled" aria-disabled="true">기밀실</span> <span v-else class="game-shell__action disabled" aria-disabled="true">기밀실</span>
<RouterLink class="ghost" to="/nation/info">세력 정보</RouterLink> <RouterLink class="game-shell__action" to="/nation/info">세력 정보</RouterLink>
<RouterLink class="ghost" to="/nation/cities">세력 도시</RouterLink> <RouterLink class="game-shell__action" to="/nation/cities">세력 도시</RouterLink>
<RouterLink class="ghost" to="/global-info">중원 정보</RouterLink> <RouterLink class="game-shell__action" to="/global-info">중원 정보</RouterLink>
<RouterLink class="ghost" to="/nation-list">세력일람</RouterLink> <RouterLink class="game-shell__action" to="/nation-list">세력일람</RouterLink>
<RouterLink class="ghost" to="/general-list">장수일람</RouterLink> <RouterLink class="game-shell__action" to="/general-list">장수일람</RouterLink>
<RouterLink class="ghost" to="/current-city">현재 도시</RouterLink> <RouterLink class="game-shell__action" to="/current-city">현재 도시</RouterLink>
<RouterLink class="ghost" to="/nation/generals">세력 장수</RouterLink> <RouterLink class="game-shell__action" to="/nation/generals">세력 장수</RouterLink>
<RouterLink v-if="(boardAccess?.permission ?? -1) >= 1" class="ghost" to="/nation/secret" <RouterLink v-if="(boardAccess?.permission ?? -1) >= 1" class="game-shell__action" to="/nation/secret"
>암행부</RouterLink >암행부</RouterLink
> >
<span v-else class="ghost disabled" aria-disabled="true">암행부</span> <span v-else class="game-shell__action disabled" aria-disabled="true">암행부</span>
<RouterLink class="ghost" to="/nation/personnel">인사부</RouterLink> <RouterLink class="game-shell__action" to="/nation/personnel">인사부</RouterLink>
<RouterLink class="ghost" to="/troop">부대 편성</RouterLink> <RouterLink class="game-shell__action" to="/troop">부대 편성</RouterLink>
<RouterLink class="ghost" to="/nation/finance">내무부</RouterLink> <RouterLink class="game-shell__action" to="/nation/finance">내무부</RouterLink>
<RouterLink class="ghost" to="/diplomacy">외교부</RouterLink> <RouterLink class="game-shell__action" to="/diplomacy">외교부</RouterLink>
<RouterLink class="ghost" to="/chief-center">사령부</RouterLink> <RouterLink class="game-shell__action" to="/chief-center">사령부</RouterLink>
<RouterLink class="ghost" to="/battle-center">감찰부</RouterLink> <RouterLink class="game-shell__action" to="/battle-center">감찰부</RouterLink>
<RouterLink class="ghost" to="/best-general">명장일람</RouterLink> <RouterLink class="game-shell__action" to="/best-general">명장일람</RouterLink>
<RouterLink class="ghost" to="/hall-of-fame">명예의 전당</RouterLink> <RouterLink class="game-shell__action" to="/hall-of-fame">명예의 전당</RouterLink>
<RouterLink class="ghost" to="/dynasty">왕조일람</RouterLink> <RouterLink class="game-shell__action" to="/dynasty">왕조일람</RouterLink>
<RouterLink class="ghost" to="/yearbook">연감</RouterLink> <RouterLink class="game-shell__action" to="/yearbook">연감</RouterLink>
<RouterLink class="ghost" to="/nation-betting">천통국 베팅</RouterLink> <RouterLink class="game-shell__action" to="/nation-betting">천통국 베팅</RouterLink>
<RouterLink class="ghost" to="/traffic">접속량정보</RouterLink> <RouterLink class="game-shell__action" to="/traffic">접속량정보</RouterLink>
<RouterLink class="ghost" to="/npc-list">빙의일람</RouterLink> <RouterLink class="game-shell__action" to="/npc-list">빙의일람</RouterLink>
<a class="ghost" href="/xe/community" target="_blank" rel="noopener">게시판</a> <a class="game-shell__action" href="/xe/community" target="_blank" rel="noopener">게시판</a>
<RouterLink class="ghost" to="/battle-simulator">전투 시뮬레이터</RouterLink> <RouterLink class="game-shell__action" to="/battle-simulator">전투 시뮬레이터</RouterLink>
<RouterLink class="ghost" to="/my-page"> 정보&amp;설정</RouterLink> <RouterLink class="game-shell__action" to="/my-page"> 정보&amp;설정</RouterLink>
<RouterLink class="ghost" to="/past-plays"> 지난 플레이</RouterLink> <RouterLink class="game-shell__action" to="/past-plays"> 지난 플레이</RouterLink>
<RouterLink class="ghost" :class="{ highlight: tournamentStage === 1 }" to="/tournament" <RouterLink class="game-shell__action" :class="{ highlight: tournamentStage === 1 }" to="/tournament"
>토너먼트</RouterLink >토너먼트</RouterLink
> >
<RouterLink class="ghost" :class="{ highlight: tournamentStage === 6 }" to="/betting" <RouterLink class="game-shell__action" :class="{ highlight: tournamentStage === 6 }" to="/betting"
>베팅장</RouterLink >베팅장</RouterLink
> >
<RouterLink class="ghost" to="/auction">거래장</RouterLink> <RouterLink class="game-shell__action" to="/auction">거래장</RouterLink>
<RouterLink class="ghost" to="/survey">설문조사</RouterLink> <RouterLink class="game-shell__action" to="/survey">설문조사</RouterLink>
<RouterLink class="ghost" to="/npc-control">NPC 정책</RouterLink> <RouterLink class="game-shell__action" to="/npc-control">NPC 정책</RouterLink>
<RouterLink class="ghost" to="/inherit">유산 강화</RouterLink> <RouterLink class="game-shell__action" to="/inherit">유산 강화</RouterLink>
<button <button
class="toggle" class="game-shell__action toggle"
:class="{ active: realtimeEnabled }" :class="{ active: realtimeEnabled }"
@click="dashboard.setRealtimeEnabled(!realtimeEnabled)" @click="dashboard.setRealtimeEnabled(!realtimeEnabled)"
> >
실시간 동기화: {{ realtimeLabel }} 실시간 동기화: {{ realtimeLabel }}
</button> </button>
<button class="ghost" @click="loadMainData">새로고침</button> <button class="game-shell__action" @click="loadMainData">새로고침</button>
</div> </div>
</header> </header>
<div v-if="error" class="error">{{ error }}</div> <div v-if="error" class="game-feedback game-feedback--error" role="alert">{{ error }}</div>
<div v-if="frontStatusError" class="front-status-error" role="alert">{{ frontStatusError }}</div> <div v-if="frontStatusError" class="front-status-error" role="alert">{{ frontStatusError }}</div>
<div v-if="session.needsGeneral" class="warning"> <div v-if="session.needsGeneral" class="warning">
@@ -413,39 +413,6 @@ watch(
</template> </template>
<style scoped> <style scoped>
.main-page {
min-height: 100vh;
padding: 24px;
display: flex;
flex-direction: column;
gap: 16px;
}
.page-header {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 12px;
border-bottom: 1px solid rgba(201, 164, 90, 0.4);
padding-bottom: 12px;
}
.page-title {
font-size: 1.6rem;
font-weight: 600;
}
.page-subtitle {
font-size: 0.85rem;
color: rgba(232, 221, 196, 0.7);
}
.header-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
button { button {
font-family: inherit; font-family: inherit;
background: none; background: none;
@@ -453,38 +420,21 @@ button {
color: inherit; color: inherit;
} }
.toggle,
.ghost {
border: 1px solid rgba(201, 164, 90, 0.4);
padding: 6px 12px;
font-size: 0.8rem;
cursor: pointer;
}
.toggle.active { .toggle.active {
background: rgba(201, 164, 90, 0.2); background: rgba(201, 164, 90, 0.2);
} }
.ghost { .game-shell__action.highlight {
background: rgba(16, 16, 16, 0.6);
}
.ghost.highlight {
border-color: #f39c12; border-color: #f39c12;
background: #8a5b13; background: #8a5b13;
color: #fff; color: #fff;
} }
.ghost.disabled { .game-shell__action.disabled {
cursor: not-allowed; cursor: not-allowed;
opacity: 0.5; opacity: 0.5;
} }
.error {
color: #f5b7b1;
font-size: 0.85rem;
}
.front-status-error { .front-status-error {
color: #ff8a80; color: #ff8a80;
font-size: 0.85rem; font-size: 0.85rem;
+5 -5
View File
@@ -448,8 +448,8 @@ onMounted(() => {
padding: 0; padding: 0;
color: #fff; color: #fff;
background-color: #111; background-color: #111;
background-image: url('/image/game/back_walnut.jpg'); background-image: var(--sammo-texture-walnut);
font-family: Pretendard, 'Apple SD Gothic Neo', 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-family: var(--sammo-font-sans);
font-size: 14px; font-size: 14px;
line-height: 1.3; line-height: 1.3;
} }
@@ -522,7 +522,7 @@ button:disabled {
.settings-column, .settings-column,
.log-panel { .log-panel {
border: 1px solid #666; border: 1px solid #666;
background-image: url('/image/game/back_walnut.jpg'); background-image: var(--sammo-texture-walnut);
} }
.section-title, .section-title,
.log-panel h2 { .log-panel h2 {
@@ -533,7 +533,7 @@ button:disabled {
justify-content: center; justify-content: center;
border-bottom: 1px solid #666; border-bottom: 1px solid #666;
background-color: #14241b; background-color: #14241b;
background-image: url('/image/game/back_green.jpg'); background-image: var(--sammo-texture-green);
font-size: 1.25em; font-size: 1.25em;
font-weight: 500; font-weight: 500;
} }
@@ -545,7 +545,7 @@ button:disabled {
grid-template-columns: 150px 1fr; grid-template-columns: 150px 1fr;
padding: 0; padding: 0;
background-color: #172a52; background-color: #172a52;
background-image: url('/image/game/back_blue.jpg'); background-image: var(--sammo-texture-blue);
} }
.portrait-cell { .portrait-cell {
display: flex; display: flex;
@@ -150,7 +150,7 @@ onMounted(async () => {
.city th { .city th {
width: 60px; width: 60px;
text-align: center; text-align: center;
background-image: url('/image/game/back_green.jpg'); background-image: var(--sammo-texture-green);
} }
.city td { .city td {
width: 140px; width: 140px;
@@ -199,7 +199,7 @@ td {
} }
th { th {
height: 30px; height: 30px;
background: #14241b url('/image/game/back_green.jpg'); background: #14241b var(--sammo-texture-green);
font-weight: 400; font-weight: 400;
} }
tbody tr { tbody tr {
@@ -140,7 +140,7 @@ onMounted(async () => {
.info-table th { .info-table th {
width: 98px; width: 98px;
text-align: center; text-align: center;
background-image: url('/image/game/back_green.jpg'); background-image: var(--sammo-texture-green);
} }
.info-table td { .info-table td {
text-align: center; text-align: center;
@@ -247,7 +247,7 @@ onMounted(() => {
width: 80px; width: 80px;
text-align: center; text-align: center;
background-color: #14241b; background-color: #14241b;
background-image: url('/image/game/back_green.jpg'); background-image: var(--sammo-texture-green);
} }
.value-wide { .value-wide {
width: 170px; width: 170px;
@@ -273,7 +273,7 @@ onMounted(() => {
} }
.neutral-label { .neutral-label {
background-color: #14241b; background-color: #14241b;
background-image: url('/image/game/back_green.jpg'); background-image: var(--sammo-texture-green);
} }
.center { .center {
text-align: center; text-align: center;
@@ -501,7 +501,7 @@ onMounted(() => void loadPersonnel());
margin: 0 auto; margin: 0 auto;
border-collapse: collapse; border-collapse: collapse;
table-layout: fixed; table-layout: fixed;
background: url('/image/game/back_walnut.jpg'); background: var(--sammo-texture-walnut);
} }
.legacy-table td { .legacy-table td {
border: 1px solid gray; border: 1px solid gray;
@@ -618,10 +618,10 @@ select[multiple] {
.green-cell, .green-cell,
.city-header, .city-header,
.region-heading { .region-heading {
background: url('/image/game/back_green.jpg'); background: var(--sammo-texture-green);
} }
.blue-cell { .blue-cell {
background: url('/image/game/back_blue.jpg'); background: var(--sammo-texture-blue);
} }
.spacer { .spacer {
height: 5px; height: 5px;
@@ -700,7 +700,7 @@ select[multiple] {
box-sizing: border-box; box-sizing: border-box;
border: 1px solid gray; border: 1px solid gray;
padding: 6px 8px; padding: 6px 8px;
background: url('/image/game/back_walnut.jpg'); background: var(--sammo-texture-walnut);
} }
.error { .error {
color: #ff8080; color: #ff8080;
@@ -176,7 +176,7 @@ select {
} }
.summary th, .summary th,
.list th { .list th {
background: #14241b url('/image/game/back_green.jpg'); background: #14241b var(--sammo-texture-green);
} }
.summary th { .summary th {
width: 120px; width: 120px;
@@ -414,7 +414,7 @@ onMounted(() => void loadStratFinan());
min-height: 100vh; min-height: 100vh;
margin: 0 auto; margin: 0 auto;
color: #fff; color: #fff;
background: url('/image/game/back_walnut.jpg'); background: var(--sammo-texture-walnut);
font: font:
14px/1.3 Pretendard, 14px/1.3 Pretendard,
'Apple SD Gothic Neo', 'Apple SD Gothic Neo',
@@ -502,14 +502,14 @@ textarea:focus-visible {
white-space: nowrap; white-space: nowrap;
} }
.diplomacy-header { .diplomacy-header {
background: url('/image/game/back_green.jpg'); background: var(--sammo-texture-green);
} }
.green-header { .green-header {
display: flex; display: flex;
min-height: 32px; min-height: 32px;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
background: url('/image/game/back_green.jpg'); background: var(--sammo-texture-green);
} }
.message-preview, .message-preview,
textarea { textarea {
@@ -545,7 +545,7 @@ textarea {
.blue-heading { .blue-heading {
height: 18.19px; height: 18.19px;
text-align: center; text-align: center;
background: url('/image/game/back_blue.jpg'); background: var(--sammo-texture-blue);
} }
.budget-row, .budget-row,
.policy-cell { .policy-cell {
@@ -556,7 +556,7 @@ textarea {
} }
.budget-row span:first-child, .budget-row span:first-child,
.green-label { .green-label {
background: url('/image/game/back_green.jpg'); background: var(--sammo-texture-green);
} }
.budget-row > span, .budget-row > span,
.green-label, .green-label,
@@ -560,7 +560,7 @@ const dropPriority = (event: DragEvent, section: PrioritySectionKey, bucket: Pri
min-width: 500px; min-width: 500px;
margin: 0; margin: 0;
background: #000; background: #000;
font-family: Pretendard, 'Apple SD Gothic Neo', 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-family: var(--sammo-font-sans);
font-size: 14px; font-size: 14px;
line-height: 21px; line-height: 21px;
} }
@@ -573,21 +573,21 @@ const dropPriority = (event: DragEvent, section: PrioritySectionKey, bucket: Pri
.npc-page { .npc-page {
min-height: 100vh; min-height: 100vh;
color: #fff; color: #fff;
font-family: Pretendard, 'Apple SD Gothic Neo', 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-family: var(--sammo-font-sans);
font-size: 14px; font-size: 14px;
line-height: 21px; line-height: 21px;
} }
.legacy-bg0 { .legacy-bg0 {
background-image: url('/image/game/back_walnut.jpg'); background-image: var(--sammo-texture-walnut);
} }
.legacy-bg1 { .legacy-bg1 {
background-image: url('/image/game/back_green.jpg'); background-image: var(--sammo-texture-green);
} }
.legacy-bg2 { .legacy-bg2 {
background-image: url('/image/game/back_blue.jpg'); background-image: var(--sammo-texture-blue);
} }
.top-back-bar { .top-back-bar {
+12 -60
View File
@@ -97,24 +97,24 @@ onMounted(() => {
</script> </script>
<template> <template>
<main class="public-page"> <main class="game-shell public-page">
<header class="page-header"> <header class="game-shell__header">
<div> <div>
<h1 class="page-title">공개 동향</h1> <h1 class="game-shell__title">공개 동향</h1>
<p class="page-subtitle">{{ trendSummary }}</p> <p class="game-shell__subtitle">{{ trendSummary }}</p>
<p class="page-hint">지도/정세는 10 캐시된 정보로 제공됩니다.</p> <p class="page-hint">지도/정세는 10 캐시된 정보로 제공됩니다.</p>
</div> </div>
<div class="header-actions"> <div class="game-shell__actions">
<RouterLink v-if="!session.isAuthed" class="ghost" to="/login">로그인</RouterLink> <RouterLink v-if="!session.isAuthed" class="game-shell__action" to="/login">로그인</RouterLink>
<RouterLink v-else-if="session.needsGeneral" class="ghost" to="/join">장수 생성/빙의</RouterLink> <RouterLink v-else-if="session.needsGeneral" class="game-shell__action" to="/join">장수 생성/빙의</RouterLink>
<RouterLink v-else class="ghost" to="/">메인으로</RouterLink> <RouterLink v-else class="game-shell__action" to="/">메인으로</RouterLink>
<RouterLink class="ghost" to="/npc-list">빙의일람</RouterLink> <RouterLink class="game-shell__action" to="/npc-list">빙의일람</RouterLink>
<RouterLink class="ghost" to="/traffic">접속량정보</RouterLink> <RouterLink class="game-shell__action" to="/traffic">접속량정보</RouterLink>
<button class="ghost" @click="refreshPublicData">새로고침</button> <button class="game-shell__action" @click="refreshPublicData">새로고침</button>
</div> </div>
</header> </header>
<div v-if="error" class="error">{{ error }}</div> <div v-if="error" class="game-feedback game-feedback--error" role="alert">{{ error }}</div>
<section v-if="isMobile" class="layout-mobile"> <section v-if="isMobile" class="layout-mobile">
<PanelCard title="캐시 지도"> <PanelCard title="캐시 지도">
@@ -256,60 +256,12 @@ onMounted(() => {
</template> </template>
<style scoped> <style scoped>
.public-page {
min-height: 100vh;
padding: 24px;
display: flex;
flex-direction: column;
gap: 16px;
}
.page-header {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 12px;
border-bottom: 1px solid rgba(201, 164, 90, 0.4);
padding-bottom: 12px;
}
.page-title {
font-size: 1.6rem;
font-weight: 600;
}
.page-subtitle {
font-size: 0.85rem;
color: rgba(232, 221, 196, 0.7);
}
.page-hint { .page-hint {
margin-top: 6px; margin-top: 6px;
font-size: 0.75rem; font-size: 0.75rem;
color: rgba(232, 221, 196, 0.5); color: rgba(232, 221, 196, 0.5);
} }
.header-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.ghost {
border: 1px solid rgba(201, 164, 90, 0.4);
padding: 6px 12px;
font-size: 0.8rem;
cursor: pointer;
text-decoration: none;
color: inherit;
background: rgba(16, 16, 16, 0.6);
}
.error {
color: #f5b7b1;
font-size: 0.85rem;
}
.layout-desktop { .layout-desktop {
display: grid; display: grid;
grid-template-columns: minmax(320px, 1.3fr) minmax(320px, 1fr); grid-template-columns: minmax(320px, 1.3fr) minmax(320px, 1fr);
+3 -3
View File
@@ -416,15 +416,15 @@ onMounted(() => {
} }
.bg0 { .bg0 {
background-image: url('/image/game/back_walnut.jpg'); background-image: var(--sammo-texture-walnut);
} }
.bg1 { .bg1 {
background-image: url('/image/game/back_green.jpg'); background-image: var(--sammo-texture-green);
} }
.bg2 { .bg2 {
background-image: url('/image/game/back_blue.jpg'); background-image: var(--sammo-texture-blue);
} }
.back_bar { .back_bar {
@@ -267,7 +267,7 @@ const start = async () => {
min-height: 100vh; min-height: 100vh;
margin: 0 auto; margin: 0 auto;
color: #fff; color: #fff;
font-family: Pretendard, 'Apple SD Gothic Neo', 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-family: var(--sammo-font-sans);
font-size: 14px; font-size: 14px;
line-height: 1.3; line-height: 1.3;
text-align: center; text-align: center;
@@ -277,10 +277,10 @@ const start = async () => {
box-sizing: border-box; box-sizing: border-box;
} }
.bg0 { .bg0 {
background: #3a2118 url('/image/game/back_walnut.jpg'); background: #3a2118 var(--sammo-texture-walnut);
} }
.bg2 { .bg2 {
background: #142b42 url('/image/game/back_blue.jpg'); background: #142b42 var(--sammo-texture-blue);
} }
.legacy-title { .legacy-title {
height: 55.6875px; height: 55.6875px;
@@ -420,7 +420,7 @@ caption {
color: #fff; color: #fff;
} }
th { th {
background: #154b2a url('/image/game/back_green.jpg'); background: #154b2a var(--sammo-texture-green);
font-weight: 400; font-weight: 400;
} }
th, th,
+3 -3
View File
@@ -181,7 +181,7 @@ onMounted(() => {
margin: 0 auto; margin: 0 auto;
padding: 0; padding: 0;
color: #fff; color: #fff;
font-family: Pretendard, 'Apple SD Gothic Neo', 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-family: var(--sammo-font-sans);
font-size: 14px; font-size: 14px;
line-height: normal; line-height: normal;
} }
@@ -203,7 +203,7 @@ onMounted(() => {
.legacy-bg0 { .legacy-bg0 {
background-color: #302016; background-color: #302016;
background-image: url('/image/game/back_walnut.jpg'); background-image: var(--sammo-texture-walnut);
} }
.legacy-bg1 { .legacy-bg1 {
@@ -213,7 +213,7 @@ onMounted(() => {
.legacy-bg2 { .legacy-bg2 {
background-color: #14241b; background-color: #14241b;
background-image: url('/image/game/back_green.jpg'); background-image: var(--sammo-texture-green);
} }
.title-table, .title-table,
+2 -2
View File
@@ -334,11 +334,11 @@ onMounted(() => {
} }
.bg0 { .bg0 {
background-image: url('/image/game/back_walnut.jpg'); background-image: var(--sammo-texture-walnut);
} }
.bg1 { .bg1 {
background-image: url('/image/game/back_green.jpg'); background-image: var(--sammo-texture-green);
} }
.center { .center {
+1 -1
View File
@@ -206,7 +206,7 @@ onMounted(async () => {
min-height: 100vh; min-height: 100vh;
margin: 0 auto; margin: 0 auto;
color: #fff; color: #fff;
font-family: Pretendard, 'Apple SD Gothic Neo', 'Noto Sans KR', 'Malgun Gothic', sans-serif; font-family: var(--sammo-font-sans);
font-size: 14px; font-size: 14px;
} }
+4 -1
View File
@@ -158,5 +158,8 @@
## Open Questions ## Open Questions
- Public 상태 동향 범위는 캐싱된 지도, 중원 정세, 세력일람으로 제한한다. 장수일람은 실시간 제공하되 이름/NPC 여부/국가/기본 능력치만 노출한다. 그 외 장수 정보는 캐싱된 자료에 기반하며, 빈번한 접근 제한 우회를 막기 위해 캐싱 전략을 유지한다. - Public 상태 동향 범위는 캐싱된 지도, 중원 정세, 세력일람으로 제한한다. 장수일람은 실시간 제공하되 이름/NPC 여부/국가/기본 능력치만 노출한다. 그 외 장수 정보는 캐싱된 자료에 기반하며, 빈번한 접근 제한 우회를 막기 위해 캐싱 전략을 유지한다.
- UI 스타일은 당분간 \"고전 게임\" 감성을 유지한다. 전체 이식이 완료된 뒤 현대화하며, 새 UI는 Tailwind 등 CSS 라이브러리를 적극 활용한다. - UI 스타일은 ref의 렌더링 계약을 유지한다. CSS 구조화는 공통 token과
검증된 shell 단위로 수행하되, ref의 computed DOM 치수·typography·texture·
interaction과 충돌하면 페이지별 ref 규칙을 우선한다. 새로운 디자인으로의
현대화는 이 이관의 기본 후속 단계로 간주하지 않는다.
- 실시간 업데이트는 메인 화면에 한정한다. 대상: 지도, 명령 목록, 현재 도시 정보, 소속 국가 정보, 장수 스탯, 장수 동향, 개인 기록, 중원 정세, 메시지함. 메인 화면에는 \"실시간 동기화 켬/끔\" 토글이 필요하다. - 실시간 업데이트는 메인 화면에 한정한다. 대상: 지도, 명령 목록, 현재 도시 정보, 소속 국가 정보, 장수 스탯, 장수 동향, 개인 기록, 중원 정세, 메시지함. 메인 화면에는 \"실시간 동기화 켬/끔\" 토글이 필요하다.
+61
View File
@@ -0,0 +1,61 @@
# Game frontend CSS architecture
The game frontend preserves the rendered contract of `ref/sam`; CSS reuse is
not a reason to normalize a page's width, height, typography, texture, or
interaction states. When reuse and the reference geometry conflict, the
reference geometry wins.
## Layers
`app/game-frontend/src/assets/main.css` is the single global entry point. It
loads the following layers:
1. `styles/tokens.css`: exact shared font, color, and `/image/game` texture
values. These are value aliases only and must resolve to the same computed
value as the ref page.
2. `styles/game-shell.css`: the flexible shell shared by the main dashboard,
public dashboard, and chief center. Only declarations proven identical
across those screens belong here.
3. `styles/ref-shell.css`: fixed ref geometry, including the 1000px desktop /
500px mobile family used by the battle center. Its namespace stays separate
from the flexible shell so a generic responsive rule cannot override it.
4. Scoped SFC styles: page-specific grids, fixed table dimensions, selectors,
and state styling. These remain closest to the DOM contract they implement.
## Class naming
- `.game-shell`, `.game-shell__header`, `.game-shell__actions`: flexible
application shell.
- `.ref-shell`, `.ref-shell__topbar`, `.ref-shell__control`: measured legacy
shell and controls.
- `.game-feedback--error`, `.ref-feedback--error`: feedback scoped to its
visual family.
- Feature-specific classes stay namespaced by their feature or component.
Generic names such as `.title`, `.error`, `.ghost`, `.stack`, and
`.layout-grid` must not be promoted from a scoped SFC merely because the same
spelling appears elsewhere.
Existing feature hooks may remain while a screen is migrated, but new shared
presentation must be selected through one of the explicit shell namespaces.
## Consolidation rule
Before moving declarations out of an SFC:
1. Compare every same-named selector's declarations and semantic role.
2. Confirm the affected pages use the same layout family.
3. Record desktop and mobile `getBoundingClientRect()` and
`getComputedStyle()` values before the move.
4. Move only identical declarations; keep exceptions in the owning SFC.
5. Re-run Chromium geometry plus hover, focus, active, and disabled states.
The main page and chief center are the flexible-shell references. The battle
center is the fixed ref-shell reference. If another page has a measured ref
contract that differs from both, preserve that page's local contract rather
than forcing it into either family.
## Asset boundary
The CSS variables contain `/image/game/*` URLs but do not import or copy image
files. Caddy continues to own `/image/*`; Vite must not rewrite the image tree
as application assets.