diff --git a/public/demo-day/restore1.jpg b/public/demo-day/restore1.jpg new file mode 100644 index 00000000..bf885c38 Binary files /dev/null and b/public/demo-day/restore1.jpg differ diff --git a/public/demo-day/restore2.jpg b/public/demo-day/restore2.jpg new file mode 100644 index 00000000..24fcd9ad Binary files /dev/null and b/public/demo-day/restore2.jpg differ diff --git a/public/demo-day/restore3.jpg b/public/demo-day/restore3.jpg new file mode 100644 index 00000000..81dae959 Binary files /dev/null and b/public/demo-day/restore3.jpg differ diff --git a/public/demo-day/restore4.jpg b/public/demo-day/restore4.jpg new file mode 100644 index 00000000..cea1bc86 Binary files /dev/null and b/public/demo-day/restore4.jpg differ diff --git a/src/index.css b/src/index.css index 41193b4b..6fc0e199 100644 --- a/src/index.css +++ b/src/index.css @@ -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; +} + +.animate-demoday-reveal { + animation: demoday-reveal 1.8s ease-out both; + will-change: opacity; +} + +.animate-demoday-tint { + animation: demoday-tint 2.4s ease-out both; + 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( diff --git a/src/pages/auth/KakaoCallbackPage.tsx b/src/pages/auth/KakaoCallbackPage.tsx index baabca14..c27a24bb 100644 --- a/src/pages/auth/KakaoCallbackPage.tsx +++ b/src/pages/auth/KakaoCallbackPage.tsx @@ -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 }); + }, onNewMember: () => navigate("/auth/onboarding", { replace: true }), onFail: () => navigate("/auth/login", { replace: true }), }); diff --git a/src/pages/demoDay/DemoDayPage.tsx b/src/pages/demoDay/DemoDayPage.tsx new file mode 100644 index 00000000..af0c9901 --- /dev/null +++ b/src/pages/demoDay/DemoDayPage.tsx @@ -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 ( +
+ {/* 히어로 그라데이션 */} +
+ +
+ +
+ {/* 히어로 영역 */} +
+
+

+ + 타버린 사진도 + +
+ + AI로 다시 + {" "} + + 살려보세요 + +

+

+ 사진을 꾹 눌러 저장하고, 복원해 보세요 +

+
+
+ + {/* 필름 스트립 */} +
+ {/* 필름 베이스 — 양쪽 스프로킷 레일 포함 */} +
+ {DEMO_IMAGES.map((image, index) => ( + handleRestore(index)} + /> + ))} +
+
+
+
+ ); +} + +// ─── 양쪽 스프로킷 홀 ─── +function SprocketRail({ count }: { count: number }) { + return ( + <> + {Array.from({ length: count }, (_, i) => ( +
+ ))} + + ); +} + +// ─── 필름 프레임 ─── + +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; + + return ( +
+ {/* 프레임 한 줄: 좌 스프로킷 | 이미지 | 우 스프로킷 */} +
+ {/* 좌측 스프로킷 레일 */} +
+ +
+ + {/* 이미지 + 프레임 정보 */} +
+ {/* 사진 */} +
+ {imgError ? ( +
+ 이미지 준비 중 +
+ ) : ( + <> + {image.label} setImgError(true)} + className="aspect-[3/2] w-full object-cover" + style={{ + WebkitTouchCallout: "default", + WebkitUserSelect: "auto", + userSelect: "auto", + }} + /> + {/* 현상 오버레이 — 세피아 톤 */} +
+ {/* 현상 오버레이 — 암흑 */} +
+ + )} + {/* 힌트 */} +
+ + 꾹 눌러서 저장 + +
+
+ + {/* 프레임 정보 (필름 리베이트 영역) */} +
+
+ + {frameCode} + + {"▶ "} + + FINDERS 400 + +
+ + {frameCode}A + +
+ + {/* 복원하러 가기 CTA */} + +
+ + {/* 우측 스프로킷 레일 */} +
+ +
+
+
+ ); +} diff --git a/src/pages/demoDay/redirectAfterLogin.ts b/src/pages/demoDay/redirectAfterLogin.ts new file mode 100644 index 00000000..0cfddc99 --- /dev/null +++ b/src/pages/demoDay/redirectAfterLogin.ts @@ -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분 + if (Date.now() - data.timestamp > TTL) return null; + return data.path; + } catch { + return null; + } +} diff --git a/src/pages/photoFeed/FindPhotoLabPage.tsx b/src/pages/photoFeed/FindPhotoLabPage.tsx index 33413d68..5df3364e 100644 --- a/src/pages/photoFeed/FindPhotoLabPage.tsx +++ b/src/pages/photoFeed/FindPhotoLabPage.tsx @@ -228,7 +228,7 @@ export default function FindPhotoLabPage() {

{/* 하단 버튼 영역 */} -
+
( initialLabName ? "LAB_NAME" : "TITLE", ); + const [tempFilter, setTempFilter] = useState(filter); + const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [bottomSheetOpen, setBottomSheetOpen] = useState(false); @@ -67,6 +69,12 @@ export default function PhotoFeedSearchPage() { const inputTrimmed = inputText.trim(); const searchTrimmed = searchText.trim(); + useEffect(() => { + if (bottomSheetOpen) { + setTempFilter(filter); + } + }, [bottomSheetOpen, filter]); + // 최근 검색어 조회 API const { data: recentSearches = [], @@ -386,7 +394,7 @@ export default function PhotoFeedSearchPage() { >
- +
{ + setFilter(tempFilter); setBottomSheetOpen(false); }} /> diff --git a/src/pages/photoFeed/PostPage.tsx b/src/pages/photoFeed/PostPage.tsx index 01be4951..0343b25e 100644 --- a/src/pages/photoFeed/PostPage.tsx +++ b/src/pages/photoFeed/PostPage.tsx @@ -115,19 +115,22 @@ export default function PostPage() {
{/** 상단 */} -
- - +
+
+ + +
+