319 lines
12 KiB
JavaScript
319 lines
12 KiB
JavaScript
const caseData = {
|
|
election: {
|
|
kicker: "2.1 Election Broadcast",
|
|
title: "2026 지방선거 그래픽 송출 시스템",
|
|
client: "Yonhapnews TV / Local Stations",
|
|
field: "Tornado3 / Karisma",
|
|
status: "ELECT 2026",
|
|
description: "지역민방별 후보, 판세, 투표율 화면을 실시간 송출 구조로 정리했습니다.",
|
|
tags: ["Tornado3", "Karisma", "SBS API", "Regional CG"],
|
|
statValue: "89 Cuts",
|
|
statLabel: "지역민방용 그래픽 컷 89종",
|
|
bullets: [
|
|
"7개 지역민방 운영 필터 구성",
|
|
"후보·판세·투표율·당선 화면 제작",
|
|
"대기·반복·송출 상태를 나눈 운영 UI"
|
|
],
|
|
images: [
|
|
{ src: "assets/media/election-candidate-screen.webp", alt: "2026 local election candidate result graphic" },
|
|
{ src: "assets/media/local-election-2026-premium.webp", alt: "2026 지방선거 지역민방 방송 그래픽 운영 쇼케이스" },
|
|
{ src: "assets/media/local-election-2026-race.webp", alt: "2026 지방선거 광역단체장 1위 후보 그래픽" },
|
|
{ src: "assets/media/local-election-2026-landscape.webp", alt: "2026 지방선거 기초단체장 판세 그래픽" }
|
|
]
|
|
},
|
|
virtual: {
|
|
kicker: "2.2 Virtual Graphic",
|
|
title: "버추얼 그래픽 시스템",
|
|
client: "Broadcast Studio",
|
|
field: "VR / AR System",
|
|
status: "3D READY",
|
|
description: "Unreal·Unity 기반 VR/AR 그래픽을 방송 화면에 맞춰 구현합니다.",
|
|
tags: ["Unreal Engine", "Unity", "3D Data"],
|
|
statValue: "VR / AR",
|
|
statLabel: "실시간 3D 그래픽 처리",
|
|
bullets: [
|
|
"VR/AR 스튜디오 화면 구성",
|
|
"데이터 기반 3D 오브젝트 제어",
|
|
"Unity·Unreal 콘텐츠 제작"
|
|
],
|
|
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 Finance Broadcast",
|
|
title: "경제방송 데이터 그래픽",
|
|
client: "Finance Media",
|
|
field: "Database / API",
|
|
status: "MARKET API",
|
|
description: "시장 데이터를 방송 그래픽과 API 서버로 연결합니다.",
|
|
tags: ["Koscom", "REST API", "DB Server"],
|
|
statValue: "API",
|
|
statLabel: "국내외 지수 데이터 연동",
|
|
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 Vision",
|
|
title: "AI 거리 측정 솔루션",
|
|
client: "KPGA",
|
|
field: "AI Learning",
|
|
status: "AI RESULT",
|
|
description: "KPGA 공식 기록 환경에 적용된 거리 측정 기능을 개발했습니다.",
|
|
tags: ["CUDA", "Deep Learning", "Coordinate"],
|
|
statValue: "KPGA",
|
|
statLabel: "공식 기록 환경에 적용된 거리 측정 솔루션",
|
|
bullets: [
|
|
"CUDA 기반 딥러닝 학습",
|
|
"영상 좌표 변환과 위치 분석",
|
|
"골프 기록 환경에 맞춘 거리 측정"
|
|
],
|
|
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 Commerce Broadcast",
|
|
title: "홈쇼핑 송출 자동화",
|
|
client: "Commerce Channel",
|
|
field: "Automation",
|
|
status: "CONTROL",
|
|
description: "스케줄, 원격 제어, 모바일 화면을 하나의 운영 흐름으로 연결합니다.",
|
|
tags: ["Remote Control", "Mobile", "Schedule"],
|
|
statValue: "Automation",
|
|
statLabel: "스케줄·모바일 운영 연동",
|
|
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 Sports TV Graphic",
|
|
title: "스포츠 TV 그래픽 시스템",
|
|
client: "Sports & Game",
|
|
field: "TV Graphic",
|
|
status: "UHD CG",
|
|
description: "경기 데이터를 고해상도 그래픽과 스코어 화면으로 송출합니다.",
|
|
tags: ["HD/UHD", "International Signal", "Scorebug"],
|
|
statValue: "IS",
|
|
statLabel: "국제신호와 고해상도 CG 대응",
|
|
bullets: [
|
|
"HD/UHD급 CG 제작",
|
|
"2D/3D 컷과 애니메이션 적용",
|
|
"종목별 스코어 화면 구성"
|
|
],
|
|
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 caseStage = document.querySelector(".case-stage");
|
|
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 caseClient = document.getElementById("caseClient");
|
|
const caseField = document.getElementById("caseField");
|
|
const caseStatus = document.getElementById("caseStatus");
|
|
const caseTags = document.getElementById("caseTags");
|
|
const caseStatValue = document.getElementById("caseStatValue");
|
|
const caseStatLabel = document.getElementById("caseStatLabel");
|
|
const navLinks = [...document.querySelectorAll(".primary-nav a")];
|
|
|
|
let loaderProgress = 0;
|
|
const finishLoading = () => {
|
|
body.classList.remove("is-loading");
|
|
body.classList.add("is-ready");
|
|
};
|
|
|
|
if (loaderBar && loaderValue) {
|
|
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(finishLoading, 220);
|
|
}
|
|
}, 55);
|
|
} else {
|
|
finishLoading();
|
|
}
|
|
|
|
const updateScrollState = () => {
|
|
if (!header) return;
|
|
|
|
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 => {
|
|
if (!menuToggle || !mobilePanel) return;
|
|
|
|
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 fillList = (element, items, tagName = "span") => {
|
|
if (!element) return;
|
|
const nodes = items.map(item => {
|
|
const node = document.createElement(tagName);
|
|
node.textContent = item;
|
|
return node;
|
|
});
|
|
element.replaceChildren(...nodes);
|
|
};
|
|
|
|
const renderCase = (key, imageIndex = 0) => {
|
|
const data = caseData[key];
|
|
if (!data || !caseImage) return;
|
|
|
|
const image = data.images[imageIndex] || data.images[0];
|
|
caseStage?.classList.add("is-switching");
|
|
window.setTimeout(() => caseStage?.classList.remove("is-switching"), 180);
|
|
|
|
caseImage.src = image.src;
|
|
caseImage.alt = image.alt;
|
|
if (caseKicker) caseKicker.textContent = data.kicker;
|
|
if (caseName) caseName.textContent = data.title;
|
|
if (caseDescription) caseDescription.textContent = data.description;
|
|
if (caseClient) caseClient.textContent = data.client;
|
|
if (caseField) caseField.textContent = data.field;
|
|
if (caseStatus) caseStatus.textContent = data.status;
|
|
if (caseStatValue) caseStatValue.textContent = data.statValue;
|
|
if (caseStatLabel) caseStatLabel.textContent = data.statLabel;
|
|
fillList(caseTags, data.tags);
|
|
fillList(caseBullets, data.bullets, "li");
|
|
|
|
if (caseThumbs) {
|
|
const buttons = data.images.map((item, index) => {
|
|
const button = document.createElement("button");
|
|
button.type = "button";
|
|
button.className = index === imageIndex ? "is-active" : "";
|
|
button.dataset.caseThumb = String(index);
|
|
button.setAttribute("aria-label", item.alt);
|
|
|
|
const thumb = document.createElement("img");
|
|
thumb.src = item.src;
|
|
thumb.alt = "";
|
|
thumb.loading = "lazy";
|
|
thumb.decoding = "async";
|
|
button.appendChild(thumb);
|
|
button.addEventListener("click", () => renderCase(key, index));
|
|
return button;
|
|
});
|
|
caseThumbs.replaceChildren(...buttons);
|
|
}
|
|
};
|
|
|
|
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();
|
|
}
|