feat: GTSIT 사이트 구축

This commit is contained in:
박성필
2026-08-10 21:19:10 +09:00
commit dbb35d35c4
87 changed files with 48246 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
.admin-body{background:#f8fafc}.admin-header{background:#fff;border-bottom:1px solid var(--border)}.admin-header-inner{width:min(100% - 48px,1100px);height:64px;margin:auto;display:flex;align-items:center;justify-content:space-between;gap:20px}.admin-brand{display:flex;align-items:center;gap:10px;text-decoration:none}.admin-brand .brand-mark{width:38px;height:38px}.admin-account{display:flex;align-items:center;gap:14px;font-size:13px}.admin-account form{margin:0}.admin-account form button{border:0;background:none;cursor:pointer;color:var(--secondary)}.admin-nav{background:#fff;border-bottom:1px solid var(--border)}.admin-nav>div{width:min(100% - 48px,1100px);margin:auto;display:flex;gap:6px;overflow:auto}.admin-nav a{padding:14px 12px;text-decoration:none;color:var(--secondary);white-space:nowrap}.admin-nav a[aria-current=page]{color:var(--primary);font-weight:800;border-bottom:2px solid var(--primary)}.admin-main{width:min(100% - 48px,1100px);margin:auto;padding:38px 0 80px}.login-panel{max-width:420px;margin:50px auto;padding:34px;background:#fff;border:1px solid var(--border);border-radius:14px;box-shadow:var(--shadow)}.login-heading{text-align:center}.login-heading .brand-mark{margin:auto}.login-heading h1{margin:18px 0 0}.login-heading p{margin:7px 0 24px;color:var(--muted)}.login-panel label{display:block;margin-bottom:17px;color:var(--secondary);font-size:13px;font-weight:700}.login-panel input{width:100%;height:44px;margin-top:7px;padding:10px 12px;border:1px solid #d4d4d8;border-radius:9px}.admin-error{padding:12px 14px;border-radius:9px;background:#fef2f2;color:#991b1b;font-size:13px}.admin-page-heading{display:flex;justify-content:space-between;align-items:end;gap:20px;margin-bottom:28px}.admin-page-heading span{color:var(--primary);font-size:12px;font-weight:800;letter-spacing:.1em}.admin-page-heading h1{margin:5px 0 0;font-size:30px}.admin-page-heading p{margin:6px 0 0;color:var(--muted)}.metric-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:18px}.metric-grid a{padding:24px;background:#fff;border:1px solid var(--border);border-radius:var(--radius);text-decoration:none}.metric-grid span{display:block;color:var(--muted);font-size:13px}.metric-grid strong{display:block;margin-top:8px;font-size:32px}.admin-table-wrap{overflow:hidden;background:#fff;border:1px solid var(--border);border-radius:var(--radius)}.admin-table{width:100%;border-collapse:collapse}.admin-table th,.admin-table td{padding:14px 16px;border-bottom:1px solid var(--border);text-align:left;font-size:13px}.admin-table th{background:#f8fafc;color:var(--muted)}.admin-table tr:last-child td{border:0}.admin-table td:first-child{font-size:14px}.admin-table td small{display:block;margin-top:3px;color:var(--muted)}.admin-table td a{color:var(--primary);font-weight:700}.status{display:inline-flex;padding:4px 9px;border-radius:999px;background:#f1f5f9;color:#475569;font-size:12px;font-weight:700}.status-published,.status-replied{background:#dcfce7;color:#15803d}.status-pending{background:#fef3c7;color:#b45309}.status-in_progress{background:#dbeafe;color:#1d4ed8}.inquiry-admin-list{display:grid;gap:14px}.inquiry-admin-list article{padding:20px;background:#fff;border:1px solid var(--border);border-radius:var(--radius)}.inquiry-admin-list article>div{display:flex;justify-content:space-between}.inquiry-admin-list small{color:var(--muted)}.inquiry-admin-list h2{margin:12px 0 0;font-size:17px}.inquiry-admin-list h2 a{text-decoration:none}.inquiry-admin-list p{margin:7px 0 0;color:var(--secondary);font-size:13px}.admin-placeholder{padding:28px;background:#fff;border:1px solid var(--border);border-radius:var(--radius)}
@media(max-width:760px){.admin-header-inner,.admin-nav>div,.admin-main{width:min(100% - 32px,1100px)}.admin-account>span{display:none}.metric-grid{grid-template-columns:1fr 1fr}.admin-page-heading{align-items:stretch;flex-direction:column}.admin-table-wrap{overflow:visible;background:transparent;border:0}.admin-table thead{display:none}.admin-table,.admin-table tbody,.admin-table tr,.admin-table td{display:block}.admin-table tr{margin-bottom:14px;padding:16px;background:#fff;border:1px solid var(--border);border-radius:var(--radius)}.admin-table td{display:grid;grid-template-columns:72px 1fr;gap:10px;padding:7px 0;border:0}.admin-table td::before{content:attr(data-label);color:var(--muted);font-size:12px}.admin-table td:first-child{display:block;padding-bottom:12px;margin-bottom:6px;border-bottom:1px solid var(--border)}.admin-table td:first-child::before{display:none}}
@media(max-width:480px){.admin-account .button{display:none}.admin-main{padding-top:28px}.metric-grid{grid-template-columns:1fr 1fr;gap:10px}.metric-grid a{padding:18px}.login-panel{margin:26px 0;padding:24px}}
.admin-form{max-width:780px;padding:28px;background:#fff;border:1px solid var(--border);border-radius:var(--radius)}.admin-form label{display:block;margin-bottom:17px;color:var(--secondary);font-size:13px;font-weight:700}.admin-form label>span{color:#dc2626}.admin-form input,.admin-form select,.admin-form textarea{width:100%;margin-top:7px;padding:10px 12px;border:1px solid #d4d4d8;border-radius:9px;background:#fff}.admin-form input,.admin-form select{height:44px}.admin-form textarea{resize:vertical;line-height:1.6}.admin-form label small{display:block;margin-top:5px;color:var(--muted);font-weight:400}.admin-field-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}.admin-form-actions{display:flex;gap:10px;margin-top:24px}.danger-zone{max-width:780px;margin-top:24px;padding:20px 24px;display:flex;align-items:center;justify-content:space-between;gap:20px;border:1px solid #fecaca;border-radius:var(--radius);background:#fff}.danger-zone strong{color:#991b1b}.danger-zone p{margin:4px 0 0;color:var(--muted);font-size:13px}.danger-zone button{padding:9px 14px;border:1px solid #fecaca;border-radius:8px;background:#fff;color:#b91c1c;cursor:pointer}.inquiry-heading{margin-top:22px}.inquiry-detail-grid{display:grid;grid-template-columns:1.1fr .9fr;gap:20px;align-items:start}.inquiry-content{padding:26px;background:#fff;border:1px solid var(--border);border-radius:var(--radius)}.contact-summary{padding:16px;background:#f8fafc;border-radius:9px}.contact-summary>span{float:right;color:var(--primary);font-size:12px;font-weight:800}.contact-summary p{margin:0;line-height:1.8}.inquiry-content h2{margin-top:25px;font-size:17px}.inquiry-content>p{line-height:1.85}.reply-form{padding:26px}.reply-form h2{margin:0 0 20px}
@media(max-width:760px){.admin-field-grid,.inquiry-detail-grid{grid-template-columns:1fr}.danger-zone{align-items:stretch;flex-direction:column}.danger-zone button{width:100%}}

View File

@@ -0,0 +1,67 @@
.post-thumbnail img{width:100%;height:100%;object-fit:cover}
.post-card{position:relative;transition:transform .2s ease,box-shadow .2s ease}
.post-card:hover{transform:translateY(-3px);box-shadow:0 12px 28px rgba(15,23,42,.1)}
.post-thumbnail>.post-category{z-index:3}
.post-placeholder{--placeholder-accent:#2563eb;position:absolute;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:9px;overflow:hidden;background:linear-gradient(145deg,#eff6ff,#dbeafe);color:var(--placeholder-accent)}
.post-placeholder::before{content:"";position:absolute;inset:0;background-image:linear-gradient(rgba(37,99,235,.08) 1px,transparent 1px),linear-gradient(90deg,rgba(37,99,235,.08) 1px,transparent 1px);background-size:34px 34px;mask-image:linear-gradient(to bottom right,#000,transparent 78%)}
.post-placeholder::after{content:"";position:absolute;right:-13%;bottom:-45%;width:62%;aspect-ratio:1;border:34px solid color-mix(in srgb,var(--placeholder-accent) 14%,transparent);border-radius:50%}
.post-placeholder[data-category="키폰시스템"]{--placeholder-accent:#7c3aed;background:linear-gradient(145deg,#f5f3ff,#ede9fe)}
.post-placeholder[data-category="CCTV"]{--placeholder-accent:#0f766e;background:linear-gradient(145deg,#f0fdfa,#ccfbf1)}
.post-placeholder[data-category="시공사례"]{--placeholder-accent:#c2410c;background:linear-gradient(145deg,#fff7ed,#ffedd5)}
.post-thumbnail .post-placeholder img{position:relative;z-index:1;width:136px;height:auto;object-fit:contain;filter:drop-shadow(0 5px 10px rgba(15,23,42,.08))}
.post-thumbnail .post-placeholder small{position:relative;z-index:1;padding:0;background:transparent;color:var(--placeholder-accent);font-size:10px;font-weight:800;letter-spacing:.2em}
.post-body h2 a::after,.post-body h3 a::after{inset:0;z-index:2}
.post-body time{position:relative;z-index:3}
.article-image{margin-inline:0;overflow:hidden}
.article-image img{width:100%;height:100%;object-fit:cover}
.admin-upload-field{margin:0 0 20px;padding:18px;border:1px solid var(--border);border-radius:10px}
.admin-upload-field legend{padding:0 7px;color:var(--secondary);font-size:13px;font-weight:800}
.admin-upload-field img{width:min(100%,360px);aspect-ratio:16/9;object-fit:cover;border-radius:8px}
.admin-upload-field>p{margin:8px 0 16px;color:var(--muted);font-size:13px}
.admin-notice{margin:0 0 18px;padding:12px 14px;border:1px solid #bbf7d0;border-radius:8px;background:#f0fdf4;color:#166534;font-size:13px}
.floating-call{display:inline-flex;align-items:center;gap:8px}
.floating-call svg{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.brand-logo{width:64px;height:auto;object-fit:contain;flex:none}.footer-brand .brand-logo{width:56px}.admin-brand .brand-logo{width:60px}.login-logo{width:112px;height:auto;margin:auto;object-fit:contain}
.hero-visual{overflow:hidden;background:#111827;display:block;text-align:initial;color:#fff}
.hero-slider{position:relative;isolation:isolate}
.hero-slides,.hero-slide{position:absolute;inset:0;margin:0}
.hero-slide{opacity:0;visibility:hidden;transition:opacity .55s ease,visibility .55s ease}
.hero-slide.is-active{opacity:1;visibility:visible}
.hero-slide::after{content:"";position:absolute;inset:auto 0 0;height:42%;background:linear-gradient(transparent,rgba(9,9,11,.72));pointer-events:none}
.hero-slide img{width:100%;height:100%;object-fit:cover;object-position:center}
.hero-slide figcaption{position:absolute;left:18px;right:18px;bottom:54px;z-index:1;color:#fff;font-size:14px;font-weight:700;text-shadow:0 1px 4px rgba(0,0,0,.5)}
.hero-slider .hero-slider-controls{position:absolute;left:14px;right:14px;bottom:12px;z-index:2;display:flex;align-items:center;gap:8px}
.hero-slider-controls button{border:0;color:#fff;cursor:pointer}
.hero-slider-arrow{width:32px;height:32px;border-radius:50%;background:rgba(9,9,11,.64);font-size:25px;line-height:1}
.hero-slider .hero-slider-dots{display:flex;align-items:center;gap:7px}
.hero-slider-dots button{width:8px;height:8px;padding:0;border-radius:50%;background:rgba(255,255,255,.5)}
.hero-slider-dots button.is-active{width:22px;border-radius:999px;background:#fff}
.hero-slider-pause{min-height:32px;margin-left:auto;padding:0 11px;border-radius:999px;background:rgba(9,9,11,.64);font-size:11px;font-weight:700}
@media(prefers-reduced-motion:reduce){.hero-slide{transition:none}}
@media(prefers-reduced-motion:reduce){.post-card{transition:none}.post-card:hover{transform:none}}
.contact-map-card>p{margin-bottom:18px}
.contact-form input::placeholder,.contact-form textarea::placeholder{color:#a1a1aa;font-weight:400;opacity:1}
.contact-form input::placeholder{font-size:14px}
.contact-form select{appearance:none;padding-right:42px;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2352565f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 14px center;background-size:16px 16px}
.contact-form .form-field{min-width:0;margin:0 0 17px;padding:0;border:0}
.contact-form .form-field legend{padding:0;color:var(--secondary);font-size:13px;font-weight:700}
.email-template{display:grid;grid-template-columns:minmax(0,1fr) auto minmax(128px,.8fr);align-items:center;gap:8px}
.email-template[hidden],.email-direct-row[hidden]{display:none}
.email-template>span{margin-top:7px;color:var(--secondary);font-size:15px;font-weight:700}
.contact-form .email-template input,.contact-form .email-template select,.contact-form .email-direct-row input{margin-top:7px}
.contact-form .email-template input::placeholder{font-size:13px;color:#b0b3ba}
.email-direct-row{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:end;gap:8px}
.email-template-return{height:44px;padding:0 12px;border:1px solid #d4d4d8;border-radius:9px;background:#fff;color:var(--secondary);font-size:13px;font-weight:700;cursor:pointer;white-space:nowrap}
.naver-map-frame{position:relative;height:300px;margin-top:18px;overflow:hidden;border:1px solid var(--border);border-radius:10px;background:#f1f5f9}
.naver-map{width:100%;height:100%!important}
.naver-map-status{position:absolute;inset:0;z-index:1;display:grid;place-items:center;margin:0!important;padding:24px;text-align:center;background:#f8fafc;color:var(--secondary)!important;font-size:14px}
.naver-map-frame[data-map-state=ready] .naver-map-status{display:none}
.naver-map-frame[data-map-state=error] .naver-map-status{background:#fff7ed;color:#9a3412!important}
.naver-map-fullscreen{position:absolute;right:10px;top:10px;z-index:5;width:38px;height:38px;display:grid;place-items:center;padding:0;border:1px solid rgba(15,23,42,.14);border-radius:8px;background:#fff;color:#27272a;box-shadow:0 2px 8px rgba(15,23,42,.14);cursor:pointer}
.naver-map-fullscreen[hidden]{display:none}
.naver-map-fullscreen svg{width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.naver-map-frame:fullscreen{width:100vw;height:100vh;margin:0;border:0;border-radius:0;background:#fff}
.naver-map-frame.is-fullscreen .naver-map-fullscreen{right:16px;top:16px}
.naver-map-external{display:inline-flex;align-items:center;min-height:44px;margin-top:14px;color:var(--primary);font-weight:700}
.naver-map-frame[data-map-state=loading]+.naver-map-external,.naver-map-frame[data-map-state=ready]+.naver-map-external{display:none}
@media(max-width:640px){.post-card:hover{transform:none}.email-template{grid-template-columns:minmax(0,1fr) auto minmax(116px,.85fr);gap:6px}.email-direct-row{grid-template-columns:1fr}.email-template-return{width:100%;margin-top:0}.floating-call{display:none}}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

315
public/assets/js/site.js Normal file
View File

@@ -0,0 +1,315 @@
(() => {
const toggle = document.querySelector('.menu-toggle');
const navigation = document.querySelector('.site-navigation');
if (!toggle || !navigation) return;
const close = () => {
navigation.classList.remove('is-open');
toggle.setAttribute('aria-expanded', 'false');
toggle.querySelector('.sr-only').textContent = '메뉴 열기';
};
toggle.addEventListener('click', () => {
const open = !navigation.classList.contains('is-open');
navigation.classList.toggle('is-open', open);
toggle.setAttribute('aria-expanded', String(open));
toggle.querySelector('.sr-only').textContent = open ? '메뉴 닫기' : '메뉴 열기';
});
navigation.addEventListener('click', (event) => {
if (event.target.closest('a')) close();
});
window.addEventListener('resize', () => {
if (window.innerWidth > 960) close();
});
})();
(() => {
const slider = document.querySelector('[data-hero-slider]');
if (!slider) return;
const slides = [...slider.querySelectorAll('.hero-slide')];
const dots = [...slider.querySelectorAll('[data-slider-dot]')];
const previous = slider.querySelector('[data-slider-prev]');
const next = slider.querySelector('[data-slider-next]');
const pause = slider.querySelector('[data-slider-pause]');
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
let current = 0;
let timer = null;
let isPaused = false;
let isInteracting = false;
const show = (index) => {
current = (index + slides.length) % slides.length;
slides.forEach((slide, slideIndex) => {
const isActive = slideIndex === current;
slide.classList.toggle('is-active', isActive);
slide.setAttribute('aria-hidden', String(!isActive));
});
dots.forEach((dot, dotIndex) => {
const isActive = dotIndex === current;
dot.classList.toggle('is-active', isActive);
if (isActive) dot.setAttribute('aria-current', 'true');
else dot.removeAttribute('aria-current');
});
};
const stop = () => {
if (timer !== null) window.clearInterval(timer);
timer = null;
};
const start = () => {
stop();
if (isPaused || isInteracting || reducedMotion.matches || document.hidden) return;
timer = window.setInterval(() => show(current + 1), 5000);
};
const move = (direction) => {
show(current + direction);
start();
};
previous?.addEventListener('click', () => move(-1));
next?.addEventListener('click', () => move(1));
dots.forEach((dot) => dot.addEventListener('click', () => {
show(Number(dot.dataset.sliderDot));
start();
}));
pause?.addEventListener('click', () => {
isPaused = !isPaused;
pause.setAttribute('aria-pressed', String(isPaused));
pause.textContent = isPaused ? '자동재생 시작' : '자동재생 일시정지';
start();
});
slider.addEventListener('mouseenter', () => {
isInteracting = true;
stop();
});
slider.addEventListener('mouseleave', () => {
isInteracting = false;
start();
});
slider.addEventListener('focusin', () => {
isInteracting = true;
stop();
});
slider.addEventListener('focusout', () => {
isInteracting = false;
start();
});
document.addEventListener('visibilitychange', start);
reducedMotion.addEventListener('change', start);
start();
})();
(() => {
const phoneInput = document.querySelector('[data-phone-input]');
const emailField = document.querySelector('[data-email-field]');
if (phoneInput) {
const formatPhone = (value) => {
let numbers = value.replace(/\D/g, '');
if (numbers.startsWith('02')) {
numbers = numbers.slice(0, 10);
if (numbers.length <= 2) return numbers;
if (numbers.length <= 5) return `${numbers.slice(0, 2)}-${numbers.slice(2)}`;
if (numbers.length <= 9) return `${numbers.slice(0, 2)}-${numbers.slice(2, 5)}-${numbers.slice(5)}`;
return `${numbers.slice(0, 2)}-${numbers.slice(2, 6)}-${numbers.slice(6)}`;
}
if (/^1\d{3}/.test(numbers)) {
numbers = numbers.slice(0, 8);
return numbers.length <= 4 ? numbers : `${numbers.slice(0, 4)}-${numbers.slice(4)}`;
}
if (/^050\d/.test(numbers)) {
numbers = numbers.slice(0, 12);
if (numbers.length <= 4) return numbers;
if (numbers.length <= 8) return `${numbers.slice(0, 4)}-${numbers.slice(4)}`;
return `${numbers.slice(0, 4)}-${numbers.slice(4, 8)}-${numbers.slice(8)}`;
}
numbers = numbers.slice(0, 11);
if (numbers.length <= 3) return numbers;
if (numbers.length <= 6) return `${numbers.slice(0, 3)}-${numbers.slice(3)}`;
if (numbers.length <= 10) return `${numbers.slice(0, 3)}-${numbers.slice(3, 6)}-${numbers.slice(6)}`;
return `${numbers.slice(0, 3)}-${numbers.slice(3, 7)}-${numbers.slice(7)}`;
};
const applyPhoneFormat = () => {
phoneInput.value = formatPhone(phoneInput.value);
};
phoneInput.addEventListener('input', applyPhoneFormat);
applyPhoneFormat();
}
document.querySelectorAll('[data-pin-input]').forEach((pinInput) => {
const applyPinFormat = () => {
pinInput.value = pinInput.value.replace(/\D/g, '').slice(0, 4);
};
pinInput.addEventListener('input', applyPinFormat);
applyPinFormat();
});
if (!emailField) return;
const template = emailField.querySelector('[data-email-template]');
const localInput = emailField.querySelector('[data-email-local]');
const domainSelect = emailField.querySelector('[data-email-domain]');
const directRow = emailField.querySelector('[data-email-direct]');
const fullInput = emailField.querySelector('[data-email-full]');
const returnButton = emailField.querySelector('[data-email-return]');
if (!template || !localInput || !domainSelect || !directRow || !fullInput || !returnButton) return;
const domains = [...domainSelect.options].map((option) => option.value).filter((value) => value !== 'direct');
const splitEmail = (email) => {
const separator = email.lastIndexOf('@');
if (separator <= 0) return null;
return { local: email.slice(0, separator), domain: email.slice(separator + 1).toLowerCase() };
};
const syncEmail = () => {
fullInput.value = localInput.value && domainSelect.value !== 'direct'
? `${localInput.value}@${domainSelect.value}`
: '';
};
const showTemplate = () => {
const parsed = splitEmail(fullInput.value);
localInput.value = parsed?.local || fullInput.value;
domainSelect.value = parsed && domains.includes(parsed.domain) ? parsed.domain : 'naver.com';
template.hidden = false;
directRow.hidden = true;
returnButton.hidden = true;
syncEmail();
};
const showDirect = () => {
const local = localInput.value.trim();
fullInput.value = local ? `${local}@` : '';
template.hidden = true;
directRow.hidden = false;
returnButton.hidden = false;
fullInput.focus();
};
localInput.addEventListener('input', syncEmail);
domainSelect.addEventListener('change', () => {
if (domainSelect.value === 'direct') showDirect();
else syncEmail();
});
returnButton.addEventListener('click', () => {
showTemplate();
localInput.focus();
});
emailField.closest('form')?.addEventListener('submit', () => {
if (!template.hidden) syncEmail();
});
const initialEmail = splitEmail(fullInput.value);
if (!fullInput.value || (initialEmail && domains.includes(initialEmail.domain))) showTemplate();
else returnButton.hidden = false;
})();
(() => {
const mapFrame = document.querySelector('[data-naver-map]');
if (!mapFrame) return;
const mapElement = mapFrame.querySelector('.naver-map');
const statusElement = mapFrame.querySelector('[data-naver-map-status]');
const fullscreenButton = mapFrame.querySelector('[data-naver-map-fullscreen]');
const externalLink = mapFrame.parentElement?.querySelector('[data-naver-map-external]');
const clientId = mapFrame.dataset.clientId;
const latitude = Number(mapFrame.dataset.latitude);
const longitude = Number(mapFrame.dataset.longitude);
const label = mapFrame.dataset.label;
let settled = false;
const setState = (state, message = '') => {
settled = state !== 'loading';
mapFrame.dataset.mapState = state;
mapFrame.setAttribute('aria-busy', String(state === 'loading'));
if (fullscreenButton) fullscreenButton.hidden = state !== 'ready';
if (statusElement && message) statusElement.textContent = message;
};
const fail = () => {
setState('error', '지도를 불러오지 못했습니다. 아래 네이버 지도 링크를 이용해 주세요.');
};
const renderMap = () => {
if (!mapElement || !window.naver?.maps || !Number.isFinite(latitude) || !Number.isFinite(longitude)) {
fail();
return;
}
const position = new window.naver.maps.LatLng(latitude, longitude);
const map = new window.naver.maps.Map(mapElement, {
center: position,
zoom: 17,
minZoom: 10,
zoomControl: true,
zoomControlOptions: {
position: window.naver.maps.Position.RIGHT_CENTER,
},
});
new window.naver.maps.Marker({
map,
position,
title: label,
});
const refreshMapSize = () => {
window.setTimeout(() => {
window.naver.maps.Event.trigger(map, 'resize');
map.setCenter(position);
}, 0);
};
document.addEventListener('fullscreenchange', () => {
const isFullscreen = document.fullscreenElement === mapFrame;
mapFrame.classList.toggle('is-fullscreen', isFullscreen);
if (fullscreenButton) {
fullscreenButton.setAttribute('aria-label', isFullscreen ? '전체 화면 종료' : '지도를 전체 화면으로 보기');
fullscreenButton.title = isFullscreen ? '전체 화면 종료' : '전체 화면';
}
refreshMapSize();
});
if (fullscreenButton && !mapFrame.requestFullscreen) {
fullscreenButton.setAttribute('aria-label', '네이버 지도에서 크게 보기');
fullscreenButton.title = '네이버 지도에서 열기';
}
fullscreenButton?.addEventListener('click', async () => {
if (document.fullscreenElement === mapFrame) {
await document.exitFullscreen();
} else if (mapFrame.requestFullscreen) {
await mapFrame.requestFullscreen();
} else if (externalLink) {
window.open(externalLink.href, '_blank', 'noopener');
}
});
setState('ready', '지도를 불러왔습니다.');
};
if (!clientId) {
fail();
return;
}
setState('loading');
window.navermap_authFailure = fail;
const script = document.createElement('script');
const parameters = new URLSearchParams({
ncpKeyId: clientId,
});
script.src = `https://oapi.map.naver.com/openapi/v3/maps.js?${parameters}`;
script.async = true;
script.onload = renderMap;
script.onerror = fail;
document.head.append(script);
window.setTimeout(() => {
if (!settled) fail();
}, 12000);
})();