Add multi-office seat maps and dev/prod DB sync protocol

This commit is contained in:
hyunho
2026-03-27 16:34:43 +09:00
parent 1d15cf9b9b
commit d66614123e
16 changed files with 3427 additions and 146 deletions

View File

@@ -1316,8 +1316,9 @@
</div>
<div id="mh-topbar-actions" class="flex flex-wrap items-center justify-end gap-3">
<div id="mh-scope-toggle" aria-label="조직 구분 선택">
<button type="button" class="mh-scope-btn active" data-scope="gpd">GPD</button>
<button type="button" class="mh-scope-btn" data-scope="tdc">TDC</button>
<button type="button" class="mh-scope-btn active" data-scope="all">전체</button>
<button type="button" class="mh-scope-btn" data-scope="gpd">GPD</button>
<button type="button" class="mh-scope-btn" data-scope="tdc">TDC</button>
</div>
<div id="mh-inline-filters">
<div id="mh-inline-search" class="relative">
@@ -1483,13 +1484,16 @@
let teamData = [];
let allTeams = [];
let allPeopleData = [];
let currentScope = 'gpd';
let searchTeams = [];
let searchPeopleData = [];
let currentScope = 'all';
let matrixBizOpenState = {};
let matrixProjectFilter = 'all';
let currentMatrixData = {};
let personChartOpenState = {};
let personCalendarState = { name: '', team: '', year: 2026, month: 0 };
let lastUploadedBinary = '';
let lastPmSheetData = [];
let projectPmMap = new Map();
let memberTeamMap = new Map();
const GPD_TEAM_DIVISIONS = new Set(['총괄', '영업']);
@@ -1621,10 +1625,18 @@
if (scope === 'tdc') return !GPD_TEAM_DIVISIONS.has(division);
return true;
};
const getScopedRows = (scope = currentScope) => teamData.slice(1).filter(row => isRowInScope(row, scope));
const buildScopedPeopleData = (rows) => {
const seen = new Set();
return rows.map(r => ({
const getScopedRows = (scope = currentScope) => teamData.slice(1).filter(row => isRowInScope(row, scope));
const getTeamScope = (teamName) => {
const normalizedTeam = String(teamName || '').trim();
if (!normalizedTeam) return 'all';
const matchedRow = teamData.slice(1).find(row => String(row?.[columnMap.team] || '').trim() === normalizedTeam);
const division = getRowTeamDivision(matchedRow);
if (!division) return 'all';
return GPD_TEAM_DIVISIONS.has(division) ? 'gpd' : 'tdc';
};
const buildScopedPeopleData = (rows) => {
const seen = new Set();
return rows.map(r => ({
name: String(r[columnMap.name] || '').trim(),
team: String(r[columnMap.team] || '').trim()
})).filter(p => {
@@ -2239,6 +2251,8 @@
if (!payload) return;
if (payload.binary) {
loadWorkbookBinary(payload.binary);
} else if (Array.isArray(payload.teamData)) {
applyMhSourceRows(payload.teamData, Array.isArray(payload.pmSheet) ? payload.pmSheet : []);
}
if (payload.scope) {
currentScope = payload.scope === 'tdc' ? 'tdc' : 'gpd';
@@ -2261,7 +2275,7 @@
function openTeamAnalysisPopup(team) {
if (!teamData.length || !lastUploadedBinary) return;
if (!teamData.length) return;
closeProjectModal();
@@ -2280,12 +2294,17 @@
const payload = {
source: 'team-popup-init',
binary: lastUploadedBinary,
team,
scope: popupScope,
startDate: document.getElementById('start-date').value || '',
endDate: document.getElementById('end-date').value || ''
};
if (lastUploadedBinary) {
payload.binary = lastUploadedBinary;
} else {
payload.teamData = teamData;
payload.pmSheet = lastPmSheetData;
}
const sendPayload = () => {
if (!popupWindow || popupWindow.closed) return;
try {
@@ -2327,24 +2346,30 @@
}
// --- Handlers ---
function loadWorkbookBinary(binaryStr) {
if (!binaryStr) return;
lastUploadedBinary = binaryStr;
const workbook = XLSX.read(binaryStr, {type: 'binary', cellDates: true, dateNF: 'yyyy-mm-dd'});
teamData = XLSX.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]], {header: 1, defval: ""});
const pmSheet = workbook.SheetNames[1] ? XLSX.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[1]], {header: 1, defval: ""}) : [];
function applyMhSourceRows(nextTeamData, nextPmSheet = []) {
teamData = Array.isArray(nextTeamData) ? nextTeamData : [];
lastPmSheetData = Array.isArray(nextPmSheet) ? nextPmSheet : [];
buildColumnMap();
rebuildMemberTeamMap();
projectPmMap = new Map();
pmSheet.forEach(row => {
lastPmSheetData.forEach(row => {
const projectCode = String(row?.[0] || '').trim();
const pmName = String(row?.[1] || '').trim();
if (projectCode && pmName) projectPmMap.set(projectCode, pmName);
});
initTeamTabAfterUpload();
}
function loadWorkbookBinary(binaryStr) {
if (!binaryStr) return;
lastUploadedBinary = binaryStr;
const workbook = XLSX.read(binaryStr, {type: 'binary', cellDates: true, dateNF: 'yyyy-mm-dd'});
const nextTeamData = XLSX.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]], {header: 1, defval: ""});
const nextPmSheet = workbook.SheetNames[1] ? XLSX.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[1]], {header: 1, defval: ""}) : [];
applyMhSourceRows(nextTeamData, nextPmSheet);
}
const fileInput = document.getElementById('file-input');
const uploadMhButton = document.getElementById('btn-upload-mh');
@@ -2408,16 +2433,10 @@
const response = await fetch('/api/integration/mh-source', { credentials: 'same-origin' });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const payload = await response.json();
teamData = Array.isArray(payload.teamData) ? payload.teamData : [];
buildColumnMap();
rebuildMemberTeamMap();
projectPmMap = new Map();
(Array.isArray(payload.pmSheet) ? payload.pmSheet : []).forEach(row => {
const projectCode = String(row?.[0] || '').trim();
const pmName = String(row?.[1] || '').trim();
if (projectCode && pmName) projectPmMap.set(projectCode, pmName);
});
initTeamTabAfterUpload();
applyMhSourceRows(
Array.isArray(payload.teamData) ? payload.teamData : [],
Array.isArray(payload.pmSheet) ? payload.pmSheet : []
);
} catch (error) {
console.error(error);
}
@@ -2436,12 +2455,15 @@
const teamSelect = document.getElementById('team-select');
const personSelect = document.getElementById('person-select');
const currentTeam = preserveSelection ? teamSelect.value : '';
const currentPerson = preserveSelection ? personSelect.value : '';
const scopedRows = getScopedRows();
allTeams = [...new Set(scopedRows.map(r => String(r[columnMap.team] || '').trim()))].filter(Boolean).sort();
allPeopleData = buildScopedPeopleData(scopedRows);
const currentTeam = preserveSelection ? teamSelect.value : '';
const currentPerson = preserveSelection ? personSelect.value : '';
const scopedRows = getScopedRows();
const allRows = getScopedRows('all');
allTeams = [...new Set(scopedRows.map(r => String(r[columnMap.team] || '').trim()))].filter(Boolean).sort();
allPeopleData = buildScopedPeopleData(scopedRows);
searchTeams = [...new Set(allRows.map(r => String(r[columnMap.team] || '').trim()))].filter(Boolean).sort();
searchPeopleData = buildScopedPeopleData(allRows);
teamSelect.innerHTML = '<option value="">팀 선택</option>' + allTeams.map(t => `<option value="${t}">${t}</option>`).join('');
@@ -2463,10 +2485,10 @@
}
function setScope(scope, preserveSelection = true) {
currentScope = scope === 'tdc' ? 'tdc' : 'gpd';
syncScopeButtons();
refreshScopedSelections(preserveSelection);
render();
currentScope = scope === 'gpd' || scope === 'tdc' ? scope : 'all';
syncScopeButtons();
refreshScopedSelections(preserveSelection);
render();
}
function initTeamTabAfterUpload() {
@@ -2520,9 +2542,11 @@
}
const matchedTeams = allTeams.filter(t => t.toLowerCase().includes(val));
const matchedPeople = allPeopleData.filter(p => p.name.toLowerCase().includes(val));
const matchedTeams = searchTeams.filter(t => t.toLowerCase().includes(val));
const matchedPeople = searchPeopleData.filter(p =>
p.name.toLowerCase().includes(val) || p.team.toLowerCase().includes(val)
);
@@ -2588,23 +2612,17 @@
if (type === 'team') {
teamSelect.value = item.dataset.value;
updateFilters();
personSelect.value = "";
} else {
teamSelect.value = item.dataset.team;
updateFilters();
personSelect.value = item.dataset.name;
}
if (type === 'team') {
setScope(getTeamScope(item.dataset.value), false);
teamSelect.value = item.dataset.value;
updateFilters();
personSelect.value = "";
} else {
setScope(getTeamScope(item.dataset.team), false);
teamSelect.value = item.dataset.team;
updateFilters();
personSelect.value = item.dataset.name;
}
mainSearchInput.value = "";