Add multi-office seat maps and dev/prod DB sync protocol
This commit is contained in:
@@ -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 = "";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user