merge: align main command editor with ref

# Conflicts:
#	app/game-frontend/e2e/mainNavigation.spec.ts
This commit is contained in:
2026-08-12 15:49:31 +00:00
8 changed files with 237 additions and 103 deletions
@@ -50,6 +50,7 @@ const commandArgsValid = ref(false);
const expanded = ref(false);
const menuRevision = ref(0);
const pendingReservation = ref<CommandPatternEntry | null>(null);
const collapsedRowCount = 15;
const loadStorage = (key: string) => {
storage.value = new CommandStorage(key);
@@ -108,7 +109,11 @@ const labelMap = computed(() => {
for (const group of groups) for (const command of group.values) map.set(command.key, command.name);
return map;
});
const displayRows = computed(() => props.rows.slice(0, expanded.value || props.compact ? props.rows.length : 14));
const displayRows = computed(() =>
props.rows.slice(0, expanded.value || props.compact ? props.rows.length : collapsedRowCount)
);
const quickPickerTop = computed(() => `${70 + (quickTarget.value ?? 0) * 34.4}px`);
const rowLabel = (row: ReservedCommandRow): string => row.label ?? labelMap.value.get(row.action) ?? row.action;
const selectedIndices = () => normalizedSelection(selected.value, previousSelected.value, props.rows.length);
const pattern = () => extractPattern(props.rows, selectedIndices());
const touchMenus = () => (menuRevision.value += 1);
@@ -396,6 +401,87 @@ const clickOutsideMenu = (event: Event) => {
</details>
</aside>
<div v-if="editMode" class="advanced-actions">
<details class="legacy-menu selected-menu">
<summary>선택한 턴을</summary>
<div class="menu-items">
<button
@click="
cut();
clickOutsideMenu($event);
"
>
잘라내기
</button>
<button
@click="
copy();
clickOutsideMenu($event);
"
>
복사하기
</button>
<button
@click="
paste();
clickOutsideMenu($event);
"
>
붙여넣기
</button>
<button
@click="
textCopy();
clickOutsideMenu($event);
"
>
텍스트 복사
</button>
<button
@click="
saveTemplate();
clickOutsideMenu($event);
"
>
보관하기
</button>
<button
@click="
repeatPattern();
clickOutsideMenu($event);
"
>
반복하기
</button>
<button
@click="
clearSelection();
clickOutsideMenu($event);
"
>
비우기
</button>
<button
@click="
rearrange('pull');
clickOutsideMenu($event);
"
>
지우고 당기기
</button>
<button
@click="
rearrange('push');
clickOutsideMenu($event);
"
>
뒤로 밀기
</button>
</div>
</details>
<button type="button" class="select-command" @click="openPicker()">명령 선택 </button>
</div>
<div class="queue-area">
<div class="queue-grid" :class="{ advanced: editMode }">
<DragSelect
@@ -443,9 +529,11 @@ const clickOutsideMenu = (event: Event) => {
<div
v-for="row in displayRows"
:key="row.index"
:title="row.label ?? labelMap.get(row.action) ?? row.action"
:title="row.autonomous ? `${rowLabel(row)} · 자율 행동` : rowLabel(row)"
:class="{ autonomous: row.autonomous }"
>
{{ row.label ?? labelMap.get(row.action) ?? row.action }}
<span>{{ rowLabel(row) }}</span>
<small v-if="row.autonomous && row.action === '휴식'">(자율 행동)</small>
</div>
</div>
<div v-if="!editMode" class="edit-column">
@@ -461,87 +549,6 @@ const clickOutsideMenu = (event: Event) => {
</div>
</div>
<div v-if="editMode" class="advanced-actions">
<details class="legacy-menu selected-menu">
<summary>선택한 턴을</summary>
<div class="menu-items">
<button
@click="
cut();
clickOutsideMenu($event);
"
>
잘라내기
</button>
<button
@click="
copy();
clickOutsideMenu($event);
"
>
복사하기
</button>
<button
@click="
paste();
clickOutsideMenu($event);
"
>
붙여넣기
</button>
<button
@click="
textCopy();
clickOutsideMenu($event);
"
>
텍스트 복사
</button>
<button
@click="
saveTemplate();
clickOutsideMenu($event);
"
>
보관하기
</button>
<button
@click="
repeatPattern();
clickOutsideMenu($event);
"
>
반복하기
</button>
<button
@click="
clearSelection();
clickOutsideMenu($event);
"
>
비우기
</button>
<button
@click="
rearrange('pull');
clickOutsideMenu($event);
"
>
지우고 당기기
</button>
<button
@click="
rearrange('push');
clickOutsideMenu($event);
"
>
뒤로 밀기
</button>
</div>
</details>
<button type="button" class="select-command" @click="openPicker()">명령 선택 </button>
</div>
<div v-if="!props.compact" class="bottom-actions">
<button type="button" @click="emit('shift', -1)">당기기</button>
<button type="button" @click="emit('shift', 1)">미루기</button>
@@ -550,7 +557,12 @@ const clickOutsideMenu = (event: Event) => {
</div>
</div>
<div v-if="pickerOpen" class="command-picker" data-testid="command-picker">
<div
v-if="pickerOpen"
class="command-picker"
data-testid="command-picker"
:style="quickTarget === null || props.compact ? undefined : { top: quickPickerTop }"
>
<header>
<strong>{{ quickTarget === null ? '선택한 턴' : `${quickTarget + 1}` }} 명령 입력</strong
><button type="button" aria-label="명령 입력 닫기" @click="closePicker">×</button>
@@ -628,6 +640,9 @@ const clickOutsideMenu = (event: Event) => {
gap: 4px;
padding: 3px 0;
}
.queue-area {
order: 2;
}
.control-pad > button,
.clock,
.legacy-menu > summary,
@@ -779,6 +794,13 @@ const clickOutsideMenu = (event: Event) => {
.action-column > div:nth-child(even) {
background: #071638;
}
.action-column > div.autonomous {
color: #aaffff;
}
.action-column small {
font-size: 0.72em;
line-height: 1;
}
.edit-column button {
background: #444;
cursor: pointer;
@@ -786,6 +808,7 @@ const clickOutsideMenu = (event: Event) => {
.advanced-actions {
display: grid;
grid-template-columns: 5fr 7fr;
order: 1;
}
.advanced-actions > * {
border-radius: 0 !important;
@@ -808,6 +831,10 @@ const clickOutsideMenu = (event: Event) => {
background: #303030;
box-shadow: 0 6px 16px #000;
}
.reserved-command-editor:not(.compact) .command-picker {
max-height: none;
overflow: visible;
}
.command-picker > header {
display: flex;
align-items: center;
@@ -49,6 +49,7 @@ export type ReservedCommandRow = {
time?: string;
year?: number;
month?: number;
autonomous?: boolean;
};
export type CommandPatternEntry = {
@@ -12,6 +12,7 @@ const props = defineProps<{
currentYear?: number;
currentMonth?: number;
turnTermMinutes?: number;
autorunLimit?: number | null;
storageKey?: string;
}>();
@@ -43,6 +44,7 @@ const rows = computed<ReservedCommandRow[]>(() => {
label: labelMap.value.get(turn.action) ?? turn.action,
year: Math.floor(absoluteMonth / 12),
month: (absoluteMonth % 12) + 1,
autonomous: props.autorunLimit != null && absoluteMonth <= props.autorunLimit - 1,
time: date
? term >= 5
? `${String(date.getUTCHours()).padStart(2, '0')}:${String(date.getUTCMinutes()).padStart(2, '0')}`
@@ -65,6 +65,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
boardAccess?: BoardAccess | null;
reservedGeneralTurns?: ReservedTurnView[] | null;
reservedGeneralRevision?: number;
reservedGeneralAutorunLimit?: number | null;
globalRecords?: RecentRecord[];
generalRecords?: RecentRecord[];
worldHistory?: RecentRecord[];
@@ -94,6 +95,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
const boardAccess = ref<BoardAccess | null>(null);
const reservedGeneralTurns = ref<ReservedTurnView[] | null>(null);
const reservedGeneralRevision = ref(0);
const reservedGeneralAutorunLimit = ref<number | null>(null);
const globalRecords = ref<RecentRecord[]>([]);
const generalRecords = ref<RecentRecord[]>([]);
const worldHistory = ref<RecentRecord[]>([]);
@@ -331,6 +333,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
boardAccess.value = null;
reservedGeneralTurns.value = null;
reservedGeneralRevision.value = 0;
reservedGeneralAutorunLimit.value = null;
resetRecentRecords(null);
commandTableRevision = null;
boardAccessRevision = null;
@@ -351,6 +354,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
boardAccess.value = null;
reservedGeneralTurns.value = null;
reservedGeneralRevision.value = 0;
reservedGeneralAutorunLimit.value = null;
resetRecentRecords(null);
contextRevision = null;
commandTableRevision = null;
@@ -384,6 +388,9 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
if (patch.reservedGeneralRevision !== undefined) {
reservedGeneralRevision.value = patch.reservedGeneralRevision;
}
if (patch.reservedGeneralAutorunLimit !== undefined) {
reservedGeneralAutorunLimit.value = patch.reservedGeneralAutorunLimit;
}
if (patch.globalRecords !== undefined) {
globalRecords.value = structurallyShare(globalRecords.value, patch.globalRecords);
lastGeneralRecordId = Math.max(lastGeneralRecordId, patch.globalRecords[0]?.id ?? 0);
@@ -425,6 +432,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
patch.boardAccess = boardAccessSnapshot ?? null;
patch.reservedGeneralTurns = toRaw(reservedGeneralTurns.value as unknown) as ReservedTurnView[] | null;
patch.reservedGeneralRevision = reservedGeneralRevision.value;
patch.reservedGeneralAutorunLimit = reservedGeneralAutorunLimit.value;
patch.globalRecords = toRaw(globalRecords.value);
patch.generalRecords = toRaw(generalRecords.value);
patch.worldHistory = toRaw(worldHistory.value);
@@ -552,6 +560,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
generalTurns.turns
) as ReservedTurnView[];
reservedGeneralRevision.value = generalTurns.revision;
reservedGeneralAutorunLimit.value = generalTurns.autorunLimit ?? null;
if (records) {
applyRecentRecords(records);
}
@@ -672,6 +681,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
if (generalTurns !== undefined) {
patch.reservedGeneralTurns = generalTurns.turns;
patch.reservedGeneralRevision = generalTurns.revision;
patch.reservedGeneralAutorunLimit = generalTurns.autorunLimit ?? null;
}
if (records) {
const nextGlobalRecords = mergeRecentRecords(globalRecords.value, records.global);
@@ -836,12 +846,14 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
});
reservedGeneralTurns.value = result.turns;
reservedGeneralRevision.value = result.revision;
reservedGeneralAutorunLimit.value = result.autorunLimit ?? null;
} catch (err) {
error.value = resolveErrorMessage(err);
const snapshot = await trpc.turns.reserved.getGeneral.query({ generalId: id }).catch(() => null);
if (snapshot) {
reservedGeneralTurns.value = snapshot.turns;
reservedGeneralRevision.value = snapshot.revision;
reservedGeneralAutorunLimit.value = snapshot.autorunLimit ?? null;
}
}
};
@@ -859,12 +871,14 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
});
reservedGeneralTurns.value = result.turns;
reservedGeneralRevision.value = result.revision;
reservedGeneralAutorunLimit.value = result.autorunLimit ?? null;
} catch (err) {
error.value = resolveErrorMessage(err);
const snapshot = await trpc.turns.reserved.getGeneral.query({ generalId: id }).catch(() => null);
if (snapshot) {
reservedGeneralTurns.value = snapshot.turns;
reservedGeneralRevision.value = snapshot.revision;
reservedGeneralAutorunLimit.value = snapshot.autorunLimit ?? null;
}
}
};
@@ -882,12 +896,14 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
});
reservedGeneralTurns.value = result.turns;
reservedGeneralRevision.value = result.revision;
reservedGeneralAutorunLimit.value = result.autorunLimit ?? null;
} catch (err) {
error.value = resolveErrorMessage(err);
const snapshot = await trpc.turns.reserved.getGeneral.query({ generalId: id }).catch(() => null);
if (snapshot) {
reservedGeneralTurns.value = snapshot.turns;
reservedGeneralRevision.value = snapshot.revision;
reservedGeneralAutorunLimit.value = snapshot.autorunLimit ?? null;
}
}
};
@@ -903,12 +919,14 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
});
reservedGeneralTurns.value = result.turns;
reservedGeneralRevision.value = result.revision;
reservedGeneralAutorunLimit.value = result.autorunLimit ?? null;
} catch (err) {
error.value = resolveErrorMessage(err);
const snapshot = await trpc.turns.reserved.getGeneral.query({ generalId: id }).catch(() => null);
if (snapshot) {
reservedGeneralTurns.value = snapshot.turns;
reservedGeneralRevision.value = snapshot.revision;
reservedGeneralAutorunLimit.value = snapshot.autorunLimit ?? null;
}
}
};
@@ -1192,6 +1210,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
messageContacts,
boardAccess,
reservedGeneralTurns,
reservedGeneralAutorunLimit,
globalRecords,
generalRecords,
worldHistory,
+13 -11
View File
@@ -46,6 +46,7 @@ const {
messages,
boardAccess,
reservedGeneralTurns,
reservedGeneralAutorunLimit,
globalRecords,
generalRecords,
worldHistory,
@@ -218,6 +219,7 @@ watch(
:current-year="lobbyInfo?.year"
:current-month="lobbyInfo?.month"
:turn-term-minutes="lobbyInfo?.turnTerm"
:autorun-limit="reservedGeneralAutorunLimit"
@set-general-turns="reserveGeneralTurns"
@shift-general-turns="shiftGeneralTurns"
@repeat-general-turns="repeatGeneralTurns"
@@ -341,6 +343,7 @@ watch(
:current-year="lobbyInfo?.year"
:current-month="lobbyInfo?.month"
:turn-term-minutes="lobbyInfo?.turnTerm"
:autorun-limit="reservedGeneralAutorunLimit"
@set-general-turns="reserveGeneralTurns"
@shift-general-turns="shiftGeneralTurns"
@repeat-general-turns="repeatGeneralTurns"
@@ -571,6 +574,7 @@ button {
.layout-desktop {
display: grid;
grid-template-columns: repeat(10, minmax(0, 1fr));
grid-template-rows: 520px minmax(125px, auto) auto;
gap: 0;
align-items: start;
}
@@ -584,32 +588,30 @@ button {
.layout-desktop > [data-main-target='commands'] {
grid-column: 8 / 11;
grid-row: 1;
height: 645px;
grid-row: 1 / 3;
min-height: 645px;
width: 290px;
margin-left: 10px;
overflow-y: auto;
overflow: visible;
}
.layout-desktop > [data-main-target='city'] {
grid-column: 1 / 8;
grid-row: 1;
grid-row: 2;
align-self: stretch;
min-height: 125px;
margin-top: 520px;
}
.layout-desktop > [data-main-target='nation'] {
grid-column: 1 / 6;
grid-row: 1;
grid-row: 3;
min-height: 193px;
margin-top: 645px;
}
.layout-desktop > [data-main-target='general'] {
grid-column: 6 / 11;
grid-row: 1;
grid-row: 3;
min-height: 193px;
margin-top: 645px;
}
.layout-desktop > [data-main-target],
@@ -719,8 +721,8 @@ button {
}
.layout-mobile [data-main-target='commands'] {
height: 645px;
overflow-y: auto;
min-height: 645px;
overflow: visible;
}
.layout-mobile [data-main-target='nation'],