차량 모니터 복귀 링크 경로 정리

This commit is contained in:
seo
2026-07-01 00:55:28 +09:00
parent 783035deee
commit dba2c74d9e
2 changed files with 83 additions and 2 deletions
+3 -1
View File
@@ -15,7 +15,7 @@ Car는 차량 모뎀/게이트웨이에서 받은 상태를 수집해 DB에 저
- 허용된 차량 명령 TCP 전송 - 허용된 차량 명령 TCP 전송
- monitor 화면과 AJAX 상태 갱신 - monitor 화면과 AJAX 상태 갱신
- monitor 화면 WakeLock, 언어, 테마 토글 - monitor 화면 WakeLock, 언어, 테마 토글
- monitor 좌측 상단 블로그 복귀 링크 - monitor 좌측 상단 이전 페이지 복귀 링크
- 모바일 및 좁은 카드 폭에 맞춘 사용량 2줄 배치, 로그 가로 스크롤, 시간 문구 축약 - 모바일 및 좁은 카드 폭에 맞춘 사용량 2줄 배치, 로그 가로 스크롤, 시간 문구 축약
- 사용량/요금 표시와 통신사 과금 단위 보정 metadata - 사용량/요금 표시와 통신사 과금 단위 보정 metadata
- 4.95초 UI timeout과 60초 사용량 정산 분리 - 4.95초 UI timeout과 60초 사용량 정산 분리
@@ -48,6 +48,8 @@ Car는 차량 모뎀/게이트웨이에서 받은 상태를 수집해 DB에 저
헤더 우측 컨트롤은 WakeLock, 언어, 테마 순서입니다. WakeLock은 Screen Wake Lock API를 지원하는 브라우저에서 화면 꺼짐을 방지하며, 활성 상태는 초록색 아이콘 버튼으로 표시합니다. 사용자의 선택은 `localStorage`에 저장하고, 새로고침이나 탭 복귀 후에는 브라우저 정책에 맞춰 다시 획득을 시도합니다. 헤더 우측 컨트롤은 WakeLock, 언어, 테마 순서입니다. WakeLock은 Screen Wake Lock API를 지원하는 브라우저에서 화면 꺼짐을 방지하며, 활성 상태는 초록색 아이콘 버튼으로 표시합니다. 사용자의 선택은 `localStorage`에 저장하고, 새로고침이나 탭 복귀 후에는 브라우저 정책에 맞춰 다시 획득을 시도합니다.
좌측 상단 복귀 링크는 이전 페이지가 `chaegeon.com` 계열이면 해당 URL을 사용합니다. 같은 출처에서 이동한 경우에는 이전 문서의 title을 읽어 표시하고, 직접 접속이나 이전 페이지 확인이 어려운 경우에는 `https://chaegeon.com/blog`를 기본 목적지로 사용합니다. `seo.chaegeon.com`의 상대 경로로 이동하지 않도록 절대 URL을 기본값으로 둡니다.
## 구성 ## 구성
- `api.php`: 차량 상태/제어 API - `api.php`: 차량 상태/제어 API
+80 -1
View File
@@ -956,6 +956,10 @@ car_monitor_require_access(false);
line-height: 1; line-height: 1;
text-decoration: none; text-decoration: none;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.55); text-shadow: 0 1px 0 rgba(255, 255, 255, 0.55);
max-width: min(44vw, 260px);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
.blog-back-link:hover, .blog-back-link:hover,
.blog-back-link:focus { .blog-back-link:focus {
@@ -1368,7 +1372,7 @@ car_monitor_require_access(false);
<i class="fas fa-car"></i> <i class="fas fa-car"></i>
</div> </div>
<div> <div>
<a href="/blog" class="blog-back-link"><span class="backArrow"></span> 블로그</a> <a href="https://chaegeon.com/blog" id="blogBackLink" class="blog-back-link"><span class="backArrow"></span> 블로그</a>
<div class="d-flex align-items-center gap-2"> <div class="d-flex align-items-center gap-2">
<div class="app-title" data-i18n="appTitle">Car Monitor</div> <div class="app-title" data-i18n="appTitle">Car Monitor</div>
<span id="live-badge" class="badge bg-secondary bg-opacity-10 text-secondary border border-secondary border-opacity-20 rounded-pill" style="font-size:0.6rem; vertical-align: top;">WAIT</span> <span id="live-badge" class="badge bg-secondary bg-opacity-10 text-secondary border border-secondary border-opacity-20 rounded-pill" style="font-size:0.6rem; vertical-align: top;">WAIT</span>
@@ -1822,8 +1826,15 @@ car_monitor_require_access(false);
let wakeLockWanted = false; let wakeLockWanted = false;
let latestDashboardData = null; let latestDashboardData = null;
let latestDashboardMeta = null; let latestDashboardMeta = null;
const FALLBACK_BACK_TARGET = 'https://chaegeon.com/blog';
const BACK_LINK_PATH_TITLE = {
'/': '채건닷컴',
'/home': '홈',
'/blog': '블로그',
};
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
setupBackLink();
initLanguage(); initLanguage();
initTheme(); initTheme();
initWakeLock(); initWakeLock();
@@ -1858,6 +1869,74 @@ car_monitor_require_access(false);
return I18N[currentLang]?.[key] || I18N.en[key] || key; return I18N[currentLang]?.[key] || I18N.en[key] || key;
} }
function cleanPageTitle(title) {
return String(title || '')
.replace(/^채건닷컴\s*-\s*/i, '')
.replace(/\s*-\s*채건닷컴$/i, '')
.trim();
}
function sameSiteUrl(url) {
try {
const parsed = new URL(url, location.href);
return parsed.hostname === location.hostname || parsed.hostname.endsWith('.chaegeon.com') || parsed.hostname === 'chaegeon.com'
? parsed
: null;
} catch (e) {
return null;
}
}
function titleFromBackUrl(url) {
const parsed = sameSiteUrl(url);
if (!parsed) return '';
return BACK_LINK_PATH_TITLE[parsed.pathname.replace(/\/$/, '') || '/'] || cleanPageTitle(parsed.pathname.split('/').filter(Boolean).pop() || '');
}
function updateBackLinkLabel(label) {
const link = document.getElementById('blogBackLink');
if (!link) return;
link.innerHTML = `<span class="backArrow"></span> ${label}`;
}
async function resolveBackTarget() {
const ref = sameSiteUrl(document.referrer || '');
const current = new URL(location.href);
const target = ref && ref.href !== current.href ? ref.href : FALLBACK_BACK_TARGET;
let label = titleFromBackUrl(target);
if (ref && ref.origin === location.origin) {
try {
const res = await fetch(ref.href, { credentials: 'same-origin', cache: 'force-cache' });
if (res.ok) {
const html = await res.text();
const doc = new DOMParser().parseFromString(html, 'text/html');
label = cleanPageTitle(doc.querySelector('title')?.textContent) || label;
}
} catch (e) {}
}
return { href: target, label: label || '블로그' };
}
function setupBackLink() {
const link = document.getElementById('blogBackLink');
if (!link) return;
link.href = FALLBACK_BACK_TARGET;
updateBackLinkLabel('블로그');
resolveBackTarget().then(({ href, label }) => {
link.href = href;
updateBackLinkLabel(label);
});
link.addEventListener('click', (event) => {
event.preventDefault();
location.href = link.href || FALLBACK_BACK_TARGET;
});
}
function applyLanguage() { function applyLanguage() {
document.documentElement.setAttribute('lang', currentLang); document.documentElement.setAttribute('lang', currentLang);
document.title = t('appTitle'); document.title = t('appTitle');