feat: refine seat assignment flow and profile seat preview
This commit is contained in:
@@ -541,6 +541,14 @@ body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.seat-preview-frame {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
border: 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.seat-preview-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -551,6 +559,7 @@ body {
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
|
||||
.seat-preview-placeholder-icon {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
|
||||
@@ -6,6 +6,7 @@ let collapsedUnits = new Set();
|
||||
let isListMode = false;
|
||||
let emptyStateMessage = '서버에 조직 데이터가 없습니다. 상단의 업로드 버튼으로 초기 데이터를 넣어주세요.';
|
||||
let photoPreviewObjectUrl = null;
|
||||
let seatMapLayoutCache = null;
|
||||
|
||||
const levelOrder = ['부서', '그룹', '디비전', '팀', '셀'];
|
||||
const dropdownFields = ['소속회사', '직급', '직책', ...levelOrder];
|
||||
@@ -146,6 +147,44 @@ async function loadMembers(message) {
|
||||
render();
|
||||
}
|
||||
|
||||
async function loadActiveSeatMapLayout(force = false) {
|
||||
if (seatMapLayoutCache && !force) {
|
||||
return seatMapLayoutCache;
|
||||
}
|
||||
try {
|
||||
const activePayload = await apiFetch('/api/seat-maps/active');
|
||||
const seatMap = activePayload?.item;
|
||||
if (!seatMap?.id) {
|
||||
seatMapLayoutCache = null;
|
||||
return null;
|
||||
}
|
||||
const layoutPayload = await apiFetch(`/api/seat-maps/${seatMap.id}/layout`);
|
||||
seatMapLayoutCache = layoutPayload;
|
||||
return layoutPayload;
|
||||
} catch {
|
||||
seatMapLayoutCache = null;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getMemberSeatInfo(layout, memberId) {
|
||||
if (!layout || !memberId) {
|
||||
return null;
|
||||
}
|
||||
const placement = (layout.placements || []).find((item) => Number(item.member_id) === Number(memberId));
|
||||
if (!placement) {
|
||||
return null;
|
||||
}
|
||||
const slot = (layout.slots || []).find((item) => Number(item.id) === Number(placement.seat_slot_id));
|
||||
return {
|
||||
seatMapId: layout.seat_map?.id || null,
|
||||
seatMapName: layout.seat_map?.name || '자리배치도',
|
||||
seatLabel: placement.seat_label || slot?.label || '',
|
||||
slotKey: slot?.slot_key || '',
|
||||
assigned: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function syncMembers(nextMembers) {
|
||||
const payload = await apiFetch('/api/members/bulk-sync', {
|
||||
method: 'PUT',
|
||||
@@ -846,31 +885,86 @@ function handlePhotoFileChange(event) {
|
||||
updatePhotoPreview(photoPreviewObjectUrl, name);
|
||||
}
|
||||
|
||||
function renderSeatPreviewCard(seatLabel) {
|
||||
const safeLabel = escapeHtml(seatLabel || '');
|
||||
const badge = safeLabel
|
||||
? `<span class="seat-preview-badge">${safeLabel}</span>`
|
||||
function renderSeatPreviewCard(seatInfo) {
|
||||
const assigned = Boolean(seatInfo?.assigned);
|
||||
const safeLabel = escapeHtml(seatInfo?.seatLabel || '');
|
||||
const safeSeatMapName = escapeHtml(seatInfo?.seatMapName || '자리배치도');
|
||||
const safeSlotKey = escapeHtml(seatInfo?.slotKey || '');
|
||||
const badge = assigned
|
||||
? `<span class="seat-preview-badge">${safeLabel || '배치완료'}</span>`
|
||||
: '<span class="seat-preview-badge seat-preview-badge-muted">미배치</span>';
|
||||
const body = assigned
|
||||
? `
|
||||
<iframe
|
||||
id="member-seat-preview-frame"
|
||||
class="seat-preview-frame"
|
||||
src="/api/seat-maps/${Number(seatInfo.seatMapId || 0)}/viewer"
|
||||
title="${safeSeatMapName} 좌석 미리보기"
|
||||
loading="eager"
|
||||
referrerpolicy="same-origin"
|
||||
></iframe>
|
||||
`
|
||||
: `
|
||||
<div class="seat-preview-placeholder">
|
||||
<span class="seat-preview-placeholder-icon">⌖</span>
|
||||
<span>현재 공석 또는 미배치 상태입니다.</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return `
|
||||
<div class="seat-preview-card">
|
||||
<div class="seat-preview-head">
|
||||
<div>
|
||||
<strong>재석위치</strong>
|
||||
<p>향후 해당 인원의 좌석 영역을 크롭해 표시하고, 스크롤 확대/축소를 지원할 예정입니다.</p>
|
||||
<p>${assigned ? '현재 자리배치도 기준으로 배치된 좌석 정보를 표시합니다.' : '현재 자리배치도에서 배치된 좌석이 없습니다.'}</p>
|
||||
</div>
|
||||
${badge}
|
||||
</div>
|
||||
<div class="seat-preview-canvas">
|
||||
<div class="seat-preview-placeholder">
|
||||
<span class="seat-preview-placeholder-icon">⌖</span>
|
||||
<span>좌석 이미지 연동 예정</span>
|
||||
</div>
|
||||
${body}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async function hydrateMemberSeatPreview(member) {
|
||||
const target = document.getElementById('member-seat-preview');
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
target.innerHTML = renderSeatPreviewCard({
|
||||
assigned: false,
|
||||
seatMapName: '자리배치도',
|
||||
seatLabel: member['자리위치'] || '',
|
||||
slotKey: '',
|
||||
});
|
||||
const layout = await loadActiveSeatMapLayout(true);
|
||||
if (!document.getElementById('member-seat-preview')) {
|
||||
return;
|
||||
}
|
||||
const seatInfo = getMemberSeatInfo(layout, member.id) || {
|
||||
seatMapName: layout?.seat_map?.name || '자리배치도',
|
||||
seatLabel: member['자리위치'] || '',
|
||||
slotKey: '',
|
||||
assigned: Boolean(member['자리위치']),
|
||||
};
|
||||
target.innerHTML = renderSeatPreviewCard(seatInfo);
|
||||
if (!seatInfo.assigned || !seatInfo.seatMapId || !seatInfo.slotKey) {
|
||||
return;
|
||||
}
|
||||
const frame = document.getElementById('member-seat-preview-frame');
|
||||
if (!frame) {
|
||||
return;
|
||||
}
|
||||
frame.addEventListener('load', () => {
|
||||
if (!frame.contentWindow) {
|
||||
return;
|
||||
}
|
||||
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 });
|
||||
}
|
||||
|
||||
function switchModalTab(tab) {
|
||||
const isBasic = tab === 'basic';
|
||||
document.getElementById('modal-sec-basic').classList.toggle('hidden', !isBasic);
|
||||
@@ -919,11 +1013,12 @@ function openModal(id) {
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full mt-2">
|
||||
${renderSeatPreviewCard(member['자리위치'] || '')}
|
||||
<div id="member-seat-preview">${renderSeatPreviewCard({ assigned: false, seatLabel: member['자리위치'] || '', seatMapName: '자리배치도', slotKey: '' })}</div>
|
||||
</div>
|
||||
`;
|
||||
footer.innerHTML = '<button onclick="closeModal()" class="w-full bg-slate-800 text-white py-4 rounded-xl font-bold text-sm shadow-lg">닫기</button>';
|
||||
modal.style.display = 'flex';
|
||||
hydrateMemberSeatPreview(member);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user