-
Notifications
You must be signed in to change notification settings - Fork 0
[RELEASE] 데모데이 전용 페이지 배포 #262
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
df7af08
6905970
9ff4c5b
2c41943
9ac731a
e7aa591
e4480e6
3611998
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -146,6 +146,76 @@ | |
| animation: toast-out 0.1s ease-in forwards; | ||
| } | ||
|
|
||
| /* DEMO-DAY 애니메이션 (제거 시 이 블록 전체 삭제) */ | ||
| @keyframes demoday-fade-in { | ||
| from { | ||
| opacity: 0; | ||
| } | ||
| to { | ||
| opacity: 1; | ||
| } | ||
| } | ||
|
|
||
| @keyframes demoday-slide-up { | ||
| from { | ||
| opacity: 0; | ||
| transform: translateY(1rem); | ||
| } | ||
| to { | ||
| opacity: 1; | ||
| transform: translateY(0); | ||
| } | ||
| } | ||
|
|
||
| @keyframes demoday-reveal { | ||
| from { | ||
| opacity: 1; | ||
| } | ||
| to { | ||
| opacity: 0; | ||
| } | ||
| } | ||
|
|
||
| @keyframes demoday-tint { | ||
| 0% { | ||
| opacity: 0.75; | ||
| } | ||
| 60% { | ||
| opacity: 0.3; | ||
| } | ||
| 100% { | ||
| opacity: 0; | ||
| } | ||
| } | ||
|
|
||
| .animate-demoday-fade-in { | ||
| animation: demoday-fade-in 0.6s ease-out both; | ||
| } | ||
|
|
||
| .animate-demoday-slide-up { | ||
| animation: demoday-slide-up 0.5s ease-out both; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
|
|
||
| .animate-demoday-reveal { | ||
| animation: demoday-reveal 1.8s ease-out both; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| will-change: opacity; | ||
| } | ||
|
|
||
| .animate-demoday-tint { | ||
| animation: demoday-tint 2.4s ease-out both; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| will-change: opacity; | ||
| } | ||
|
|
||
| .film-strip { | ||
| background: linear-gradient(180deg, #1c1810 0%, #1a1712 50%, #1c1810 100%); | ||
| border-left: 1px solid rgba(80, 70, 50, 0.3); | ||
| border-right: 1px solid rgba(80, 70, 50, 0.3); | ||
| box-shadow: | ||
| inset 0 0 20px rgba(0, 0, 0, 0.5), | ||
| 0 2px 12px rgba(0, 0, 0, 0.4); | ||
| } | ||
| /* DEMO-DAY 애니메이션 끝 */ | ||
|
|
||
| .bg-splash-gradient { | ||
| background: | ||
| radial-gradient( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import { useEffect } from "react"; | ||
| import { useNavigate, useLocation } from "react-router"; | ||
| import { useKakaoOauth } from "@/hooks/auth/login"; | ||
| import { consumeRedirectAfterLogin } from "@/pages/demoDay/redirectAfterLogin"; // DEMO-DAY | ||
|
|
||
| export function KakaoCallbackPage() { | ||
| const navigate = useNavigate(); | ||
|
|
@@ -32,7 +33,11 @@ export function KakaoCallbackPage() { | |
| //2. 신규 회원: 온보딩 화면으로 리다이렉 | ||
| //3. 실패시 login 페이지로 리다이렉 | ||
| const { isPending } = useKakaoOauth({ | ||
| onExistingMember: () => navigate("/mainpage", { replace: true }), | ||
| onExistingMember: () => { | ||
| // DEMO-DAY: 원래는 navigate("/mainpage", { replace: true }) | ||
| const redirect = consumeRedirectAfterLogin(); | ||
| navigate(redirect ?? "/mainpage", { replace: true }); | ||
|
Comment on lines
+36
to
+39
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The comment |
||
| }, | ||
| onNewMember: () => navigate("/auth/onboarding", { replace: true }), | ||
| onFail: () => navigate("/auth/login", { replace: true }), | ||
| }); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,235 @@ | ||
| import { useCallback, useEffect, useState } from "react"; | ||
| import { useNavigate } from "react-router"; | ||
|
|
||
| import Header from "@/components/common/Header"; | ||
| import { RestoraionSparkleIcon } from "@/assets/icon"; | ||
| import { useAuthStore } from "@/store/useAuth.store"; | ||
| import { useLoginModalStore } from "@/store/useLoginModal.store"; | ||
|
|
||
| import { setRedirectAfterLogin } from "./redirectAfterLogin"; | ||
|
|
||
| // ─── 데모 이미지 설정 (파일 추가 후 여기만 수정) ─── | ||
| const DEMO_IMAGES = [ | ||
| { src: "/demo-day/restore1.jpg", label: "사진 1" }, | ||
| { src: "/demo-day/restore2.jpg", label: "사진 2" }, | ||
| { src: "/demo-day/restore3.jpg", label: "사진 3" }, | ||
| { src: "/demo-day/restore4.jpg", label: "사진 4" }, | ||
| ] as const; | ||
|
|
||
| const FRAME_CODES = ["15", "16", "17", "18"]; | ||
|
|
||
| const PENDING_KEY = "finders:demoday-pending-image"; | ||
|
|
||
| export default function DemoDayPage() { | ||
| const navigate = useNavigate(); | ||
| const user = useAuthStore((s) => s.user); | ||
| const openLoginModal = useLoginModalStore((s) => s.openLoginModal); | ||
|
|
||
| // ─── 로그인 후 자동 복원 진행 ─── | ||
| useEffect(() => { | ||
| const pendingRaw = sessionStorage.getItem(PENDING_KEY); | ||
| if (!pendingRaw) return; | ||
| if (!user) return; | ||
|
|
||
| sessionStorage.removeItem(PENDING_KEY); | ||
| const index = Number(pendingRaw); | ||
| const image = DEMO_IMAGES[index]; | ||
| if (!image) return; | ||
|
|
||
| navigate("/restore/editor", { | ||
| state: { imageUrl: image.src }, | ||
| }); | ||
| }, [user, navigate]); | ||
|
|
||
| const handleRestore = useCallback( | ||
| (index: number) => { | ||
| const image = DEMO_IMAGES[index]; | ||
| if (!image) return; | ||
|
|
||
| const isAuthed = Boolean(user && user.memberId > 0); | ||
|
|
||
| if (isAuthed) { | ||
| navigate("/restore/editor", { | ||
| state: { imageUrl: image.src }, | ||
| }); | ||
| } else { | ||
| sessionStorage.setItem(PENDING_KEY, String(index)); | ||
| setRedirectAfterLogin("/demo-day"); | ||
| openLoginModal(); | ||
| } | ||
| }, | ||
| [user, navigate, openLoginModal], | ||
| ); | ||
|
|
||
| return ( | ||
| <div className="relative flex w-full flex-col bg-neutral-900"> | ||
| {/* 히어로 그라데이션 */} | ||
| <div className="pointer-events-none absolute -inset-x-4 top-0 z-0 h-80 bg-[radial-gradient(ellipse_at_15%_55%,rgba(233,78,22,0.18)_0%,transparent_55%)] sm:-inset-x-6 lg:-inset-x-8" /> | ||
|
|
||
| <Header title="파인더스 데모데이" showBack={false} /> | ||
|
|
||
| <div className="scrollbar-hide relative z-10 flex-1 overflow-y-auto"> | ||
| {/* 히어로 영역 */} | ||
| <section className="animate-demoday-fade-in relative px-2 pt-4 pb-5"> | ||
| <div className="relative"> | ||
| <h2 className="text-[1.5rem] leading-tight font-bold text-neutral-100"> | ||
| <span className="animate-demoday-slide-up inline-block"> | ||
| 타버린 사진도 | ||
| </span> | ||
| <br /> | ||
| <span className="animate-demoday-slide-up inline-block [animation-delay:0.1s]"> | ||
| AI로 다시 | ||
| </span>{" "} | ||
| <span className="animate-demoday-slide-up inline-block text-orange-500 [animation-delay:0.15s]"> | ||
| 살려보세요 | ||
| </span> | ||
| </h2> | ||
| <p className="animate-demoday-fade-in mt-2 text-[0.875rem] text-neutral-400 [animation-delay:0.3s]"> | ||
| 사진을 꾹 눌러 저장하고, 복원해 보세요 | ||
| </p> | ||
| </div> | ||
| </section> | ||
|
|
||
| {/* 필름 스트립 */} | ||
| <div className="relative mx-1 mb-10"> | ||
| {/* 필름 베이스 — 양쪽 스프로킷 레일 포함 */} | ||
| <div className="film-strip animate-demoday-fade-in rounded-sm"> | ||
| {DEMO_IMAGES.map((image, index) => ( | ||
| <FilmFrame | ||
| key={image.src} | ||
| image={image} | ||
| index={index} | ||
| frameCode={FRAME_CODES[index]} | ||
| onRestore={() => handleRestore(index)} | ||
| /> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| // ─── 양쪽 스프로킷 홀 ─── | ||
| function SprocketRail({ count }: { count: number }) { | ||
| return ( | ||
| <> | ||
| {Array.from({ length: count }, (_, i) => ( | ||
| <div | ||
| key={i} | ||
| className="h-[0.625rem] w-[0.375rem] rounded-[1px] bg-neutral-900" | ||
| style={{ | ||
| boxShadow: "inset 0 1px 2px rgba(0,0,0,0.6)", | ||
| }} | ||
| /> | ||
| ))} | ||
| </> | ||
| ); | ||
| } | ||
|
|
||
| // ─── 필름 프레임 ─── | ||
|
|
||
| interface FilmFrameProps { | ||
| image: (typeof DEMO_IMAGES)[number]; | ||
| index: number; | ||
| frameCode: string | undefined; | ||
| onRestore: () => void; | ||
| } | ||
|
|
||
| function FilmFrame({ image, index, frameCode, onRestore }: FilmFrameProps) { | ||
| const [imgError, setImgError] = useState(false); | ||
| const developDelay = 0.4 + index * 0.8; | ||
| const ctaDelay = developDelay + 1.6; | ||
|
Comment on lines
+141
to
+142
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The magic numbers |
||
|
|
||
| return ( | ||
| <div> | ||
| {/* 프레임 한 줄: 좌 스프로킷 | 이미지 | 우 스프로킷 */} | ||
| <div className="flex"> | ||
| {/* 좌측 스프로킷 레일 */} | ||
| <div className="flex w-5 shrink-0 flex-col items-center justify-evenly"> | ||
| <SprocketRail count={6} /> | ||
| </div> | ||
|
|
||
| {/* 이미지 + 프레임 정보 */} | ||
| <div className="min-w-0 flex-1 py-1.5"> | ||
| {/* 사진 */} | ||
| <div className="relative overflow-hidden bg-[#15120a]"> | ||
| {imgError ? ( | ||
| <div className="bg-neutral-850 flex aspect-[3/2] items-center justify-center text-sm text-neutral-600"> | ||
| 이미지 준비 중 | ||
| </div> | ||
| ) : ( | ||
| <> | ||
| <img | ||
| src={image.src} | ||
| alt={image.label} | ||
| onError={() => setImgError(true)} | ||
| className="aspect-[3/2] w-full object-cover" | ||
| style={{ | ||
| WebkitTouchCallout: "default", | ||
| WebkitUserSelect: "auto", | ||
| userSelect: "auto", | ||
| }} | ||
| /> | ||
| {/* 현상 오버레이 — 세피아 톤 */} | ||
| <div | ||
| className="animate-demoday-tint pointer-events-none absolute inset-0" | ||
| style={{ | ||
| animationDelay: `${developDelay}s`, | ||
| backgroundColor: "#3a1800", | ||
| }} | ||
| /> | ||
| {/* 현상 오버레이 — 암흑 */} | ||
| <div | ||
| className="animate-demoday-reveal pointer-events-none absolute inset-0 bg-black" | ||
| style={{ animationDelay: `${developDelay}s` }} | ||
| /> | ||
| </> | ||
| )} | ||
| {/* 힌트 */} | ||
| <div | ||
| className="animate-demoday-fade-in pointer-events-none absolute inset-x-0 bottom-0 flex items-end justify-end bg-gradient-to-t from-black/40 to-transparent p-2 opacity-0" | ||
| style={{ animationDelay: `${developDelay + 1.5}s` }} | ||
| > | ||
| <span className="text-[0.625rem] text-white/50"> | ||
| 꾹 눌러서 저장 | ||
| </span> | ||
| </div> | ||
| </div> | ||
|
|
||
| {/* 프레임 정보 (필름 리베이트 영역) */} | ||
| <div className="flex items-center justify-between px-0.5 pt-1.5 pb-0.5"> | ||
| <div className="flex items-center gap-1.5"> | ||
| <span className="font-mono text-[0.625rem] font-bold text-orange-400/60"> | ||
| {frameCode} | ||
| </span> | ||
| <span className="text-[0.5rem] text-orange-400/30">{"▶ "}</span> | ||
| <span className="font-mono text-[0.5rem] tracking-[0.15em] text-orange-400/30"> | ||
| FINDERS 400 | ||
| </span> | ||
| </div> | ||
| <span className="font-mono text-[0.5rem] tracking-wider text-orange-400/30"> | ||
| {frameCode}A | ||
| </span> | ||
| </div> | ||
|
|
||
| {/* 복원하러 가기 CTA */} | ||
| <button | ||
| type="button" | ||
| onClick={onRestore} | ||
| className="animate-demoday-fade-in mt-1 flex w-full items-center justify-center gap-1.5 rounded-lg bg-orange-500 py-2.5 text-[0.8125rem] font-semibold text-white opacity-0 transition-all active:scale-[0.98] active:brightness-90" | ||
| style={{ animationDelay: `${ctaDelay}s` }} | ||
| > | ||
| <RestoraionSparkleIcon className="h-4 w-4" strokeWidth={0} /> | ||
| 복원하러 가기 | ||
| </button> | ||
| </div> | ||
|
|
||
| {/* 우측 스프로킷 레일 */} | ||
| <div className="flex w-5 shrink-0 flex-col items-center justify-evenly"> | ||
| <SprocketRail count={6} /> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,24 @@ | ||||||||
| const REDIRECT_KEY = "finders:redirectAfterLogin"; | ||||||||
|
|
||||||||
| type RedirectData = { path: string; timestamp: number }; | ||||||||
|
|
||||||||
| export function setRedirectAfterLogin(path: string): void { | ||||||||
| sessionStorage.setItem( | ||||||||
| REDIRECT_KEY, | ||||||||
| JSON.stringify({ path, timestamp: Date.now() }), | ||||||||
| ); | ||||||||
| } | ||||||||
|
|
||||||||
| export function consumeRedirectAfterLogin(): string | null { | ||||||||
| const raw = sessionStorage.getItem(REDIRECT_KEY); | ||||||||
| sessionStorage.removeItem(REDIRECT_KEY); | ||||||||
| if (!raw) return null; | ||||||||
| try { | ||||||||
| const data: RedirectData = JSON.parse(raw); | ||||||||
| const TTL = 30 * 60 * 1000; // 30분 | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||||
| if (Date.now() - data.timestamp > TTL) return null; | ||||||||
| return data.path; | ||||||||
| } catch { | ||||||||
| return null; | ||||||||
| } | ||||||||
| } | ||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -228,7 +228,7 @@ export default function FindPhotoLabPage() { | |
| </p> | ||
| </div> | ||
| {/* 하단 버튼 영역 */} | ||
| <div className="fixed right-0 bottom-0 left-0 flex justify-center gap-3 px-5 py-5"> | ||
| <div className="fixed right-0 bottom-0 left-0 flex justify-center gap-3 px-5 py-5 pb-[calc(1.25rem+env(safe-area-inset-bottom,2.125rem))]"> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
| <CTA_Button | ||
| text="아니요 달라요" | ||
| size="medium" | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider defining
0.6sas a CSS variable for better maintainability and consistency if this duration is used elsewhere or might change in the future. This makes it easier to update animation durations across the application.