feat: 대시보드 생성
This commit is contained in:
@@ -1,16 +1,15 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect } from "react";
|
import { UserProfileSimple, useUserProfile } from "@/features/user";
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { useAuthStore } from "@/store/useAuthStore";
|
import { useAuthStore } from "@/store/useAuthStore";
|
||||||
import { UserProfileCard, useUserProfile } from "@/features/user";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { logout, isAuthenticated } = useAuthStore();
|
const { logout, isAuthenticated } = useAuthStore();
|
||||||
|
const { user, isLoading } = useUserProfile();
|
||||||
// Feature 모듈에서 로직과 데이터를 가져옴
|
const [focusGoal, setFocusGoal] = useState("");
|
||||||
const { user, isLoading, error } = useUserProfile();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
@@ -24,30 +23,84 @@ export default function DashboardPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl mx-auto p-6">
|
<div className="min-h-screen bg-slate-100 text-brand-dark">
|
||||||
<header className="flex justify-between items-center mb-12">
|
<div className="flex min-h-screen w-full flex-col px-5 pb-8 pt-5 sm:px-8 md:px-12">
|
||||||
<h1 className="text-2xl font-bold text-brand-dark">VibeRoom Dashboard</h1>
|
<header className="flex items-center justify-between">
|
||||||
<button
|
<div className="flex items-center gap-3">
|
||||||
onClick={handleLogout}
|
<span className="inline-flex h-7 w-7 items-center justify-center rounded-full border border-slate-300 bg-white text-xs font-bold text-brand-dark">
|
||||||
className="text-sm text-slate-500 hover:text-red-500 transition-colors"
|
V
|
||||||
>
|
</span>
|
||||||
로그아웃
|
<p className="text-sm font-semibold tracking-tight">VibeRoom</p>
|
||||||
</button>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main className="grid gap-6">
|
|
||||||
{/* Feature 컴포넌트에 데이터와 상태만 전달 */}
|
|
||||||
<UserProfileCard user={user} isLoading={isLoading} error={error} />
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div className="bg-slate-50 p-6 rounded-xl border border-dashed border-slate-200 flex flex-col items-center justify-center h-40 text-slate-400">
|
|
||||||
<p>나의 스페이스 관리 (준비 중)</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-slate-50 p-6 rounded-xl border border-dashed border-slate-200 flex flex-col items-center justify-center h-40 text-slate-400">
|
|
||||||
<p>타이머 통계 (준비 중)</p>
|
<div className="flex items-center gap-3">
|
||||||
|
<UserProfileSimple user={user} isLoading={isLoading} />
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="px-1 py-1 text-xs font-semibold text-brand-dark/70 transition hover:text-brand-primary"
|
||||||
|
>
|
||||||
|
로그아웃
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</header>
|
||||||
</main>
|
|
||||||
|
<main className="relative flex flex-1 flex-col items-center justify-center py-10">
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className="pointer-events-none absolute left-1/2 top-[-2rem] hidden -translate-x-1/2 select-none text-[14rem] font-semibold leading-none tracking-[-0.08em] text-brand-dark/[0.05] md:block"
|
||||||
|
>
|
||||||
|
V
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section className="relative z-10 w-full text-center">
|
||||||
|
<h1 className="text-4xl font-semibold tracking-tight text-brand-dark sm:text-5xl">
|
||||||
|
지금, 몰입을 시작해보세요
|
||||||
|
</h1>
|
||||||
|
<p className="mt-4 w-full text-sm leading-relaxed text-brand-dark/70 sm:text-base">
|
||||||
|
VibeRoom은 집중 루틴을 빠르게 시작하는 앱 홈입니다. 가상공간을
|
||||||
|
선택하고 바로 세션에 들어가세요.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mx-auto mt-9 w-[80%] overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-[0_6px_24px_rgba(48,77,109,0.08)]">
|
||||||
|
<div className="border-b border-slate-200 px-4 py-3 text-left text-sm font-medium text-brand-dark/70 sm:px-5">
|
||||||
|
여기서 집중해보세요! 세션 시작과 동시에 공간 톤과 사운드가
|
||||||
|
맞춰집니다.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mx-auto w-full max-w-2xl px-4 py-5 sm:px-5">
|
||||||
|
<div className="flex w-full flex-col gap-3 sm:flex-row sm:items-center">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={focusGoal}
|
||||||
|
onChange={(event) => setFocusGoal(event.target.value)}
|
||||||
|
placeholder="오늘 무엇에 집중할까요?"
|
||||||
|
className="w-full rounded-xl border border-slate-200 bg-slate-50 px-4 py-4 text-sm text-brand-dark placeholder:text-brand-dark/45 focus:border-brand-primary/45 focus:bg-white focus:outline-none sm:flex-1"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
aria-label="가상공간 입장"
|
||||||
|
className="inline-flex h-11 w-11 items-center justify-center rounded-full bg-brand-primary text-white transition hover:bg-brand-primary/90 sm:shrink-0"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
className="h-5 w-5"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M5 10h10M11 6l4 4-4 4"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.8"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
30
src/features/user/components/UserProfileSimple.tsx
Normal file
30
src/features/user/components/UserProfileSimple.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { UserProfile } from "../types";
|
||||||
|
|
||||||
|
interface UserProfileSimpleProps {
|
||||||
|
user: UserProfile | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserProfileSimple = ({ user, isLoading }: UserProfileSimpleProps) => {
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-3 animate-pulse">
|
||||||
|
<div className="h-10 w-10 rounded-full bg-slate-200" />
|
||||||
|
<div className="h-4 w-24 rounded bg-slate-200" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="group flex items-center gap-3">
|
||||||
|
<div className="flex h-10 w-10 items-center justify-center rounded-full border border-brand-primary/10 bg-brand-primary/20 font-bold text-brand-dark transition-colors group-hover:bg-brand-primary/30">
|
||||||
|
{user.name[0]}
|
||||||
|
</div>
|
||||||
|
<span className="truncate text-sm font-bold leading-tight text-brand-dark">
|
||||||
|
{user.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from "./types";
|
export * from "./types";
|
||||||
export * from "./hooks/useUserProfile";
|
export * from "./hooks/useUserProfile";
|
||||||
export * from "./components/UserProfileCard";
|
export * from "./components/UserProfileCard";
|
||||||
|
export * from "./components/UserProfileSimple";
|
||||||
export * from "./api/userApi";
|
export * from "./api/userApi";
|
||||||
|
|||||||
Reference in New Issue
Block a user