fix(game-ui): 자동 갱신 중 커맨드 입력값 보존

같은 명령의 field 계약과 유효한 select option이 유지되는 동안 사용자가 편집한 값을 보존한다. 장수 동향과 명령표 realtime 갱신을 함께 발생시키는 desktop/mobile Chromium 회귀 테스트를 추가한다.
This commit is contained in:
2026-08-19 13:28:11 +00:00
parent 54a5b91ca9
commit f01661bc08
4 changed files with 380 additions and 12 deletions
@@ -0,0 +1,22 @@
import type { CommandInputField, CommandOption } from './types';
export type CommandArgumentFieldContract = Pick<CommandInputField, 'kind' | 'optionSource'>;
export const commandArgumentFieldContract = (field: CommandInputField): CommandArgumentFieldContract => ({
kind: field.kind,
optionSource: field.optionSource,
});
export const shouldPreserveCommandArgumentValue = (
field: CommandInputField,
previousField: CommandArgumentFieldContract | undefined,
values: Readonly<Record<string, unknown>>,
options: readonly CommandOption[]
): boolean => {
if (field.kind === 'hidden' || !Object.prototype.hasOwnProperty.call(values, field.key)) return false;
if (!previousField || previousField.kind !== field.kind || previousField.optionSource !== field.optionSource) {
return false;
}
if (field.kind !== 'select') return true;
return options.some((option) => option.value === values[field.key]);
};
@@ -2,6 +2,11 @@
import { computed, reactive, watch, type CSSProperties } from 'vue';
import MapViewer from './MapViewer.vue';
import { commandArgumentPresentation } from '../command/commandArgumentPresentation';
import {
commandArgumentFieldContract,
shouldPreserveCommandArgumentValue,
type CommandArgumentFieldContract,
} from '../command/commandArgumentDraft';
import { legacyNationTextColor } from '../../utils/legacyNationColor';
import type {
CommandInputContext,
@@ -28,6 +33,8 @@ const emit = defineEmits<{
}>();
const values = reactive<Record<string, unknown>>({});
const previousFieldContracts = new Map<string, CommandArgumentFieldContract>();
let initializedCommandKey: string | null = null;
const presentation = computed(() => commandArgumentPresentation(props.commandKey));
const visibleFields = computed(() => props.fields.filter((entry) => entry.kind !== 'hidden'));
@@ -87,11 +94,39 @@ const defaultValue = (field: CommandInputField): unknown => {
return '';
};
const initialize = () => {
for (const key of Object.keys(values)) delete values[key];
for (const field of props.fields) values[field.key] = defaultValue(field);
const synchronizeValues = () => {
const commandChanged = initializedCommandKey !== props.commandKey;
initializedCommandKey = props.commandKey;
const activeKeys = new Set(props.fields.map((field) => field.key));
for (const key of Object.keys(values)) {
if (!activeKeys.has(key)) delete values[key];
}
for (const field of props.fields) {
const preserve =
!commandChanged &&
shouldPreserveCommandArgumentValue(
field,
previousFieldContracts.get(field.key),
values,
optionsFor(field)
);
if (!preserve) values[field.key] = defaultValue(field);
}
const itemCodeField = props.fields.find((field) => field.key === 'itemCode');
if (itemCodeField) values.itemCode = defaultValue(itemCodeField);
if (
itemCodeField &&
(commandChanged ||
!shouldPreserveCommandArgumentValue(
itemCodeField,
previousFieldContracts.get(itemCodeField.key),
values,
optionsFor(itemCodeField)
))
) {
values.itemCode = defaultValue(itemCodeField);
}
previousFieldContracts.clear();
for (const field of props.fields) previousFieldContracts.set(field.key, commandArgumentFieldContract(field));
};
const setSelectValue = (field: CommandInputField, rawValue: string) => {
@@ -352,7 +387,11 @@ const isValid = computed(() =>
})
);
watch(() => [props.commandKey, props.fields, props.options] as const, initialize, { immediate: true, deep: true });
watch(
() => [props.commandKey, props.fields, props.options, props.mapData?.myCity, props.mapData?.myNation] as const,
synchronizeValues,
{ immediate: true, deep: true }
);
watch(
() => ({ ...values }),
() => {