Update project dashboard routing and map directions

This commit is contained in:
2026-06-10 16:22:04 +09:00
parent 62b25b045b
commit 0c052abfa7
3 changed files with 294 additions and 4 deletions

View File

@@ -352,6 +352,31 @@
opacity: 0.45;
cursor: default;
}
.map-button {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 48px;
height: 28px;
margin-top: 6px;
padding: 0 10px;
border: 1px solid rgba(15, 118, 110, 0.24);
border-radius: 999px;
background: #e9f8f5;
color: var(--accent-strong);
font-size: 12px;
font-weight: 800;
cursor: pointer;
white-space: nowrap;
}
.map-button:hover {
filter: brightness(0.96);
}
.map-location {
display: grid;
gap: 4px;
justify-items: start;
}
.modal-backdrop {
position: fixed;
inset: 0;
@@ -620,6 +645,7 @@
sourceTable: '',
sourceColumns: {},
};
const FACTORY_ADDRESS = '충청남도 당진시 고대면 성산로 464';
const searchInput = document.getElementById('searchInput');
const contractTypeFilter = document.getElementById('contractTypeFilter');
@@ -788,6 +814,13 @@
.filter(Boolean);
}
function compareProjectCodeDesc(a, b) {
return String(b.projectCode || '').localeCompare(String(a.projectCode || ''), 'ko', {
numeric: true,
sensitivity: 'base',
});
}
function renderRows(rows) {
if (!rows.length) {
resultBody.innerHTML = '<tr><td colspan="2" class="empty">조건에 맞는 프로젝트가 없습니다.</td></tr>';
@@ -860,7 +893,7 @@
['사업코드', detail.businessCode],
['약칭', detail.projectName],
['시공코드', detail.projectCode],
['현장위치', detail.siteLocation],
['현장위치', renderSiteLocationCell(detail.siteLocation, detail.projectName)],
['발주처', detail.clientName],
['최종계약금액', detail.finalContractAmountText],
['계약종류', detail.contractType],
@@ -875,9 +908,32 @@
detailBody.innerHTML = rows.map(([label, value]) => `
<tr>
<th>${escapeHtml(label)}</th>
<td>${typeof value === 'string' && value.includes('<table') ? value : escapeHtml(value || '-')}</td>
<td>${typeof value === 'string' && (value.includes('<table') || value.includes('class="map-location"')) ? value : escapeHtml(value || '-')}</td>
</tr>
`).join('');
Array.from(detailBody.querySelectorAll('.map-button')).forEach((button) => {
button.addEventListener('click', () => {
openBridgeMap(button.getAttribute('data-bridge') || '', button.getAttribute('data-location') || '');
});
});
}
function renderSiteLocationCell(siteLocation, projectName) {
const location = normalizeValue(siteLocation);
const disabled = !location || location === '-';
return `
<div class="map-location">
<div>${toHtmlWithBreaks(location)}</div>
<button
class="map-button"
type="button"
data-bridge="${escapeAttr(projectName || '')}"
data-location="${escapeAttr(location)}"
${disabled ? 'disabled' : ''}
title="공장 위치와 현장 위치를 지도에서 보기"
>지도</button>
</div>
`;
}
function normalizeBridgeName(value) {
@@ -918,6 +974,75 @@
return `${text.slice(0, splitIndex).trim()}\n${text.slice(splitIndex + 1).trim()}`;
}
function normalizeMapQuery(value) {
return String(value || '')
.replace(/\s+/g, ' ')
.replace(/\n+/g, ' ')
.trim();
}
function extractAddressForMap(value) {
let text = normalizeMapQuery(value)
.replace(/^\(주\)\s*장헌\s*/, '')
.replace(/^\(주\)장헌\s*/, '')
.replace(/^\[.*?\]\s*/, '')
.replace(/\(.*?\)/g, ' ')
.replace(/\[.*?\]/g, ' ')
.replace(/\s+/g, ' ')
.trim();
const addressStart = text.search(/(서울특별시|서울시|부산광역시|부산시|대구광역시|대구시|인천광역시|인천시|광주광역시|광주시|대전광역시|대전시|울산광역시|울산시|세종특별자치시|세종시|경기도|강원특별자치도|강원도|충청북도|충북|충청남도|충남|전북특별자치도|전라북도|전북|전라남도|전남|경상북도|경북|경상남도|경남|제주특별자치도|제주도)/);
if (addressStart > 0) {
text = text.slice(addressStart).trim();
}
text = text
.split(/\s*~\s*/)[0]
.replace(/\s+/g, ' ')
.trim();
return text;
}
async function openBridgeMap(bridgeName, bridgeLocation) {
const site = extractAddressForMap(bridgeLocation);
if (!site || site === '-') {
alert('교량 위치 정보가 없습니다.');
return;
}
const popup = window.open(
'',
`jhBridgeRouteMap_${Date.now()}`,
'popup=yes,width=1280,height=820,left=80,top=60,resizable=yes,scrollbars=yes'
);
if (!popup) {
alert('팝업이 차단되었습니다. 브라우저 팝업 허용 후 다시 눌러주세요.');
return;
}
popup.document.write('<!doctype html><meta charset="utf-8"><title>길찾기 준비 중</title><body style="font-family:sans-serif;padding:24px;">네이버 자동차 길찾기를 준비하는 중입니다...</body>');
popup.focus();
try {
const params = new URLSearchParams({
origin: FACTORY_ADDRESS,
destination: site,
});
const response = await fetch(`/api/naver-route-url?${params.toString()}`, { cache: 'no-store' });
const result = await response.json();
if (!response.ok || !result.ok || !result.url) {
throw new Error(result.error || '네이버 길찾기 URL을 만들지 못했습니다.');
}
popup.location.href = result.url;
} catch (error) {
const fallbackUrl = `https://map.naver.com/p/search/${encodeURIComponent(site)}`;
popup.document.body.innerHTML = `
<div style="font-family:sans-serif;padding:24px;line-height:1.6;">
<h2 style="margin:0 0 12px;color:#b91c1c;">네이버 자동차 길찾기를 바로 열지 못했습니다.</h2>
<p>현장 주소: <strong>${escapeHtml(site)}</strong></p>
<p style="white-space:pre-wrap;color:#7f1d1d;">${escapeHtml(error.message || String(error))}</p>
<p><a href="${escapeAttr(fallbackUrl)}" style="color:#0f766e;font-weight:700;">네이버지도에서 현장 위치 먼저 열기</a></p>
</div>
`;
}
}
function formatStatus(scaleRow, overviewRow) {
const status = overviewRow?.constructionStatus || scaleRow?.constructionStatus || '';
return status || '-';
@@ -1033,6 +1158,7 @@
crossbeamCount: scaleRow.crossbeamCount || '-',
panelCount: scaleRow.panelCount || '-',
rebarCount: scaleRow.rebarCount || '-',
rawBridgeLocation: overviewRow.bridgeLocation || '',
bridgeLocation: formatBridgeLocation(overviewRow.bridgeLocation),
remarks: overviewRow.remarks || '-',
};
@@ -1041,6 +1167,8 @@
function renderMergedBridgeRows(scaleRows, overviews, budgetPlan) {
const mergedRows = mergeBridgeRows(scaleRows, overviews, budgetPlan);
const siteLocation = normalizeValue(state.detail?.siteLocation || state.selectedRow?.siteLocation || '');
const hasSiteLocation = siteLocation && siteLocation !== '-';
bridgeMeta.textContent = mergedRows.length
? `공사규모 ${scaleRows.length}건 · 공사개요 ${overviews.length}건을 교량명 기준으로 매칭했습니다.`
: '연결된 공사규모나 공사개요 데이터가 없습니다.';
@@ -1068,7 +1196,11 @@
<td class="cell-num bridge-summary">${toHtmlWithBreaks(formatCellValue(row.crossbeamCount))}</td>
<td class="cell-num bridge-summary">${toHtmlWithBreaks(formatCellValue(row.panelCount))}</td>
<td class="cell-num bridge-summary">${toHtmlWithBreaks(formatCellValue(row.rebarCount))}</td>
<td class="bridge-summary cell-wide">${toHtmlWithBreaks(row.bridgeLocation)}</td>
<td class="bridge-summary cell-wide">
<div class="map-location">
<div>${toHtmlWithBreaks(row.bridgeLocation)}</div>
</div>
</td>
<td>
<button
class="remark-button"
@@ -1133,6 +1265,7 @@
if (selectedApplicationType) {
state.filteredRows = state.filteredRows.filter((row) => splitApplicationTypes(row.applicationType).includes(selectedApplicationType));
}
state.filteredRows.sort(compareProjectCodeDesc);
countChip.textContent = `${state.rows.length.toLocaleString()}건 / 표시 ${state.filteredRows.length.toLocaleString()}`;
statusText.textContent = keyword