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/personas/family.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 17 additions & 1 deletion src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,27 @@ body {
}
@media (prefers-reduced-motion: reduce) {
.animate-step-next,
.animate-step-prev {
.animate-step-prev,
.animate-persona-hero-in {
animation: none;
}
}

@keyframes persona-hero-in {
from {
opacity: 0;
transform: scale(0.96);
}
to {
opacity: 1;
transform: scale(1);
}
}

.animate-persona-hero-in {
animation: persona-hero-in 300ms ease-in-out both;
}

/* hide the native scrollbar (used by the custom ScrollArea component) */
.scrollbar-none {
scrollbar-width: none;
Expand Down
2 changes: 1 addition & 1 deletion src/components/WorkspaceLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export function WorkspaceLayout({
// lives in localStorage, so redirecting before it loads would bounce a
// logged-in user straight back to /login on every refresh.
useEffect(() => {
if (hydrated && !authed) router.replace("/login");
if (hydrated && !authed) router.replace("/prologue");
}, [hydrated, authed, router]);

if (hydrated && !authed) return null;
Expand Down
39 changes: 16 additions & 23 deletions src/components/prologue/PersonaCenterPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,40 +13,33 @@ const accentVar: Record<NonNullable<ProloguePersona["accent"]>, string> = {
green: "var(--color-event-green)",
};

/**
* 페르소나를 고르면 화면 위쪽에 뜨는 소개.
*
* 카드(테두리+배경)를 쓰지 않는다 — 목업(`.mockup/시나리오선택.png`)처럼
* **어두운 장면 위에 글자만 떠 있고** 이름의 발광과 좌우 점 패턴이 깊이를 만든다.
* 상자를 두면 뒤의 도로·캐릭터가 잘려 장면이 두 겹으로 갈라진다.
*/
/** 선택된 페르소나 — 상단 Hero (목업: 글자만 떠 있고 이름 뒤 발광) */
export function PersonaCenterPanel({ persona }: { persona: ProloguePersona }) {
const accent = persona.accent ?? "orange";
const accent = persona.accent ?? "amber";

return (
<div
className={styles.personaCenterPanel}
style={{ "--persona-accent": accentVar[accent] } as CSSProperties}
aria-live="polite"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
{/* 이름 좌우로 흩뿌린 점. 장식이라 읽히지 않아야 한다 */}
<span className={styles.personaCenterDots} data-side="left" aria-hidden />
<span className={styles.personaCenterDots} data-side="right" aria-hidden />
<div key={persona.id} className={styles.personaCenterInner}>
<span className={styles.personaCenterDots} data-side="left" aria-hidden />
<span className={styles.personaCenterDots} data-side="right" aria-hidden />

<HomeIcon className={styles.personaCenterIcon} aria-hidden />
<h2 className={styles.personaCenterName}>{persona.name}</h2>
<p className={styles.personaCenterTitle}>{persona.title}</p>
<p className={styles.personaCenterDesc}>{persona.description}</p>
<HomeIcon className={styles.personaCenterIcon} aria-hidden />
<h2 className={styles.personaCenterName}>{persona.name}</h2>
<p className={styles.personaCenterTitle}>{persona.title}</p>
<p className={styles.personaCenterDesc}>{persona.description}</p>

<span className={styles.personaCenterRule} aria-hidden />
<span className={styles.personaCenterRule} aria-hidden />

<ul className={styles.personaCenterList}>
{persona.details.map((line) => (
<li key={line}>{line}</li>
))}
</ul>
<ul className={styles.personaCenterList}>
{persona.details.map((line) => (
<li key={line}>{line}</li>
))}
</ul>
</div>
</div>
);
}
4 changes: 2 additions & 2 deletions src/components/prologue/PersonaRoadCharacter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function PersonaRoadCharacter({
interactive: boolean;
onSelect: () => void;
}) {
const accent = persona.accent ?? "orange";
const accent = persona.accent ?? "amber";

return (
<div
Expand Down Expand Up @@ -60,7 +60,7 @@ export function PersonaRoadCharacter({
<PersonaPixelSprite
imageSrc={persona.imageSrc}
name={persona.name}
selected={selected}
selected={false}
/>
<span className={styles.roadFeetShadow} aria-hidden />
</button>
Expand Down
141 changes: 69 additions & 72 deletions src/components/prologue/PersonaSelectStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ export function PersonaSelectStep({
selectedId: string | null;
onSelect: (id: string | null) => void;
onConfirm: () => void;
/** 데모 진입은 로그인을 한 번 다녀온다 — 그동안 버튼을 잠근다. */
busy?: boolean;
error?: string;
}) {
Expand Down Expand Up @@ -54,45 +53,38 @@ export function PersonaSelectStep({
data-phase={phase}
data-selected={selectedId ? "1" : "0"}
>
<div className={styles.skyGlow} aria-hidden />
<div className={styles.hillsPersona} aria-hidden />

<div className={styles.road}>
<div className={styles.roadLinesPersona} aria-hidden />
</div>

{phase === "truck" && (
<div className={styles.personaTruckTrack} aria-hidden>
<div className={styles.personaTruckRunner}>
<TruckLogo
height={PERSONA_TRUCK_HEIGHT}
className="drop-shadow-[0_10px_32px_rgba(255,90,31,0.45)]"
/>
<>
<div className={styles.skyGlow} aria-hidden />
<div className={styles.hillsPersona} aria-hidden />
<div className={styles.road}>
<div className={styles.roadLinesPersona} aria-hidden />
</div>
</div>
<div className={styles.personaTruckTrack} aria-hidden>
<div className={styles.personaTruckRunner}>
<TruckLogo
height={PERSONA_TRUCK_HEIGHT}
className="drop-shadow-[0_10px_32px_rgba(255,90,31,0.45)]"
/>
</div>
</div>
<div className={styles.personaOverlay}>
<div className={styles.personaOverlayTop}>
<header className={styles.personaHeader}>
<h2 className="font-display text-2xl leading-snug text-text md:text-[1.65rem]">
잠시만요,
<br />
<span className="text-primary">쓩—</span>
</h2>
<p className="mt-2 text-[13px] leading-relaxed text-text-muted">
트럭이 도로를 지나갑니다…
</p>
</header>
</div>
</div>
</>
)}

<div
className={[
styles.personaField,
phase === "select" ? styles.personaFieldVisible : "",
].join(" ")}
>
{showCharacters &&
personas.map((p) => (
<PersonaRoadCharacter
key={p.id}
persona={p}
selected={selectedId === p.id}
dimmed={selectedId !== null && selectedId !== p.id}
interactive={phase === "select"}
onSelect={() =>
onSelect(selectedId === p.id ? null : p.id)
}
/>
))}
</div>

{phase === "select" && selectedId && (
<button
type="button"
Expand All @@ -106,50 +98,55 @@ export function PersonaSelectStep({
<PersonaCenterPanel persona={selectedPersona} />
)}

<div className={styles.personaOverlay}>
{phase === "truck" && (
<div className={styles.personaOverlayTop}>
<header className={styles.personaHeader}>
<h2 className="text-2xl font-bold leading-snug text-text md:text-[1.65rem]">
잠시만요,
<br />
<span className="text-primary">쓩—</span>
</h2>
<p className="mt-2 text-[13px] leading-relaxed text-text-muted">
트럭이 도로를 지나갑니다…
</p>
</header>
{showCharacters && (
<>
<div className={styles.road}>
<div className={styles.roadLinesPersona} aria-hidden />
</div>
)}

{phase === "select" && (
<div
className={[
styles.personaPromptCenter,
selectedId ? styles.personaPromptHide : "",
]
.filter(Boolean)
.join(" ")}
aria-hidden={!!selectedId}
>
<h2 className={styles.personaPromptText}>
어떤 모습으로 이사할까요?
</h2>
<div className={[styles.personaField, styles.personaFieldVisible].join(" ")}>
{personas.map((p) => (
<PersonaRoadCharacter
key={p.id}
persona={p}
selected={selectedId === p.id}
dimmed={selectedId !== null && selectedId !== p.id}
interactive
onSelect={() => onSelect(p.id)}
/>
))}
</div>
)}
</>
)}

{phase === "select" && (
<div
className={[
styles.personaPromptCenter,
selectedId ? styles.personaPromptHide : "",
]
.filter(Boolean)
.join(" ")}
aria-hidden={!!selectedId}
>
<h2 className={styles.personaPromptText}>
어떤 모습으로 이사할까요?
</h2>
</div>
)}

<div className={styles.personaFooter}>
{/* footer는 flex row(오른쪽 정렬)라 버튼 왼쪽에 붙는다. */}
{phase === "select" && (
<div className="pointer-events-none absolute inset-x-0 top-0 z-30 flex items-start justify-end gap-3 p-5 md:p-8">
{error && (
<p className="mr-3 self-center text-[12px] text-event-pink">{error}</p>
<p className="pointer-events-auto self-center text-[12px] text-event-pink">
{error}
</p>
)}
<button
type="button"
disabled={!selectedId || phase !== "select" || busy}
disabled={!selectedId || busy}
onClick={onConfirm}
className="rounded-full bg-brand-gradient px-8 py-2.5 text-sm font-medium text-white transition-opacity disabled:cursor-not-allowed disabled:opacity-40 hover:opacity-90"
className="pointer-events-auto rounded-full bg-brand-gradient px-7 py-2.5 text-sm font-medium text-white transition-opacity duration-300 disabled:cursor-not-allowed disabled:opacity-40 hover:opacity-90"
>
{/* 브랜드 이름만 브랜드 서체 — 로그인의 "Hey, hello"와 같은 방식 */}
{busy ? (
"준비하는 중…"
) : (
Expand All @@ -159,7 +156,7 @@ export function PersonaSelectStep({
)}
</button>
</div>
</div>
)}
</div>
);
}
5 changes: 4 additions & 1 deletion src/components/prologue/PrologueFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ export function PrologueFlow() {
onAnswer={(qid, oid) =>
setAnswers((prev) => ({ ...prev, [qid]: oid }))
}
onComplete={() => setPhase("persona")}
onComplete={() => {
setPersonaId(null);
setPhase("persona");
}}
/>
)}

Expand Down
49 changes: 23 additions & 26 deletions src/components/prologue/prologue.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -298,16 +298,15 @@
.personaField {
position: absolute;
left: 50%;
right: auto;
bottom: calc(18% + var(--road-char-base, 48px));
z-index: 6;
bottom: calc(18% + var(--road-char-base, 58px));
z-index: 22;
display: flex;
width: min(100%, 760px);
transform: translateX(-50%);
justify-content: center;
align-items: flex-end;
gap: clamp(28px, 9vw, 72px);
padding: 0 clamp(16px, 4vw, 32px);
transform: translateX(-50%);
pointer-events: none;
opacity: 0;
transition: opacity 0.28s ease;
Expand Down Expand Up @@ -368,18 +367,7 @@
}

.personaScene[data-selected="1"]::before {
content: "";
position: absolute;
inset: 0;
z-index: 14;
pointer-events: none;
background: linear-gradient(
180deg,
rgba(0, 0, 0, 0.55) 0%,
rgba(0, 0, 0, 0.28) 42%,
transparent 58%,
rgba(0, 0, 0, 0.38) 100%
);
content: none;
}

.personaDeselectBackdrop {
Expand Down Expand Up @@ -512,8 +500,22 @@
width: min(92vw, 560px);
transform: translateX(-50%);
text-align: center;
pointer-events: auto;
animation: personaCenterIn 0.28s cubic-bezier(0.33, 0, 0.2, 1) forwards;
pointer-events: none;
}

.personaCenterInner {
animation: personaHeroContentIn 300ms ease-in-out both;
}

@keyframes personaHeroContentIn {
from {
opacity: 0;
transform: scale(0.96);
}
to {
opacity: 1;
transform: scale(1);
}
}

/* 이름 뒤에서 번지는 빛. 상자를 없앤 자리를 이게 대신 잡아준다 —
Expand Down Expand Up @@ -627,14 +629,9 @@
margin-top: clamp(0.35rem, 0.9vw, 0.6rem);
}

@keyframes personaCenterIn {
from {
opacity: 0;
transform: translate(-50%, -10px);
}
to {
opacity: 1;
transform: translate(-50%, 0);
@media (prefers-reduced-motion: reduce) {
.personaCenterInner {
animation: none;
}
}

Expand Down
Loading
Loading