feat: split seatmap admin and readonly flows

This commit is contained in:
hyunho
2026-03-26 11:32:33 +09:00
parent 8efb5da65f
commit 69a14fab51
7 changed files with 852 additions and 182 deletions

View File

@@ -593,8 +593,8 @@ function updateFabMenu() {
const menu = document.getElementById('fab-menu');
let html = '<button class="fab-sub shadow-xl" data-label="리스트" onclick="openListViewModal(event)">📋</button>';
html += '<button class="fab-sub shadow-xl" data-label="조직도 인쇄(A3)" onclick="printA3()">🖨️</button>';
html += '<button class="fab-sub shadow-xl" data-label="자리배치도" onclick="openSeatMapView(event)">🪑</button>';
if (isAdmin) {
html += '<button class="fab-sub shadow-xl" data-label="자리배치도" onclick="openSeatMapView(event)">🪑</button>';
html += '<button class="fab-sub shadow-xl" data-label="조직현황 업로드" onclick="triggerUpload(event)">⬆️</button>';
html += '<button class="fab-sub shadow-xl" data-label="신규 구성원" onclick="openAddModal(event)">👤</button>';
html += '<button class="fab-sub shadow-xl" data-label="신규 팀/그룹/셀" onclick="openUnitAddModal(event)">🏢</button>';
@@ -606,7 +606,7 @@ function openSeatMapView(event) {
event.stopPropagation();
document.getElementById('fab-container').classList.remove('active');
if (window.parent && window.parent !== window) {
window.parent.postMessage({ type: 'open-seatmap' }, '*');
window.parent.postMessage({ type: 'open-seatmap', readOnly: !isAdmin }, '*');
}
}
@@ -960,6 +960,22 @@ async function hydrateMemberSeatPreview(member) {
if (!frame.contentWindow) {
return;
}
frame.contentWindow.postMessage({
type: 'seatmap-set-assignments',
items: Array.isArray(layout?.placements) && Array.isArray(layout?.members) && Array.isArray(layout?.slots)
? layout.placements.map((placement) => {
const slot = layout.slots.find((item) => Number(item.id) === Number(placement.seat_slot_id));
const memberItem = layout.members.find((item) => Number(item.id) === Number(placement.member_id));
if (!slot || !memberItem) return null;
return {
key: String(slot.slot_key || ''),
member_id: Number(memberItem.id),
name: memberItem.name || '-',
rank: memberItem.rank || '-',
};
}).filter(Boolean)
: [],
}, window.location.origin);
frame.contentWindow.postMessage({ type: 'seatmap-set-mode', mode: 'compact' }, window.location.origin);
frame.contentWindow.postMessage({ type: 'seatmap-focus-chair', key: seatInfo.slotKey, padding: 2600 }, window.location.origin);
}, { once: true });
@@ -1023,6 +1039,7 @@ function openModal(id) {
}
document.getElementById('modal-title').innerText = id ? '구성원 정보 수정' : '신규 구성원 추가';
modal.querySelector('.modal-content').classList.add('wide');
fieldsArea.className = 'flex flex-col w-full';
fieldsArea.style.maxHeight = 'none';
fieldsArea.style.overflowY = 'visible';
@@ -1078,44 +1095,47 @@ function openModal(id) {
<input type="hidden" id="m-id" value="${id || ''}">
<input type="hidden" id="m-photo-hidden" value="${member['사진'] || ''}">
<input type="hidden" id="m-seat-hidden" value="${member['자리위치'] || ''}">
<div class="col-span-2 member-basic-top-row">
<div class="member-photo-field">
<label class="text-[11px] font-black text-slate-600 block">프로필 사진</label>
<div class="member-photo-upload-card member-photo-upload-card-compact">
<div class="member-photo-preview-wrap">
<img id="m-photo-preview" src="${member['사진'] || getPhotoPlaceholder(member['이름'] || '')}" alt="프로필 미리보기" class="member-photo-preview">
<div class="col-span-2 member-edit-layout">
<div class="member-edit-left-pane">
<div class="member-photo-field">
<label class="text-[11px] font-black text-slate-600 block">프로필 사진</label>
<div class="member-photo-upload-card member-photo-upload-card-compact">
<div class="member-photo-preview-wrap">
<img id="m-photo-preview" src="${member['사진'] || getPhotoPlaceholder(member['이름'] || '')}" alt="프로필 미리보기" class="member-photo-preview">
</div>
<div class="member-photo-upload-controls">
<label class="member-photo-file-label" for="m-photo-file">
<input id="m-photo-file" type="file" accept="image/png,image/jpeg,image/webp,image/gif" onchange="handlePhotoFileChange(event)">
<span>사진 파일 선택</span>
</label>
<strong id="m-photo-file-name" class="member-photo-file-name">선택된 파일 없음</strong>
</div>
</div>
<div class="member-photo-upload-controls">
<label class="member-photo-file-label" for="m-photo-file">
<input id="m-photo-file" type="file" accept="image/png,image/jpeg,image/webp,image/gif" onchange="handlePhotoFileChange(event)">
<span>사진 파일 선택</span>
</label>
<strong id="m-photo-file-name" class="member-photo-file-name">선택된 파일 없음</strong>
</div>
<div class="member-seat-field">
<div id="member-seat-preview">${renderSeatPreviewCard({ assigned: false, seatLabel: member['자리위치'] || '', seatMapName: '자리배치도', slotKey: '' })}</div>
</div>
</div>
<div class="member-edit-right-pane">
<div class="member-name-field">
<label class="text-[11px] font-black text-slate-600 block">이름 (필수)</label>
<input id="m-name" value="${member['이름'] || ''}" oninput="syncPhotoPreviewFromUrl()" class="w-full bg-slate-50 p-3 rounded-xl border font-bold text-sm outline-none">
<div class="member-inline-info-grid member-inline-info-grid-edit">
<div class="member-inline-info-card">
<label>사번</label>
<input id="m-employee-id" value="${member['사번'] || ''}" class="w-full bg-white p-3 rounded-xl border font-bold text-sm outline-none">
</div>
<div class="member-inline-info-card">
<label>전화번호</label>
<input id="m-phone" value="${member['전화번호'] || ''}" class="w-full bg-white p-3 rounded-xl border font-bold text-sm outline-none">
</div>
<div class="member-inline-info-card member-inline-info-card-full">
<label>이메일</label>
<input id="m-email" value="${member['이메일'] || ''}" class="w-full bg-white p-3 rounded-xl border font-bold text-sm outline-none">
</div>
</div>
</div>
</div>
<div class="member-name-field">
<label class="text-[11px] font-black text-slate-600 block">이름 (필수)</label>
<input id="m-name" value="${member['이름'] || ''}" oninput="syncPhotoPreviewFromUrl()" class="w-full bg-slate-50 p-3 rounded-xl border font-bold text-sm outline-none">
<div class="member-inline-info-grid member-inline-info-grid-edit">
<div class="member-inline-info-card">
<label>사번</label>
<input id="m-employee-id" value="${member['사번'] || ''}" class="w-full bg-white p-3 rounded-xl border font-bold text-sm outline-none">
</div>
<div class="member-inline-info-card">
<label>전화번호</label>
<input id="m-phone" value="${member['전화번호'] || ''}" class="w-full bg-white p-3 rounded-xl border font-bold text-sm outline-none">
</div>
<div class="member-inline-info-card member-inline-info-card-full">
<label>이메일</label>
<input id="m-email" value="${member['이메일'] || ''}" class="w-full bg-white p-3 rounded-xl border font-bold text-sm outline-none">
</div>
</div>
</div>
</div>
<div class="col-span-2">
<label class="text-[11px] font-black text-slate-600 block mb-2">자리 위치</label>
${renderSeatPreviewCard(member['자리위치'] || '')}
</div>
</div>
${orgFields}
@@ -1130,6 +1150,9 @@ function openModal(id) {
<button onclick="saveMember()" class="flex-1 bg-indigo-600 text-white py-3.5 rounded-xl font-bold text-sm">저장</button>
`;
modal.style.display = 'flex';
if (id) {
hydrateMemberSeatPreview(member);
}
}
function closeModal() {