Fix seatmap slot matching and update member modal layout

This commit is contained in:
hyunho
2026-03-27 18:12:20 +09:00
parent d66614123e
commit 24852d4401
11 changed files with 517 additions and 89 deletions

View File

@@ -351,11 +351,21 @@ body {
gap: 12px;
}
.member-edit-profile-card {
display: flex;
flex-direction: column;
gap: 12px;
}
.member-seat-field {
flex: 1 1 auto;
min-height: 0;
}
.member-seat-field-emphasis .seat-preview-card {
min-height: 100%;
}
.member-detail-top-row {
width: 100%;
display: grid;
@@ -386,6 +396,17 @@ body {
margin-top: 10px;
}
.member-inline-info-grid-stacked {
grid-template-columns: minmax(0, 1fr);
margin-top: 0;
}
.member-name-field-compact {
display: flex;
flex-direction: column;
gap: 6px;
}
.member-inline-info-card {
min-width: 0;
display: flex;
@@ -496,6 +517,7 @@ body {
border-radius: 18px;
background: linear-gradient(180deg, #f8fafc 0%, #eef2ff 100%);
overflow: hidden;
min-height: 100%;
}
.seat-preview-head {
@@ -579,6 +601,32 @@ body {
font-weight: 900;
}
.member-edit-right-pane .seat-preview-head {
padding: 18px 20px 12px;
}
.member-edit-right-pane .seat-preview-head strong {
font-size: 18px;
}
.member-edit-right-pane .seat-preview-head p {
font-size: 12px;
}
.member-edit-right-pane .seat-preview-badge {
font-size: 12px;
padding: 8px 12px;
}
.member-edit-right-pane .seat-preview-canvas {
min-height: 360px;
}
.member-edit-right-pane .seat-preview-frame,
.member-edit-right-pane .seat-preview-placeholder {
min-height: 320px;
}
.seat-preview-placeholder-icon {
width: 52px;
@@ -1105,4 +1153,4 @@ body {
color: white;
border-color: #4f46e5;
box-shadow: 0 4px 10px rgba(79, 70, 229, 0.2);
}
}

View File

@@ -7,6 +7,7 @@ let isListMode = false;
let emptyStateMessage = '서버에 조직 데이터가 없습니다. 상단의 업로드 버튼으로 초기 데이터를 넣어주세요.';
let photoPreviewObjectUrl = null;
let seatMapLayoutCache = null;
const seatMapOfficeKeys = ['technical-development-center', 'hanmac-building-6f', 'hanmac-building-7f'];
const levelOrder = ['부서', '그룹', '디비전', '팀', '셀'];
const dropdownFields = ['소속회사', '직급', '직책', ...levelOrder];
@@ -147,23 +148,28 @@ async function loadMembers(message) {
render();
}
async function loadActiveSeatMapLayout(force = false) {
async function loadSeatMapLayouts(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;
const layouts = (await Promise.all(seatMapOfficeKeys.map(async (officeKey) => {
try {
const activePayload = await apiFetch(`/api/seat-maps/active?office_key=${encodeURIComponent(officeKey)}`);
const seatMap = activePayload?.item;
if (!seatMap?.id) {
return null;
}
return await apiFetch(`/api/seat-maps/${seatMap.id}/layout`);
} catch {
return null;
}
}))).filter(Boolean);
seatMapLayoutCache = layouts;
return layouts;
} catch {
seatMapLayoutCache = null;
return null;
return [];
}
}
@@ -172,22 +178,62 @@ function handleSeatMapLayoutUpdated() {
loadMembers().catch(() => { });
}
function getMemberSeatInfo(layout, memberId) {
if (!layout || !memberId) {
function getMemberSeatInfo(layouts, memberId) {
if (!Array.isArray(layouts) || !memberId) {
return null;
}
const placement = (layout.placements || []).find((item) => Number(item.member_id) === Number(memberId));
if (!placement) {
return null;
for (const layout of layouts) {
const placement = (layout.placements || []).find((item) => Number(item.member_id) === Number(memberId));
if (!placement) {
continue;
}
const slot = (layout.slots || []).find((item) => Number(item.id) === Number(placement.seat_slot_id));
return {
layout,
seatMapId: layout.seat_map?.id || null,
seatMapName: layout.seat_map?.name || '자리배치도',
seatLabel: placement.seat_label || slot?.label || '',
slotKey: slot?.slot_key || '',
assigned: true,
};
}
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,
return null;
}
function buildSeatAssignments(layout) {
if (!layout || !Array.isArray(layout.placements) || !Array.isArray(layout.members) || !Array.isArray(layout.slots)) {
return [];
}
return 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);
}
function applySeatPreviewFrameState(frame, seatInfo, layout) {
if (!frame?.contentWindow || !seatInfo?.slotKey) {
return;
}
const postState = () => {
if (!frame.contentWindow) {
return;
}
frame.contentWindow.postMessage({
type: 'seatmap-set-assignments',
items: buildSeatAssignments(layout),
}, 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);
};
postState();
setTimeout(postState, 120);
}
async function syncMembers(nextMembers) {
@@ -953,15 +999,16 @@ async function hydrateMemberSeatPreview(member) {
seatLabel: member['자리위치'] || '',
slotKey: '',
});
const layout = await loadActiveSeatMapLayout(true);
const layouts = await loadSeatMapLayouts(true);
if (!document.getElementById('member-seat-preview')) {
return;
}
const seatInfo = getMemberSeatInfo(layout, member.id) || {
seatMapName: layout?.seat_map?.name || '자리배치도',
const seatInfo = getMemberSeatInfo(layouts, member.id) || {
layout: null,
seatMapName: '자리배치도',
seatLabel: member['자리위치'] || '',
slotKey: '',
assigned: Boolean(member['자리위치']),
assigned: false,
};
target.innerHTML = renderSeatPreviewCard(seatInfo);
if (!seatInfo.assigned || !seatInfo.seatMapId || !seatInfo.slotKey) {
@@ -972,27 +1019,7 @@ async function hydrateMemberSeatPreview(member) {
return;
}
frame.addEventListener('load', () => {
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);
applySeatPreviewFrameState(frame, seatInfo, seatInfo.layout);
}, { once: true });
}
@@ -1112,7 +1139,7 @@ function openModal(id) {
<input type="hidden" id="m-seat-hidden" value="${member['자리위치'] || ''}">
<div class="col-span-2 member-edit-layout">
<div class="member-edit-left-pane">
<div class="member-photo-field">
<div class="member-edit-profile-card">
<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">
@@ -1126,21 +1153,16 @@ function openModal(id) {
<strong id="m-photo-file-name" class="member-photo-file-name">선택된 파일 없음</strong>
</div>
</div>
</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">
<div class="member-name-field member-name-field-compact">
<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>
<div class="member-inline-info-grid member-inline-info-grid-stacked">
<div class="member-inline-info-card member-inline-info-card-full">
<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">
<div class="member-inline-info-card member-inline-info-card-full">
<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>
@@ -1151,6 +1173,11 @@ function openModal(id) {
</div>
</div>
</div>
<div class="member-edit-right-pane">
<div class="member-seat-field member-seat-field-emphasis">
<div id="member-seat-preview">${renderSeatPreviewCard({ assigned: false, seatLabel: member['자리위치'] || '', seatMapName: '자리배치도', slotKey: '' })}</div>
</div>
</div>
</div>
</div>
${orgFields}