feat: build company introduction webpage
This commit is contained in:
236
script.js
Normal file
236
script.js
Normal file
@@ -0,0 +1,236 @@
|
||||
const caseData = {
|
||||
election: {
|
||||
kicker: "2.1 선거방송",
|
||||
title: "선거방송 데이터 방송 그래픽 솔루션 개발",
|
||||
description: "선관위 데이터와 방송 그래픽을 실시간으로 연동해 자막 송출 시스템을 구성합니다.",
|
||||
bullets: [
|
||||
"선관위 데이터 실시간 연동 코드 적용",
|
||||
"데이터 스케줄러와 송출 컨트롤러 시스템 개발",
|
||||
"방송 그래픽과 데이터 연동을 통한 실시간 자막 송출"
|
||||
],
|
||||
images: [
|
||||
{ src: "assets/media/election-candidate-screen.webp", alt: "선거방송 후보자 득표 그래픽 화면" },
|
||||
{ src: "assets/media/election-seat-map.webp", alt: "선거방송 의석 현황 그래픽 화면" },
|
||||
{ src: "assets/media/election-region-map.webp", alt: "선거방송 전국 판세 지도 그래픽" }
|
||||
]
|
||||
},
|
||||
virtual: {
|
||||
kicker: "2.2 버추얼 그래픽 시스템",
|
||||
title: "버추얼 그래픽 솔루션 개발",
|
||||
description: "Unreal Engine과 Unity를 활용해 VR, AR 기반의 실시간 3D 그래픽 시스템을 개발합니다.",
|
||||
bullets: [
|
||||
"Unreal Engine 기반 VR/AR 시스템 개발",
|
||||
"실시간 데이터 3D 그래픽 처리 시스템 적용",
|
||||
"Unity 활용 영상 콘텐츠 제작"
|
||||
],
|
||||
images: [
|
||||
{ src: "assets/media/virtual-election-studio.webp", alt: "버추얼 선거방송 스튜디오 그래픽" },
|
||||
{ src: "assets/media/virtual-candidate-wall.webp", alt: "버추얼 후보자 데이터 월" },
|
||||
{ src: "assets/media/virtual-debate-wall.webp", alt: "버추얼 토론 방송 그래픽 월" }
|
||||
]
|
||||
},
|
||||
finance: {
|
||||
kicker: "2.3 데이터베이스 / 경제 방송",
|
||||
title: "증권 데이터 및 경제 방송 송출 그래픽 시스템",
|
||||
description: "코스콤 데이터와 해외 지수 데이터를 연계해 방송 송출 그래픽과 API 서버를 구현합니다.",
|
||||
bullets: [
|
||||
"코스콤 데이터와 해외 지수 연계 데이터베이스 서버 구축",
|
||||
"실시간 그래픽 연계 데이터 송수신 프로그램 개발",
|
||||
"REST 서버 API 구현 및 방송 송출 시스템 적용"
|
||||
],
|
||||
images: [
|
||||
{ src: "assets/media/finance-chart.webp", alt: "경제 방송 주가 차트 그래픽" },
|
||||
{ src: "assets/media/finance-index.webp", alt: "경제 방송 지수 그래픽" },
|
||||
{ src: "assets/media/finance-db-console.webp", alt: "경제 방송 데이터베이스 콘솔" }
|
||||
]
|
||||
},
|
||||
ai: {
|
||||
kicker: "2.4 AI 학습 솔루션",
|
||||
title: "AI 기반 거리 측정 시스템",
|
||||
description: "KPGA 공식 기록에 사용되는 그린 홀 컵 간 거리 측정 솔루션을 개발했습니다.",
|
||||
bullets: [
|
||||
"CUDA 기반 딥러닝 모델 학습",
|
||||
"카메라 영상 신호 수신 후 공 위치와 좌표 변환",
|
||||
"골프 중계와 기록 환경에 맞춘 AI 거리 측정"
|
||||
],
|
||||
images: [
|
||||
{ src: "assets/media/ai-distance-result.webp", alt: "AI 기반 거리 측정 결과 화면" },
|
||||
{ src: "assets/media/ai-training-green.webp", alt: "골프 그린 AI 학습 화면" },
|
||||
{ src: "assets/media/ai-detection-console.webp", alt: "AI 영상 분석 콘솔 화면" }
|
||||
]
|
||||
},
|
||||
shopping: {
|
||||
kicker: "2.5 홈쇼핑 송출 시스템",
|
||||
title: "홈쇼핑 자동화 송출 시스템 / 라이브 톡",
|
||||
description: "송출 스케줄과 원격 제어, 모바일 연동을 포함한 홈쇼핑 방송 운영 시스템을 개발합니다.",
|
||||
bullets: [
|
||||
"송출 스케줄 관리 데이터 송수신 모듈 개발",
|
||||
"홈쇼핑 송출 프로그램 원격제어 로컬/모바일 연동",
|
||||
"웹 페이지와 앱을 활용한 실시간 방송 데이터 송출"
|
||||
],
|
||||
images: [
|
||||
{ src: "assets/media/shopping-control-dashboard.webp", alt: "홈쇼핑 송출 제어 대시보드" },
|
||||
{ src: "assets/media/shopping-live-output.webp", alt: "홈쇼핑 라이브 방송 송출 화면" },
|
||||
{ src: "assets/media/mobile-live-talk.webp", alt: "홈쇼핑 모바일 라이브 톡 화면" }
|
||||
]
|
||||
},
|
||||
sports: {
|
||||
kicker: "2.6 스포츠 TV Graphic",
|
||||
title: "스포츠 / 게임 데이터 솔루션 송출 시스템 개발",
|
||||
description: "Live Data를 자막시스템에서 사용 가능한 형태로 변환하고 다양한 종목의 TVG 시스템을 제작합니다.",
|
||||
bullets: [
|
||||
"HD/UHD급 CG 제작과 국제신호 제작",
|
||||
"3D/2D Cut, 애니메이션 등 특수 효과 적용",
|
||||
"구기, 투기, 계측 종목 등 다양한 스포츠 TVG 구성"
|
||||
],
|
||||
images: [
|
||||
{ src: "assets/media/sports-game-result.webp", alt: "게임 결과 방송 그래픽" },
|
||||
{ src: "assets/media/sports-player-profile.webp", alt: "선수 프로필 방송 그래픽" },
|
||||
{ src: "assets/media/football-scorebug.webp", alt: "축구 스코어버그 방송 그래픽" }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const body = document.body;
|
||||
const header = document.getElementById("siteHeader");
|
||||
const mobilePanel = document.getElementById("mobilePanel");
|
||||
const menuToggle = document.querySelector(".menu-toggle");
|
||||
const cursor = document.querySelector(".cursor");
|
||||
const loaderBar = document.getElementById("loaderBar");
|
||||
const loaderValue = document.getElementById("loaderValue");
|
||||
const caseImage = document.getElementById("caseImage");
|
||||
const caseKicker = document.getElementById("caseKicker");
|
||||
const caseName = document.getElementById("caseName");
|
||||
const caseDescription = document.getElementById("caseDescription");
|
||||
const caseBullets = document.getElementById("caseBullets");
|
||||
const caseThumbs = document.getElementById("caseThumbs");
|
||||
const navLinks = [...document.querySelectorAll(".primary-nav a")];
|
||||
|
||||
let loaderProgress = 0;
|
||||
const loaderTimer = window.setInterval(() => {
|
||||
loaderProgress = Math.min(loaderProgress + Math.ceil(Math.random() * 12), 100);
|
||||
loaderBar.style.width = `${loaderProgress}%`;
|
||||
loaderValue.textContent = String(loaderProgress).padStart(2, "0");
|
||||
if (loaderProgress >= 100) {
|
||||
window.clearInterval(loaderTimer);
|
||||
window.setTimeout(() => {
|
||||
body.classList.remove("is-loading");
|
||||
body.classList.add("is-ready");
|
||||
}, 220);
|
||||
}
|
||||
}, 55);
|
||||
|
||||
const updateScrollState = () => {
|
||||
const scrollTop = window.scrollY || document.documentElement.scrollTop;
|
||||
const scrollable = document.documentElement.scrollHeight - window.innerHeight;
|
||||
const progress = scrollable > 0 ? (scrollTop / scrollable) * 100 : 0;
|
||||
header.classList.toggle("is-scrolled", scrollTop > 20);
|
||||
header.style.setProperty("--page-progress", `${progress}%`);
|
||||
|
||||
let currentHref = navLinks[0]?.getAttribute("href");
|
||||
navLinks.forEach(link => {
|
||||
const target = document.querySelector(link.getAttribute("href"));
|
||||
if (target && target.getBoundingClientRect().top <= window.innerHeight * 0.45) {
|
||||
currentHref = link.getAttribute("href");
|
||||
}
|
||||
});
|
||||
navLinks.forEach(link => {
|
||||
link.setAttribute("aria-current", link.getAttribute("href") === currentHref ? "true" : "false");
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener("scroll", updateScrollState, { passive: true });
|
||||
window.addEventListener("resize", updateScrollState);
|
||||
updateScrollState();
|
||||
|
||||
const revealObserver = new IntersectionObserver(
|
||||
entries => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add("in-view");
|
||||
revealObserver.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.16 }
|
||||
);
|
||||
|
||||
document.querySelectorAll(".reveal").forEach(element => revealObserver.observe(element));
|
||||
|
||||
const setMenuOpen = open => {
|
||||
menuToggle.setAttribute("aria-expanded", String(open));
|
||||
menuToggle.setAttribute("aria-label", open ? "메뉴 닫기" : "메뉴 열기");
|
||||
mobilePanel.classList.toggle("is-open", open);
|
||||
mobilePanel.setAttribute("aria-hidden", String(!open));
|
||||
menuToggle.innerHTML = `<i data-lucide="${open ? "x" : "menu"}"></i>`;
|
||||
if (window.lucide) window.lucide.createIcons();
|
||||
};
|
||||
|
||||
if (menuToggle) {
|
||||
menuToggle.addEventListener("click", () => {
|
||||
const open = menuToggle.getAttribute("aria-expanded") !== "true";
|
||||
setMenuOpen(open);
|
||||
});
|
||||
}
|
||||
|
||||
mobilePanel.querySelectorAll("a").forEach(link => {
|
||||
link.addEventListener("click", () => setMenuOpen(false));
|
||||
});
|
||||
|
||||
const renderCase = (key, imageIndex = 0) => {
|
||||
const data = caseData[key];
|
||||
const image = data.images[imageIndex];
|
||||
caseImage.src = image.src;
|
||||
caseImage.alt = image.alt;
|
||||
caseKicker.textContent = data.kicker;
|
||||
caseName.textContent = data.title;
|
||||
caseDescription.textContent = data.description;
|
||||
caseBullets.innerHTML = data.bullets.map(item => `<li>${item}</li>`).join("");
|
||||
caseThumbs.innerHTML = data.images
|
||||
.map(
|
||||
(item, index) => `
|
||||
<button type="button" class="${index === imageIndex ? "is-active" : ""}" data-case-thumb="${index}" aria-label="${item.alt}">
|
||||
<img src="${item.src}" alt="">
|
||||
</button>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
|
||||
caseThumbs.querySelectorAll("button").forEach(button => {
|
||||
button.addEventListener("click", () => renderCase(key, Number(button.dataset.caseThumb)));
|
||||
});
|
||||
};
|
||||
|
||||
document.querySelectorAll(".case-tabs button").forEach(button => {
|
||||
button.addEventListener("click", () => {
|
||||
document.querySelectorAll(".case-tabs button").forEach(item => {
|
||||
item.classList.remove("is-active");
|
||||
item.setAttribute("aria-selected", "false");
|
||||
});
|
||||
button.classList.add("is-active");
|
||||
button.setAttribute("aria-selected", "true");
|
||||
renderCase(button.dataset.case);
|
||||
});
|
||||
});
|
||||
|
||||
renderCase("election");
|
||||
|
||||
if (window.matchMedia("(pointer: fine)").matches && cursor) {
|
||||
window.addEventListener("pointermove", event => {
|
||||
cursor.style.transform = `translate3d(${event.clientX}px, ${event.clientY}px, 0) translate(-50%, -50%)`;
|
||||
cursor.classList.add("is-visible");
|
||||
});
|
||||
|
||||
document.querySelectorAll("a, button").forEach(element => {
|
||||
element.addEventListener("pointerenter", () => cursor.classList.add("is-active"));
|
||||
element.addEventListener("pointerleave", () => cursor.classList.remove("is-active"));
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", event => {
|
||||
if (event.key === "Escape") setMenuOpen(false);
|
||||
});
|
||||
|
||||
if (window.lucide) {
|
||||
window.lucide.createIcons();
|
||||
}
|
||||
Reference in New Issue
Block a user