fix: 개인 전투기록 색상을 레거시와 맞춤

Ref small_war_log의 화살표, 장수명, 병력 색상과 글자 크기를 공통 CSS에 복원한다. 동일 전투 문구를 desktop/mobile Chromium에서 비교하는 회귀 검증과 Ref 측정 도구를 추가한다.
This commit is contained in:
2026-08-19 13:58:42 +00:00
parent daf98722de
commit 6c501dd129
4 changed files with 159 additions and 7 deletions
+35 -7
View File
@@ -676,7 +676,7 @@ test('메인 카드의 국가·수도·관직·계급·병종은 Ref 출력명
await expect(page.locator('.main-page')).not.toContainText('che_'); await expect(page.locator('.main-page')).not.toContainText('che_');
}); });
test('메인 장수 동향과 개인 전투 기록은 모두 21px 행 간격을 유지한다', async ({ page }) => { test('메인 장수 동향과 개인 전투 기록은 Ref 행 간격·색상·글자 크기를 유지한다', async ({ page }) => {
const state: FixtureState = { const state: FixtureState = {
permission: 'head', permission: 'head',
myset: 3, myset: 3,
@@ -704,13 +704,15 @@ test('메인 장수 동향과 개인 전투 기록은 모두 21px 행 간격을
id: 18608, id: 18608,
text: text:
'<S>◆</>186년 9월:<div class="small_war_log">' + '<S>◆</>186년 9월:<div class="small_war_log">' +
'<span class="me"><span class="crew_type">귀병</span> ' + '<span class="me"><span class="name_plate"><span class="crew_type">귀병</span> ' +
'<span class="name_plate_cover">【<span class="name">Administrator</span>】</span> ' + '<span class="name_plate_cover">【<span class="name">Administrator</span>】</span></span> ' +
'<span class="remain_crew">0</span>(<span class="killed_crew">-2209</span>)</span> ' + '<span class="crew_plate"><span class="remain_crew">0</span>' +
'<span class="killed_plate">(<span class="killed_crew">-2209</span>)</span></span></span> ' +
'<span class="war_type war_type_defense">←</span> ' + '<span class="war_type war_type_defense">←</span> ' +
'<span class="you"><span class="remain_crew">1361</span>' + '<span class="you"><span class="crew_plate"><span class="remain_crew">1361</span>' +
'(<span class="killed_crew">-5539</span>) <span class="crew_type">기병</span> ' + '<span class="killed_plate">(<span class="killed_crew">-5539</span>)</span></span> ' +
'<span class="name_plate_cover"><span class="name">ⓝ뇌동</span>】</span></span></div>', '<span class="name_plate"><span class="crew_type">기병</span> ' +
'<span class="name_plate_cover">【<span class="name">ⓝ뇌동</span>】</span></span></span></div>',
createdAt: '2026-01-01T03:54:00.000Z', createdAt: '2026-01-01T03:54:00.000Z',
}, },
], ],
@@ -754,6 +756,16 @@ test('메인 장수 동향과 개인 전투 기록은 모두 21px 행 간격을
const battle = element.querySelector<HTMLElement>('.small_war_log'); const battle = element.querySelector<HTMLElement>('.small_war_log');
if (!battle) throw new Error('전투 요약 markup을 찾지 못했습니다.'); if (!battle) throw new Error('전투 요약 markup을 찾지 못했습니다.');
const battleRect = battle.getBoundingClientRect(); const battleRect = battle.getBoundingClientRect();
const requireElement = (selector: string): HTMLElement => {
const target = element.querySelector<HTMLElement>(selector);
if (!target) throw new Error(`전투 요약 요소를 찾지 못했습니다: ${selector}`);
return target;
};
const diamond = requireElement('span[style*="skyblue"]');
const namePlate = requireElement('.me .name_plate');
const nameCover = requireElement('.me .name_plate_cover');
const crewPlate = requireElement('.me .crew_plate');
const arrow = requireElement('.war_type_defense');
return { return {
line: { top: lineRect.top, height: lineRect.height }, line: { top: lineRect.top, height: lineRect.height },
battle: { battle: {
@@ -762,6 +774,14 @@ test('메인 장수 동향과 개인 전투 기록은 모두 21px 행 간격을
display: getComputedStyle(battle).display, display: getComputedStyle(battle).display,
}, },
lineHeight: getComputedStyle(element).lineHeight, lineHeight: getComputedStyle(element).lineHeight,
styles: {
diamondColor: getComputedStyle(diamond).color,
namePlateFontSize: getComputedStyle(namePlate).fontSize,
nameCoverColor: getComputedStyle(nameCover).color,
crewPlateColor: getComputedStyle(crewPlate).color,
crewPlateFontSize: getComputedStyle(crewPlate).fontSize,
defenseArrowColor: getComputedStyle(arrow).color,
},
}; };
}); });
}; };
@@ -770,6 +790,14 @@ test('메인 장수 동향과 개인 전투 기록은 모두 21px 행 간격을
expect(geometry.line.height).toBe(21); expect(geometry.line.height).toBe(21);
expect(geometry.battle.height).toBe(21); expect(geometry.battle.height).toBe(21);
expect(geometry.battle.top).toBeCloseTo(geometry.line.top, 0); expect(geometry.battle.top).toBeCloseTo(geometry.line.top, 0);
expect(geometry.styles).toEqual({
diamondColor: 'rgb(135, 206, 235)',
namePlateFontSize: '10.5px',
nameCoverColor: 'rgb(255, 255, 0)',
crewPlateColor: 'rgb(255, 69, 0)',
crewPlateFontSize: '12.6px',
defenseArrowColor: 'rgb(255, 0, 255)',
});
}; };
const desktopGeometry = await inspect( const desktopGeometry = await inspect(
+25
View File
@@ -87,3 +87,28 @@ textarea {
.small_war_log { .small_war_log {
display: inline-block; display: inline-block;
} }
.small_war_log .war_type_attack {
color: cyan;
}
.small_war_log .war_type_defense {
color: magenta;
}
.small_war_log .war_type_siege {
color: white;
}
.small_war_log .name_plate {
font-size: 0.75em;
}
.small_war_log .name_plate_cover {
color: yellow;
}
.small_war_log .crew_plate {
color: orangered;
font-size: 90%;
}
+14
View File
@@ -137,6 +137,20 @@ REF_PERSONAL_WAR_LOG_ARTIFACT_DIR=/path/to/ignored/artifacts \
`MENU_PARITY_ARTIFACT_DIR`를 지정하면 1200×900·500×900 screenshot과 computed `MENU_PARITY_ARTIFACT_DIR`를 지정하면 1200×900·500×900 screenshot과 computed
style JSON을 남깁니다. style JSON을 남깁니다.
개인 전투 결과 요약의 Ref 색상·글자 크기는 실제 `small_war_log` class 구조와
빌드 CSS를 사용하는 별도 정적 Chromium fixture로 재현합니다. 방어 화살표,
장수명 괄호, 병력 수치와 병종·장수/병력 글자 크기를 desktop/mobile에서
수집하며 live session·DB writer 검증과는 구분합니다.
```sh
REF_SAM_ROOT=/path/to/ref/sam \
REF_PERSONAL_BATTLE_LOG_ARTIFACT_DIR=/path/to/ignored/artifacts \
node tools/frontend-legacy-parity/reference-personal-battle-log-colors.mjs
```
대응하는 Core 검증은 `inGameMenus.spec.ts`의 “개인 전투 기록” test이며 같은
viewport에서 한 줄 geometry와 computed 색상·글자 크기를 함께 검사합니다.
To refresh the PHP ranking evidence after building the ignored reference To refresh the PHP ranking evidence after building the ignored reference
webpack assets, run: webpack assets, run:
@@ -0,0 +1,85 @@
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
import { chromium } from '@playwright/test';
const refRoot = resolve(process.env.REF_SAM_ROOT ?? '/home/letrhee/sam_rebuild/ref/sam');
const artifactDir = process.env.REF_PERSONAL_BATTLE_LOG_ARTIFACT_DIR;
const formatterUrl = pathToFileURL(resolve(refRoot, 'hwe/ts/utilGame/formatLog.ts')).href;
const { formatLog } = await import(formatterUrl);
const css = await readFile(resolve(refRoot, 'dist_js/hwe_dynamic/vue/v_main.css'), 'utf8');
const record =
'<S>◆</>188년 2월:<div class="small_war_log">' +
'<span class="me"><span class="name_plate"><span class="crew_type">남귀</span> ' +
'<span class="name_plate_cover">【<span class="name">운영자</span>】</span></span> ' +
'<span class="crew_plate"><span class="remain_crew">0</span>' +
'<span class="killed_plate">(<span class="killed_crew">-4404</span>)</span></span></span> ' +
'<span class="war_type war_type_defense">←</span> ' +
'<span class="you"><span class="crew_plate"><span class="remain_crew">555</span>' +
'<span class="killed_plate">(<span class="killed_crew">-6845</span>)</span></span> ' +
'<span class="name_plate"><span class="crew_type">기병</span> ' +
'<span class="name_plate_cover">【<span class="name">ⓝ독야청정</span>】</span></span></span></div>';
const browser = await chromium.launch({ headless: true });
try {
const context = await browser.newContext({
colorScheme: 'dark',
deviceScaleFactor: 1,
locale: 'ko-KR',
timezoneId: 'UTC',
});
const page = await context.newPage();
await page.setContent(
`<!doctype html><html><head><style>${css}</style></head><body>` +
`<div id="container"><div class="RecordZone row gx-0"><div class="GeneralLog col col-12 col-lg-6">` +
`<div class="bg1 center s-border-tb title">개인 기록</div><div class="fixture-line">${formatLog(record)}</div>` +
`</div></div></div></body></html>`,
{ waitUntil: 'networkidle' }
);
await page.evaluate(() => document.fonts.ready);
for (const viewport of [
{ name: 'desktop', width: 1200, height: 900 },
{ name: 'mobile', width: 500, height: 900 },
]) {
await page.setViewportSize(viewport);
const measurement = await page.locator('.fixture-line').evaluate((element) => {
const requireElement = (selector) => {
const target = element.querySelector(selector);
if (!(target instanceof HTMLElement)) throw new Error(`Ref 전투기록 요소를 찾지 못했습니다: ${selector}`);
return target;
};
const rect = element.getBoundingClientRect();
const battle = requireElement('.small_war_log');
const battleRect = battle.getBoundingClientRect();
return {
text: element.textContent,
line: { height: rect.height, fontSize: getComputedStyle(element).fontSize },
battle: { height: battleRect.height, display: getComputedStyle(battle).display },
styles: {
diamondColor: getComputedStyle(requireElement('span[style*="skyblue"]')).color,
namePlateFontSize: getComputedStyle(requireElement('.me .name_plate')).fontSize,
nameCoverColor: getComputedStyle(requireElement('.me .name_plate_cover')).color,
crewPlateColor: getComputedStyle(requireElement('.me .crew_plate')).color,
crewPlateFontSize: getComputedStyle(requireElement('.me .crew_plate')).fontSize,
defenseArrowColor: getComputedStyle(requireElement('.war_type_defense')).color,
},
};
});
const output = { viewport, measurement };
console.log(JSON.stringify(output));
if (artifactDir) {
await mkdir(artifactDir, { recursive: true });
await Promise.all([
page.screenshot({ path: resolve(artifactDir, `ref-personal-battle-log-colors-${viewport.name}.png`) }),
writeFile(
resolve(artifactDir, `ref-personal-battle-log-colors-${viewport.name}.json`),
`${JSON.stringify(output, null, 2)}\n`
),
]);
}
}
} finally {
await browser.close();
}