|
|
|
@ -1,5 +1,5 @@ |
|
|
|
import { useRef, useState, useEffect } from "react"; |
|
|
|
import { useRef } from "react"; |
|
|
|
import { motion, useInView, animate } from "framer-motion"; |
|
|
|
import { motion, useInView, useAnimationFrame } from "framer-motion"; |
|
|
|
import SubHero from "../../components/SubHero"; |
|
|
|
import SubHero from "../../components/SubHero"; |
|
|
|
import useFadeIn from "../../hooks/useFadeIn"; |
|
|
|
import useFadeIn from "../../hooks/useFadeIn"; |
|
|
|
|
|
|
|
|
|
|
|
@ -15,58 +15,139 @@ const SERVICES = [ |
|
|
|
{ |
|
|
|
{ |
|
|
|
num: "01", |
|
|
|
num: "01", |
|
|
|
title: "모니터링", |
|
|
|
title: "모니터링", |
|
|
|
desc: "시스템 및 네트워크 상태를 24/7 실시간 모니터링", |
|
|
|
desc: "시스템·네트워크·애플리케이션 전 계층을 24/7 실시간으로 감시하여 장애 발생 전 이상 징후를 선제적으로 포착합니다.", |
|
|
|
img: "./images/mt_icon01.png", |
|
|
|
img: "./images/mt_icon01.png", |
|
|
|
}, |
|
|
|
}, |
|
|
|
{ |
|
|
|
{ |
|
|
|
num: "02", |
|
|
|
num: "02", |
|
|
|
title: "장애 대응", |
|
|
|
title: "장애 대응", |
|
|
|
desc: "이상 감지 시 전문 인력이 신속하게 원인 분석 및 복구", |
|
|
|
desc: "이상 감지 즉시 전담 엔지니어가 원인을 분석하고 신속하게 복구합니다. 단순 대응을 넘어 재발 방지까지 책임집니다.", |
|
|
|
img: "./images/mt_icon02.png", |
|
|
|
img: "./images/mt_icon02.png", |
|
|
|
}, |
|
|
|
}, |
|
|
|
{ |
|
|
|
{ |
|
|
|
num: "03", |
|
|
|
num: "03", |
|
|
|
title: "보안 관리", |
|
|
|
title: "보안 관리", |
|
|
|
desc: "취약점 점검 및 패치 관리로 안전한 시스템 운영", |
|
|
|
desc: "정기 취약점 점검과 패치 관리로 외부 위협으로부터 시스템을 보호합니다. 안전한 운영 환경을 지속적으로 유지합니다.", |
|
|
|
img: "./images/mt_icon03.png", |
|
|
|
img: "./images/mt_icon03.png", |
|
|
|
}, |
|
|
|
}, |
|
|
|
{ |
|
|
|
{ |
|
|
|
num: "04", |
|
|
|
num: "04", |
|
|
|
title: "기술 지원", |
|
|
|
title: "기술 지원", |
|
|
|
desc: "운영 가이드, 문의 응대 및 기술 지원 제공", |
|
|
|
desc: "운영 중 발생하는 기술적 문의에 신속히 응대하고, 가이드 및 교육을 통해 고객 역량 강화까지 지원합니다.", |
|
|
|
img: "./images/mt_icon04.png", |
|
|
|
img: "./images/mt_icon04.png", |
|
|
|
}, |
|
|
|
}, |
|
|
|
{ |
|
|
|
{ |
|
|
|
num: "05", |
|
|
|
num: "05", |
|
|
|
title: "지속적 개선", |
|
|
|
title: "지속적 개선", |
|
|
|
desc: "정기 리포트 및 분석을 통한 지속적인 서비스 개선", |
|
|
|
desc: "정기 리포트와 데이터 분석을 기반으로 서비스 품질을 꾸준히 높입니다. 운영 효율과 안정성을 함께 끌어올립니다.", |
|
|
|
img: "./images/mt_icon05.png", |
|
|
|
img: "./images/mt_icon05.png", |
|
|
|
}, |
|
|
|
}, |
|
|
|
]; |
|
|
|
]; |
|
|
|
|
|
|
|
|
|
|
|
const KPI = [ |
|
|
|
const KPI = [ |
|
|
|
{ value: 99.9, suffix: "%", label: "서비스 가용성", icon: "⏱" }, |
|
|
|
{ value: "99.9%", label: "서비스 가용성" }, |
|
|
|
{ value: 24, suffix: "/7", label: "365일 실시간 운영", icon: "📡" }, |
|
|
|
{ value: "24/7", label: "365일 실시간 운영" }, |
|
|
|
{ value: 10, suffix: "m 24s", label: "평균 응답 시간", icon: "⚡" }, |
|
|
|
{ value: "10m 24s", label: "평균 응답 시간" }, |
|
|
|
{ value: 100, suffix: "%", label: "SLA 준수율", icon: "✓" }, |
|
|
|
{ value: "100%", label: "SLA 준수율" }, |
|
|
|
]; |
|
|
|
]; |
|
|
|
|
|
|
|
|
|
|
|
function CountUp({ target, suffix, inView }) { |
|
|
|
/* 아이콘 5개 위치 — 정오각형, 상단에서 시작 */ |
|
|
|
const [display, setDisplay] = useState(0); |
|
|
|
const NODE_ANGLES = [-90, -18, 54, 126, 198]; |
|
|
|
useEffect(() => { |
|
|
|
const CX = 320, |
|
|
|
if (!inView) return; |
|
|
|
CY = 320, |
|
|
|
const controls = animate(0, target, { |
|
|
|
R = 190, |
|
|
|
duration: 1.8, |
|
|
|
ICON_R = 44; |
|
|
|
ease: "easeOut", |
|
|
|
|
|
|
|
onUpdate: (v) => setDisplay(Math.round(v * 10) / 10), |
|
|
|
function OpsCircle({ inView }) { |
|
|
|
}); |
|
|
|
|
|
|
|
return controls.stop; |
|
|
|
|
|
|
|
}, [inView, target]); |
|
|
|
|
|
|
|
return ( |
|
|
|
return ( |
|
|
|
<span> |
|
|
|
<div className="mt-ops"> |
|
|
|
{display} |
|
|
|
<svg className="mt-ops__svg" viewBox="0 0 640 640" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|
|
|
{suffix} |
|
|
|
<defs> |
|
|
|
</span> |
|
|
|
<linearGradient id="mtOrbitGrad" x1="0" y1="0" x2="1" y2="1"> |
|
|
|
|
|
|
|
<stop offset="0%" stopColor="#d94889" stopOpacity="0.5" /> |
|
|
|
|
|
|
|
<stop offset="100%" stopColor="#1a1f5e" stopOpacity="0.2" /> |
|
|
|
|
|
|
|
</linearGradient> |
|
|
|
|
|
|
|
<linearGradient id="mtLineGrad" x1="0" y1="0" x2="1" y2="1"> |
|
|
|
|
|
|
|
<stop offset="0%" stopColor="#d94889" /> |
|
|
|
|
|
|
|
<stop offset="100%" stopColor="#7b3fa0" /> |
|
|
|
|
|
|
|
</linearGradient> |
|
|
|
|
|
|
|
<radialGradient id="mtPulse1" cx="50%" cy="50%" r="50%"> |
|
|
|
|
|
|
|
<stop offset="0%" stopColor="#d94889" stopOpacity="0.08" /> |
|
|
|
|
|
|
|
<stop offset="100%" stopColor="#d94889" stopOpacity="0" /> |
|
|
|
|
|
|
|
</radialGradient> |
|
|
|
|
|
|
|
<radialGradient id="mtPulse2" cx="50%" cy="50%" r="50%"> |
|
|
|
|
|
|
|
<stop offset="0%" stopColor="#7b3fa0" stopOpacity="0.06" /> |
|
|
|
|
|
|
|
<stop offset="100%" stopColor="#7b3fa0" stopOpacity="0" /> |
|
|
|
|
|
|
|
</radialGradient> |
|
|
|
|
|
|
|
<filter id="mtGlow"> |
|
|
|
|
|
|
|
<feGaussianBlur stdDeviation="3" result="blur" /> |
|
|
|
|
|
|
|
<feMerge> |
|
|
|
|
|
|
|
<feMergeNode in="blur" /> |
|
|
|
|
|
|
|
<feMergeNode in="SourceGraphic" /> |
|
|
|
|
|
|
|
</feMerge> |
|
|
|
|
|
|
|
</filter> |
|
|
|
|
|
|
|
</defs> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{/* pulse 링 — CSS 애니메이션 */} |
|
|
|
|
|
|
|
<circle cx={CX} cy={CY} r="80" fill="url(#mtPulse1)" className="mt-ops__pulse mt-ops__pulse--1" /> |
|
|
|
|
|
|
|
<circle cx={CX} cy={CY} r="80" fill="url(#mtPulse2)" className="mt-ops__pulse mt-ops__pulse--2" /> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{/* 궤도 링 */} |
|
|
|
|
|
|
|
<circle cx={CX} cy={CY} r={R} stroke="url(#mtOrbitGrad)" strokeWidth="1.2" strokeDasharray="6 5" /> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{/* 연결선 아이콘 → 중심 */} |
|
|
|
|
|
|
|
{NODE_ANGLES.map((deg, i) => { |
|
|
|
|
|
|
|
const rad = (deg * Math.PI) / 180; |
|
|
|
|
|
|
|
const ix = CX + R * Math.cos(rad); |
|
|
|
|
|
|
|
const iy = CY + R * Math.sin(rad); |
|
|
|
|
|
|
|
const mx = CX + 80 * Math.cos(rad); |
|
|
|
|
|
|
|
const my = CY + 80 * Math.sin(rad); |
|
|
|
|
|
|
|
return <motion.line key={i} x1={ix} y1={iy} x2={mx} y2={my} stroke="url(#mtLineGrad)" strokeWidth="1" strokeDasharray="5 4" initial={{ pathLength: 0, opacity: 0 }} animate={inView ? { pathLength: 1, opacity: 0.45 } : {}} transition={{ delay: 0.8 + i * 0.12, duration: 0.6, ease }} />; |
|
|
|
|
|
|
|
})} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{/* 아이콘 노드 원 배경 */} |
|
|
|
|
|
|
|
{NODE_ANGLES.map((deg, i) => { |
|
|
|
|
|
|
|
const rad = (deg * Math.PI) / 180; |
|
|
|
|
|
|
|
const ix = CX + R * Math.cos(rad); |
|
|
|
|
|
|
|
const iy = CY + R * Math.sin(rad); |
|
|
|
|
|
|
|
return <motion.circle key={i} cx={ix} cy={iy} r={ICON_R} fill="white" stroke="rgba(217,72,137,0.2)" strokeWidth="1.2" style={{ filter: "drop-shadow(0 4px 16px rgba(123,63,160,0.13))" }} initial={{ scale: 0, opacity: 0 }} animate={inView ? { scale: 1, opacity: 1 } : {}} transition={{ delay: 0.4 + i * 0.1, duration: 0.55, ease }} />; |
|
|
|
|
|
|
|
})} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{/* 아이콘 이미지 — SVG foreignObject */} |
|
|
|
|
|
|
|
{NODE_ANGLES.map((deg, i) => { |
|
|
|
|
|
|
|
const rad = (deg * Math.PI) / 180; |
|
|
|
|
|
|
|
const ix = CX + R * Math.cos(rad); |
|
|
|
|
|
|
|
const iy = CY + R * Math.sin(rad); |
|
|
|
|
|
|
|
const size = 54; |
|
|
|
|
|
|
|
return ( |
|
|
|
|
|
|
|
<motion.foreignObject key={i} x={ix - size / 2} y={iy - size / 2} width={size} height={size} initial={{ opacity: 0, scale: 0.6 }} animate={inView ? { opacity: 1, scale: 1 } : {}} transition={{ delay: 0.5 + i * 0.1, duration: 0.55, ease }}> |
|
|
|
|
|
|
|
<img src={SERVICES[i].img} alt={SERVICES[i].title} style={{ width: "100%", height: "100%", objectFit: "contain" }} /> |
|
|
|
|
|
|
|
</motion.foreignObject> |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
})} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{/* 아이콘 라벨 */} |
|
|
|
|
|
|
|
{NODE_ANGLES.map((deg, i) => { |
|
|
|
|
|
|
|
const rad = (deg * Math.PI) / 180; |
|
|
|
|
|
|
|
const ix = CX + R * Math.cos(rad); |
|
|
|
|
|
|
|
const iy = CY + R * Math.sin(rad); |
|
|
|
|
|
|
|
const labelOffset = 58; |
|
|
|
|
|
|
|
const lx = CX + (R + 68) * Math.cos(rad); |
|
|
|
|
|
|
|
const ly = CY + (R + 68) * Math.sin(rad); |
|
|
|
|
|
|
|
return ( |
|
|
|
|
|
|
|
<motion.text key={i} x={lx} y={ly} textAnchor="middle" dominantBaseline="middle" fontSize="14" fontWeight="600" fill="#1a1f5e" opacity="0.75" initial={{ opacity: 0 }} animate={inView ? { opacity: 0.75 } : {}} transition={{ delay: 0.7 + i * 0.1, duration: 0.5 }}> |
|
|
|
|
|
|
|
{SERVICES[i].title} |
|
|
|
|
|
|
|
</motion.text> |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
})} |
|
|
|
|
|
|
|
</svg> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{/* OPS CENTER 중앙 — DOM 오버레이 */} |
|
|
|
|
|
|
|
<motion.div className="mt-ops__center" initial={{ opacity: 0, scale: 0.75 }} animate={inView ? { opacity: 1, scale: 1 } : {}} transition={{ delay: 0.2, duration: 0.7, ease }}> |
|
|
|
|
|
|
|
<span className="mt-ops__center-label">OPS</span> |
|
|
|
|
|
|
|
<span className="mt-ops__center-sub">CENTER</span> |
|
|
|
|
|
|
|
</motion.div> |
|
|
|
|
|
|
|
</div> |
|
|
|
); |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -74,11 +155,10 @@ function MaintenancePage() { |
|
|
|
const ref = useFadeIn(); |
|
|
|
const ref = useFadeIn(); |
|
|
|
const introRef = useRef(null); |
|
|
|
const introRef = useRef(null); |
|
|
|
const kpiRef = useRef(null); |
|
|
|
const kpiRef = useRef(null); |
|
|
|
const servicesRef = useRef(null); |
|
|
|
const colsRef = useRef(null); |
|
|
|
const introInView = useInView(introRef, { once: true, margin: "-80px" }); |
|
|
|
const introInView = useInView(introRef, { once: true, margin: "-80px" }); |
|
|
|
const kpiInView = useInView(kpiRef, { once: true, margin: "-80px" }); |
|
|
|
const kpiInView = useInView(kpiRef, { once: true, margin: "-80px" }); |
|
|
|
const servicesInView = useInView(servicesRef, { once: true, margin: "-60px" }); |
|
|
|
const colsInView = useInView(colsRef, { once: true, margin: "-60px" }); |
|
|
|
const [activeIdx, setActiveIdx] = useState(0); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
return ( |
|
|
|
<article ref={ref}> |
|
|
|
<article ref={ref}> |
|
|
|
@ -94,16 +174,15 @@ function MaintenancePage() { |
|
|
|
|
|
|
|
|
|
|
|
<div className="sub-content"> |
|
|
|
<div className="sub-content"> |
|
|
|
<div className="inner-wrap"> |
|
|
|
<div className="inner-wrap"> |
|
|
|
{/* ── 인트로 섹션 ── */} |
|
|
|
{/* ── 인트로 ── */} |
|
|
|
<section className="mt-intro" ref={introRef}> |
|
|
|
<section className="mt-intro" ref={introRef}> |
|
|
|
{/* 좌측 */} |
|
|
|
|
|
|
|
<div className="mt-intro__left"> |
|
|
|
<div className="mt-intro__left"> |
|
|
|
<motion.span className="fc-eyebrow" initial={{ opacity: 0, y: 14 }} animate={introInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, ease }}> |
|
|
|
<motion.span className="fc-eyebrow" initial={{ opacity: 0, y: 14 }} animate={introInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, ease }}> |
|
|
|
OUR SERVICE |
|
|
|
OUR SERVICE |
|
|
|
</motion.span> |
|
|
|
</motion.span> |
|
|
|
|
|
|
|
|
|
|
|
<div className="mt-intro__title-wrap"> |
|
|
|
<div className="mt-intro__title-wrap"> |
|
|
|
{["운영부터 개선까지", "전문적인 관리 서비스"].map((line, i) => ( |
|
|
|
{["운영·유지보수는", "서비스의 안정성을", "완성합니다"].map((line, i) => ( |
|
|
|
<div className="mt-title-line" key={i}> |
|
|
|
<div className="mt-title-line" key={i}> |
|
|
|
<motion.h2 className="mt-intro__title" initial={{ y: "105%" }} animate={introInView ? { y: "0%" } : {}} transition={{ duration: 1.1, ease, delay: 0.15 + i * 0.1 }}> |
|
|
|
<motion.h2 className="mt-intro__title" initial={{ y: "105%" }} animate={introInView ? { y: "0%" } : {}} transition={{ duration: 1.1, ease, delay: 0.15 + i * 0.1 }}> |
|
|
|
{line} |
|
|
|
{line} |
|
|
|
@ -112,50 +191,22 @@ function MaintenancePage() { |
|
|
|
))} |
|
|
|
))} |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
<motion.p className="mt-intro__desc" initial={{ opacity: 0, y: 16 }} animate={introInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.7, ease, delay: 0.45 }}> |
|
|
|
<motion.p className="mt-intro__desc" initial={{ opacity: 0, y: 16 }} animate={introInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.7, ease, delay: 0.5 }}> |
|
|
|
전문 엔지니어가 시스템 전반을 관리하고, |
|
|
|
PAL Networks는 24/7 통합 모니터링과 체계적인 유지보수로 |
|
|
|
<br /> |
|
|
|
<br /> |
|
|
|
문제 발생 시 신속하게 대응하여 서비스 안정성을 유지합니다. |
|
|
|
시스템의 가용성과 안정성을 지속적으로 보장합니다. |
|
|
|
</motion.p> |
|
|
|
</motion.p> |
|
|
|
|
|
|
|
|
|
|
|
<motion.a className="mt-intro__cta" href="/contact" initial={{ opacity: 0, y: 12 }} animate={introInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, ease, delay: 0.58 }}> |
|
|
|
<motion.a className="mt-intro__cta" href="/contact" initial={{ opacity: 0, y: 12 }} animate={introInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, ease, delay: 0.62 }}> |
|
|
|
서비스 자세히 보기 |
|
|
|
서비스 자세히 보기 |
|
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"> |
|
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"> |
|
|
|
<path d="M3 8h10M9 4l4 4-4 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> |
|
|
|
<path d="M3 8h10M9 4l4 4-4 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> |
|
|
|
</svg> |
|
|
|
</svg> |
|
|
|
</motion.a> |
|
|
|
</motion.a> |
|
|
|
|
|
|
|
|
|
|
|
{/* quote 카드 */} |
|
|
|
|
|
|
|
<motion.div className="mt-quote" initial={{ opacity: 0, y: 20 }} animate={introInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.8, ease, delay: 0.7 }}> |
|
|
|
|
|
|
|
<div className="mt-quote__mark">"</div> |
|
|
|
|
|
|
|
<p className="mt-quote__text"> |
|
|
|
|
|
|
|
안정적인 운영은 |
|
|
|
|
|
|
|
<br /> |
|
|
|
|
|
|
|
지속 가능한 성장의 |
|
|
|
|
|
|
|
<br /> |
|
|
|
|
|
|
|
기반입니다. |
|
|
|
|
|
|
|
</p> |
|
|
|
|
|
|
|
<span className="mt-quote__brand">PAL Networks</span> |
|
|
|
|
|
|
|
</motion.div> |
|
|
|
|
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
{/* 우측 서비스 리스트 */} |
|
|
|
|
|
|
|
<div className="mt-intro__right"> |
|
|
|
<div className="mt-intro__right"> |
|
|
|
<ul className="mt-svc-list"> |
|
|
|
<OpsCircle inView={introInView} /> |
|
|
|
{SERVICES.map((svc, i) => ( |
|
|
|
|
|
|
|
<motion.li key={i} className={`mt-svc-item${activeIdx === i ? " is-active" : ""}`} initial={{ opacity: 0, x: 24 }} animate={introInView ? { opacity: 1, x: 0 } : {}} transition={{ duration: 0.6, ease, delay: 0.3 + i * 0.08 }} onMouseEnter={() => setActiveIdx(i)}> |
|
|
|
|
|
|
|
<span className="mt-svc-item__num">{svc.num}</span> |
|
|
|
|
|
|
|
<div className="mt-svc-item__body"> |
|
|
|
|
|
|
|
<span className="mt-svc-item__title">{svc.title}</span> |
|
|
|
|
|
|
|
<span className="mt-svc-item__desc">{svc.desc}</span> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
<div className="mt-svc-item__icon-wrap"> |
|
|
|
|
|
|
|
<img src={svc.img} alt={svc.title} className="mt-svc-item__icon" /> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
<div className="mt-svc-item__bar" /> |
|
|
|
|
|
|
|
</motion.li> |
|
|
|
|
|
|
|
))} |
|
|
|
|
|
|
|
</ul> |
|
|
|
|
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</section> |
|
|
|
</section> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
@ -166,10 +217,7 @@ function MaintenancePage() { |
|
|
|
<div className="mt-kpi__grid"> |
|
|
|
<div className="mt-kpi__grid"> |
|
|
|
{KPI.map((k, i) => ( |
|
|
|
{KPI.map((k, i) => ( |
|
|
|
<motion.div key={i} className="mt-kpi__item" initial={{ opacity: 0, y: 20 }} animate={kpiInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, ease, delay: i * 0.1 }}> |
|
|
|
<motion.div key={i} className="mt-kpi__item" initial={{ opacity: 0, y: 20 }} animate={kpiInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, ease, delay: i * 0.1 }}> |
|
|
|
<span className="mt-kpi__icon">{k.icon}</span> |
|
|
|
<span className="mt-kpi__value">{k.value}</span> |
|
|
|
<span className="mt-kpi__value"> |
|
|
|
|
|
|
|
<CountUp target={k.value} suffix={k.suffix} inView={kpiInView} /> |
|
|
|
|
|
|
|
</span> |
|
|
|
|
|
|
|
<span className="mt-kpi__label">{k.label}</span> |
|
|
|
<span className="mt-kpi__label">{k.label}</span> |
|
|
|
</motion.div> |
|
|
|
</motion.div> |
|
|
|
))} |
|
|
|
))} |
|
|
|
@ -179,13 +227,10 @@ function MaintenancePage() { |
|
|
|
|
|
|
|
|
|
|
|
{/* ── 5컬럼 서비스 ── */} |
|
|
|
{/* ── 5컬럼 서비스 ── */} |
|
|
|
<div className="inner-wrap"> |
|
|
|
<div className="inner-wrap"> |
|
|
|
<section className="mt-services" ref={servicesRef}> |
|
|
|
<section className="mt-services" ref={colsRef}> |
|
|
|
<motion.span className="fc-eyebrow" initial={{ opacity: 0, y: 14 }} animate={servicesInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.6, ease }}> |
|
|
|
|
|
|
|
SERVICE DETAIL |
|
|
|
|
|
|
|
</motion.span> |
|
|
|
|
|
|
|
<div className="mt-services__grid"> |
|
|
|
<div className="mt-services__grid"> |
|
|
|
{SERVICES.map((svc, i) => ( |
|
|
|
{SERVICES.map((svc, i) => ( |
|
|
|
<motion.div key={i} className="mt-service-card" initial={{ opacity: 0, y: 32 }} animate={servicesInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.65, ease, delay: i * 0.1 }}> |
|
|
|
<motion.div key={i} className="mt-service-card" initial={{ opacity: 0, y: 32 }} animate={colsInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.65, ease, delay: i * 0.1 }}> |
|
|
|
<div className="mt-service-card__img-wrap"> |
|
|
|
<div className="mt-service-card__img-wrap"> |
|
|
|
<img src={svc.img} alt={svc.title} className="mt-service-card__img" /> |
|
|
|
<img src={svc.img} alt={svc.title} className="mt-service-card__img" /> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|