Skip to content

Commit 12c1d10

Browse files
authored
Merge pull request #234 from CSE-Shaco/develop
feat(event/homecoming): 초대장 초안 골격 완성, //TODO 타이포 및 내용 수정
2 parents e39ab43 + 4867d12 commit 12c1d10

File tree

15 files changed

+580
-0
lines changed

15 files changed

+580
-0
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
'use client';
2+
3+
import React, {useMemo} from 'react';
4+
import {GoogleMap, Marker, useJsApiLoader} from '@react-google-maps/api';
5+
import {useSearchParams} from "next/navigation";
6+
import decodeHashToName from "@/app/event/homecoming/util/decoder";
7+
8+
export default function HomecomingInviteCard() {
9+
const sp = useSearchParams();
10+
const hash = sp.get('hash');
11+
const userName = useMemo(() => decodeHashToName(hash)?.trim() ?? '', [hash]);
12+
13+
return (<div
14+
className="flex flex-col w-screen h-[calc(100dvh-64px)] pt-2 pb-12 rounded-t-[50px] bg-cblack overflow-hidden shadow-[0_-1.5px_#d9d9d940]">
15+
<div className="flex flex-col self-center w-[390px] h-[calc(100dvh-64px)]">
16+
{/* 상단 바 */}
17+
<div className="w-[30px] h-1 self-center bg-[#d9d9d9] rounded-full"/>
18+
19+
{/* 상단 컬러 라인 */}
20+
<div className="flex justify-around items-center self-center relative h-[52px] w-[326px] mt-7">
21+
<div className="h-2 w-[169.5px] -rotate-15 rounded-full bg-cred absolute left-0"/>
22+
<div className="h-2 w-[169.5px] rotate-15 rounded-full bg-cblue absolute right-0"/>
23+
</div>
24+
25+
<div
26+
className="w-[326px] h-[calc(100dvh-210px)] my-4 self-center overflow-hidden overflow-y-auto no-scrollbar">
27+
{/* GDGoC 로고 */}
28+
<div className="flex flex-col items-center">
29+
<div className="flex items-center text-[28px] font-ocra tight-[-2.5%]">
30+
<span className="text-cred">G</span>
31+
<span className="text-cgreen">D</span>
32+
<span className="text-cyellow">G</span>
33+
<span className="text-cblue">o</span>
34+
<span className="text-cred mr-2">C</span>
35+
<span className="text-white ml-1">INHA</span>
36+
</div>
37+
</div>
38+
39+
{/* 초대 문구 */}
40+
<div className="text-center mb-12">
41+
<p className="text-[24px] leading-snug tight-[-2.5%]">
42+
<span className="font-extrabold">제 1회 홈커밍 데이</span>
43+
<br/>
44+
{userName ? (<>
45+
<span className="font-extrabold">{userName}</span>님을 초대합니다!
46+
</>) : (<>여러분을 초대합니다!</>)}
47+
</p>
48+
</div>
49+
50+
{/* 내용 블록 */}
51+
<div className="space-y-4 text-[14px]">
52+
53+
{/* 일시 */}
54+
<div className="flex-col gap-1">
55+
<div className="shrink-0 text-white font-bold text-xl">일시</div>
56+
<div className="flex-1">
57+
<span className="font-semibold">2025년 12월 20일 (토) 16:00 ~</span>
58+
</div>
59+
</div>
60+
61+
{/* 일정 */}
62+
<div className="flex-col gap-1">
63+
<div className="shrink-0 text-white font-bold text-xl">프로그램</div>
64+
<div className="flex-1 space-y-1">
65+
<div className="flex gap-2">
66+
<span className="font-bold w-[106px]">OB &amp; YB 네트워킹</span>
67+
<span className="text-white text-[12px] pt-px">선후배 간의 네트워킹 프로그램</span>
68+
</div>
69+
<div className="flex gap-2">
70+
<span className="font-semibold w-[106px]">다과 및 경품 추첨</span>
71+
<span className="text-white text-[12px] pt-px">네트워킹을 위한 다과와 이벤트</span>
72+
</div>
73+
<div className="flex gap-2">
74+
<span className="font-bold w-[106px]">전체 회식</span>
75+
<span className="text-white text-[12px] pt-px">행사 종료 후 식당으로 이동</span>
76+
</div>
77+
</div>
78+
</div>
79+
80+
{/* 장소 */}
81+
<div className="flex-col gap-2">
82+
<div className="shrink-0 text-white font-bold text-xl">장소</div>
83+
<div className="flex-1">
84+
<p className="font-bold">신한 스퀘어브릿지 인천</p>
85+
<p className="text-white text-[12px]">(인천 연수구 컨벤시아대로 204 인스타2)</p>
86+
</div>
87+
</div>
88+
</div>
89+
90+
{/* 지도 */}
91+
<HomecomingMap></HomecomingMap>
92+
</div>
93+
94+
{/* 하단 장식 */}
95+
<div className="flex justify-around items-center self-center relative h-2 w-[326px]">
96+
<div className="absolute right-0 h-2 w-[166px] rounded-full bg-cyellow"/>
97+
<div className="absolute left-0 h-2 w-[166px] rounded-full bg-cgreen"/>
98+
</div>
99+
</div>
100+
</div>);
101+
}
102+
103+
104+
function HomecomingMap() {
105+
const {isLoaded, loadError} = useJsApiLoader({
106+
googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY, id: 'homecoming-map-script',
107+
});
108+
109+
const center = {lat: 37.388493, lng: 126.639989};
110+
111+
if (loadError) {
112+
return (<div
113+
className="rounded-2xl border border-red-300 bg-red-50 text-red-700 text-xs md:text-sm flex items-center justify-center h-[220px] md:h-[320px] lg:h-[420px]">
114+
지도를 불러오는 중 오류가 발생했습니다.
115+
</div>);
116+
}
117+
118+
if (!isLoaded) {
119+
return (<div
120+
className="rounded-2xl border border-neutral-200 bg-neutral-100 text-neutral-500 text-xs md:text-sm flex items-center justify-center h-[220px] md:h-[320px] lg:h-[420px]">
121+
지도를 불러오는 중입니다...
122+
</div>);
123+
}
124+
125+
return (<div className="mt-6 rounded-2xl h-[170px] overflow-hidden">
126+
<GoogleMap
127+
mapContainerClassName="w-full h-full"
128+
center={center}
129+
zoom={17}
130+
options={{disableDefaultUI: true, clickableIcons: false}}
131+
>
132+
<Marker position={center}/>
133+
</GoogleMap>
134+
</div>);
135+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
'use client';
2+
3+
import {useEffect, useRef, useState} from 'react';
4+
import HomecomingInviteCard from './HomecomingInviteCard';
5+
6+
export default function HomecomingMobile() {
7+
const MIN_TOP = 64;
8+
const IMAGE_HEIGHT = 278;
9+
const EXTRA_GAP = 24;
10+
11+
const MAX_TOP = MIN_TOP + IMAGE_HEIGHT + EXTRA_GAP;
12+
13+
const [top, setTop] = useState(MAX_TOP);
14+
const scrollRef = useRef(null);
15+
16+
useEffect(() => {
17+
const el = scrollRef.current;
18+
if (!el) return;
19+
20+
const RANGE = IMAGE_HEIGHT + EXTRA_GAP; // 302
21+
22+
const handleScroll = () => {
23+
const scrollY = el.scrollTop;
24+
25+
const progress = Math.min(scrollY / RANGE, 1);
26+
const nextTop = MAX_TOP - RANGE * progress;
27+
28+
setTop(nextTop);
29+
};
30+
31+
handleScroll();
32+
el.addEventListener('scroll', handleScroll, {passive: true});
33+
return () => el.removeEventListener('scroll', handleScroll);
34+
}, []);
35+
36+
return (<div
37+
ref={scrollRef}
38+
className="relative w-full h-dvh overflow-y-auto no-scrollbar"
39+
>
40+
{/* 상단 고정 영역 */}
41+
<div className="px-4 pt-4 fixed z-10">
42+
<header className="flex items-center gap-2 mb-6">
43+
<img src="/logo.png" alt="GDGoC logo" className="h-6 w-auto"/>
44+
</header>
45+
46+
<div className="w-full max-w-[390px] left-1/2 -translate-x-1/2 fixed">
47+
<img
48+
src="/homecoming_main_img.png"
49+
alt="Homecoming illustration"
50+
className="h-auto block"
51+
/>
52+
</div>
53+
</div>
54+
55+
{/* 카드 */}
56+
<div
57+
className="absolute left-1/2 -translate-x-1/2 transition-[top] duration-300 ease-out z-10"
58+
style={{top: `${top}px`}}
59+
>
60+
<HomecomingInviteCard/>
61+
</div>
62+
</div>);
63+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
'use client';
2+
3+
export default function Frame() {
4+
return (<div className="absolute inset-0 w-[1400px] h-[1000px] m-auto pointer-events-none">
5+
{/* =====================
6+
상단 장식 라인
7+
===================== */}
8+
<div className="absolute top-0 left-0 w-full h-60 overflow-visible">
9+
{/* 왼쪽 상단 라인 */}
10+
<div
11+
className="
12+
absolute top-1/2 left-0
13+
h-10 w-[732px]
14+
rounded-full bg-cred
15+
-translate-y-1/2
16+
-rotate-15
17+
"
18+
/>
19+
20+
{/* 오른쪽 상단 라인 */}
21+
<div
22+
className="
23+
absolute top-1/2 right-0
24+
h-10 w-[732px]
25+
rounded-full bg-cblue
26+
-translate-y-1/2
27+
rotate-15
28+
"
29+
/>
30+
</div>
31+
32+
{/* =====================
33+
하단 장식 라인
34+
===================== */}
35+
<div className="absolute bottom-0 left-0 w-full h-10 overflow-visible">
36+
{/* 오른쪽 하단 라인 */}
37+
<div
38+
className="
39+
absolute bottom-0 right-0
40+
h-10 w-[720px]
41+
rounded-full bg-cyellow
42+
"
43+
/>
44+
45+
{/* 왼쪽 하단 라인 */}
46+
<div
47+
className="
48+
absolute bottom-0 left-0
49+
h-10 w-[720px]
50+
rounded-full bg-cgreen
51+
"
52+
/>
53+
</div>
54+
</div>);
55+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
'use client';
2+
3+
import {useRef, useState} from 'react';
4+
import Frame from './Frame';
5+
import FrameViewport from './FrameViewport';
6+
import ScrollDots from './ScrollDots';
7+
8+
export default function FrameLayout({visible}) {
9+
const viewportRef = useRef(null);
10+
const [activeIndex, setActiveIndex] = useState(0);
11+
const TOTAL = 4;
12+
13+
const onScroll = () => {
14+
const el = viewportRef.current;
15+
if (!el) return;
16+
const idx = Math.round(el.scrollTop / el.clientHeight);
17+
setActiveIndex(idx);
18+
};
19+
20+
const onJump = (index) => {
21+
const el = viewportRef.current;
22+
if (!el) return;
23+
el.scrollTo({top: index * el.clientHeight, behavior: 'auto'});
24+
};
25+
26+
const onWheel = (e) => {
27+
if (!visible) return;
28+
const el = viewportRef.current;
29+
if (!el) return;
30+
31+
// 내부가 스크롤 가능할 때만 page 스크롤을 막고 내부로 보냄
32+
const delta = e.deltaY;
33+
const atTop = el.scrollTop <= 0;
34+
const atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 1;
35+
const canScrollInside = (delta > 0 && !atBottom) || (delta < 0 && !atTop);
36+
37+
if (!canScrollInside) return; // 내부 못 움직이면 바깥 스크롤/스냅에 맡김
38+
39+
e.preventDefault();
40+
el.scrollTop += delta;
41+
};
42+
43+
return (<div
44+
className={`
45+
absolute inset-0 w-[1400px] h-[1000px] m-auto pt-60 pb-10
46+
${visible ? '' : 'hidden'}
47+
`}
48+
onWheel={onWheel}
49+
>
50+
<Frame/>
51+
52+
<div className="relative h-full w-full pointer-events-auto">
53+
<FrameViewport ref={viewportRef} onScroll={onScroll}/>
54+
</div>
55+
56+
<ScrollDots count={TOTAL} activeIndex={activeIndex} onJump={onJump}/>
57+
</div>);
58+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
'use client';
2+
3+
export default function FrameSection({ children }) {
4+
return (
5+
<section className="h-full w-full snap-start flex items-center justify-center">
6+
{children}
7+
</section>
8+
);
9+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use client';
2+
3+
import React, {forwardRef} from 'react';
4+
import FrameSection from './FrameSection';
5+
6+
const FrameViewport = forwardRef(function FrameViewport({onScroll}, ref) {
7+
return (<div
8+
ref={ref}
9+
onScroll={onScroll}
10+
className="
11+
relative
12+
h-full w-full
13+
overflow-y-auto no-scrollbar
14+
snap-y snap-mandatory
15+
"
16+
>
17+
<FrameSection><p>1st</p></FrameSection>
18+
<FrameSection><p>2nd</p></FrameSection>
19+
<FrameSection><p>3rd</p></FrameSection>
20+
<FrameSection><p>4th</p></FrameSection>
21+
</div>);
22+
});
23+
24+
export default FrameViewport;

0 commit comments

Comments
 (0)