You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

502 lines
16 KiB

import { useRef, useState, useEffect } from "react";
import { motion, AnimatePresence, useInView } from "framer-motion";
import SubHero from "../../components/SubHero";
import useFadeIn from "../../hooks/useFadeIn";
import {
Plane,
Globe,
UtensilsCrossed,
Thermometer,
MapPin,
Link,
QrCode,
Users,
Siren,
Wrench,
PlusCircle,
Settings,
ShieldCheck,
Monitor,
Building2,
ClipboardList,
Radio,
Database,
Navigation,
} from "lucide-react";
const ease = [0.25, 0.1, 0.25, 1];
const PROJECTS = [
{
id: "01",
title: "KAC UTM 시스템 구축",
tags: ["항공/관제", "UTM"],
image: "/images/si_kac_utm.png",
desc: [
{
icon: <Radio size={14} />,
title: "실시간 관제",
text: "무인기 실시간 공역 감시 및 관제 시스템",
},
{
icon: <Navigation size={14} />,
title: "비행 경로 관리",
text: "비행 계획 승인 및 충돌 위험 사전 분석",
},
{
icon: <Database size={14} />,
title: "데이터 통합",
text: "비행 데이터 수집·분석 통합 플랫폼 구축",
},
],
},
{
id: "02",
title: "불법드론 탐지 시스템 구축",
tags: ["항공/보안", "드론"],
image: "/images/si_injustice_drone.png",
desc: [
{
icon: <ShieldCheck size={14} />,
title: "불법드론 탐지",
text: "비인가 드론 실시간 탐지 및 식별 시스템",
},
{
icon: <Siren size={14} />,
title: "위협 대응",
text: "불법 비행체 침입 시 신속 경보 및 대응",
},
{
icon: <Radio size={14} />,
title: "관제 연동",
text: "UTM 관제 시스템과 실시간 연계 운영",
},
],
},
{
id: "03",
title: "제주패스 OTA 항공 서비스 구축",
tags: ["항공/여행", "OTA"],
image: "/images/si_img1.png",
desc: [
{
icon: <Plane size={14} />,
title: "통합 여행 포털",
text: "렌터카·숙박·항공 통합 서비스 구축",
},
{
icon: <Globe size={14} />,
title: "해외시장 진출",
text: "국제선 항공으로 해외시장 초석 마련",
},
{
icon: <UtensilsCrossed size={14} />,
title: "종합 서비스",
text: "맛집·카페로 이어지는 제주패스 구현",
},
],
},
{
id: "04",
title: "안전관광 방역 시스템 구축",
tags: ["공공/방역", "방역/보안"],
image: "/images/si_img2.png",
desc: [
{
icon: <Thermometer size={14} />,
title: "건강 상태 관리",
text: "체류 기간 중 건강 상태 체크 및 관리",
},
{
icon: <MapPin size={14} />,
title: "동선 파악",
text: "이동 동선 파악 및 정보 관리 시스템",
},
{
icon: <Link size={14} />,
title: "시스템 연계",
text: "중국전담여행사 전자관리시스템 연계",
},
],
},
{
id: "05",
title: "클린인천 출입인증 시스템 구축",
tags: ["공공/출입", "인증/보안"],
image: "/images/si_img3.png",
desc: [
{
icon: <QrCode size={14} />,
title: "QR 방역 관리",
text: "QR코드 활용 방문자 방역 관리 구축",
},
{
icon: <Users size={14} />,
title: "방문자 관리",
text: "체계적 방문자 관리 및 출입 정보 제공",
},
{
icon: <Siren size={14} />,
title: "신속 대응",
text: "확진자 발생 시 신속한 방역 대응 지원",
},
],
},
{
id: "06",
title: "SSG.COM 항공서비스 운영 및 유지보수",
tags: ["항공/이커머스", "운영 · 유지보수"],
image: "/images/si_img4.png",
desc: [
{
icon: <Wrench size={14} />,
title: "오류 수정",
text: "시스템 오류 수정 및 불편 요소 개선",
},
{
icon: <PlusCircle size={14} />,
title: "기능 추가",
text: "필요 기능 추가 개발 및 데이터 추출",
},
{
icon: <Settings size={14} />,
title: "운영 안정화",
text: "시스템 최적화를 통한 운영 안정화",
},
],
},
{
id: "07",
title: "현대자동차 출입인증 시스템 구축",
tags: ["기업/보안", "인증/보안"],
image: "/images/si_img5.png",
desc: [
{
icon: <QrCode size={14} />,
title: "QR 방문자 관리",
text: "QR코드 활용 방문자 방역 관리 시스템",
},
{
icon: <Siren size={14} />,
title: "출입 정보 제공",
text: "확진자 발생 시 정확한 출입 정보 제공",
},
{
icon: <Monitor size={14} />,
title: "미디어 월 연동",
text: "미디어 월 연동을 통한 고객 응대",
},
],
},
{
id: "08",
title: "하이에어 항공운항 시스템 구축",
tags: ["항공", "인증/스케줄"],
image: "/images/si_img6.png",
desc: [
{
icon: <Building2 size={14} />,
title: "인프라 구축",
text: "항공 운송사업자 필수 서비스 및 인프라",
},
{
icon: <Plane size={14} />,
title: "국제선 대응",
text: "국제선 취항 대응 및 부가서비스 매출 증대",
},
{
icon: <ClipboardList size={14} />,
title: "고도화 체계",
text: "국토부 필수 시스템 및 서비스 고도화",
},
],
},
];
const AUTO_DELAY = 5000;
function SiPage() {
const basePath = import.meta.env.BASE_URL;
const ref = useFadeIn();
const sectionRef = useRef(null);
const sliderRef = useRef(null);
const cardRef = useRef(null);
const timerRef = useRef(null);
const dragStartX = useRef(null);
const dragStartCurrent = useRef(null);
const inView = useInView(sectionRef, { once: true, margin: "-100px" });
const [current, setCurrent] = useState(0);
const total = PROJECTS.length;
const [cardWidth, setCardWidth] = useState(0);
const scrollToCard = (index) => {
if (window.innerWidth <= 768 && sliderRef.current && cardRef.current) {
const cardW = cardRef.current.offsetWidth;
const gap = window.innerWidth <= 480 ? 14 : 14;
sliderRef.current.scrollTo({
left: index * (cardW + gap),
behavior: "smooth",
});
}
};
const resetTimer = () => {
if (timerRef.current) clearInterval(timerRef.current);
timerRef.current = setInterval(() => {
setCurrent((c) => {
const nextIdx = (c + 1) % total;
scrollToCard(nextIdx);
return nextIdx;
});
}, AUTO_DELAY);
};
const prev = () => {
resetTimer();
setCurrent((c) => {
const nextIdx = c <= 0 ? total - 1 : c - 1;
scrollToCard(nextIdx);
return nextIdx;
});
};
const next = () => {
resetTimer();
setCurrent((c) => {
const nextIdx = c >= total - 1 ? 0 : c + 1;
scrollToCard(nextIdx);
return nextIdx;
});
};
const handleDragStart = (e) => {
dragStartX.current =
e.type === "touchstart" ? e.touches[0].clientX : e.clientX;
dragStartCurrent.current = current;
};
const handleDragEnd = (e) => {
if (dragStartX.current === null) return;
const endX =
e.type === "touchend" ? e.changedTouches[0].clientX : e.clientX;
const diff = dragStartX.current - endX;
if (diff > 50) next();
else if (diff < -50) prev();
dragStartX.current = null;
};
useEffect(() => {
const update = () => {
if (cardRef.current) {
setCardWidth(cardRef.current.offsetWidth);
}
};
update();
window.addEventListener("resize", update);
return () => window.removeEventListener("resize", update);
}, []);
useEffect(() => {
resetTimer();
return () => clearInterval(timerRef.current);
}, [total]);
const BUSINESS_NAV = [
{ label: "System Integration", to: "/business/si" },
{ label: "R&D", to: "/business/rnd" },
{ label: "운영 · 유지보수", to: "/business/maintenance" },
];
return (
<article ref={ref}>
<SubHero
label="BUSINESS"
title={
<>
<em>SI Solutions</em>
</>
}
navItems={BUSINESS_NAV}
/>
<div className="sub-content si-archive-wrap">
<div className="inner-wrap">
<section className="si_archive" ref={sectionRef}>
<div className="si_archive__main">
{/* 헤더 */}
<div className="si_archive__header">
<motion.span
className="fc-eyebrow"
initial={{ opacity: 0, y: 16 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, ease }}
>
PROJECT ARCHIVE
</motion.span>
<motion.h2
className="si_archive__title"
initial={{ opacity: 0, y: 20 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.1, ease }}
>
{window.innerWidth <= 768 ? (
"수행사업 아카이브"
) : (
<>
수행사업
<br />
아카이브
</>
)}
</motion.h2>
<motion.p
className="si_archive__desc"
initial={{ opacity: 0, y: 16 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.2, ease }}
>
{window.innerWidth <= 768 ? (
"PAL Networks가 구축한 주요 프로젝트를 소개합니다."
) : (
<>
PAL Networks가 구축한
<br />
주요 프로젝트를 소개합니다.
</>
)}
</motion.p>
{/* 네비게이션 */}
<div className="si_archive__nav">
<motion.button
className="si_archive__nav-btn"
onClick={prev}
aria-label="이전"
initial={{ opacity: 0, y: 16 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.4, ease }}
>
</motion.button>
<motion.div
className="si_archive__progress"
initial={{ opacity: 0, y: 16 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.4, ease }}
>
<span className="si_archive__progress-cur">
{String(current + 1).padStart(2, "0")}
</span>
<span className="si_archive__progress-divider">/</span>
<span className="si_archive__progress-total">
{String(total).padStart(2, "0")}
</span>
</motion.div>
<motion.button
className="si_archive__nav-btn"
onClick={next}
aria-label="다음"
initial={{ opacity: 0, y: 16 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.4, ease }}
>
</motion.button>
</div>
</div>
{/* 슬라이더 */}
<div className="si_archive__right">
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.3, ease }}
>
<div
className="si_archive__slider"
style={{ display: "flex" }}
onMouseDown={handleDragStart}
onMouseUp={handleDragEnd}
onTouchStart={handleDragStart}
onTouchEnd={handleDragEnd}
ref={sliderRef}
>
<motion.div
className="si_archive__track"
animate={{
x:
window.innerWidth <= 768
? 0
: cardWidth
? -(current * (cardWidth + 18))
: 0,
}}
transition={{ duration: 0.55, ease }}
style={{ alignItems: "stretch" }}
>
{PROJECTS.map((project, idx) => (
<div
key={project.id}
className="si_archive__card"
ref={idx === 0 ? cardRef : null}
>
<div className="si_archive__card-img">
<img
src={`${basePath}images/${project.image.split("/").pop()}`}
alt={project.title}
draggable="false"
/>
<div className="si_archive__card-img-placeholder" />
</div>
<div className="si_archive__card-body">
<div className="si_archive__card-header">
<div className="si_archive__card-num">
{project.id}
</div>
<h3 className="si_archive__card-title">
{project.title}
</h3>
</div>
<div className="si_archive__card-tags">
{project.tags.map((tag) => (
<span key={tag} className="si_archive__tag">
{tag}
</span>
))}
</div>
<ul className="si_archive__card-desc">
{project.desc.map((item, i) => (
<li key={i}>
<div className="si_archive__card-desc-icon">
{item.icon}
</div>
<div className="si_archive__card-desc-title">
{item.title}
</div>
<div className="si_archive__card-desc-text">
{item.text}
</div>
</li>
))}
</ul>
</div>
</div>
))}
</motion.div>
</div>
</motion.div>
</div>
</div>
</section>
</div>
</div>
</article>
);
}
export default SiPage;