Fix seatmap slot matching and update member modal layout
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user