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.
 
 
 

251 lines
11 KiB

import { useRef } from "react";
import { motion, useInView, useAnimationFrame } from "framer-motion";
import SubHero from "../../components/SubHero";
import useFadeIn from "../../hooks/useFadeIn";
const ease = [0.22, 1, 0.36, 1];
const BUSINESS_NAV = [
{ label: "System Integration", to: "/business/si" },
{ label: "R&D", to: "/business/rnd" },
{ label: "운영 · 유지보수", to: "/business/maintenance" },
];
const SERVICES = [
{
num: "01",
title: "모니터링",
desc: "시스템·네트워크·애플리케이션 전 계층을 24/7 실시간으로 감시하여 장애 발생 전 이상 징후를 선제적으로 포착합니다.",
img: "./images/mt_icon01.png",
},
{
num: "02",
title: "장애 대응",
desc: "이상 감지 즉시 전담 엔지니어가 원인을 분석하고 신속하게 복구합니다. 단순 대응을 넘어 재발 방지까지 책임집니다.",
img: "./images/mt_icon02.png",
},
{
num: "03",
title: "보안 관리",
desc: "정기 취약점 점검과 패치 관리로 외부 위협으로부터 시스템을 보호합니다. 안전한 운영 환경을 지속적으로 유지합니다.",
img: "./images/mt_icon03.png",
},
{
num: "04",
title: "기술 지원",
desc: "운영 중 발생하는 기술적 문의에 신속히 응대하고, 가이드 및 교육을 통해 고객 역량 강화까지 지원합니다.",
img: "./images/mt_icon04.png",
},
{
num: "05",
title: "지속적 개선",
desc: "정기 리포트와 데이터 분석을 기반으로 서비스 품질을 꾸준히 높입니다. 운영 효율과 안정성을 함께 끌어올립니다.",
img: "./images/mt_icon05.png",
},
];
const KPI = [
{ value: "99.9%", label: "서비스 가용성" },
{ value: "24/7", label: "365일 실시간 운영" },
{ value: "10m 24s", label: "평균 응답 시간" },
{ value: "100%", label: "SLA 준수율" },
];
/* 아이콘 5개 위치 — 정오각형, 상단에서 시작 */
const NODE_ANGLES = [-90, -18, 54, 126, 198];
const CX = 320,
CY = 320,
R = 190,
ICON_R = 44;
function OpsCircle({ inView }) {
return (
<div className="mt-ops">
<svg className="mt-ops__svg" viewBox="0 0 640 640" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<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>
);
}
function MaintenancePage() {
const ref = useFadeIn();
const introRef = useRef(null);
const kpiRef = useRef(null);
const colsRef = useRef(null);
const introInView = useInView(introRef, { once: true, margin: "-80px" });
const kpiInView = useInView(kpiRef, { once: true, margin: "-80px" });
const colsInView = useInView(colsRef, { once: true, margin: "-60px" });
return (
<article ref={ref}>
<SubHero
label="BUSINESS"
title={
<>
<em>Maintenance</em>
</>
}
navItems={BUSINESS_NAV}
/>
<div className="sub-content">
<div className="inner-wrap">
{/* ── 인트로 ── */}
<section className="mt-intro" ref={introRef}>
<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 }}>
OUR SERVICE
</motion.span>
<div className="mt-intro__title-wrap">
{["운영·유지보수는", "서비스의 안정성을", "완성합니다"].map((line, 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 }}>
{line}
</motion.h2>
</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.5 }}>
PAL Networks는 24/7 통합 모니터링과 체계적인 유지보수로
<br />
시스템의 가용성과 안정성을 지속적으로 보장합니다.
</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.62 }}>
서비스 자세히 보기
<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" />
</svg>
</motion.a>
</div>
<div className="mt-intro__right">
<OpsCircle inView={introInView} />
</div>
</section>
</div>
{/* ── KPI 바 ── */}
<section className="mt-kpi" ref={kpiRef}>
<div className="inner-wrap">
<div className="mt-kpi__grid">
{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 }}>
<span className="mt-kpi__value">{k.value}</span>
<span className="mt-kpi__label">{k.label}</span>
</motion.div>
))}
</div>
</div>
</section>
{/* ── 5컬럼 서비스 ── */}
<div className="inner-wrap">
<section className="mt-services" ref={colsRef}>
<div className="mt-services__grid">
{SERVICES.map((svc, i) => (
<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">
<img src={svc.img} alt={svc.title} className="mt-service-card__img" />
</div>
<span className="mt-service-card__num">{svc.num}</span>
<h3 className="mt-service-card__title">{svc.title}</h3>
<p className="mt-service-card__desc">{svc.desc}</p>
<div className="mt-service-card__line" />
</motion.div>
))}
</div>
</section>
</div>
</div>
</article>
);
}
export default MaintenancePage;