Browse Source

feat : rnd 페이지 추가

remotes/origin/main
이시연 3 weeks ago
parent
commit
a03fe4892c
  1. BIN
      public/images/rnd_img1.png
  2. BIN
      public/images/rnd_img2.png
  3. BIN
      public/images/rnd_img3.png
  4. BIN
      public/images/rnd_img4.png
  5. BIN
      public/images/rnd_img5.png
  6. BIN
      public/images/rnd_img6.png
  7. 6
      src/css/common.css
  8. 440
      src/pages/business/RndPage.jsx

BIN
public/images/rnd_img1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

BIN
public/images/rnd_img2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

BIN
public/images/rnd_img3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
public/images/rnd_img4.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
public/images/rnd_img5.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
public/images/rnd_img6.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

6
src/css/common.css

@ -913,7 +913,7 @@ body{overflow-x:hidden;}
/* ======================================== /* ========================================
SI PAGE - PROJECT ARCHIVE SECTION SI PAGE, RND - PROJECT ARCHIVE SECTION
======================================== */ ======================================== */
.si-archive-wrap .inner-wrap { max-width: none; padding: 0; margin: 0; } .si-archive-wrap .inner-wrap { max-width: none; padding: 0; margin: 0; }
.si_archive { padding:100px 0 120px; max-width:1660px; margin-left: auto; margin-right: 0;} .si_archive { padding:100px 0 120px; max-width:1660px; margin-left: auto; margin-right: 0;}
@ -930,8 +930,8 @@ body{overflow-x:hidden;}
.si_archive__slider { overflow: hidden; user-select: none; cursor: grab} .si_archive__slider { overflow: hidden; user-select: none; cursor: grab}
.si_archive__slider:active { cursor: grabbing; } .si_archive__slider:active { cursor: grabbing; }
.si_archive__track { display: flex; gap: 18px; will-change: transform; padding-right: 40px; } .si_archive__track { display: flex; gap: 18px; will-change: transform; padding-right: 40px; padding-left: 1px; padding-bottom: 1px;}
.si_archive__card { flex: 0 0 65%; padding: 20px; overflow: hidden; background: #fff; border: 1px solid #e5e7eb; border-radius: 14px; box-sizing: border-box; display: flex; flex-direction: column; isolation: isolate; } .si_archive__card { flex: 0 0 65%; padding: 20px; background: #fff; border: 1px solid #e5e7eb; border-radius: 14px; box-sizing: border-box; display: flex; flex-direction: column; isolation: isolate; }
.si_archive__card-header { display: flex; align-items: center; gap: 12px; } .si_archive__card-header { display: flex; align-items: center; gap: 12px; }
.si_archive__card-img { width: 100%; aspect-ratio: 16/8; flex-shrink: 0; background: #eef2f7; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; }.si_archive__card-img img { width: 100%; height: 100%; object-fit: cover; object-position: top; display: block; border-radius: 8px; } .si_archive__card-img { width: 100%; aspect-ratio: 16/8; flex-shrink: 0; background: #eef2f7; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; }.si_archive__card-img img { width: 100%; height: 100%; object-fit: cover; object-position: top; display: block; border-radius: 8px; }
.si_archive__card-img-placeholder { position:absolute; inset:0; background:linear-gradient(135deg,#cbd5e1 0%,#94a3b8 100%); } .si_archive__card-img-placeholder { position:absolute; inset:0; background:linear-gradient(135deg,#cbd5e1 0%,#94a3b8 100%); }

440
src/pages/business/RndPage.jsx

@ -1,13 +1,264 @@
import { useEffect, useRef, useState } from "react";
import SubHero from "../../components/SubHero"; import SubHero from "../../components/SubHero";
import useFadeIn from "../../hooks/useFadeIn"; import useFadeIn from "../../hooks/useFadeIn";
import { motion, AnimatePresence, useInView } from "framer-motion";
import {
Shield,
Radio,
Cpu,
Wind,
Eye,
BarChart2,
Globe,
Users,
Layers,
GraduationCap,
Wrench,
Building2,
Navigation,
MapPin,
AlertTriangle,
Plane,
Network,
Settings,
} from "lucide-react";
const ease = [0.25, 0.1, 0.25, 1];
const PROJECTS = [
{
id: "01",
title: "드론 규제 샌드박스",
tags: ["드론", "규제/실증"],
image: "/images/rnd_img1.png",
desc: [
{
icon: <Shield size={14} />,
title: "안전 관리 체계",
text: "드론 이용 증가 대비 안전 관리 기반 마련",
},
{
icon: <Radio size={14} />,
title: "식별 장치 구축",
text: "드론 식별 장치 및 관리 체계 구축·실증",
},
{
icon: <Cpu size={14} />,
title: "비행 관제 시스템",
text: "드론 비행 관제 시스템 연구개발 수행",
},
],
},
{
id: "02",
title: "환경 드론 관제 시스템 구축",
tags: ["드론", "환경/대기"],
image: "/images/rnd_img2.png",
desc: [
{
icon: <Wind size={14} />,
title: "대기오염 감시",
text: "미세먼지·악취 등 대기환경 오염 감시 체계",
},
{
icon: <Eye size={14} />,
title: "실시간 모니터링",
text: "드론 탑재 센서로 실시간 오염 정보 수집",
},
{
icon: <BarChart2 size={14} />,
title: "정보 시스템 구축",
text: "대기오염 모니터링 시스템 구축 및 실증",
},
],
},
{
id: "03",
title: "메타버스 시스템 구축",
tags: ["메타버스", "디지털전환"],
image: "/images/rnd_img3.png",
desc: [
{
icon: <Globe size={14} />,
title: "디지털 전환",
text: "현실을 디지털 기반 가상 세계로 확장",
},
{
icon: <Users size={14} />,
title: "비대면 환경",
text: "코로나 대비 비대면 활동 지원 시스템",
},
{
icon: <Layers size={14} />,
title: "가상공간 구축",
text: "가상공간 내 모든 활동 가능 시스템 실증",
},
],
},
{
id: "04",
title: "항공인력양성팀 운영 시스템",
tags: ["항공", "인력양성"],
image: "/images/rnd_img4.png",
desc: [
{
icon: <GraduationCap size={14} />,
title: "교육 훈련 기반",
text: "고교생 중심 항공정비 인력 교육 훈련 체계",
},
{
icon: <Wrench size={14} />,
title: "현장실무 인력",
text: "항공정비 현장실무 인력 안정적 공급 지원",
},
{
icon: <Building2 size={14} />,
title: "민간 기반 조성",
text: "항공기초인력양성 민간분야 기반 구축·실증",
},
],
},
{
id: "05",
title: "경량항공기 내비게이션 및 비행상황관리 시스템",
tags: ["경량항공기", "비행안전"],
image: "/images/rnd_img5.png",
desc: [
{
icon: <Navigation size={14} />,
title: "전용 내비게이션",
text: "경량항공기 전용 실시간 안전정보 내비게이션",
},
{
icon: <MapPin size={14} />,
title: "위치 모니터링",
text: "경량항공기 위치 기반 모니터링 체계 구축",
},
{
icon: <AlertTriangle size={14} />,
title: "사고 예방",
text: "항공레저 증가에 따른 비행안전 서비스 실증",
},
],
},
{
id: "06",
title: "UTM 드론 교통관리체계",
tags: ["드론", "교통관리"],
image: "/images/rnd_img6.png",
desc: [
{
icon: <Plane size={14} />,
title: "저고도 공역 관리",
text: "150m 이하 저고도 공역 드론 운항 체계 연구",
},
{
icon: <Network size={14} />,
title: "교통관리 시스템",
text: "드론 안전하고 효율적인 교통관리체계 개발",
},
{
icon: <Settings size={14} />,
title: "운항 최적화",
text: "무인비행장치 운항 안전·효율 극대화 실증",
},
],
},
];
const AUTO_DELAY = 5000;
function RndPage() { function RndPage() {
const basePath = import.meta.env.BASE_URL;
const ref = useFadeIn(); 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 = [ const BUSINESS_NAV = [
{ label: "System Integration", to: "/business/si" }, { label: "System Integration", to: "/business/si" },
{ label: "R&D", to: "/business/rnd" }, { label: "R&D", to: "/business/rnd" },
{ label: "운영 · 유지보수", to: "/business/maintenance" }, { label: "운영 · 유지보수", to: "/business/maintenance" },
]; ];
return ( return (
<article ref={ref}> <article ref={ref}>
<SubHero <SubHero
@ -23,29 +274,180 @@ function RndPage() {
navItems={BUSINESS_NAV} navItems={BUSINESS_NAV}
/> />
<div className="sub-content"> <div className="sub-content si-archive-wrap">
<section className="sub-section"> <div className="inner-wrap">
<span className="sub-section-eyebrow sub-fade-in">UTM/UATM 소개</span> <section className="si_archive" ref={sectionRef}>
{/* <main className="sub-page"> <div className="si_archive__main">
<section className="sub-visual"> {/* 헤더 */}
<div className="inner"> <div className="si_archive__header">
<h2>R&amp;D</h2> <motion.span
<p>Research &amp; Development</p> 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 ? (
"미래 항공 모빌리티를 위한 연구개발 활동과 성과를 기록합니다."
) : (
<>
미래 항공 모빌리티를 위한
<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/rnd_img${parseInt(project.id)}.png`}
alt={project.title}
/>
<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>
</section>
<section className="sub-content-section"> <div className="si_archive__card-tags">
<div className="inner"> {project.tags.map((tag) => (
<h3>연구 개발 기술 고도화</h3> <span key={tag} className="si_archive__tag">
<p> {tag}
항공·관제 도메인 특화 기술 연구로 </span>
<br /> ))}
차세대 솔루션을 만들어갑니다. </div>
</p>
<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> </div>
</section> </section>
</main> */} </div>
</section>
</div> </div>
</article> </article>
); );

Loading…
Cancel
Save