Browse Source

재작업

remotes/origin/main
김지은 3 months ago
parent
commit
7a7ebde61c
  1. 24
      .gitignore
  2. 20
      README.md
  3. 29
      eslint.config.js
  4. 71
      index.html
  5. 2647
      package-lock.json
  6. 30
      package.json
  7. 15
      public/404.html
  8. BIN
      public/favicon/apple-touch-icon.png
  9. BIN
      public/favicon/favicon-96x96.png
  10. BIN
      public/favicon/favicon.ico
  11. 1
      public/favicon/favicon.svg
  12. 21
      public/favicon/site.webmanifest
  13. BIN
      public/favicon/web-app-manifest-192x192.png
  14. BIN
      public/favicon/web-app-manifest-512x512.png
  15. BIN
      public/images/pal_logo.png
  16. BIN
      public/images/pal_logo_wh.png
  17. 23
      src/App.jsx
  18. 19
      src/Router.jsx
  19. BIN
      src/assets/hero.png
  20. 1
      src/assets/react.svg
  21. 1
      src/assets/vite.svg
  22. 23
      src/components/Footer.jsx
  23. 458
      src/components/Header.jsx
  24. 17
      src/components/MainLayout.jsx
  25. 17
      src/components/SubLayout.jsx
  26. 298
      src/css/common.css
  27. 372
      src/css/reset.css
  28. 14
      src/main.jsx
  29. 21
      src/pages/AboutPage.jsx
  30. 717
      src/pages/MainPage.jsx
  31. 11
      vite.config.js

24
.gitignore vendored

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

20
README.md

@ -1,2 +1,18 @@
# PALNetworks # React + Vite
NEW PALNetworks
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
## React Compiler
The React Compiler is enabled on this template. See [this documentation](https://react.dev/learn/react-compiler) for more information.
Note: This will impact Vite dev & build performances.
## Expanding the ESLint configuration
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.

29
eslint.config.js

@ -0,0 +1,29 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{js,jsx}'],
extends: [
js.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
rules: {
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
},
},
])

71
index.html

@ -0,0 +1,71 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<!-- 기본 SEO 설명: 검색 결과 제목 아래 요약문으로 사용될 수 있음 -->
<meta
name="description"
content="팔네트웍스는 소프트웨어 개발 및 공급, 항공 솔루션 구축을 수행하는 IT 전문 기업입니다."
/>
<!-- 검색 키워드: 최신 SEO 영향은 크지 않지만 브랜드/서비스 키워드 정리용으로 사용 가능 -->
<meta
name="keywords"
content="팔네트웍스, PAL Networks, 소프트웨어개발, 응용소프트웨어, IT, 관제솔루션, 항공솔루션, 솔루션구축, SI, 네트워크, 항공산업, 공역관리"
/>
<!-- 오픈그래프 제목: 카카오톡, 슬랙, 페이스북 등 링크 공유 시 제목으로 노출 -->
<meta property="og:title" content="팔네트웍스" />
<!-- 오픈그래프 설명: 카카오톡, 슬랙, 페이스북 등 링크 공유 시 설명문으로 노출 -->
<meta
property="og:description"
content="항공산업의 기술혁신을 선도하는 소프트웨어·IT 솔루션 파트너"
/>
<!-- 오픈그래프 이미지: 카카오톡, 슬랙, 페이스북, 디스코드 등 링크 공유 시 썸네일 이미지로 사용 -->
<!-- 권장 이미지 사이즈: 1200x630px / JPG 또는 PNG / 1MB 이하 권장 -->
<!-- 로고만 있는 작은 이미지보다 브랜드 메시지가 포함된 대표 썸네일 이미지 권장 -->
<meta property="og:image" content="https://도메인주소/img/og-image.jpg" />
<!-- 오픈그래프 타입: 기업/브랜드 홈페이지 메인은 보통 website 사용 -->
<meta property="og:type" content="website" />
<!-- 공유 기준 대표 URL -->
<meta property="og:url" content="https://도메인주소" />
<!-- 사이트명 -->
<meta property="og:site_name" content="팔네트웍스" />
<!-- 트위터/X 공유 카드 형식: 큰 대표 이미지 형식 -->
<!-- 보통 og:image와 함께 사용되며, X(트위터) 공유 시 썸네일 크게 표시 -->
<meta name="twitter:card" content="summary_large_image" />
<!-- 트위터/X 제목 -->
<meta name="twitter:title" content="팔네트웍스" />
<!-- 트위터/X 설명 -->
<meta
name="twitter:description"
content="항공산업의 기술혁신을 선도하는 소프트웨어·IT 솔루션 파트너"
/>
<!-- 트위터/X 공유 이미지 -->
<!-- 권장 사이즈: 1200x628px 정도 / og:image와 동일 이미지 사용해도 무방 -->
<meta name="twitter:image" content="https://도메인주소/img/og-image.jpg" />
<link rel="icon" type="image/png" href="favicon/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="favicon/favicon.svg" />
<link rel="shortcut icon" href="favicon/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="favicon/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-title" content="PAL Networks" />
<link rel="manifest" href="favicon/site.webmanifest" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="format-detection" content="telephone=no" />
<title>PAL Networks</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

2647
package-lock.json generated

File diff suppressed because it is too large Load Diff

30
package.json

@ -0,0 +1,30 @@
{
"name": "test",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.2.4",
"react-dom": "^19.2.4"
},
"devDependencies": {
"@babel/core": "^7.29.0",
"@eslint/js": "^9.39.4",
"@rolldown/plugin-babel": "^0.2.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"babel-plugin-react-compiler": "^1.0.0",
"eslint": "^9.39.4",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.4.0",
"vite": "^8.0.4"
}
}

15
public/404.html

@ -0,0 +1,15 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<script>
var path = window.location.pathname.replace('/PALNetworks', '')
var search = window.location.search || ''
var hash = window.location.hash || ''
window.location.replace(
'/PALNetworks/' + '?redirect=' + encodeURIComponent(path + search + hash)
)
</script>
</head>
<body></body>
</html>

BIN
public/favicon/apple-touch-icon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
public/favicon/favicon-96x96.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
public/favicon/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

1
public/favicon/favicon.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 73 KiB

21
public/favicon/site.webmanifest

@ -0,0 +1,21 @@
{
"name": "PAL Networks",
"short_name": "PAL",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

BIN
public/favicon/web-app-manifest-192x192.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
public/favicon/web-app-manifest-512x512.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

BIN
public/images/pal_logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

BIN
public/images/pal_logo_wh.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

23
src/App.jsx

@ -0,0 +1,23 @@
import { Routes, Route, Navigate } from "react-router-dom";
import MainLayout from "./components/MainLayout";
import SubLayout from "./components/SubLayout";
import MainPage from "./pages/MainPage";
import AboutPage from "./pages/AboutPage";
function App() {
return (
<Routes>
<Route path="/" element={<Navigate to="/Main" replace />} />
<Route element={<MainLayout />}>
<Route path="/Main" element={<MainPage />} />
</Route>
<Route element={<SubLayout />}>
<Route path="/About" element={<AboutPage />} />
</Route>
</Routes>
);
}
export default App;

19
src/Router.jsx

@ -0,0 +1,19 @@
import { Routes, Route, Navigate } from 'react-router-dom'
import MainLayout from './components/MainLayout'
import MainPage from './pages/MainPage'
import AboutPage from './pages/AboutPage'
function Router() {
return (
<Routes>
<Route element={<MainLayout />}>
<Route index element={<Navigate to="/Main" replace />} />
<Route path="/Main" element={<MainPage />} />
<Route path="/About" element={<AboutPage />} />
</Route>
</Routes>
)
}
export default Router

BIN
src/assets/hero.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

1
src/assets/react.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

1
src/assets/vite.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

23
src/components/Footer.jsx

@ -0,0 +1,23 @@
function Footer() {
return (
<footer className="site-footer">
<div className="footer-inner">
<div className="footer-left">
<strong className="footer-logo">PALNETWORKS</strong>
<p className="footer-copy">© 2026 PALNETWORKS. All rights reserved.</p>
</div>
<div className="footer-right">
<a href="#none" className="footer-link">
Privacy Policy
</a>
<a href="#none" className="footer-link">
Terms of Use
</a>
</div>
</div>
</footer>
)
}
export default Footer

458
src/components/Header.jsx

@ -0,0 +1,458 @@
import { useEffect, useRef, useState } from "react";
import { Link, NavLink } from "react-router-dom";
const menuData = [
{
key: "about",
label: "Company",
panelTitle: "PAL Networks",
panelDesc: "회사 소개와 비전, 연혁, 인증 및 조직 정보를 확인할 수 있습니다.",
sections: [
{
title: "회사 소개",
items: [
{ label: "회사소개", to: "/About", desc: "기업 철학과 핵심 가치" },
{ label: "연혁", to: "/history", desc: "주요 실적과 성장 과정" },
{ label: "조직도", to: "/organization", desc: "조직 구성과 역할" },
],
},
{
title: "신뢰 정보",
items: [
{ label: "인증 및 특허", to: "/certification", desc: "기술력과 공인 인증 현황" },
{ label: "파트너십", to: "/partners", desc: "협력사 및 네트워크" },
{ label: "오시는 길", to: "/location", desc: "위치 및 연락처 안내" },
],
},
],
featured: {
eyebrow: "About Us",
title: "신뢰를 기반으로\n항공·플랫폼 기술을 확장합니다.",
text: "기업 소개 영역은 가볍게 보이지 않도록, 핵심 메시지와 신뢰 요소를 함께 노출하는 구성이 좋습니다.",
cta: { label: "회사소개 보기", to: "/company" },
},
},
{
key: "business",
label: "Business",
panelTitle: "Business Area",
panelDesc: "팔네트웍스의 주요 사업 영역과 구축 역량을 한눈에 볼 수 있습니다.",
sections: [
{
title: "사업 분야",
items: [
{
label: "System Integration",
to: "/business/system-integration",
desc: "맞춤형 정보시스템 구축",
},
{ label: "플랫폼 구축", to: "/business/platform", desc: "서비스 플랫폼 기획·개발" },
{ label: "R&D", to: "/business/rnd", desc: "연구 개발 및 기술 고도화" },
],
},
{
title: "특화 영역",
items: [
{ label: "항공/관제", to: "/business/aviation", desc: "항공·관제 특화 서비스" },
{ label: "스마트 관광", to: "/business/tourism", desc: "관광·예약 플랫폼 구축" },
{
label: "공공/기업 SI",
to: "/business/public-enterprise",
desc: "공공·기업 대상 구축 경험",
},
],
},
],
featured: {
eyebrow: "Core Capability",
title: "기획부터 구축, 운영까지\n실무형 역량으로 연결합니다.",
text: "사업영역은 단순 나열보다 “무엇을 잘하는 회사인지” 바로 이해되게 보여주는 게 중요합니다.",
cta: { label: "사업영역 보기", to: "/business" },
},
},
{
key: "solution",
label: "Solution",
panelTitle: "Solution & Service",
panelDesc: "산업별 솔루션과 서비스 포트폴리오를 확장 가능한 구조로 구성합니다.",
sections: [
{
title: "주요 솔루션",
items: [
{ label: "항공예약 플랫폼", to: "/solution/air-booking", desc: "예약·발권·운영 플랫폼" },
{
label: "스마트 관광 플랫폼",
to: "/solution/smart-tour",
desc: "관광 서비스 통합 운영",
},
{ label: "상황관제 시스템", to: "/solution/control", desc: "실시간 상황 모니터링" },
],
},
{
title: "확장 서비스",
items: [
{ label: "VR/AR 시뮬레이터", to: "/solution/simulator", desc: "훈련·체험형 시뮬레이션" },
{ label: "클라우드 서비스", to: "/solution/cloud", desc: "운영 인프라 및 클라우드 연계" },
{ label: "커스텀 솔루션", to: "/solution/custom", desc: "고객 맞춤형 확장 개발" },
],
},
],
featured: {
eyebrow: "Scalable Navigation",
title: "지금은 기업 사이트,\n나중에는 솔루션 허브까지.",
text: "메가패널 구조를 미리 잡아두면 추후 메뉴가 늘어나도 헤더를 다시 뜯지 않아도 됩니다.",
cta: { label: "솔루션 보기", to: "/solution" },
},
},
{
key: "reference",
label: "Reference",
panelTitle: "Reference",
panelDesc: "구축 사례와 주요 프로젝트를 통해 신뢰도를 높일 수 있습니다.",
sections: [
{
title: "프로젝트",
items: [
{ label: "구축 사례", to: "/reference/case", desc: "주요 구축 사례 소개" },
{ label: "프로젝트 실적", to: "/reference/project", desc: "산업별 수행 실적" },
{ label: "고객사", to: "/reference/client", desc: "주요 고객 및 협력 기관" },
],
},
{
title: "콘텐츠",
items: [
{ label: "뉴스", to: "/news", desc: "기업 소식 및 업데이트" },
{ label: "홍보자료", to: "/media", desc: "브로슈어 및 홍보 콘텐츠" },
{ label: "공지사항", to: "/notice", desc: "공지 및 안내 정보" },
],
},
],
featured: {
eyebrow: "Trust Point",
title: "결국 회사 사이트는\n“무엇을 해왔는지”가 보여야 합니다.",
text: "레퍼런스와 뉴스는 단순 보조 메뉴가 아니라 신뢰를 만드는 핵심 축으로 잡는 게 좋습니다.",
cta: { label: "레퍼런스 보기", to: "/reference" },
},
},
{
key: "contact",
label: "Contact",
to: "/contact",
simple: true,
},
];
export default function PalRenewalHeader() {
const [activeMenu, setActiveMenu] = useState(null);
const [isHeaderHover, setIsHeaderHover] = useState(false);
const [isScrolled, setIsScrolled] = useState(false);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [mobileOpenKey, setMobileOpenKey] = useState(null);
const closeTimer = useRef(null);
const navRefs = useRef({});
const mobileFirstFocusableRef = useRef(null);
const activeData = menuData.find((item) => item.key === activeMenu);
const showPanel = Boolean(activeData && !activeData.simple && isHeaderHover);
useEffect(() => {
const onScroll = () => {
setIsScrolled(window.scrollY > 10);
};
onScroll();
window.addEventListener("scroll", onScroll);
return () => window.removeEventListener("scroll", onScroll);
}, []);
useEffect(() => {
return () => {
if (closeTimer.current) clearTimeout(closeTimer.current);
};
}, []);
useEffect(() => {
if (isMobileMenuOpen) {
document.body.style.overflow = "hidden";
const timer = setTimeout(() => {
mobileFirstFocusableRef.current?.focus();
}, 50);
return () => {
clearTimeout(timer);
document.body.style.overflow = "";
};
}
document.body.style.overflow = "";
return () => {
document.body.style.overflow = "";
};
}, [isMobileMenuOpen]);
const clearCloseTimer = () => {
if (closeTimer.current) clearTimeout(closeTimer.current);
};
const openMenu = (key) => {
clearCloseTimer();
setIsHeaderHover(true);
setActiveMenu(key);
};
const scheduleClose = () => {
clearCloseTimer();
closeTimer.current = setTimeout(() => {
setIsHeaderHover(false);
setActiveMenu(null);
}, 120);
};
const closeDesktopMenu = () => {
setIsHeaderHover(false);
setActiveMenu(null);
};
const closeMobileMenu = () => {
setIsMobileMenuOpen(false);
setMobileOpenKey(null);
};
const closeAllMenus = () => {
closeDesktopMenu();
closeMobileMenu();
};
const toggleMobileMenu = () => {
setIsMobileMenuOpen((prev) => !prev);
};
const handleMobileAccordion = (key) => {
setMobileOpenKey((prev) => (prev === key ? null : key));
};
const handleNavKeyDown = (e, index, item) => {
if (e.key === "ArrowRight") {
const nextItem = menuData[index + 1] || menuData[0];
navRefs.current[nextItem.key]?.focus();
}
if (e.key === "ArrowLeft") {
const prevItem = menuData[index - 1] || menuData[menuData.length - 1];
navRefs.current[prevItem.key]?.focus();
}
if (e.key === "ArrowDown" && !item.simple) {
e.preventDefault();
openMenu(item.key);
}
if (e.key === "Escape") {
scheduleClose();
}
};
return (
<>
<header className={`pal-header ${isScrolled || showPanel || isMobileMenuOpen ? "is-scrolled" : ""} ${showPanel ? "is-open" : ""}`} onMouseEnter={clearCloseTimer} onMouseLeave={scheduleClose}>
<div className="pal-header-inner">
<h1 className="pal-header-logo">
<Link to="/Main" onClick={closeAllMenus}>
<img src="/images/pal_logo.png" />
</Link>
</h1>
<nav className="pal-gnb" aria-label="Primary Navigation">
<ul className="pal-gnb-depth1">
{menuData.map((item, index) => {
const isActive = activeMenu === item.key;
return (
<li
className={`pal-gnb-item ${isActive ? "is-active" : ""}`}
key={item.key}
onMouseEnter={() => {
if (!item.simple) {
openMenu(item.key);
} else {
clearCloseTimer();
closeDesktopMenu();
}
}}
>
{item.simple ? (
<NavLink
to={item.to}
className="pal-gnb-link"
ref={(el) => {
navRefs.current[item.key] = el;
}}
onClick={closeAllMenus}
onFocus={() => {
closeDesktopMenu();
}}
onKeyDown={(e) => handleNavKeyDown(e, index, item)}
>
<span className="pal-gnb-link-text">{item.label}</span>
<span className="pal-gnb-link-line"></span>
</NavLink>
) : (
<button
type="button"
className="pal-gnb-link"
ref={(el) => {
navRefs.current[item.key] = el;
}}
aria-expanded={isActive}
aria-haspopup="true"
onFocus={() => openMenu(item.key)}
onClick={() => openMenu(item.key)}
onKeyDown={(e) => handleNavKeyDown(e, index, item)}
>
<span className="pal-gnb-link-text">{item.label}</span>
<span className="pal-gnb-link-line"></span>
</button>
)}
</li>
);
})}
</ul>
</nav>
<div className="pal-header-util">
<Link to="/contact" className="pal-header-contact" onClick={closeAllMenus}>
Contact
</Link>
<button type="button" className={`pal-header-hamburger ${isMobileMenuOpen ? "is-active" : ""}`} aria-label={isMobileMenuOpen ? "모바일 메뉴 닫기" : "모바일 메뉴 열기"} aria-expanded={isMobileMenuOpen} aria-controls="pal-mobile-menu" onClick={toggleMobileMenu}>
<span></span>
<span></span>
<span></span>
</button>
</div>
</div>
<div
className={`pal-mega-panel ${showPanel ? "is-visible" : ""}`}
onMouseEnter={() => {
clearCloseTimer();
setIsHeaderHover(true);
}}
onMouseLeave={scheduleClose}
>
{activeData && !activeData.simple && (
<div className="pal-mega-panel-inner">
<div className="pal-mega-panel-intro">
<span className="pal-mega-panel-eyebrow">{activeData.panelTitle}</span>
<h2>{activeData.featured.title}</h2>
<p>{activeData.featured.text}</p>
<Link to={activeData.featured.cta.to} className="pal-mega-panel-cta" onClick={closeAllMenus}>
{activeData.featured.cta.label}
</Link>
</div>
<div className="pal-mega-panel-content">
<div className="pal-mega-panel-top">
<strong>{activeData.panelTitle}</strong>
<p>{activeData.panelDesc}</p>
</div>
<div className="pal-mega-panel-grid">
{activeData.sections.map((section) => (
<div className="pal-mega-section" key={section.title}>
<h3>{section.title}</h3>
<ul>
{section.items.map((item) => (
<li key={item.label}>
<Link to={item.to} className="pal-mega-item" onClick={closeAllMenus}>
<span className="pal-mega-item-title">{item.label}</span>
<span className="pal-mega-item-desc">{item.desc}</span>
</Link>
</li>
))}
</ul>
</div>
))}
</div>
</div>
</div>
)}
</div>
</header>
<button type="button" className={`pal-header-dim ${showPanel ? "is-visible" : ""}`} aria-label="메뉴 닫기" onClick={closeDesktopMenu}></button>
<div className={`pal-mobile-dim ${isMobileMenuOpen ? "is-visible" : ""}`} onClick={closeAllMenus}></div>
<aside id="pal-mobile-menu" className={`pal-mobile-menu ${isMobileMenuOpen ? "is-open" : ""}`} aria-hidden={!isMobileMenuOpen}>
<div className="pal-mobile-menu-head">
<strong>MENU</strong>
<button type="button" className="pal-mobile-menu-close" aria-label="모바일 메뉴 닫기" onClick={closeAllMenus} ref={mobileFirstFocusableRef}>
<span></span>
<span></span>
</button>
</div>
<div className="pal-mobile-menu-body">
<ul className="pal-mobile-nav">
{menuData.map((menu) => {
const isOpen = mobileOpenKey === menu.key;
return (
<li className={`pal-mobile-nav-item ${isOpen ? "is-open" : ""}`} key={menu.key}>
{menu.simple ? (
<Link to={menu.to} className="pal-mobile-nav-link" onClick={closeAllMenus}>
<span>{menu.label}</span>
</Link>
) : (
<>
<button type="button" className="pal-mobile-nav-toggle" onClick={() => handleMobileAccordion(menu.key)} aria-expanded={isOpen}>
<span>{menu.label}</span>
<i className="pal-mobile-nav-arrow"></i>
</button>
<div className="pal-mobile-submenu">
{menu.sections.map((section) => (
<div className="pal-mobile-submenu-group" key={section.title}>
<h3>{section.title}</h3>
<ul>
{section.items.map((item) => (
<li key={item.label}>
<Link to={item.to} className="pal-mobile-submenu-link" onClick={closeAllMenus}>
<strong>{item.label}</strong>
<p>{item.desc}</p>
</Link>
</li>
))}
</ul>
</div>
))}
<Link to={menu.featured.cta.to} className="pal-mobile-featured-link" onClick={closeAllMenus}>
<span>{menu.featured.eyebrow}</span>
<strong>{menu.featured.cta.label}</strong>
</Link>
</div>
</>
)}
</li>
);
})}
</ul>
<div className="pal-mobile-contact-box">
<p>프로젝트 문의 협업 상담이 필요하시면 연락해 주세요.</p>
<Link to="/contact" className="pal-mobile-contact-link" onClick={closeAllMenus}>
Contact Us
</Link>
</div>
</div>
</aside>
</>
);
}

17
src/components/MainLayout.jsx

@ -0,0 +1,17 @@
import { Outlet } from "react-router-dom";
import Header from "./Header";
import Footer from "./Footer";
function MainLayout() {
return (
<>
<Header />
<main className="main-layout">
<Outlet />
</main>
<Footer />
</>
);
}
export default MainLayout;

17
src/components/SubLayout.jsx

@ -0,0 +1,17 @@
import { Outlet } from "react-router-dom";
import Header from "./Header";
import Footer from "./Footer";
function SubLayout() {
return (
<>
<Header />
<main className="sub-layout">
<Outlet />
</main>
<Footer />
</>
);
}
export default SubLayout;

298
src/css/common.css

@ -0,0 +1,298 @@
:root{
--header-height:96px;
--color-primary:#3A4081;
--color-primary-hover:#2F5DAA;
--color-primary-light:#6B78B5;
--color-primary-soft:rgba(58,64,129,.08);
--color-primary-soft-strong:rgba(58,64,129,.12);
--color-primary-soft-border:rgba(58,64,129,.16);
--color-primary-border:rgba(58,64,129,.2);
--color-primary-border-strong:rgba(58,64,129,.24);
--color-primary-shadow:rgba(58,64,129,.18);
}
html{scrollbar-gutter:inherit;}
body{overflow-x:hidden;}
.main-layout{min-height:calc(100vh - var(--header-height));padding-top:var(--header-height);}
.sub-layout{min-height:calc(100vh - var(--header-height));padding-top:var(--header-height);}
/*header.*/
.pal-header{position:fixed;top:0;left:0;right:0;z-index:1000;transition:box-shadow .35s ease;}
.pal-header::before{content:"";position:absolute;left:0;top:0;width:100%;height:100%;background:rgba(255,255,255,.96);backdrop-filter:blur(16px);opacity:0;transition:opacity .35s ease;}
.pal-header.is-scrolled::before,.pal-header.is-open::before{opacity:1;}
.pal-header.is-open{box-shadow:0 18px 40px rgba(15,23,42,.08);}
.pal-header-inner{position:relative;display:flex;align-items:center;justify-content:space-between;max-width:1440px;height:96px;margin:0 auto;padding:0 40px;z-index:2;}
.pal-header-logo{flex:0 0 auto;margin:0;font-size:0;line-height:1;}
.pal-header-logo a{display:inline-flex;align-items:center;font-size:28px;font-weight:800;line-height:1;color:#111;text-decoration:none;letter-spacing:-0.03em;}
.pal-header-logo img{width:180px}
.pal-gnb{flex:1 1 auto;display:flex;justify-content:center;height:100%;}
.pal-gnb-depth1{display:flex;align-items:center;gap:4px;height:100%;margin:0;padding:0;list-style:none;}
.pal-gnb-item{position:relative;height:100%;}
.pal-gnb-link{position:relative;display:inline-flex;align-items:center;justify-content:center;height:100%;padding:0 20px;border:0;background:none;color:#111;text-decoration:none;cursor:pointer;transition:color .3s ease,transform .3s ease;}
.pal-gnb-link:hover,.pal-gnb-link:focus-visible{color:var(--color-primary);transform:translateY(-2px);outline:none;}
.pal-gnb-link-text{position:relative;z-index:1;font-size:16px;font-weight:700;line-height:1;letter-spacing:-0.02em;}
.pal-gnb-link-line{position:absolute;left:20px;right:20px;bottom:26px;height:2px;border-radius:999px;background:var(--color-primary);transform:scaleX(0);transform-origin:center;transition:transform .32s ease;}
.pal-gnb-item.is-active .pal-gnb-link{color:var(--color-primary);transform:translateY(-2px);}
.pal-gnb-item.is-active .pal-gnb-link-line{transform:scaleX(1);}
.pal-header-util{flex:0 0 auto;display:flex;align-items:center;justify-content:flex-end;gap:12px;min-width:120px;}
.pal-header-contact{display:inline-flex;align-items:center;justify-content:center;height:44px;padding:0 18px;border:1px solid rgba(17,17,17,.12);border-radius:999px;background:#fff;color:#111;font-size:14px;font-weight:700;line-height:1;text-decoration:none;transition:border-color .3s ease,background .3s ease,color .3s ease,transform .3s ease,box-shadow .3s ease;}
.pal-header-contact:hover,.pal-header-contact:focus-visible{border-color:var(--color-primary);background:var(--color-primary);color:#fff;transform:translateY(-2px);box-shadow:0 12px 24px var(--color-primary-shadow);outline:none;}
.pal-header-hamburger{display:none;position:relative;width:46px;height:46px;padding:0;border:1px solid rgba(17,17,17,.08);border-radius:14px;background:#fff;cursor:pointer;transition:border-color .3s ease,transform .3s ease,box-shadow .3s ease;}
.pal-header-hamburger:hover,.pal-header-hamburger:focus-visible{border-color:var(--color-primary-border-strong);transform:translateY(-2px);box-shadow:0 12px 24px rgba(15,23,42,.08);outline:none;}
.pal-header-hamburger span{position:absolute;left:50%;width:18px;height:2px;border-radius:999px;background:#111;transform:translateX(-50%);transition:top .28s ease,transform .28s ease,opacity .2s ease,background .28s ease;}
.pal-header-hamburger span:nth-child(1){top:15px;}
.pal-header-hamburger span:nth-child(2){top:22px;}
.pal-header-hamburger span:nth-child(3){top:29px;}
.pal-header-hamburger.is-active span:nth-child(1){top:22px;transform:translateX(-50%) rotate(45deg);background:var(--color-primary);}
.pal-header-hamburger.is-active span:nth-child(2){opacity:0;}
.pal-header-hamburger.is-active span:nth-child(3){top:22px;transform:translateX(-50%) rotate(-45deg);background:var(--color-primary);}
.pal-mega-panel{position:absolute;top:100%;left:0;width:100%;pointer-events:none;opacity:0;visibility:hidden;transition:opacity .18s ease;z-index:1;}
.pal-mega-panel.is-visible{pointer-events:auto;opacity:1;visibility:visible;}
.pal-mega-panel::before{content:"";position:absolute;left:0;top:0;width:100%;height:100%;background:rgba(255,255,255,.98);backdrop-filter:blur(10px);border-top:1px solid rgba(17,17,17,.06);box-shadow:0 24px 50px rgba(15,23,42,.08);}
.pal-mega-panel-inner{position:relative;display:grid;grid-template-columns:360px 1fr;gap:40px;max-width:1440px;margin:0 auto;padding:24px 40px 28px;z-index:1;}
.pal-mega-panel-intro{display:flex;flex-direction:column;justify-content:space-between;min-height:340px;padding:32px;border:1px solid rgba(17,17,17,.06);border-radius:32px;background:linear-gradient(180deg,var(--color-primary-soft-strong) 0%,rgba(42,145,201,.05) 100%);box-shadow:0 24px 50px rgba(15,23,42,.08);}
.pal-mega-panel-eyebrow{display:inline-block;margin-bottom:14px;font-size:13px;font-weight:700;line-height:1.2;color:var(--color-primary);letter-spacing:.08em;text-transform:uppercase;}
.pal-mega-panel-intro h2{margin:0 0 16px;font-size:34px;font-weight:800;line-height:1.25;color:#111;letter-spacing:-0.03em;white-space:pre-line;}
.pal-mega-panel-intro p{margin:0 0 24px;font-size:15px;font-weight:400;line-height:1.7;color:#555;word-break:keep-all;}
.pal-mega-panel-cta{display:inline-flex;align-items:center;justify-content:center;align-self:flex-start;height:46px;padding:0 18px;border-radius:999px;background:var(--color-primary);color:#fff;font-size:14px;font-weight:700;line-height:1;text-decoration:none;transition:background .3s ease,transform .3s ease,box-shadow .3s ease;}
.pal-mega-panel-cta:hover,.pal-mega-panel-cta:focus-visible{background:var(--color-primary-light);transform:translateY(-2px);box-shadow:0 12px 24px rgba(42,145,201,.22);outline:none;}
.pal-mega-panel-content{padding:16px 0 0;}
.pal-mega-panel-top{display:flex;align-items:flex-end;justify-content:space-between;gap:24px;padding:0 8px 24px;border-bottom:1px solid rgba(17,17,17,.08);}
.pal-mega-panel-top strong{font-size:28px;font-weight:800;line-height:1.2;color:#111;letter-spacing:-0.03em;}
.pal-mega-panel-top p{max-width:520px;margin:0;font-size:15px;line-height:1.7;color:#666;word-break:keep-all;text-align:right;}
.pal-mega-panel-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px;padding-top:24px;}
.pal-mega-section{padding:8px;}
.pal-mega-section h3{margin:0 0 14px;font-size:15px;font-weight:800;line-height:1.4;color:#111;letter-spacing:-0.02em;}
.pal-mega-section ul{margin:0;padding:0;list-style:none;}
.pal-mega-section li + li{margin-top:10px;}
.pal-mega-item{display:block;padding:18px 20px;border:1px solid rgba(17,17,17,.06);border-radius:22px;background:#fff;text-decoration:none;transition:border-color .28s ease,transform .28s ease,box-shadow .28s ease,background .28s ease;}
.pal-mega-item:hover,.pal-mega-item:focus-visible{border-color:var(--color-primary-border);background:#fff;transform:translateY(-3px);box-shadow:0 18px 36px rgba(15,23,42,.08);outline:none;}
.pal-mega-item-title{display:block;margin-bottom:6px;font-size:16px;font-weight:700;line-height:1.4;color:#111;letter-spacing:-0.02em;transition:color .28s ease;}
.pal-mega-item-desc{display:block;font-size:14px;line-height:1.6;color:#666;word-break:keep-all;}
.pal-mega-item:hover .pal-mega-item-title,.pal-mega-item:focus-visible .pal-mega-item-title{color:var(--color-primary);}
.pal-header-dim{position:fixed;left:0;top:96px;width:100%;height:calc(100vh - 96px);z-index:999;border:0;background:rgba(17,17,17,.32);opacity:0;visibility:hidden;pointer-events:none;transition:opacity .28s ease,visibility .28s ease;}
.pal-header-dim.is-visible{opacity:1;visibility:visible;pointer-events:auto;}
.pal-mobile-dim{position:fixed;inset:0;z-index:1198;background:rgba(17,17,17,.42);opacity:0;visibility:hidden;pointer-events:none;transition:opacity .28s ease,visibility .28s ease;}
.pal-mobile-dim.is-visible{opacity:1;visibility:visible;pointer-events:auto;}
.pal-mobile-menu{position:fixed;top:0;right:0;z-index:1199;width:min(92vw,420px);height:100vh;background:#fff;box-shadow:-24px 0 60px rgba(15,23,42,.14);transform:translateX(100%);transition:transform .34s ease;display:flex;flex-direction:column;}
.pal-mobile-menu.is-open{transform:translateX(0);}
.pal-mobile-menu-head{display:flex;align-items:center;justify-content:space-between;min-height:88px;padding:24px 24px 20px;border-bottom:1px solid rgba(17,17,17,.08);}
.pal-mobile-menu-head strong{font-size:14px;font-weight:800;line-height:1;color:var(--color-primary);letter-spacing:.12em;}
.pal-mobile-menu-close{position:relative;display:inline-flex;align-items:center;justify-content:center;width:42px;height:42px;padding:0;border:1px solid rgba(17,17,17,.08);border-radius:14px;background:#fff;cursor:pointer;transition:border-color .28s ease,transform .28s ease;}
.pal-mobile-menu-close:hover,.pal-mobile-menu-close:focus-visible{border-color:var(--color-primary-border-strong);transform:translateY(-2px);outline:none;}
.pal-mobile-menu-close span{position:absolute;width:18px;height:2px;border-radius:999px;background:#111;}
.pal-mobile-menu-close span:first-child{transform:rotate(45deg);}
.pal-mobile-menu-close span:last-child{transform:rotate(-45deg);}
.pal-mobile-menu-body{flex:1 1 auto;overflow-y:auto;padding:10px 20px 24px;}
.pal-mobile-nav{margin:0;padding:0;list-style:none;}
.pal-mobile-nav-item{border-bottom:1px solid rgba(17,17,17,.06);}
.pal-mobile-nav-link,.pal-mobile-nav-toggle{display:flex;align-items:center;justify-content:space-between;width:100%;padding:20px 4px;border:0;background:none;color:#111;text-align:left;text-decoration:none;cursor:pointer;}
.pal-mobile-nav-link span,.pal-mobile-nav-toggle span{font-size:20px;font-weight:800;line-height:1.3;letter-spacing:-0.03em;}
.pal-mobile-nav-toggle:focus-visible,.pal-mobile-nav-link:focus-visible{outline:none;}
.pal-mobile-nav-arrow{position:relative;display:block;flex:0 0 auto;width:18px;height:18px;}
.pal-mobile-nav-arrow::before,.pal-mobile-nav-arrow::after{content:"";position:absolute;top:8px;width:10px;height:2px;border-radius:999px;background:#111;transition:transform .28s ease,background .28s ease;}
.pal-mobile-nav-arrow::before{left:0;transform:rotate(45deg);}
.pal-mobile-nav-arrow::after{right:0;transform:rotate(-45deg);}
.pal-mobile-nav-item.is-open .pal-mobile-nav-toggle span{color:var(--color-primary);}
.pal-mobile-nav-item.is-open .pal-mobile-nav-arrow::before{background:var(--color-primary);transform:rotate(-45deg);}
.pal-mobile-nav-item.is-open .pal-mobile-nav-arrow::after{background:var(--color-primary);transform:rotate(45deg);}
.pal-mobile-submenu{max-height:0;overflow:hidden;transition:max-height .36s ease;padding:0 2px;}
.pal-mobile-nav-item.is-open .pal-mobile-submenu{max-height:1200px;padding-bottom:20px;}
.pal-mobile-submenu-group + .pal-mobile-submenu-group{margin-top:20px;}
.pal-mobile-submenu-group h3{margin:0 0 10px;font-size:13px;font-weight:800;line-height:1.4;color:var(--color-primary);letter-spacing:.08em;text-transform:uppercase;}
.pal-mobile-submenu-group ul{margin:0;padding:0;list-style:none;}
.pal-mobile-submenu-group li + li{margin-top:10px;}
.pal-mobile-submenu-link{display:block;padding:16px 16px 15px;border:1px solid rgba(17,17,17,.06);border-radius:18px;background:#fafafa;text-decoration:none;transition:border-color .28s ease,transform .28s ease,box-shadow .28s ease,background .28s ease;}
.pal-mobile-submenu-link:hover,.pal-mobile-submenu-link:focus-visible{border-color:var(--color-primary-soft-border);background:#fff;transform:translateY(-2px);box-shadow:0 12px 24px rgba(15,23,42,.06);outline:none;}
.pal-mobile-submenu-link strong{display:block;font-size:15px;font-weight:700;line-height:1.4;color:#111;letter-spacing:-0.02em;}
.pal-mobile-submenu-link p{margin:4px 0 0;font-size:13px;line-height:1.6;color:#666;word-break:keep-all;}
.pal-mobile-featured-link{display:block;margin-top:18px;padding:18px;border-radius:22px;background:linear-gradient(180deg,var(--color-primary-soft-strong) 0%,rgba(42,145,201,.05) 100%);text-decoration:none;}
.pal-mobile-featured-link span{display:block;margin-bottom:8px;font-size:12px;font-weight:800;line-height:1.2;color:var(--color-primary);letter-spacing:.08em;text-transform:uppercase;}
.pal-mobile-featured-link strong{display:block;font-size:16px;font-weight:800;line-height:1.5;color:#111;letter-spacing:-0.02em;}
.pal-mobile-contact-box{margin-top:28px;padding:20px;border:1px solid rgba(17,17,17,.06);border-radius:24px;background:#fff;}
.pal-mobile-contact-box p{margin:0 0 14px;font-size:14px;line-height:1.7;color:#555;word-break:keep-all;}
.pal-mobile-contact-link{display:inline-flex;align-items:center;justify-content:center;height:44px;padding:0 16px;border-radius:999px;background:var(--color-primary);color:#fff;font-size:14px;font-weight:700;line-height:1;text-decoration:none;transition:background .28s ease,transform .28s ease,box-shadow .28s ease;}
.pal-mobile-contact-link:hover,.pal-mobile-contact-link:focus-visible{background:var(--color-primary-light);transform:translateY(-2px);box-shadow:0 12px 24px rgba(42,145,201,.22);outline:none;}
@media (max-width:1440px){
.pal-header-inner{padding:0 24px;}
.pal-mega-panel-inner{padding:24px 24px 24px;}
}
@media (max-width:1200px){
.pal-mega-panel-inner{grid-template-columns:320px 1fr;gap:24px;}
.pal-mega-panel-intro h2{font-size:28px;}
.pal-mega-panel-top{flex-direction:column;align-items:flex-start;}
.pal-mega-panel-top p{max-width:none;text-align:left;}
}
@media (max-width:1024px){
.pal-header-inner{height:82px;padding:0 20px;}
.pal-header-logo a{font-size:24px;}
.pal-gnb{display:none;}
.pal-header-contact{display:none;}
.pal-header-hamburger{display:inline-flex;}
.pal-header-util{min-width:auto;}
.pal-mega-panel{display:none;}
.pal-header-dim{display:none;}
}
@media (max-width:767px){
.pal-mobile-menu{width:100vw;}
.pal-mobile-menu-head{min-height:82px;padding:20px 20px 16px;}
.pal-mobile-menu-body{padding:8px 16px 20px;}
.pal-mobile-nav-link span,.pal-mobile-nav-toggle span{font-size:18px;}
.pal-mobile-submenu-link{padding:14px 14px 13px;border-radius:16px;}
.pal-mobile-contact-box{padding:18px;border-radius:20px;}
}
/*main visual*/
.main-page{position:relative;background:linear-gradient(180deg,#f7f9ff 0%,#eef2fb 52%,#f8faff 100%);color:#111827;overflow:hidden;}
.main-section{position:relative;width:100%;}
.section-inner{position:relative;width:100%;max-width:1400px;margin:0 auto;padding:0 56px;}
.section-head,.split-head,.showcase-head{margin-bottom:72px;}
.section-kicker{display:inline-block;margin-bottom:18px;font-size:12px;font-weight:700;letter-spacing:.22em;text-transform:uppercase;color:rgba(58,64,129,.58);}
.section-title{margin:0;font-size:clamp(38px,5vw,72px);line-height:.98;font-weight:700;letter-spacing:-.04em;color:#111827;}
.section-desc{margin:20px 0 0;font-size:17px;line-height:1.8;color:rgba(17,24,39,.68);}
.section-hero{position:relative;min-height:calc(100vh - var(--header-height));display:flex;align-items:center;overflow:hidden;background:linear-gradient(135deg,rgba(58,64,129,.06) 0%,rgba(58,64,129,.02) 36%,rgba(255,255,255,.85) 100%);}
.hero-inner{position:relative;display:grid;grid-template-columns:minmax(0,1.15fr) minmax(320px,.85fr);align-items:center;gap:40px;min-height:calc(100vh - var(--header-height));padding-top:40px;padding-bottom:60px;}
.hero-title-block{position:relative;z-index:3;max-width:900px;}
.hero-mini{display:block;margin-bottom:22px;font-size:12px;font-weight:700;letter-spacing:.22em;text-transform:uppercase;color:rgba(58,64,129,.72);}
.hero-title{margin:0;}
.hero-title-fill{display:flex;flex-direction:column;gap:2px;}
.hero-title-line{position:relative;display:block;font-size:clamp(64px,10vw,168px);line-height:.9;font-weight:800;letter-spacing:-.07em;}
.hero-title-outline{display:block;color:transparent;-webkit-text-stroke:1px rgba(58,64,129,.26);}
.hero-title-solid{position:absolute;left:0;top:0;display:block;color:var(--color-primary);white-space:nowrap;overflow:hidden;}
.hero-sub{max-width:640px;margin:30px 0 0;font-size:18px;line-height:1.8;color:rgba(17,24,39,.72);}
.hero-meta{display:flex;flex-wrap:wrap;gap:10px;margin-top:30px;}
.hero-meta span{display:inline-flex;align-items:center;height:38px;padding:0 16px;border:1px solid var(--color-primary-border);border-radius:999px;background:rgba(255,255,255,.72);backdrop-filter:blur(8px);font-size:12px;font-weight:700;letter-spacing:.14em;color:var(--color-primary);}
.hero-side-visual{position:relative;z-index:2;display:flex;flex-direction:column;align-items:flex-end;gap:16px;perspective:1000px;}
.hero-side-card{width:min(100%,360px);padding:24px 24px 26px;border:1px solid rgba(58,64,129,.12);border-radius:28px;background:rgba(255,255,255,.68);box-shadow:0 24px 60px rgba(58,64,129,.10);backdrop-filter:blur(14px);}
.hero-side-label{display:block;margin-bottom:16px;font-size:12px;font-weight:700;letter-spacing:.18em;color:rgba(58,64,129,.55);}
.hero-side-card strong{display:block;font-size:26px;line-height:1.04;letter-spacing:-.04em;color:#111827;}
.hero-side-card-a{margin-right:72px;}
.hero-side-card-b{margin-right:8px;}
.hero-side-card-c{margin-right:96px;}
.hero-grid{position:absolute;inset:0;z-index:0;background-image:linear-gradient(rgba(58,64,129,.06) 1px,transparent 1px),linear-gradient(90deg,rgba(58,64,129,.06) 1px,transparent 1px);background-size:56px 56px;mask-image:radial-gradient(circle at 50% 50%,rgba(0,0,0,1) 0%,rgba(0,0,0,.34) 70%,rgba(0,0,0,.08) 100%);}
.hero-orb{position:absolute;border-radius:50%;filter:blur(10px);pointer-events:none;z-index:1;}
.hero-orb-a{right:10%;top:12%;width:420px;height:420px;background:radial-gradient(circle,rgba(58,64,129,.18) 0%,rgba(58,64,129,.06) 42%,rgba(58,64,129,0) 72%);}
.hero-orb-b{left:-4%;bottom:-10%;width:520px;height:520px;background:radial-gradient(circle,rgba(107,120,181,.18) 0%,rgba(107,120,181,.08) 38%,rgba(107,120,181,0) 70%);}
.section-showcase{height:360vh;background:linear-gradient(180deg,#f8faff 0%,#eef2fb 100%);}
.showcase-sticky{position:relative;height:100vh;}
.showcase-shell{display:flex;flex-direction:column;justify-content:center;height:100vh;overflow:hidden;}
.showcase-head{margin-bottom:48px;}
.showcase-rail-wrap{position:relative;padding-left:56px;padding-right:56px;}
.showcase-rail{display:flex;align-items:stretch;gap:24px;width:max-content;will-change:transform;}
.showcase-card-item{position:relative;flex:0 0 480px;min-height:360px;padding:32px;border:1px solid rgba(58,64,129,.12);border-radius:32px;background:linear-gradient(180deg,rgba(255,255,255,.92) 0%,rgba(244,247,255,.86) 100%);box-shadow:0 26px 70px rgba(58,64,129,.10);overflow:hidden;}
.showcase-card-item::before{content:'';position:absolute;right:-40px;top:-30px;width:180px;height:180px;border-radius:50%;background:radial-gradient(circle,rgba(58,64,129,.12) 0%,rgba(58,64,129,0) 72%);}
.showcase-index{display:block;margin-bottom:52px;font-size:12px;font-weight:700;letter-spacing:.2em;color:rgba(58,64,129,.5);}
.showcase-card-item strong{display:block;max-width:280px;font-size:34px;line-height:1.04;font-weight:700;letter-spacing:-.04em;color:#111827;}
.showcase-card-item p{max-width:300px;margin:22px 0 0;font-size:16px;line-height:1.8;color:rgba(17,24,39,.68);}
.section-immersive{height:280vh;background:linear-gradient(180deg,#eef2fb 0%,#e8eef9 100%);}
.immersive-sticky{position:relative;height:100vh;}
.immersive-sticky-inner{position:relative;height:100vh;padding:80px 56px;display:grid;grid-template-columns:1fr 1.2fr 1fr;align-items:center;gap:32px;max-width:1600px;margin:0 auto;}
.immersive-copy{position:relative;z-index:2;}
.immersive-label{display:block;margin-bottom:18px;font-size:12px;font-weight:700;letter-spacing:.22em;color:rgba(58,64,129,.5);}
.immersive-copy h2{margin:0;font-size:clamp(34px,4.2vw,68px);line-height:1.05;font-weight:700;letter-spacing:-.04em;color:#111827;}
.immersive-copy p{margin:0;font-size:17px;line-height:1.9;color:rgba(17,24,39,.7);}
.immersive-visual{display:flex;align-items:center;justify-content:center;}
.visual-frame{position:relative;width:min(42vw,700px);aspect-ratio:1/1.08;border-radius:36px;border:1px solid rgba(58,64,129,.12);background:linear-gradient(180deg,rgba(255,255,255,.72) 0%,rgba(241,245,255,.92) 100%);overflow:hidden;box-shadow:0 40px 120px rgba(58,64,129,.14);}
.visual-grid{position:absolute;inset:0;background-image:linear-gradient(rgba(58,64,129,.08) 1px,transparent 1px),linear-gradient(90deg,rgba(58,64,129,.08) 1px,transparent 1px);background-size:40px 40px;mask-image:linear-gradient(180deg,rgba(0,0,0,1) 0%,rgba(0,0,0,.35) 100%);}
.visual-core{position:absolute;left:50%;top:50%;display:flex;flex-direction:column;align-items:center;gap:14px;transform:translate(-50%,-50%);text-align:center;}
.visual-core span{font-size:clamp(28px,4vw,64px);font-weight:800;letter-spacing:-.05em;color:var(--color-primary);}
.visual-core small{font-size:12px;font-weight:700;letter-spacing:.18em;color:rgba(58,64,129,.58);}
.section-split{padding:200px 0 240px;background:linear-gradient(180deg,#f8faff 0%,#eef2fb 100%);}
.split-stage{position:relative;display:grid;grid-template-columns:1fr minmax(320px,420px) 1fr;align-items:center;gap:28px;max-width:1400px;margin:0 auto;padding:0 56px;}
.split-panel{min-height:360px;padding:34px;border:1px solid rgba(58,64,129,.12);border-radius:36px;background:linear-gradient(180deg,rgba(255,255,255,.92) 0%,rgba(241,245,255,.84) 100%);box-shadow:0 24px 70px rgba(58,64,129,.10);}
.split-label{display:block;margin-bottom:24px;font-size:12px;font-weight:700;letter-spacing:.2em;color:rgba(58,64,129,.52);}
.split-panel strong{display:block;margin-bottom:18px;font-size:clamp(38px,4vw,64px);line-height:.95;font-weight:800;letter-spacing:-.05em;color:#111827;}
.split-panel p{margin:0;max-width:360px;font-size:16px;line-height:1.8;color:rgba(17,24,39,.68);}
.split-center-card{position:relative;z-index:2;min-height:300px;padding:30px;border:1px solid rgba(58,64,129,.16);border-radius:32px;background:linear-gradient(180deg,rgba(58,64,129,.98) 0%,rgba(47,93,170,.96) 100%);box-shadow:0 30px 90px rgba(58,64,129,.22);color:#fff;}
.split-center-kicker{display:block;margin-bottom:18px;font-size:12px;font-weight:700;letter-spacing:.18em;color:rgba(255,255,255,.66);}
.split-center-card strong{display:block;margin-bottom:18px;font-size:34px;line-height:1.02;font-weight:700;letter-spacing:-.04em;}
.split-center-card p{margin:0;font-size:15px;line-height:1.8;color:rgba(255,255,255,.82);}
.section-ending{padding:220px 0 240px;background:linear-gradient(180deg,#eef2fb 0%,#f8faff 100%);}
.ending-inner{text-align:center;}
.ending-label{display:block;margin-bottom:22px;font-size:12px;font-weight:700;letter-spacing:.24em;color:rgba(58,64,129,.46);}
.ending-title{display:flex;flex-wrap:wrap;justify-content:center;gap:14px;margin:0;font-size:clamp(42px,6vw,96px);line-height:.95;font-weight:800;letter-spacing:-.06em;color:#111827;}
.ending-title .word{display:inline-block;}
.ending-desc{margin:30px 0 0;font-size:18px;line-height:1.9;color:rgba(17,24,39,.68);}
@media (max-width:1280px){
.hero-inner{grid-template-columns:1fr;gap:28px;padding-top:80px;padding-bottom:80px;}
.hero-side-visual{align-items:flex-start;}
.hero-side-card-a,.hero-side-card-b,.hero-side-card-c{margin-right:0;}
.showcase-rail-wrap{padding-left:56px;}
.immersive-sticky-inner{grid-template-columns:1fr;grid-template-rows:auto auto auto;align-content:center;text-align:center;}
.immersive-copy-top,.immersive-copy-bottom{max-width:820px;margin:0 auto;}
.visual-frame{width:min(78vw,760px);}
.split-stage{grid-template-columns:1fr;max-width:880px;}
.split-center-card{order:2;}
}
@media (max-width:767px){
.section-inner{padding:0 20px;}
.section-head,.split-head,.showcase-head{margin-bottom:40px;}
.section-desc{font-size:15px;line-height:1.7;}
.hero-inner{padding-top:72px;padding-bottom:56px;}
.hero-title-line{font-size:clamp(52px,15vw,88px);}
.hero-sub{font-size:15px;line-height:1.7;}
.hero-meta{gap:8px;}
.hero-meta span{height:34px;padding:0 12px;font-size:11px;}
.hero-side-card{width:100%;padding:20px 20px 22px;border-radius:22px;}
.hero-side-card strong{font-size:22px;}
.hero-grid{background-size:34px 34px;}
.hero-orb-a{right:-10%;top:8%;width:260px;height:260px;}
.hero-orb-b{left:-18%;bottom:-12%;width:320px;height:320px;}
.section-showcase{height:320vh;}
.showcase-rail-wrap{padding-left:20px;padding-right:20px;}
.showcase-card-item{flex:0 0 84vw;min-height:300px;padding:24px;border-radius:24px;}
.showcase-index{margin-bottom:28px;}
.showcase-card-item strong{font-size:28px;}
.showcase-card-item p{font-size:15px;line-height:1.7;}
.section-immersive{height:220vh;}
.immersive-sticky-inner{padding:56px 20px;}
.visual-frame{width:100%;border-radius:28px;}
.visual-core small{font-size:11px;}
.section-split{padding:120px 0 140px;}
.split-stage{padding:0 20px;}
.split-panel{min-height:auto;padding:24px;border-radius:24px;}
.split-panel strong{font-size:34px;}
.split-panel p{font-size:15px;line-height:1.7;}
.split-center-card{min-height:auto;padding:24px;border-radius:24px;}
.split-center-card strong{font-size:28px;}
.split-center-card p{font-size:14px;line-height:1.7;}
.section-ending{padding:140px 0 160px;}
.ending-desc{font-size:15px;line-height:1.8;}
}

372
src/css/reset.css

@ -0,0 +1,372 @@
@charset "UTF-8";
html {
-webkit-text-size-adjust: 100%;
-moz-text-size-adjust: 100%;
text-size-adjust: 100%;
scroll-behavior: smooth;
}
body,
div,
dl,
dt,
dd,
ul,
ol,
li,
h1,
h2,
h3,
h4,
h5,
h6,
pre,
code,
form,
fieldset,
legend,
input,
textarea,
p,
blockquote,
th,
td,
figure,
figcaption,
hr {
margin: 0;
padding: 0;
}
html,
body {
width: 100%;
min-height: 100%;
}
body {
min-height: 100vh;
font-family: 'Pretendard', 'Noto Sans KR', 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
font-size: 16px;
font-weight: 400;
line-height: 1.5;
color: #111;
background: #fff;
word-break: keep-all;
word-wrap: break-word;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
menu,
nav,
section {
display: block;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
ul,
ol,
li {
list-style: none;
}
img,
picture,
video,
canvas,
svg {
display: block;
max-width: 100%;
height: auto;
border: 0;
vertical-align: top;
}
audio,
canvas,
iframe,
img,
svg,
video {
vertical-align: top;
}
img {
image-rendering: auto;
}
button,
input,
select,
textarea {
font: inherit;
color: inherit;
letter-spacing: inherit;
}
input,
select,
textarea,
button {
margin: 0;
}
button {
border: 0;
background: transparent;
cursor: pointer;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
}
button:disabled {
cursor: default;
}
input {
border: 0;
border-radius: 0;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background: transparent;
}
textarea {
border: 0;
resize: none;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background: transparent;
}
select {
border: 0;
border-radius: 0;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background: transparent;
}
input::-ms-clear,
input::-ms-reveal {
display: none;
}
input[type='search']::-webkit-search-decoration,
input[type='search']::-webkit-search-cancel-button,
input[type='search']::-webkit-search-results-button,
input[type='search']::-webkit-search-results-decoration {
display: none;
}
input[type='number']::-webkit-outer-spin-button,
input[type='number']::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type='number'] {
-moz-appearance: textfield;
}
input[type='checkbox'],
input[type='radio'] {
appearance: auto;
-webkit-appearance: auto;
-moz-appearance: auto;
}
textarea::placeholder,
input::placeholder {
color: #999;
opacity: 1;
}
input:focus,
select:focus,
textarea:focus,
button:focus {
outline: none;
}
a {
color: inherit;
text-decoration: none;
background-color: transparent;
}
table {
width: 100%;
border-collapse: collapse;
border-spacing: 0;
table-layout: fixed;
}
caption {
overflow: hidden;
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
border: 0;
clip: rect(0, 0, 0, 0);
clip-path: inset(50%);
white-space: nowrap;
}
fieldset {
min-width: 0;
border: 0;
}
legend {
overflow: hidden;
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
border: 0;
clip: rect(0, 0, 0, 0);
clip-path: inset(50%);
white-space: nowrap;
}
address,
caption,
cite,
code,
dfn,
em,
strong,
th,
var {
font-style: normal;
font-weight: inherit;
}
strong,
b {
font-weight: 700;
}
em,
i {
font-style: normal;
}
hr {
border: 0;
border-top: 1px solid #ddd;
}
pre,
code,
kbd,
samp {
font-family: inherit;
}
abbr[title] {
text-decoration: none;
}
sub,
sup {
position: relative;
font-size: 75%;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
[hidden] {
display: none !important;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
template {
display: none;
}
[role='button'] {
cursor: pointer;
}
:disabled {
cursor: default;
}
#root {
min-height: 100vh;
}
.blind {
overflow: hidden;
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
border: 0;
clip: rect(0, 0, 0, 0);
clip-path: inset(50%);
white-space: nowrap;
}
.clearfix::after {
content: '';
display: block;
clear: both;
}
@media (prefers-reduced-motion:reduce) {
html {
scroll-behavior: auto;
}
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}

14
src/main.jsx

@ -0,0 +1,14 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
import "./css/reset.css";
import "./css/common.css";
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>,
);

21
src/pages/AboutPage.jsx

@ -0,0 +1,21 @@
function AboutPage() {
return (
<main className="sub-page">
<section className="sub-visual">
<div className="inner">
<h2>About</h2>
<p>서브페이지 테스트 영역</p>
</div>
</section>
<section className="sub-content-section">
<div className="inner">
<h3>About Content</h3>
<p>라우터 연결 확인용 페이지입니다.</p>
</div>
</section>
</main>
)
}
export default AboutPage

717
src/pages/MainPage.jsx

@ -0,0 +1,717 @@
import { useLayoutEffect, useRef } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
function MainPage() {
const pageRef = useRef(null);
useLayoutEffect(() => {
const ctx = gsap.context(() => {
ScrollTrigger.getAll().forEach((trigger) => trigger.kill());
const heroTrigger = ".section-hero";
const showcaseTrigger = ".section-showcase";
const immersiveTrigger = ".section-immersive";
const splitTrigger = ".section-split";
const endingTrigger = ".section-ending";
/* hero - 진입할 때마다 다시 보이도록 fromTo + reverse */
gsap.fromTo(
".hero-title-line",
{
y: 80,
opacity: 0,
},
{
y: 0,
opacity: 1,
duration: 0.95,
stagger: 0.12,
ease: "power3.out",
scrollTrigger: {
trigger: heroTrigger,
start: "top 70%",
toggleActions: "play none none reverse",
},
},
);
gsap.fromTo(
".hero-title-solid",
{
clipPath: "inset(0 100% 0 0)",
},
{
clipPath: "inset(0 0% 0 0)",
duration: 1,
stagger: 0.1,
ease: "power3.out",
scrollTrigger: {
trigger: heroTrigger,
start: "top 68%",
toggleActions: "play none none reverse",
},
},
);
gsap.fromTo(
".hero-mini",
{
y: 18,
opacity: 0,
},
{
y: 0,
opacity: 1,
duration: 0.65,
ease: "power3.out",
scrollTrigger: {
trigger: heroTrigger,
start: "top 72%",
toggleActions: "play none none reverse",
},
},
);
gsap.fromTo(
".hero-sub",
{
y: 28,
opacity: 0,
},
{
y: 0,
opacity: 1,
duration: 0.8,
ease: "power3.out",
scrollTrigger: {
trigger: heroTrigger,
start: "top 70%",
toggleActions: "play none none reverse",
},
},
);
gsap.fromTo(
".hero-meta span",
{
y: 18,
opacity: 0,
},
{
y: 0,
opacity: 1,
duration: 0.55,
stagger: 0.08,
ease: "power3.out",
scrollTrigger: {
trigger: heroTrigger,
start: "top 68%",
toggleActions: "play none none reverse",
},
},
);
gsap.fromTo(
".hero-side-card",
{
y: 42,
opacity: 0,
rotateY: 8,
},
{
y: 0,
opacity: 1,
rotateY: 0,
duration: 0.8,
stagger: 0.1,
ease: "power3.out",
scrollTrigger: {
trigger: heroTrigger,
start: "top 66%",
toggleActions: "play none none reverse",
},
},
);
gsap.fromTo(
".hero-orb",
{
scale: 0.72,
opacity: 0,
},
{
scale: 1,
opacity: 1,
duration: 1.1,
stagger: 0.12,
ease: "power3.out",
scrollTrigger: {
trigger: heroTrigger,
start: "top 72%",
toggleActions: "play none none reverse",
},
},
);
gsap.to(".hero-title-block", {
yPercent: -10,
ease: "none",
scrollTrigger: {
trigger: heroTrigger,
start: "top top",
end: "bottom top",
scrub: 0.8,
},
});
gsap.to(".hero-side-visual", {
yPercent: -14,
ease: "none",
scrollTrigger: {
trigger: heroTrigger,
start: "top top",
end: "bottom top",
scrub: 1,
},
});
gsap.to(".hero-grid", {
yPercent: -8,
ease: "none",
scrollTrigger: {
trigger: heroTrigger,
start: "top top",
end: "bottom top",
scrub: 1.1,
},
});
gsap.to(".hero-orb-a", {
xPercent: 10,
yPercent: -10,
ease: "none",
scrollTrigger: {
trigger: heroTrigger,
start: "top top",
end: "bottom top",
scrub: 1,
},
});
gsap.to(".hero-orb-b", {
xPercent: -8,
yPercent: -12,
ease: "none",
scrollTrigger: {
trigger: heroTrigger,
start: "top top",
end: "bottom top",
scrub: 1.1,
},
});
/* showcase - pinned horizontal */
const showcaseTl = gsap.timeline({
scrollTrigger: {
trigger: showcaseTrigger,
start: "top top",
end: "+=260%",
scrub: 1,
pin: ".showcase-sticky",
anticipatePin: 1,
},
});
showcaseTl
.fromTo(
".showcase-head",
{
y: 40,
opacity: 0,
},
{
y: 0,
opacity: 1,
duration: 0.35,
},
)
.fromTo(
".showcase-card-item",
{
opacity: 0.35,
scale: 0.88,
y: 60,
},
{
opacity: 1,
scale: 1,
y: 0,
duration: 0.4,
stagger: 0.08,
},
0,
)
.to(
".showcase-rail",
{
xPercent: -48,
duration: 1,
ease: "none",
},
0,
);
gsap.to(".showcase-card-item", {
yPercent: -10,
stagger: 0.04,
ease: "none",
scrollTrigger: {
trigger: showcaseTrigger,
start: "top top",
end: "+=260%",
scrub: 1,
},
});
/* immersive - 기존 무드 유지 */
gsap.fromTo(
".immersive-sticky-inner",
{
scale: 0.88,
opacity: 0.45,
},
{
scale: 1,
opacity: 1,
ease: "none",
scrollTrigger: {
trigger: immersiveTrigger,
start: "top top",
end: "+=180%",
scrub: 1,
pin: ".immersive-sticky",
anticipatePin: 1,
},
},
);
gsap.fromTo(
".immersive-copy-top",
{
y: 80,
opacity: 0,
},
{
y: -36,
opacity: 1,
ease: "none",
scrollTrigger: {
trigger: immersiveTrigger,
start: "top top",
end: "+=180%",
scrub: 1,
},
},
);
gsap.fromTo(
".immersive-copy-bottom",
{
y: 110,
opacity: 0,
},
{
y: -18,
opacity: 1,
ease: "none",
scrollTrigger: {
trigger: immersiveTrigger,
start: "top top",
end: "+=180%",
scrub: 1,
},
},
);
gsap.to(".visual-grid", {
yPercent: -8,
ease: "none",
scrollTrigger: {
trigger: immersiveTrigger,
start: "top top",
end: "+=180%",
scrub: 1,
},
});
/* split - 등장 + 스크롤 중 역동성 */
gsap.fromTo(
".split-head",
{
y: 36,
opacity: 0,
},
{
y: 0,
opacity: 1,
duration: 0.8,
ease: "power3.out",
scrollTrigger: {
trigger: splitTrigger,
start: "top 76%",
toggleActions: "play none none reverse",
},
},
);
gsap.fromTo(
".split-panel-left",
{
xPercent: -12,
opacity: 0,
rotate: -2,
},
{
xPercent: 0,
opacity: 1,
rotate: 0,
duration: 1,
ease: "power3.out",
scrollTrigger: {
trigger: ".split-stage",
start: "top 75%",
toggleActions: "play none none reverse",
},
},
);
gsap.fromTo(
".split-panel-right",
{
xPercent: 12,
opacity: 0,
rotate: 2,
},
{
xPercent: 0,
opacity: 1,
rotate: 0,
duration: 1,
ease: "power3.out",
scrollTrigger: {
trigger: ".split-stage",
start: "top 75%",
toggleActions: "play none none reverse",
},
},
);
gsap.fromTo(
".split-center-card",
{
y: 60,
opacity: 0,
scale: 0.9,
},
{
y: 0,
opacity: 1,
scale: 1,
duration: 0.9,
ease: "power3.out",
scrollTrigger: {
trigger: ".split-stage",
start: "top 72%",
toggleActions: "play none none reverse",
},
},
);
gsap.to(".split-panel-left", {
yPercent: -10,
xPercent: -3,
rotate: -2,
ease: "none",
scrollTrigger: {
trigger: splitTrigger,
start: "top bottom",
end: "bottom top",
scrub: 1,
},
});
gsap.to(".split-panel-right", {
yPercent: 10,
xPercent: 3,
rotate: 2,
ease: "none",
scrollTrigger: {
trigger: splitTrigger,
start: "top bottom",
end: "bottom top",
scrub: 1,
},
});
gsap.to(".split-center-card", {
yPercent: -8,
scale: 1.04,
ease: "none",
scrollTrigger: {
trigger: splitTrigger,
start: "top bottom",
end: "bottom top",
scrub: 1,
},
});
/* ending - 초기 렌더 말고 스크롤 진입 기준 */
gsap.fromTo(
".ending-label",
{
opacity: 0,
y: 26,
},
{
opacity: 1,
y: 0,
duration: 0.6,
ease: "power3.out",
scrollTrigger: {
trigger: endingTrigger,
start: "top 78%",
toggleActions: "play none none reverse",
},
},
);
gsap.fromTo(
".ending-title .word",
{
opacity: 0,
y: 60,
},
{
opacity: 1,
y: 0,
duration: 0.85,
ease: "power4.out",
stagger: 0.08,
scrollTrigger: {
trigger: endingTrigger,
start: "top 74%",
toggleActions: "play none none reverse",
},
},
);
gsap.fromTo(
".ending-desc",
{
opacity: 0,
y: 22,
},
{
opacity: 1,
y: 0,
duration: 0.7,
ease: "power3.out",
scrollTrigger: {
trigger: endingTrigger,
start: "top 72%",
toggleActions: "play none none reverse",
},
},
);
ScrollTrigger.refresh();
}, pageRef);
return () => {
ctx.revert();
};
}, []);
return (
<main className="main-page" ref={pageRef}>
<section className="main-section section-hero">
<div className="hero-grid"></div>
<div className="hero-orb hero-orb-a"></div>
<div className="hero-orb hero-orb-b"></div>
<div className="section-inner hero-inner">
<div className="hero-title-block">
<span className="hero-mini">PALNETWORKS RENEWAL</span>
<h1 className="hero-title hero-title-fill">
<span className="hero-title-line">
<span className="hero-title-outline">STRUCTURED</span>
<span className="hero-title-solid">STRUCTURED</span>
</span>
<span className="hero-title-line">
<span className="hero-title-outline">MOTION</span>
<span className="hero-title-solid">MOTION</span>
</span>
</h1>
<p className="hero-sub">
정적인 소개보다 인터랙션과 패럴랙스로
<br />
리뉴얼 방향성과 무드를 먼저 보여주는 메인 비주얼
</p>
<div className="hero-meta">
<span>PARALLAX</span>
<span>INTERACTION</span>
<span>VISUAL FLOW</span>
</div>
</div>
<div className="hero-side-visual">
<div className="hero-side-card hero-side-card-a">
<span className="hero-side-label">01</span>
<strong>Visual Depth</strong>
</div>
<div className="hero-side-card hero-side-card-b">
<span className="hero-side-label">02</span>
<strong>Scroll Rhythm</strong>
</div>
<div className="hero-side-card hero-side-card-c">
<span className="hero-side-label">03</span>
<strong>Brand Direction</strong>
</div>
</div>
</div>
</section>
<section className="main-section section-showcase">
<div className="showcase-sticky">
<div className="showcase-shell">
<div className="section-inner showcase-head">
<span className="section-kicker">SECTION 01</span>
<h2 className="section-title">Dynamic Scroll Showcase</h2>
<p className="section-desc">단순한 카드 나열이 아니라, 스크롤 흐름 자체가 메인 비주얼의 연장처럼 느껴지도록 구성한 구간</p>
</div>
<div className="showcase-rail-wrap">
<div className="showcase-rail">
<article className="showcase-card-item">
<span className="showcase-index">01</span>
<strong>Refined Direction</strong>
<p>과하게 복잡하지 않지만 화면에서 분명한 인상을 남기는 인터랙션 구조</p>
</article>
<article className="showcase-card-item">
<span className="showcase-index">02</span>
<strong>Layered Visual Flow</strong>
<p>텍스트, 패널, 배경이 각기 다른 속도로 반응하며 화면에 깊이감을 더합니다.</p>
</article>
<article className="showcase-card-item">
<span className="showcase-index">03</span>
<strong>Clean Motion</strong>
<p>빠르고 자극적인 효과보다 정돈된 모션 리듬으로 브랜드 톤을 만듭니다.</p>
</article>
<article className="showcase-card-item">
<span className="showcase-index">04</span>
<strong>White Space First</strong>
<p>여백과 구조를 먼저 잡고 포인트 컬러는 필요한 부분에만 사용합니다.</p>
</article>
<article className="showcase-card-item">
<span className="showcase-index">05</span>
<strong>Interactive Mood</strong>
<p>리뉴얼 방향성 제안용 샘플로도 충분히 설득력 있게 보일 있는 무드 중심 섹션</p>
</article>
</div>
</div>
</div>
</div>
</section>
<section className="main-section section-immersive">
<div className="immersive-sticky">
<div className="immersive-sticky-inner">
<div className="immersive-copy immersive-copy-top">
<span className="immersive-label">SECTION 02</span>
<h2>
스크롤에 반응하는
<br />
집중도 높은 구간
</h2>
</div>
<div className="immersive-visual">
<div className="visual-frame">
<div className="visual-grid"></div>
<div className="visual-core">
<span>INTERACTION</span>
<small>VISUAL SYSTEM / SCROLL / RHYTHM</small>
</div>
</div>
</div>
<div className="immersive-copy immersive-copy-bottom">
<p> 섹션은 추후 실제 이미지나 UI 캡처, 숫자 데이터, 솔루션 키워드 등으로 교체하기 좋은 구조입니다. 지금은 비주얼 없이도 무드가 살아 있도록 최소한의 구성만 유지했습니다.</p>
</div>
</div>
</div>
</section>
<section className="main-section section-split">
<div className="section-inner split-head">
<span className="section-kicker">SECTION 03</span>
<h2 className="section-title">Split Motion Panels</h2>
<p className="section-desc">의미 없는 겹침 박스 대신, 서로 다른 방향의 패널과 중앙 카드가 화면 안에서 움직이며 역동적인 인상을 만드는 섹션</p>
</div>
<div className="split-stage">
<div className="split-panel split-panel-left">
<span className="split-label">LEFT PANEL</span>
<strong>STRUCTURE</strong>
<p>정돈된 레이아웃과 여백을 기반으로 모션이 들어가도 과해 보이지 않는 화면 구조</p>
</div>
<div className="split-center-card">
<span className="split-center-kicker">CENTER FLOW</span>
<strong>Balanced Motion</strong>
<p>패널은 좌우로 긴장감을 만들고, 중앙 카드는 정보의 중심축처럼 보이도록 구성합니다.</p>
</div>
<div className="split-panel split-panel-right">
<span className="split-label">RIGHT PANEL</span>
<strong>RHYTHM</strong>
<p>스크롤에 따라 요소마다 반응 속도를 달리 주어 지루하지 않은 인터랙션을 만듭니다.</p>
</div>
</div>
</section>
<section className="main-section section-ending">
<div className="section-inner ending-inner">
<span className="ending-label">FINAL SECTION</span>
<h2 className="ending-title">
<span className="word">SIMPLE.</span>
<span className="word">MODERN.</span>
<span className="word">IMMERSIVE.</span>
</h2>
<p className="ending-desc">
메인 비주얼은 과하지 않게,
<br />
이후 섹션에서는 스크롤과 인터랙션으로 브랜드의 감도를 보여주는 구조입니다.
</p>
</div>
</section>
</main>
);
}
export default MainPage;

11
vite.config.js

@ -0,0 +1,11 @@
import { defineConfig } from 'vite'
import react, { reactCompilerPreset } from '@vitejs/plugin-react'
import babel from '@rolldown/plugin-babel'
// https://vite.dev/config/
export default defineConfig({
plugins: [
react(),
babel({ presets: [reactCompilerPreset()] })
],
})
Loading…
Cancel
Save