diff --git a/public/images/homecoming/meta_img.png b/public/images/homecoming/meta_img.png index 16cfa4f..8cbe700 100644 Binary files a/public/images/homecoming/meta_img.png and b/public/images/homecoming/meta_img.png differ diff --git a/public/images/logo.png b/public/images/logo.png new file mode 100644 index 0000000..443ffcb Binary files /dev/null and b/public/images/logo.png differ diff --git a/src/app/event/homecoming/component/mobile/HomecomingInviteCard.jsx b/src/app/event/homecoming/component/mobile/HomecomingInviteCard.jsx new file mode 100644 index 0000000..6e4ac6f --- /dev/null +++ b/src/app/event/homecoming/component/mobile/HomecomingInviteCard.jsx @@ -0,0 +1,214 @@ +'use client'; + +import React, {useMemo} from 'react'; +import {GoogleMap, Marker, useJsApiLoader} from '@react-google-maps/api'; +import {useSearchParams} from "next/navigation"; +import decodeHashToName from "@/app/event/homecoming/util/decoder"; + +export default function HomecomingInviteCard() { + const sp = useSearchParams(); + const hash = sp.get('hash'); + const userName = useMemo(() => decodeHashToName(hash)?.trim() ?? '', [hash]); + + return (
+
+ {/* 상단 바 */} +
+ + {/* 상단 컬러 라인 */} +
+
+
+
+ +
+ {/* GDGoC 로고 */} +
+
+ G + D + G + o + C + INHA +
+
+ + {/* 초대 문구 */} +
+

+ 제 1회 홈커밍 데이에 +
+ {userName ? (<> + {userName}님을 초대합니다! + ) : (<>여러분을 초대합니다!)} +

+
+ + {/* 내용 블록 */} +
{/* 일시 */} +
+
일시
+
+
+ 2025년 12월 20일 (토) 13:00 ~ 19:00 +
+
+ * 19:00 이후 뒤풀이 장소로 함께 이동하며 마무리합니다. +
+
+
+ + {/* 프로그램 */} +
+
프로그램
+ +
+ {/* 요약 */} +
+ 13:00 입장을 시작으로 1부 프로젝트 데모데이 및 성과 발표/시상식이 진행되며, + 2부에서는 오프닝 특강과 팀별 게임·퀴즈, 자유 네트워킹으로 소통과 교류를 확장합니다. +
+ + {/* 1부 */} +
+
1부 (13:00–15:30) · GOAT 프로젝트 데모데이
+ +
+
+ 13:00–13:20 + 입장/체크인 · 오프닝 안내 · 활동 소개 영상 +
+
+ 13:20–13:30 + 라운드 운영 안내 · 발표 준비 +
+
+ 13:30–15:10 + 프로젝트 성과 발표/데모 (총 6라운드) · QnA +
+
+ 15:10–15:30 + 심사 집계 · 시상식 +
+
+
+ + {/* 2부 */} +
+
2부 (15:30–19:00) · Networking with INCHEON
+ +
+
+ 15:30–16:00 + 2부 입장 +
+
+ 16:00–16:40 + + 오프닝 특강 +
+ GDG Campus Korea 김대현님 +
+
+
+ 16:40–17:00 + GDGoC INHA 연간 활동 소개 +
+
+ 17:00–17:30 + OB 및 초청자 인사 +
+
+ 17:30–18:30 + 네트워킹 게임 · 퀴즈 프로그램 +
+
+ 18:30–19:00 + 자유 네트워킹 +
+
+ 19:00– + 뒤풀이 진행 +
+
+
+ + {/* 도착 안내 */} +
+
도착 안내
+
+ • 1부 참석자: 12:50까지 도착
+ • 2부 참석자: 15:20까지 도착 +
+
+ + {/* 문의 */} +
+
문의
+
+ 행사 관련 문의: 010-2087-1816 +
+
+
+
+ + {/* 장소 */} +
+
장소
+
+

신한 스퀘어 브릿지 인천

+

(인천광역시 연수구 컨벤시아대로 204, 인스타2)

+
+
+ + {/* 지도 */} + +
+
+ + {/* 하단 장식 */} +
+
+
+
+
+
); +} + + +function HomecomingMap() { + const {isLoaded, loadError} = useJsApiLoader({ + googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY, id: 'homecoming-map-script', + }); + + const center = {lat: 37.388493, lng: 126.639989}; + + if (loadError) { + return (
+ 지도를 불러오는 중 오류가 발생했습니다. +
); + } + + if (!isLoaded) { + return (
+ 지도를 불러오는 중입니다... +
); + } + + return (
+ + + +
); +} \ No newline at end of file diff --git a/src/app/event/homecoming/component/mobile/HomecomingMobile.jsx b/src/app/event/homecoming/component/mobile/HomecomingMobile.jsx new file mode 100644 index 0000000..349e234 --- /dev/null +++ b/src/app/event/homecoming/component/mobile/HomecomingMobile.jsx @@ -0,0 +1,63 @@ +'use client'; + +import {useEffect, useRef, useState} from 'react'; +import HomecomingInviteCard from './HomecomingInviteCard'; + +export default function HomecomingMobile() { + const MIN_TOP = 64; + const IMAGE_HEIGHT = 278; + const EXTRA_GAP = 24; + + const MAX_TOP = MIN_TOP + IMAGE_HEIGHT + EXTRA_GAP; + + const [top, setTop] = useState(MAX_TOP); + const scrollRef = useRef(null); + + useEffect(() => { + const el = scrollRef.current; + if (!el) return; + + const RANGE = IMAGE_HEIGHT + EXTRA_GAP; // 302 + + const handleScroll = () => { + const scrollY = el.scrollTop; + + const progress = Math.min(scrollY / RANGE, 1); + const nextTop = MAX_TOP - RANGE * progress; + + setTop(nextTop); + }; + + handleScroll(); + el.addEventListener('scroll', handleScroll, {passive: true}); + return () => el.removeEventListener('scroll', handleScroll); + }, []); + + return (
+ {/* 상단 고정 영역 */} +
+
+ GDGoC logo +
+ +
+ Homecoming illustration +
+
+ + {/* 카드 */} +
+ +
+
); +} \ No newline at end of file diff --git a/src/app/event/homecoming/component/pc/Frame.jsx b/src/app/event/homecoming/component/pc/Frame.jsx new file mode 100644 index 0000000..214b16f --- /dev/null +++ b/src/app/event/homecoming/component/pc/Frame.jsx @@ -0,0 +1,55 @@ +'use client'; + +export default function Frame() { + return (
+ {/* ===================== + 상단 장식 라인 + ===================== */} +
+ {/* 왼쪽 상단 라인 */} +
+ + {/* 오른쪽 상단 라인 */} +
+
+ + {/* ===================== + 하단 장식 라인 + ===================== */} +
+ {/* 오른쪽 하단 라인 */} +
+ + {/* 왼쪽 하단 라인 */} +
+
+
); +} \ No newline at end of file diff --git a/src/app/event/homecoming/component/pc/FrameLayout.jsx b/src/app/event/homecoming/component/pc/FrameLayout.jsx new file mode 100644 index 0000000..79096f2 --- /dev/null +++ b/src/app/event/homecoming/component/pc/FrameLayout.jsx @@ -0,0 +1,60 @@ +'use client'; + +import {useRef, useState} from 'react'; +import Frame from './Frame'; +import FrameViewport from './FrameViewport'; +import ScrollDots from './ScrollDots'; + +export default function FrameLayout() { + const viewportRef = useRef(null); + const [activeIndex, setActiveIndex] = useState(0); + const TOTAL = 5; + const SCROLL_DAMPING = 0.2; + const MAX_DELTA = 60; + + const onScroll = () => { + const el = viewportRef.current; + if (!el) return; + const idx = Math.round(el.scrollTop / el.clientHeight); + setActiveIndex(idx); + }; + + const onJump = (index) => { + const el = viewportRef.current; + if (!el) return; + el.scrollTo({top: index * el.clientHeight, behavior: 'auto'}); + }; + + const onWheel = (e) => { + const el = viewportRef.current; + if (!el) return; + + const raw = e.deltaY; + const clamped = Math.max(-MAX_DELTA, Math.min(MAX_DELTA, raw)); + const delta = clamped * SCROLL_DAMPING; + + const atTop = el.scrollTop <= 0; + const atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 1; + const canScrollInside = (delta > 0 && !atBottom) || (delta < 0 && !atTop); + + if (!canScrollInside) return; + + e.preventDefault(); + el.scrollTop += delta; + }; + + return (
+ + +
+ +
+ + +
); +} \ No newline at end of file diff --git a/src/app/event/homecoming/component/pc/FrameSection.jsx b/src/app/event/homecoming/component/pc/FrameSection.jsx new file mode 100644 index 0000000..f680a86 --- /dev/null +++ b/src/app/event/homecoming/component/pc/FrameSection.jsx @@ -0,0 +1,9 @@ +'use client'; + +export default function FrameSection({ children }) { + return ( +
+ {children} +
+ ); +} \ No newline at end of file diff --git a/src/app/event/homecoming/component/pc/FrameViewport.jsx b/src/app/event/homecoming/component/pc/FrameViewport.jsx new file mode 100644 index 0000000..15dbfc4 --- /dev/null +++ b/src/app/event/homecoming/component/pc/FrameViewport.jsx @@ -0,0 +1,195 @@ +'use client'; + +import React, {forwardRef} from 'react'; +import FrameSection from './FrameSection'; +import {GoogleMap, Marker, useJsApiLoader} from "@react-google-maps/api"; + +const FrameViewport = forwardRef(function FrameViewport({onScroll}, ref) { + return (
+ + + + + +
); +}); + +export default FrameViewport; + +function FirstSection() { + return (
+
+ G + D + G + o + C + INHA +
+ +
제 1회 홈커밍 데이
+ +

+ GDGoC HomeComing : Networking with INCHEON은 +
+ 오후 1시 입장을 시작으로, 1부 프로젝트 성과 발표회시상, +
+ 이후 오프닝 특강연간 활동 소개, OB 및 초청자 인사를 거쳐 +
+ 팀별 경쟁 게임·퀴즈·자유 네트워킹으로 이어지는 구성입니다. +
+ 행사는 13:00–19:00까지 진행되며, +
+ 마지막에는 전체 교류 마무리 후 뒤풀이 이동으로 마무리됩니다. +

+
); +} + +/* 2) 3컬럼 타임테이블(전체 요약) */ +function SecondSection() { + return (
+
타임테이블
+
+ {/* Col 1 */} +
+ + + + + +
+ + {/* Col 3 */} +
+ + + GDG Campus Korea 김대현님 + } + /> + + + + + +
+
+ +
+ • 1부 참석자: 12:50까지 도착
+ • 2부 참석자: 15:20까지 도착 +
+
); +} + +/* 3) 1부 상세 (2컬럼 리스트) */ +function ThirdSection() { + return (
+
1부 · GOAT 프로젝트 데모데이
+
13:00–15:30
+ +
+
+ + + + +
+
+
); +} + +/* 4) 2부 카드형 */ +function FourthSection() { + return (
+
2부 · Networking with INCHEON
+
15:30–19:00
+ +
+
+
오프닝 & 소개
+ + + + +
+ +
+
게임 & 네트워킹
+ + + +
+
+
); +} + +/* 5) 장소 + 지도 */ +function FifthSection() { + return (
+
신한 스퀘어 브릿지 인천
+
+ (인천광역시 연수구 컨벤시아대로 204 인스타2) +
+ + + +
+ 문의: 010-2087-1816 +
+
); +} + +function TimeRow({time, title, desc}) { + return (
+
{time}
+
+
{title}
+ {desc ?
{desc}
: null} +
+
); +} + + +function HomecomingMap() { + const {isLoaded, loadError} = useJsApiLoader({ + googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY, id: 'homecoming-map-script', + }); + + const center = {lat: 37.388493, lng: 126.639989}; + + if (loadError) { + return (
+ 지도를 불러오는 중 오류가 발생했습니다. +
); + } + + if (!isLoaded) { + return (
+ 지도를 불러오는 중입니다... +
); + } + + return (
+ + + +
); +} \ No newline at end of file diff --git a/src/app/event/homecoming/component/pc/HeroIntro.jsx b/src/app/event/homecoming/component/pc/HeroIntro.jsx new file mode 100644 index 0000000..856c2ea --- /dev/null +++ b/src/app/event/homecoming/component/pc/HeroIntro.jsx @@ -0,0 +1,68 @@ +'use client'; + +export default function HeroIntro({userName, phase, onEnter, leaving}) { + return (
+ {/* 배경 */} + + + {/* 콘텐츠 */} +
+
+ {/* 로고 */} +
+ G + D + G + o + C + INHA +
+ + {/* 문구 */} +

+ 제 1회 홈커밍 데이에{' '} + {userName ? (<> + {userName}님을 초대합니다! + ) : (<>여러분을 초대합니다!)} +

+ + {/* CTA 버튼 */} + +
+
+
); +} \ No newline at end of file diff --git a/src/app/event/homecoming/component/pc/HomecomingDesktop.jsx b/src/app/event/homecoming/component/pc/HomecomingDesktop.jsx new file mode 100644 index 0000000..2a96dc5 --- /dev/null +++ b/src/app/event/homecoming/component/pc/HomecomingDesktop.jsx @@ -0,0 +1,61 @@ +'use client'; + +import {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import {useSearchParams} from 'next/navigation'; + +import HeroIntro from './HeroIntro'; +import FrameLayout from './FrameLayout'; +import decodeHashToName from '../../util/decoder'; + +export default function HomecomingDesktop() { + const sp = useSearchParams(); + const hash = sp.get('hash'); + const userName = useMemo(() => decodeHashToName(hash)?.trim() ?? '', [hash]); + + const [heroPhase, setHeroPhase] = useState(0); + const [mode, setMode] = useState('hero'); // 'hero' | 'frame' + const lockRef = useRef(false); + + useEffect(() => { + const t = setTimeout(() => setHeroPhase(1), 500); + return () => clearTimeout(t); + }, []); + + const enterFrame = useCallback(() => { + if (lockRef.current || mode === 'frame') return; + lockRef.current = true; + + setTimeout(() => { + setMode('frame'); + lockRef.current = false; + }, 700); + }, [mode]); + + return (
+
+ GDGoC logo +
+ + {/* Hero */} +
+ +
+ + {/* Frame */} +
+ +
+
); +} \ No newline at end of file diff --git a/src/app/event/homecoming/component/pc/ScrollDots.jsx b/src/app/event/homecoming/component/pc/ScrollDots.jsx new file mode 100644 index 0000000..e0d2ddf --- /dev/null +++ b/src/app/event/homecoming/component/pc/ScrollDots.jsx @@ -0,0 +1,15 @@ +'use client' + +export default function ScrollDots({count, activeIndex, onJump}) { + return (
+ {Array.from({length: count}).map((_, i) => (
); +} \ No newline at end of file diff --git a/src/app/event/homecoming/hooks/useDeviceType.js b/src/app/event/homecoming/hooks/useDeviceType.js new file mode 100644 index 0000000..abc94c6 --- /dev/null +++ b/src/app/event/homecoming/hooks/useDeviceType.js @@ -0,0 +1,25 @@ +'use client'; + +import { useEffect, useState } from 'react'; + +export function useDeviceType() { + const [device, setDevice] = useState(null); + + useEffect(() => { + const check = () => { + if (typeof window === 'undefined') return; + + const width = window.innerWidth; + setDevice(width <= 768 ? 'mobile' : 'desktop'); + }; + + check(); + window.addEventListener('resize', check); + + return () => { + window.removeEventListener('resize', check); + }; + }, []); + + return device; +} \ No newline at end of file diff --git a/src/app/event/homecoming/layout.js b/src/app/event/homecoming/layout.js new file mode 100644 index 0000000..0c72678 --- /dev/null +++ b/src/app/event/homecoming/layout.js @@ -0,0 +1,34 @@ +const siteUrl = new URL("https://gdgocinha.com"); + +export const metadata = { + metadataBase: siteUrl, + + title: "Homecoming", description: "GDGoC INHA 제1회 홈커밍 데이 행사 안내 및 참여 페이지", + + alternates: { + canonical: "/homecoming", + }, + + openGraph: { + title: "GDGoC INHA 제1회 홈커밍 데이", + description: "GDGoC INHA가 처음으로 선보이는 홈커밍 데이에 여러분을 초대합니다.", + url: "/event/homecoming", + siteName: "GDGoC INHA", + images: [{ + url: "/images/homecoming/meta_img.png", width: 1143, height: 750, alt: "GDGoC INHA Homecoming Day", + },], + locale: "ko_KR", + type: "website", + }, + + twitter: { + card: "summary_large_image", + title: "GDGoC INHA 제1회 홈커밍 데이", + description: "GDGoC INHA가 처음으로 선보이는 홈커밍 데이에 여러분을 초대합니다.", + images: ["/images/homecoming/meta_img.png"], + }, +}; + +export default function HomecomingLayout({ children }) { + return children; +} \ No newline at end of file diff --git a/src/app/event/homecoming/page.jsx b/src/app/event/homecoming/page.jsx new file mode 100644 index 0000000..0165b78 --- /dev/null +++ b/src/app/event/homecoming/page.jsx @@ -0,0 +1,17 @@ +'use client'; + +import { useDeviceType } from './hooks/useDeviceType'; +import HomecomingMobile from './component/mobile/HomecomingMobile'; +import HomecomingDesktop from './component/pc/HomecomingDesktop'; + +export default function Page() { + const device = useDeviceType(); + + if (device === null) { + return
; + } + + return device === 'mobile' + ? + : ; +} \ No newline at end of file diff --git a/src/app/event/homecoming/util/decoder.js b/src/app/event/homecoming/util/decoder.js new file mode 100644 index 0000000..2eddccb --- /dev/null +++ b/src/app/event/homecoming/util/decoder.js @@ -0,0 +1,20 @@ +export default function decodeHashToName(hash) { + if (!hash) return ''; + try { + // base64url -> base64 + let b64 = hash.replace(/-/g, '+').replace(/_/g, '/'); + while (b64.length % 4 !== 0) b64 += '='; + + // base64 -> UTF-8 bytes -> string + const bin = atob(b64); + const bytes = Uint8Array.from(bin, (c) => c.charCodeAt(0)); + const shifted = new TextDecoder('utf-8').decode(bytes); + + // 문자 단위로 -3 시프트 + return [...shifted] + .map((c) => String.fromCharCode(c.charCodeAt(0) - 3)) + .join(''); + } catch { + return ''; + } +} \ No newline at end of file diff --git a/src/app/homecoming/component/HomecomingPage.jsx b/src/app/homecoming/component/HomecomingPage.jsx index 7738f80..4b7c7ef 100644 --- a/src/app/homecoming/component/HomecomingPage.jsx +++ b/src/app/homecoming/component/HomecomingPage.jsx @@ -211,7 +211,7 @@ function Invitation({onBack}) { 2부(16:00~) Networking Session

    -
  • 오프닝 특강 - GDGoC Korea Organizer 김대현님
  • +
  • 오프닝 특강 - GDG Campus Korea Organizer 김대현님
  • GDGoC INHA 연간 활동 소개 및 커뮤니티 정리
  • OB 및 초청 연사 소개
  • 팀 기반 네트워킹 게임 진행
  • diff --git a/src/app/layout.js b/src/app/layout.js index a80bad1..e44ce9e 100644 --- a/src/app/layout.js +++ b/src/app/layout.js @@ -76,6 +76,11 @@ export const metadata = { ], shortcut: ["/favicon.ico"] }, + appleWebApp: { + capable: true, + statusBarStyle: "default", + title: "GDGoC INHA", + }, openGraph: { title: "GDGoC INHA Univ.", description: "Google Developer Group on Campus at Inha University", @@ -114,21 +119,11 @@ export const metadata = { export default function RootLayout({ children }) { return ( - - - - {/* PWA 관련 메타 태그 */} - - - - - {/* 외부 스크립트 */} +