Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added public/demo-day/restore1.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/demo-day/restore2.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/demo-day/restore3.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/demo-day/restore4.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
70 changes: 70 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Consider defining 0.6s as 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.

Suggested change
animation: demoday-fade-in 0.6s ease-out both;
animation: demoday-fade-in var(--demoday-fade-in-duration, 0.6s) ease-out both;

}

.animate-demoday-slide-up {
animation: demoday-slide-up 0.5s ease-out both;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to the fade-in animation, 0.5s could be a CSS variable to centralize animation duration values.

Suggested change
animation: demoday-slide-up 0.5s ease-out both;
animation: demoday-slide-up var(--demoday-slide-up-duration, 0.5s) ease-out both;

}

.animate-demoday-reveal {
animation: demoday-reveal 1.8s ease-out both;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The 1.8s duration for demoday-reveal could also benefit from being a CSS variable.

Suggested change
animation: demoday-reveal 1.8s ease-out both;
animation: demoday-reveal var(--demoday-reveal-duration, 1.8s) ease-out both;

will-change: opacity;
}

.animate-demoday-tint {
animation: demoday-tint 2.4s ease-out both;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The 2.4s duration for demoday-tint should also be considered for a CSS variable.

Suggested change
animation: demoday-tint 2.4s ease-out both;
animation: demoday-tint var(--demoday-tint-duration, 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(
Expand Down
7 changes: 6 additions & 1 deletion src/pages/auth/KakaoCallbackPage.tsx
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();
Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The comment // DEMO-DAY: 원래는 navigate("/mainpage", { replace: true }) is helpful for understanding the change, but it's generally better to remove such comments after the feature is stable to keep the codebase clean. If the original behavior needs to be referenced, consider documenting it in a more permanent place like a design document or a more general code comment explaining the conditional navigation.

},
onNewMember: () => navigate("/auth/onboarding", { replace: true }),
onFail: () => navigate("/auth/login", { replace: true }),
});
Expand Down
235 changes: 235 additions & 0 deletions src/pages/demoDay/DemoDayPage.tsx
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The magic numbers 0.4, 0.8, and 1.6 used for developDelay and ctaDelay make the animation timing hard to understand and modify. Consider defining these as constants with descriptive names (e.g., BASE_DEVELOP_DELAY, IMAGE_DELAY_INCREMENT, CTA_DELAY_OFFSET) to improve readability and maintainability.

  const BASE_DEVELOP_DELAY = 0.4;
  const IMAGE_DELAY_INCREMENT = 0.8;
  const CTA_DELAY_OFFSET = 1.6;
  const developDelay = BASE_DEVELOP_DELAY + index * IMAGE_DELAY_INCREMENT;
  const ctaDelay = developDelay + CTA_DELAY_OFFSET;


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>
);
}
24 changes: 24 additions & 0 deletions src/pages/demoDay/redirectAfterLogin.ts
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분
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The TTL (Time To Live) of 30 * 60 * 1000 (30 minutes) is a magic number. It would be clearer and more maintainable to define this as a named constant, perhaps at the top of the file, to explain its purpose and allow for easy modification.

Suggested change
const TTL = 30 * 60 * 1000; // 30분
const REDIRECT_TTL_MS = 30 * 60 * 1000; // 30 minutes
if (Date.now() - data.timestamp > REDIRECT_TTL_MS) return null;

if (Date.now() - data.timestamp > TTL) return null;
return data.path;
} catch {
return null;
}
}
2 changes: 1 addition & 1 deletion src/pages/photoFeed/FindPhotoLabPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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))]">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The pb-[calc(1.25rem+env(safe-area-inset-bottom,2.125rem))] class is a long and complex TailwindCSS class. While functional, consider extracting this into a custom CSS utility class or a Tailwind plugin if it's reused across multiple components. This improves readability and reduces repetition in the JSX.

<CTA_Button
text="아니요 달라요"
size="medium"
Expand Down
Loading