You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
159 lines
5.2 KiB
TypeScript
159 lines
5.2 KiB
TypeScript
// frontend/components/journey/JourneyClient.tsx — 여정 페이지 오케스트레이터 (Topbar 는 셸)
|
|
"use client";
|
|
import { useEffect, useRef } from "react";
|
|
import { useJourney } from "@/lib/hooks/useJourney";
|
|
import { Icon } from "@/components/Icon";
|
|
import { JourneyBoard } from "./JourneyBoard";
|
|
import { PeopleStack } from "./PeopleStack";
|
|
import { JourneyTable } from "./JourneyTable";
|
|
import { Gauge } from "./Gauge";
|
|
|
|
export function JourneyClient() {
|
|
const { data, isLoading, error, retry } = useJourney();
|
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
|
|
// 진입 애니메이션 (원본 entered)
|
|
useEffect(() => {
|
|
const el = rootRef.current;
|
|
const id = requestAnimationFrame(() => el && el.classList.add("entered"));
|
|
return () => cancelAnimationFrame(id);
|
|
}, [data]);
|
|
|
|
if (error && !data) {
|
|
return (
|
|
<div className="journeypage">
|
|
<div className="jx-error" role="alert">
|
|
<Icon name="x" /> 여정을 불러오지 못했어요
|
|
<button onClick={() => retry()} type="button">
|
|
다시 시도
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (isLoading || !data) {
|
|
return (
|
|
<div className="journeypage">
|
|
<div className="jx">
|
|
<div className="jx-panel jx-skeleton" aria-busy="true" />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const { header, meta, people, stages, cards, links, rows, table, gauges, gaugeFoot } = data;
|
|
|
|
return (
|
|
<div className="journeypage">
|
|
<div className="jx" ref={rootRef}>
|
|
{/* ===== 페이지 헤더 ===== */}
|
|
<div className="pagehead">
|
|
<div>
|
|
<div className="ph-eyebrow">
|
|
<span>{header.dateLabel}</span>
|
|
<span className="sep" />
|
|
<span className="wx">
|
|
<Icon name="sun" />
|
|
{header.weather}
|
|
</span>
|
|
</div>
|
|
<h1 className="ph-title">오늘의 여정</h1>
|
|
</div>
|
|
<div className="ph-search">
|
|
<Icon name="search" />
|
|
<input placeholder="흐름에서 작업·사람 찾기…" aria-label="흐름에서 작업·사람 찾기" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* ===== 흐름 패널 ===== */}
|
|
<section className="jx-panel">
|
|
<div className="jx-top">
|
|
<div className="jx-label">
|
|
오늘의 흐름{" "}
|
|
<span>
|
|
· {meta.stageCount}단계 · {meta.itemCount}개 항목 · {meta.collabCount}명 협업
|
|
</span>
|
|
</div>
|
|
<PeopleStack people={people} more={meta.peopleMore} />
|
|
<div className="jx-actions">
|
|
<button className="jx-act" type="button" aria-label="추가">
|
|
<Icon name="plus" />
|
|
</button>
|
|
<button className="jx-act" type="button" aria-label="공유">
|
|
<Icon name="share" />
|
|
</button>
|
|
<button className="jx-act fill" type="button" aria-label="일정">
|
|
<Icon name="cal" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<JourneyBoard stages={stages} cards={cards} links={links} people={people} />
|
|
|
|
<div className="jx-labels">
|
|
{stages.map((s) => (
|
|
<div
|
|
className="cl"
|
|
key={s.id}
|
|
style={{ color: s.tone === "ink" ? "var(--ink)" : `var(--${s.tone})` }}
|
|
>
|
|
{s.title}
|
|
<span>{s.time}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="jx-hint">
|
|
<Icon name="link" /> 카드에 마우스를 올리면 연결된 작업 · 사람 · 미팅 흐름이 강조돼요
|
|
</div>
|
|
</section>
|
|
|
|
{/* ===== 하단 2열 ===== */}
|
|
<section className="jx-bottom">
|
|
<div className="card">
|
|
<div className="jx-phead">
|
|
<div>
|
|
<div className="pt">오늘의 작업</div>
|
|
<div className="ps">{table.subtitle}</div>
|
|
</div>
|
|
<div className="pa">
|
|
<button className="jx-act" type="button" aria-label="추가">
|
|
<Icon name="plus" />
|
|
</button>
|
|
<button className="jx-act" type="button" aria-label="공유">
|
|
<Icon name="share" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<JourneyTable rows={rows} people={people} />
|
|
</div>
|
|
|
|
<div className="card">
|
|
<div className="jx-phead">
|
|
<div>
|
|
<div className="pt">오늘의 지표</div>
|
|
<div className="ps">집중 · 활동</div>
|
|
</div>
|
|
<div className="pa">
|
|
<button className="jx-act" type="button" aria-label="더보기">
|
|
<Icon name="more" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div className="gauges">
|
|
{gauges.map((g, i) => (
|
|
<Gauge key={i} {...g} />
|
|
))}
|
|
</div>
|
|
<div className="gauge-foot">
|
|
<Icon name="drop" />
|
|
{gaugeFoot}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|