merge: 외교권자와 조언자 복수 임명을 main에 반영한다
This commit is contained in:
@@ -153,11 +153,17 @@ export const getPersonnelInfo = accessAuthedProcedure.query(async ({ ctx }) => {
|
|||||||
const canChangePermissions = me.officerLevel === 12;
|
const canChangePermissions = me.officerLevel === 12;
|
||||||
const ambassadors = canChangePermissions
|
const ambassadors = canChangePermissions
|
||||||
? permissionCandidates.filter(
|
? permissionCandidates.filter(
|
||||||
(candidate) => candidate.permission === 'ambassador' || candidate.maxPermission === 4
|
(candidate) =>
|
||||||
|
candidate.permission === 'ambassador' ||
|
||||||
|
(candidate.permission === 'normal' && candidate.maxPermission === 4)
|
||||||
)
|
)
|
||||||
: [];
|
: [];
|
||||||
const auditors = canChangePermissions
|
const auditors = canChangePermissions
|
||||||
? permissionCandidates.filter((candidate) => candidate.permission === 'auditor' || candidate.maxPermission >= 3)
|
? permissionCandidates.filter(
|
||||||
|
(candidate) =>
|
||||||
|
candidate.permission === 'auditor' ||
|
||||||
|
(candidate.permission === 'normal' && candidate.maxPermission >= 3)
|
||||||
|
)
|
||||||
: [];
|
: [];
|
||||||
const generalNameMap = new Map(mappedGenerals.map((general) => [general.id, general.name]));
|
const generalNameMap = new Map(mappedGenerals.map((general) => [general.id, general.name]));
|
||||||
const awards = {
|
const awards = {
|
||||||
|
|||||||
@@ -230,6 +230,45 @@ describe('nation personnel router', () => {
|
|||||||
expect(result.awards.eagles).toEqual([{ id: 30, name: '군사', value: 7 }]);
|
expect(result.awards.eagles).toEqual([{ id: 30, name: '군사', value: 7 }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps ambassador and auditor candidate pools mutually exclusive like Ref', async () => {
|
||||||
|
const me = { ...baseGeneral, officerLevel: 12 };
|
||||||
|
const rows = [
|
||||||
|
listRow({ id: 22, name: '군주', officerLevel: 12 }),
|
||||||
|
listRow({ id: 30, name: '현 외교권자', meta: { belong: 5, permission: 'ambassador' } }),
|
||||||
|
listRow({ id: 31, name: '현 조언자', meta: { belong: 5, permission: 'auditor' } }),
|
||||||
|
listRow({ id: 32, name: '일반 후보' }),
|
||||||
|
listRow({ id: 33, name: '외교 금지', penalty: { noAmbassador: true } }),
|
||||||
|
];
|
||||||
|
const context = createContext({
|
||||||
|
me,
|
||||||
|
db: {
|
||||||
|
nation: {
|
||||||
|
findUnique: vi.fn(async () => ({
|
||||||
|
id: 1,
|
||||||
|
name: '위',
|
||||||
|
color: '#777777',
|
||||||
|
level: 3,
|
||||||
|
typeCode: 'che_법가',
|
||||||
|
capitalCityId: 1,
|
||||||
|
meta: { chief_set: 0 },
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
city: { findMany: vi.fn(async () => []) },
|
||||||
|
troop: { findMany: vi.fn(async () => []) },
|
||||||
|
general: {
|
||||||
|
findFirst: vi.fn(async () => me),
|
||||||
|
findMany: vi.fn(async () => rows),
|
||||||
|
},
|
||||||
|
worldState: { findFirst: vi.fn(async () => ({ config: { stat: { chiefMin: 65 } } })) },
|
||||||
|
rankData: { findMany: vi.fn(async () => []) },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await appRouter.createCaller(context).nation.getPersonnelInfo();
|
||||||
|
expect(result.permissionCandidates.ambassadors.map((candidate) => candidate.id)).toEqual([30, 32]);
|
||||||
|
expect(result.permissionCandidates.auditors.map((candidate) => candidate.id)).toEqual([31, 32]);
|
||||||
|
});
|
||||||
|
|
||||||
it('allows finance mutations only for a head officer or an eligible ambassador', async () => {
|
it('allows finance mutations only for a head officer or an eligible ambassador', async () => {
|
||||||
const nationDb = {
|
const nationDb = {
|
||||||
nation: {
|
nation: {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type FixtureState = {
|
|||||||
appointedGeneralId?: number;
|
appointedGeneralId?: number;
|
||||||
appointedCityId?: number;
|
appointedCityId?: number;
|
||||||
appointedOfficerLevel?: number;
|
appointedOfficerLevel?: number;
|
||||||
|
permissionMutationInput?: { isAmbassador: boolean; targetGeneralIds: number[] };
|
||||||
noticeMutationInput?: string;
|
noticeMutationInput?: string;
|
||||||
scoutMutationInput?: string;
|
scoutMutationInput?: string;
|
||||||
uploadDataUrl?: string;
|
uploadDataUrl?: string;
|
||||||
@@ -101,6 +102,8 @@ const personnelFixture = (state: FixtureState) => {
|
|||||||
general(5, '정욱', 2),
|
general(5, '정욱', 2),
|
||||||
general(6, '장료', 1),
|
general(6, '장료', 1),
|
||||||
general(7, '허저', 1, { permission: 'ambassador' }),
|
general(7, '허저', 1, { permission: 'ambassador' }),
|
||||||
|
general(8, '가후', 1, { permission: 'auditor' }),
|
||||||
|
general(9, '전위', 1),
|
||||||
];
|
];
|
||||||
const visibleGenerals =
|
const visibleGenerals =
|
||||||
state.role === 'member' ? fullGenerals.filter((entry) => entry.officerLevel >= 2) : fullGenerals;
|
state.role === 'member' ? fullGenerals.filter((entry) => entry.officerLevel >= 2) : fullGenerals;
|
||||||
@@ -145,8 +148,13 @@ const personnelFixture = (state: FixtureState) => {
|
|||||||
ambassadors: [
|
ambassadors: [
|
||||||
{ id: 6, name: '장료', npcState: 0, permission: 'normal', maxPermission: 4 },
|
{ id: 6, name: '장료', npcState: 0, permission: 'normal', maxPermission: 4 },
|
||||||
{ id: 7, name: '허저', npcState: 0, permission: 'ambassador', maxPermission: 4 },
|
{ id: 7, name: '허저', npcState: 0, permission: 'ambassador', maxPermission: 4 },
|
||||||
|
{ id: 9, name: '전위', npcState: 0, permission: 'normal', maxPermission: 4 },
|
||||||
|
],
|
||||||
|
auditors: [
|
||||||
|
{ id: 6, name: '장료', npcState: 0, permission: 'normal', maxPermission: 4 },
|
||||||
|
{ id: 8, name: '가후', npcState: 0, permission: 'auditor', maxPermission: 4 },
|
||||||
|
{ id: 9, name: '전위', npcState: 0, permission: 'normal', maxPermission: 4 },
|
||||||
],
|
],
|
||||||
auditors: [{ id: 6, name: '장료', npcState: 0, permission: 'normal', maxPermission: 4 }],
|
|
||||||
}
|
}
|
||||||
: { ambassadors: [], auditors: [] },
|
: { ambassadors: [], auditors: [] },
|
||||||
};
|
};
|
||||||
@@ -238,7 +246,16 @@ const installFixture = async (page: Page, state: FixtureState) => {
|
|||||||
state.appointedOfficerLevel = Number(jsonInput.officerLevel ?? 0);
|
state.appointedOfficerLevel = Number(jsonInput.officerLevel ?? 0);
|
||||||
return response({ ok: true });
|
return response({ ok: true });
|
||||||
}
|
}
|
||||||
if (operation === 'nation.kick' || operation === 'nation.changePermission') return response({ ok: true });
|
if (operation === 'nation.changePermission') {
|
||||||
|
state.permissionMutationInput = {
|
||||||
|
isAmbassador: jsonInput.isAmbassador === true,
|
||||||
|
targetGeneralIds: Array.isArray(jsonInput.targetGeneralIds)
|
||||||
|
? jsonInput.targetGeneralIds.map((id) => Number(id))
|
||||||
|
: [],
|
||||||
|
};
|
||||||
|
return response({ ok: true });
|
||||||
|
}
|
||||||
|
if (operation === 'nation.kick') return response({ ok: true });
|
||||||
if (operation === 'nation.setRate') {
|
if (operation === 'nation.setRate') {
|
||||||
if (state.failNextRate) {
|
if (state.failNextRate) {
|
||||||
state.failNextRate = false;
|
state.failNextRate = false;
|
||||||
@@ -347,6 +364,62 @@ test('personnel keeps the desktop frame while exposing row-level appointment con
|
|||||||
await screenshot(page, 'core-personnel-desktop-leader.png');
|
await screenshot(page, 'core-personnel-desktop-leader.png');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('leader can grant two ambassador and auditor permissions by click or touch without modifier keys', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const state: FixtureState = { role: 'leader', rate: 20 };
|
||||||
|
await installFixture(page, state);
|
||||||
|
await page.setViewportSize({ width: 390, height: 844 });
|
||||||
|
await gotoOffice(page, 'nation/personnel');
|
||||||
|
|
||||||
|
const ambassadorTrigger = page.locator('.permission-multiselect-trigger').first();
|
||||||
|
await expect(ambassadorTrigger).toHaveAccessibleName('외교권자 선택, 현재 1명');
|
||||||
|
await ambassadorTrigger.click();
|
||||||
|
const ambassadorOptions = page.getByRole('listbox', { name: '외교권자 후보' });
|
||||||
|
await expect(ambassadorOptions).toBeVisible();
|
||||||
|
await expect(ambassadorOptions.getByRole('option', { name: '허저' })).toHaveAttribute('aria-selected', 'true');
|
||||||
|
await ambassadorOptions.getByRole('option', { name: '장료' }).click();
|
||||||
|
await expect(ambassadorOptions.getByRole('option', { name: '장료' })).toHaveAttribute('aria-selected', 'true');
|
||||||
|
await expect(ambassadorTrigger).toHaveAccessibleName('외교권자 선택, 현재 2명');
|
||||||
|
|
||||||
|
await ambassadorOptions.getByRole('option', { name: '전위' }).click();
|
||||||
|
await expect(page.getByTestId('game-toast')).toContainText('최대 2명까지 설정 가능합니다.');
|
||||||
|
await expect(ambassadorOptions.getByRole('option', { name: '전위' })).toHaveAttribute('aria-selected', 'false');
|
||||||
|
const ambassadorGeometry = await ambassadorOptions.evaluate((element) => {
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
return { left: rect.left, right: rect.right, width: rect.width };
|
||||||
|
});
|
||||||
|
expect(ambassadorGeometry.left).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(ambassadorGeometry.right).toBeLessThanOrEqual(390);
|
||||||
|
expect(ambassadorGeometry.width).toBeGreaterThanOrEqual(100);
|
||||||
|
await screenshot(page, 'core-personnel-mobile-permission-picker-open.png');
|
||||||
|
|
||||||
|
page.once('dialog', async (dialog) => {
|
||||||
|
expect(dialog.message()).toBe('외교권자를 변경할까요?');
|
||||||
|
await dialog.accept();
|
||||||
|
});
|
||||||
|
await page.getByRole('button', { name: '외교권자 임명 반영' }).click();
|
||||||
|
await expect(page.getByTestId('game-toast').filter({ hasText: '권한을 변경했습니다.' })).toBeVisible();
|
||||||
|
await expect.poll(() => state.permissionMutationInput).toEqual({ isAmbassador: true, targetGeneralIds: [7, 6] });
|
||||||
|
|
||||||
|
const auditorTrigger = page.locator('.permission-multiselect-trigger').nth(1);
|
||||||
|
await expect(auditorTrigger).toHaveAccessibleName('조언자 선택, 현재 1명');
|
||||||
|
await auditorTrigger.click();
|
||||||
|
const auditorOptions = page.getByRole('listbox', { name: '조언자 후보' });
|
||||||
|
await expect(auditorOptions.getByRole('option', { name: '허저' })).toHaveCount(0);
|
||||||
|
await auditorOptions.getByRole('option', { name: '장료' }).click();
|
||||||
|
await expect(auditorTrigger).toHaveAccessibleName('조언자 선택, 현재 2명');
|
||||||
|
page.once('dialog', async (dialog) => {
|
||||||
|
expect(dialog.message()).toBe('조언자를 변경할까요?');
|
||||||
|
await dialog.accept();
|
||||||
|
});
|
||||||
|
await page.getByRole('button', { name: '조언자 임명 반영' }).click();
|
||||||
|
await expect(page.getByTestId('game-toast').filter({ hasText: '권한을 변경했습니다.' })).toBeVisible();
|
||||||
|
await expect.poll(() => state.permissionMutationInput).toEqual({ isAmbassador: false, targetGeneralIds: [8, 6] });
|
||||||
|
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(390);
|
||||||
|
await screenshot(page, 'core-personnel-mobile-permission-picker.png');
|
||||||
|
});
|
||||||
|
|
||||||
test('personnel selects an informed general and reports the JosaUtil-composed result in a toast', async ({ page }) => {
|
test('personnel selects an informed general and reports the JosaUtil-composed result in a toast', async ({ page }) => {
|
||||||
const state: FixtureState = { role: 'head', rate: 20 };
|
const state: FixtureState = { role: 'head', rate: 20 };
|
||||||
await installFixture(page, state);
|
await installFixture(page, state);
|
||||||
|
|||||||
@@ -0,0 +1,232 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
type PermissionCandidate = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
modelValue: number[];
|
||||||
|
candidates: PermissionCandidate[];
|
||||||
|
label: string;
|
||||||
|
max?: number;
|
||||||
|
}>(),
|
||||||
|
{ max: 2 }
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: number[]];
|
||||||
|
limit: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const root = ref<HTMLElement | null>(null);
|
||||||
|
const open = ref(false);
|
||||||
|
const selectedCandidates = computed(() => {
|
||||||
|
const selected = new Set(props.modelValue);
|
||||||
|
return props.candidates.filter((candidate) => selected.has(candidate.id));
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleOpen = (): void => {
|
||||||
|
open.value = !open.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleCandidate = (id: number): void => {
|
||||||
|
if (props.modelValue.includes(id)) {
|
||||||
|
emit(
|
||||||
|
'update:modelValue',
|
||||||
|
props.modelValue.filter((selectedId) => selectedId !== id)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (props.modelValue.length >= props.max) {
|
||||||
|
emit('limit');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit('update:modelValue', [...props.modelValue, id]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDocumentPointerDown = (event: PointerEvent): void => {
|
||||||
|
if (!root.value?.contains(event.target as Node)) open.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeydown = (event: KeyboardEvent): void => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
event.preventDefault();
|
||||||
|
open.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => document.addEventListener('pointerdown', handleDocumentPointerDown));
|
||||||
|
onBeforeUnmount(() => document.removeEventListener('pointerdown', handleDocumentPointerDown));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="root" class="permission-multiselect" @keydown="handleKeydown">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="permission-multiselect-trigger"
|
||||||
|
:aria-label="`${label} 선택, 현재 ${modelValue.length}명`"
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
:aria-expanded="open"
|
||||||
|
@click="toggleOpen"
|
||||||
|
>
|
||||||
|
<span v-if="selectedCandidates.length" class="permission-multiselect-values">
|
||||||
|
<span v-for="candidate in selectedCandidates" :key="candidate.id">{{ candidate.name }}</span>
|
||||||
|
</span>
|
||||||
|
<span v-else class="permission-multiselect-placeholder">선택 안 함</span>
|
||||||
|
<span class="permission-multiselect-arrow" aria-hidden="true">▾</span>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-if="open"
|
||||||
|
class="permission-multiselect-options"
|
||||||
|
role="listbox"
|
||||||
|
aria-multiselectable="true"
|
||||||
|
:aria-label="`${label} 후보`"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="candidate in candidates"
|
||||||
|
:key="candidate.id"
|
||||||
|
type="button"
|
||||||
|
class="permission-multiselect-option"
|
||||||
|
role="option"
|
||||||
|
:aria-selected="modelValue.includes(candidate.id)"
|
||||||
|
@click="toggleCandidate(candidate.id)"
|
||||||
|
>
|
||||||
|
<span class="permission-multiselect-check" aria-hidden="true">
|
||||||
|
{{ modelValue.includes(candidate.id) ? '✓' : '' }}
|
||||||
|
</span>
|
||||||
|
<span>{{ candidate.name }}</span>
|
||||||
|
</button>
|
||||||
|
<p v-if="candidates.length === 0" class="permission-multiselect-empty">임명 가능한 장수가 없습니다.</p>
|
||||||
|
<p class="permission-multiselect-help">클릭해서 선택·해제 · 최대 {{ max }}명</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.permission-multiselect {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 300px;
|
||||||
|
max-width: calc(100% - 58px);
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.permission-multiselect-trigger {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 34px;
|
||||||
|
border: 1px solid #858585;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 3px 7px;
|
||||||
|
color: #fff;
|
||||||
|
background: #000;
|
||||||
|
font: inherit;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.permission-multiselect-trigger:hover,
|
||||||
|
.permission-multiselect-trigger[aria-expanded='true'] {
|
||||||
|
border-color: #b9b9b9;
|
||||||
|
}
|
||||||
|
.permission-multiselect-trigger:focus-visible,
|
||||||
|
.permission-multiselect-option:focus-visible {
|
||||||
|
outline: 2px solid #fff;
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
.permission-multiselect-values {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 3px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.permission-multiselect-values > span {
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 126px;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 2px 5px;
|
||||||
|
color: #fff;
|
||||||
|
background: #4d4d4d;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.permission-multiselect-placeholder {
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
.permission-multiselect-arrow {
|
||||||
|
margin-left: 5px;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
.permission-multiselect-options {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 40;
|
||||||
|
top: calc(100% + 2px);
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 220px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid #858585;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #fff;
|
||||||
|
background: #101010;
|
||||||
|
box-shadow: 0 5px 14px rgb(0 0 0 / 70%);
|
||||||
|
}
|
||||||
|
.permission-multiselect-option {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 20px minmax(0, 1fr);
|
||||||
|
gap: 5px;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid #353535;
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 7px 8px;
|
||||||
|
color: #fff;
|
||||||
|
background: #101010;
|
||||||
|
font: inherit;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.permission-multiselect-option:hover,
|
||||||
|
.permission-multiselect-option[aria-selected='true'] {
|
||||||
|
background: #424242;
|
||||||
|
}
|
||||||
|
.permission-multiselect-check {
|
||||||
|
display: grid;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
place-items: center;
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
border-radius: 2px;
|
||||||
|
color: #111;
|
||||||
|
background: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.permission-multiselect-empty,
|
||||||
|
.permission-multiselect-help {
|
||||||
|
margin: 0;
|
||||||
|
padding: 6px 8px;
|
||||||
|
color: #bbb;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.permission-multiselect-help {
|
||||||
|
border-top: 1px solid #454545;
|
||||||
|
}
|
||||||
|
@media (max-width: 620px) {
|
||||||
|
.permission-multiselect {
|
||||||
|
width: calc(100% - 54px);
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
.permission-multiselect-values > span {
|
||||||
|
max-width: 82px;
|
||||||
|
}
|
||||||
|
.permission-multiselect-option {
|
||||||
|
min-height: 38px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -4,6 +4,7 @@ import { useRouter } from 'vue-router';
|
|||||||
|
|
||||||
import { JosaUtil } from '@sammo-ts/common/util/JosaUtil';
|
import { JosaUtil } from '@sammo-ts/common/util/JosaUtil';
|
||||||
|
|
||||||
|
import PermissionMultiSelect from '../components/personnel/PermissionMultiSelect.vue';
|
||||||
import PersonnelSelectionDialog from '../components/personnel/PersonnelSelectionDialog.vue';
|
import PersonnelSelectionDialog from '../components/personnel/PersonnelSelectionDialog.vue';
|
||||||
import { useGameFeedback } from '../composables/useGameFeedback';
|
import { useGameFeedback } from '../composables/useGameFeedback';
|
||||||
import { resolveGeneralIconBackgroundImage } from '../utils/generalIcon';
|
import { resolveGeneralIconBackgroundImage } from '../utils/generalIcon';
|
||||||
@@ -251,9 +252,7 @@ const applySelection = async (id: number): Promise<void> => {
|
|||||||
else await appointCityOfficer(context.level, context.cityId, id);
|
else await appointCityOfficer(context.level, context.cityId, id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const enforcePermissionLimit = (selection: number[]) => {
|
const reportPermissionLimit = () => {
|
||||||
if (selection.length <= 2) return;
|
|
||||||
selection.splice(0, selection.length - 2);
|
|
||||||
showErrorToast('최대 2명까지 설정 가능합니다.');
|
showErrorToast('최대 2명까지 설정 가능합니다.');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -390,23 +389,16 @@ onMounted(() => void loadPersonnel());
|
|||||||
<tr>
|
<tr>
|
||||||
<td class="green-cell permission-label">외교권자</td>
|
<td class="green-cell permission-label">외교권자</td>
|
||||||
<td>
|
<td>
|
||||||
<select
|
<PermissionMultiSelect
|
||||||
v-model="ambassadorSelection"
|
v-model="ambassadorSelection"
|
||||||
multiple
|
label="외교권자"
|
||||||
aria-label="외교권자"
|
:candidates="data.permissionCandidates.ambassadors"
|
||||||
@change="enforcePermissionLimit(ambassadorSelection)"
|
@limit="reportPermissionLimit"
|
||||||
>
|
/>
|
||||||
<option
|
|
||||||
v-for="candidate in data.permissionCandidates.ambassadors"
|
|
||||||
:key="candidate.id"
|
|
||||||
:value="candidate.id"
|
|
||||||
>
|
|
||||||
{{ candidate.name }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<button
|
<button
|
||||||
class="legacy-button legacy-button--primary"
|
class="legacy-button legacy-button--primary"
|
||||||
type="button"
|
type="button"
|
||||||
|
aria-label="외교권자 임명 반영"
|
||||||
@click="changePermissions(true)"
|
@click="changePermissions(true)"
|
||||||
>
|
>
|
||||||
임명
|
임명
|
||||||
@@ -414,23 +406,16 @@ onMounted(() => void loadPersonnel());
|
|||||||
</td>
|
</td>
|
||||||
<td class="green-cell permission-label">조언자</td>
|
<td class="green-cell permission-label">조언자</td>
|
||||||
<td>
|
<td>
|
||||||
<select
|
<PermissionMultiSelect
|
||||||
v-model="auditorSelection"
|
v-model="auditorSelection"
|
||||||
multiple
|
label="조언자"
|
||||||
aria-label="조언자"
|
:candidates="data.permissionCandidates.auditors"
|
||||||
@change="enforcePermissionLimit(auditorSelection)"
|
@limit="reportPermissionLimit"
|
||||||
>
|
/>
|
||||||
<option
|
|
||||||
v-for="candidate in data.permissionCandidates.auditors"
|
|
||||||
:key="candidate.id"
|
|
||||||
:value="candidate.id"
|
|
||||||
>
|
|
||||||
{{ candidate.name }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<button
|
<button
|
||||||
class="legacy-button legacy-button--primary"
|
class="legacy-button legacy-button--primary"
|
||||||
type="button"
|
type="button"
|
||||||
|
aria-label="조언자 임명 반영"
|
||||||
@click="changePermissions(false)"
|
@click="changePermissions(false)"
|
||||||
>
|
>
|
||||||
임명
|
임명
|
||||||
@@ -666,10 +651,6 @@ select {
|
|||||||
background: #000;
|
background: #000;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
}
|
}
|
||||||
select[multiple] {
|
|
||||||
width: 300px;
|
|
||||||
height: 34px;
|
|
||||||
}
|
|
||||||
.nation-heading {
|
.nation-heading {
|
||||||
height: 32px;
|
height: 32px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user