Fix organization member editing and drag sync

This commit is contained in:
hyunho
2026-04-02 10:38:47 +09:00
parent a4480c3435
commit 8125193378
6 changed files with 155 additions and 66 deletions

View File

@@ -4,6 +4,7 @@ let selectedDept = '전체';
let editingMembers = [];
let collapsedUnits = new Set();
let isListMode = false;
let isListDetailModal = false;
let emptyStateMessage = '서버에 조직 데이터가 없습니다. 상단의 업로드 버튼으로 초기 데이터를 넣어주세요.';
let photoPreviewObjectUrl = null;
let seatMapLayoutCache = null;
@@ -485,6 +486,24 @@ function createNodeDOM(node, parentId) {
return nodeItem;
}
function applyMemberPlacementFromTarget(member, targetLevel, targetName, targetMember = null) {
const targetLevelIndex = levelOrder.indexOf(targetLevel);
if (targetLevelIndex === -1) {
return member;
}
for (let index = 0; index <= targetLevelIndex; index += 1) {
member[levelOrder[index]] = targetMember ? (targetMember[levelOrder[index]] || '') : (index === targetLevelIndex ? targetName : member[levelOrder[index]]);
}
if (!targetMember) {
member[targetLevel] = targetName;
}
for (let index = targetLevelIndex + 1; index < levelOrder.length; index += 1) {
member[levelOrder[index]] = '';
}
rebuildMemberPath(member);
return member;
}
function drawLines() {
const container = document.getElementById('tree-root');
const svg = document.getElementById('svg-canvas');
@@ -628,6 +647,11 @@ function render() {
deptBox.id = deptId;
deptBox.className = 'dept-box';
deptBox.setAttribute('data-level', '부서');
if (isAdmin) {
deptBox.ondragover = (event) => handleDragOver(event);
deptBox.ondragleave = (event) => handleDragLeave(event);
deptBox.ondrop = (event) => handleDrop(event, '부서', deptName);
}
deptBox.innerHTML = `<div class="dept-header ${isAdmin ? 'clickable-title' : ''} ${hasMembers ? 'has-members' : ''}" ${isAdmin ? `onclick="openOrgEditModal('부서', '${jsString(deptName)}')"` : ''}>${deptName} (${totalCount})</div>`;
if (hasMembers) {
@@ -827,6 +851,53 @@ function openAddModal(event) {
openModal(null);
}
function renderListViewShell(defaultDate) {
const modal = document.getElementById('modal');
modal.querySelector('.modal-content').classList.add('wide');
document.getElementById('modal-title').innerText = '인원 명단';
const fieldsArea = document.getElementById('modal-fields');
fieldsArea.className = 'flex flex-col w-full overflow-hidden';
fieldsArea.style.maxHeight = '75vh';
fieldsArea.style.overflowY = 'hidden';
fieldsArea.innerHTML = `
<div class="list-toolbar">
<div class="list-toolbar-row">
<div class="list-toolbar-group">
<button type="button" onclick="showCurrentListView()" class="list-mode-btn">현재 명단</button>
</div>
<div class="list-toolbar-divider" aria-hidden="true"></div>
<div class="list-toolbar-group list-date-group">
<input type="date" id="list-snapshot-date" value="${escapeHtml(defaultDate)}" class="list-date-input">
<button type="button" onclick="loadSnapshotListView()" class="list-mode-btn">기준일 조회</button>
</div>
<div class="list-toolbar-divider" aria-hidden="true"></div>
<div class="list-toolbar-group list-date-group">
<input type="date" id="list-compare-from" value="${escapeHtml(defaultDate)}" class="list-date-input">
<span class="list-date-separator">~</span>
<input type="date" id="list-compare-to" value="${escapeHtml(defaultDate)}" class="list-date-input">
<button type="button" onclick="loadCompareListView()" class="list-mode-btn">변경 비교</button>
</div>
</div>
<div class="list-toolbar-row">
<input type="text" id="list-search-input" placeholder="이름 또는 부서/팀 검색 (Enter 시 이동)" class="flex-1 bg-[#f6eddd] border-2 border-[#e0d0b4] p-3 rounded-xl text-sm outline-none font-bold text-[#1f2f25] focus:border-[#d68a3a] transition-all" onkeydown="if(event.key==='Enter') handleListSearch(this.value)">
<button type="button" onclick="handleListSearch(document.getElementById('list-search-input').value)" class="bg-[#214634] text-white px-5 rounded-xl font-bold text-sm">검색</button>
</div>
<div id="list-view-status" class="list-view-status"></div>
</div>
<div id="list-table-container" class="overflow-y-auto flex-1 border rounded-xl"></div>
`;
modal.style.display = 'flex';
}
function returnToListViewModal() {
if (!isListMode) {
return;
}
isListDetailModal = false;
renderListViewShell(listViewState.snapshotDate || getDefaultHistoryDate());
renderListViewModalContent();
}
function updateParentList() {
const type = document.getElementById('new-unit-type').value;
const parentSelect = document.getElementById('new-unit-parent');
@@ -1159,7 +1230,8 @@ function openModal(id) {
modal.querySelector('.modal-content').classList.add('wide');
fieldsArea.className = 'flex flex-col w-full';
fieldsArea.style.maxHeight = 'none';
fieldsArea.style.overflowY = 'visible';
fieldsArea.style.overflowY = 'auto';
isListDetailModal = isListMode;
const sourceValues = isListMode ? editingMembers : members;
let orgFields = '<div id="modal-sec-org" class="hidden grid grid-cols-2 gap-3 modal-form-grid">';
@@ -1279,11 +1351,17 @@ function openModal(id) {
}
function closeModal() {
if (isListMode && isListDetailModal) {
returnToListViewModal();
return;
}
resetPhotoPreviewObjectUrl();
document.getElementById('modal').style.display = 'none';
document.getElementById('modal-fields').className = 'grid grid-cols-2 gap-x-8 gap-y-5';
document.getElementById('modal-fields').style.maxHeight = 'none';
document.getElementById('modal-fields').style.overflowY = 'visible';
document.querySelector('.modal-content').classList.remove('wide');
isListDetailModal = false;
isListMode = false;
}
@@ -1337,8 +1415,7 @@ async function saveMember() {
if (!id) {
targetList.push(member);
}
renderListViewTable();
closeModal();
returnToListViewModal();
return;
}
@@ -1367,11 +1444,16 @@ async function deleteMember(id) {
}
if (isListMode) {
const shouldReturnToList = isListDetailModal;
const idx = editingMembers.findIndex((member) => member._id === id);
if (idx !== -1) {
editingMembers.splice(idx, 1);
}
renderListViewTable();
if (shouldReturnToList) {
returnToListViewModal();
} else {
renderListViewTable();
}
return;
}
@@ -1396,44 +1478,11 @@ function openListViewModal(event) {
listViewState.compareToDate = defaultDate;
listViewState.snapshotMembers = [];
listViewState.compareItems = [];
const modal = document.getElementById('modal');
modal.querySelector('.modal-content').classList.add('wide');
document.getElementById('modal-title').innerText = '인원 명단';
const fieldsArea = document.getElementById('modal-fields');
fieldsArea.className = 'flex flex-col w-full overflow-hidden';
fieldsArea.style.maxHeight = '75vh';
isListMode = true;
isListDetailModal = false;
editingMembers = cloneMembers(members);
fieldsArea.innerHTML = `
<div class="list-toolbar">
<div class="list-toolbar-row">
<div class="list-toolbar-group">
<button type="button" onclick="showCurrentListView()" class="list-mode-btn">현재 명단</button>
</div>
<div class="list-toolbar-divider" aria-hidden="true"></div>
<div class="list-toolbar-group list-date-group">
<input type="date" id="list-snapshot-date" value="${escapeHtml(defaultDate)}" class="list-date-input">
<button type="button" onclick="loadSnapshotListView()" class="list-mode-btn">기준일 조회</button>
</div>
<div class="list-toolbar-divider" aria-hidden="true"></div>
<div class="list-toolbar-group list-date-group">
<input type="date" id="list-compare-from" value="${escapeHtml(defaultDate)}" class="list-date-input">
<span class="list-date-separator">~</span>
<input type="date" id="list-compare-to" value="${escapeHtml(defaultDate)}" class="list-date-input">
<button type="button" onclick="loadCompareListView()" class="list-mode-btn">변경 비교</button>
</div>
</div>
<div class="list-toolbar-row">
<input type="text" id="list-search-input" placeholder="이름 또는 부서/팀 검색 (Enter 시 이동)" class="flex-1 bg-[#f6eddd] border-2 border-[#e0d0b4] p-3 rounded-xl text-sm outline-none font-bold text-[#1f2f25] focus:border-[#d68a3a] transition-all" onkeydown="if(event.key==='Enter') handleListSearch(this.value)">
<button type="button" onclick="handleListSearch(document.getElementById('list-search-input').value)" class="bg-[#214634] text-white px-5 rounded-xl font-bold text-sm">검색</button>
</div>
<div id="list-view-status" class="list-view-status"></div>
</div>
<div id="list-table-container" class="overflow-y-auto flex-1 border rounded-xl"></div>
`;
renderListViewShell(defaultDate);
renderListViewModalContent();
modal.style.display = 'flex';
}
async function applyListViewChanges() {
@@ -1697,12 +1746,29 @@ function toggleUnitCollapse(level, name) {
let draggedGroup = null;
function handleListGroupDragStart(event, level, name) {
draggedIdx = null;
draggedGroup = { level, name };
event.dataTransfer.effectAllowed = 'move';
}
function handleListGroupDrop(event, targetLevel, targetName) {
event.preventDefault();
if (draggedIdx !== null) {
const movingMember = editingMembers[draggedIdx];
if (!movingMember) {
return;
}
const moved = editingMembers.splice(draggedIdx, 1)[0];
applyMemberPlacementFromTarget(moved, targetLevel, targetName, null);
let targetIdx = editingMembers.findIndex((member) => member[targetLevel] === targetName);
if (targetIdx === -1) {
targetIdx = editingMembers.length;
}
editingMembers.splice(targetIdx, 0, moved);
draggedIdx = null;
renderListViewTable();
return;
}
if (!draggedGroup || (draggedGroup.level === targetLevel && draggedGroup.name === targetName)) {
return;
}
@@ -1743,6 +1809,7 @@ function handleListSearch(value) {
let draggedIdx = null;
function handleListDragStart(event, index) {
draggedGroup = null;
draggedIdx = index;
event.dataTransfer.effectAllowed = 'move';
event.target.classList.add('dragging');
@@ -1753,7 +1820,11 @@ function handleListDrop(event, targetIdx) {
if (draggedIdx === null || draggedIdx === targetIdx) {
return;
}
const targetMember = editingMembers[targetIdx] || null;
const moved = editingMembers.splice(draggedIdx, 1)[0];
if (targetMember) {
applyMemberPlacementFromTarget(moved, levelOrder[levelOrder.length - 1], targetMember[levelOrder[levelOrder.length - 1]] || '', targetMember);
}
editingMembers.splice(targetIdx, 0, moved);
draggedIdx = null;
renderListViewTable();
@@ -1804,13 +1875,10 @@ async function handleDrop(event, targetLevel, targetName) {
}
const nextMembers = cloneMembers(members);
const moved = nextMembers[memberIndex];
for (let index = 0; index <= targetLevelIndex; index += 1) {
moved[levelOrder[index]] = targetMember ? targetMember[levelOrder[index]] : targetName;
if (targetLevelIndex === -1) {
return;
}
for (let index = targetLevelIndex + 1; index < levelOrder.length; index += 1) {
moved[levelOrder[index]] = '';
}
rebuildMemberPath(moved);
applyMemberPlacementFromTarget(moved, targetLevel, targetName, targetMember);
nextMembers.splice(memberIndex, 1);
nextMembers.push(moved);
await syncMembers(nextMembers);
@@ -1859,10 +1927,7 @@ async function handleDropMember(event, targetId) {
}
const moved = nextMembers[movingIdx];
const target = nextMembers[targetIdx];
levelOrder.forEach((level) => {
moved[level] = target[level];
});
rebuildMemberPath(moved);
applyMemberPlacementFromTarget(moved, levelOrder[levelOrder.length - 1], target[levelOrder[levelOrder.length - 1]] || '', target);
nextMembers.splice(movingIdx, 1);
targetIdx = nextMembers.findIndex((member) => member._id === targetId);
nextMembers.splice(insertAfter ? targetIdx + 1 : targetIdx, 0, moved);