Merge branch 'fe/feat/137-home-page' into 'fe/develop'
Feat: 홈페이지 구현- S11P21S002-137 See merge request s11-s-project/S11P21S002!53
This commit is contained in:
commit
2a3d5e2672
@ -1,14 +0,0 @@
|
||||
import type { Preview } from '@storybook/react';
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
30
frontend/.storybook/preview.tsx
Normal file
30
frontend/.storybook/preview.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import type { Preview } from '@storybook/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import '../src/index.css';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const decorators = [
|
||||
(Story) => (
|
||||
<MemoryRouter initialEntries={['/']}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Story />
|
||||
</QueryClientProvider>
|
||||
</MemoryRouter>
|
||||
),
|
||||
];
|
||||
|
||||
export default preview;
|
BIN
frontend/src/assets/icons/web_neutral_rd_ctn@1x.png
Normal file
BIN
frontend/src/assets/icons/web_neutral_rd_ctn@1x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
@ -1,7 +1,6 @@
|
||||
import '@/index.css';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import AdminLayout from './index';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { Workspace } from '@/types';
|
||||
|
||||
const meta: Meta<typeof AdminLayout> = {
|
||||
@ -36,9 +35,5 @@ const workspace: Workspace = {
|
||||
};
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<Router>
|
||||
<AdminLayout workspace={workspace} />
|
||||
</Router>
|
||||
),
|
||||
render: () => <AdminLayout workspace={workspace} />,
|
||||
};
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import Header from '../Header';
|
||||
import { ResizablePanelGroup, ResizablePanel } from '../ui/resizable';
|
||||
import AdminProjectSidebar from '../AdminProjectSidebar';
|
||||
@ -6,18 +7,59 @@ import AdminMenuSidebar from '../AdminMenuSidebar';
|
||||
import { Workspace } from '@/types';
|
||||
|
||||
interface AdminLayoutProps {
|
||||
workspace: Workspace;
|
||||
workspace?: Workspace;
|
||||
}
|
||||
|
||||
export default function AdminLayout({ workspace }: AdminLayoutProps) {
|
||||
const { workspaceId } = useParams<{ workspaceId: string }>();
|
||||
|
||||
const numericWorkspaceId = workspaceId ? parseInt(workspaceId, 10) : 0;
|
||||
|
||||
const effectiveWorkspace: Workspace = workspace || {
|
||||
id: numericWorkspaceId,
|
||||
name: workspaceId ? `workspace-${workspaceId}` : 'default-workspace',
|
||||
projects: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'project1',
|
||||
type: 'Detection',
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'project2',
|
||||
type: 'Detection',
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'project3',
|
||||
type: 'Detection',
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'project4',
|
||||
type: 'Detection',
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'project5',
|
||||
type: 'Detection',
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header className="fixed left-0 top-0" />
|
||||
<div className="mt-16 h-[calc(100vh-64px)] w-screen">
|
||||
<ResizablePanelGroup direction="horizontal">
|
||||
<AdminProjectSidebar
|
||||
workspaceName={workspace.name}
|
||||
projects={workspace.projects}
|
||||
workspaceName={effectiveWorkspace.name}
|
||||
projects={effectiveWorkspace.projects}
|
||||
/>
|
||||
<ResizablePanel className="flex w-full items-center">
|
||||
<main className="h-full grow">
|
||||
|
@ -16,18 +16,24 @@ interface Project {
|
||||
|
||||
export default function AdminMemberManage({
|
||||
title = '멤버 관리',
|
||||
projects,
|
||||
onProjectChange,
|
||||
onSubmit,
|
||||
members,
|
||||
onMemberInvite,
|
||||
projects = [
|
||||
{ id: 'project-1', name: '프로젝트 A' },
|
||||
{ id: 'project-2', name: '프로젝트 B' },
|
||||
],
|
||||
onProjectChange = (projectId: string) => console.log('Selected Project:', projectId),
|
||||
onSubmit = (data: MemberManageFormValues) => console.log('Submitted:', data),
|
||||
members = [
|
||||
{ email: 'admin1@example.com', role: 'admin' },
|
||||
{ email: 'viewer2@example.com', role: 'viewer' },
|
||||
],
|
||||
onMemberInvite = () => console.log('Invite member'),
|
||||
}: {
|
||||
title?: string;
|
||||
projects: Project[];
|
||||
onProjectChange: (projectId: string) => void;
|
||||
onSubmit: (data: MemberManageFormValues) => void;
|
||||
members: Member[];
|
||||
onMemberInvite: () => void;
|
||||
projects?: Project[];
|
||||
onProjectChange?: (projectId: string) => void;
|
||||
onSubmit?: (data: MemberManageFormValues) => void;
|
||||
members?: Member[];
|
||||
onMemberInvite?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-6 border-b-[0.67px] border-[#dcdcde] bg-[#fbfafd] p-6">
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export default function AdminMenuSidebar() {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
||||
const menuItems = [
|
||||
{ label: '작업', path: '/admin/tasks' },
|
||||
{ label: '리뷰', path: '/admin/reviews' },
|
||||
{ label: '멤버 관리', path: '/admin/members' },
|
||||
{ label: '리뷰', path: `/admin/${id}/review` },
|
||||
{ label: '멤버 관리', path: `/admin/${id}/members` },
|
||||
];
|
||||
|
||||
return (
|
||||
|
@ -1,10 +1,15 @@
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Bell, User } from 'lucide-react';
|
||||
import { useLocation, Link } from 'react-router-dom';
|
||||
|
||||
export interface HeaderProps extends React.HTMLAttributes<HTMLDivElement> {}
|
||||
|
||||
export default function Header({ className, ...props }: HeaderProps) {
|
||||
const location = useLocation();
|
||||
|
||||
const isHomePage = location.pathname === '/';
|
||||
|
||||
return (
|
||||
<header
|
||||
className={cn(
|
||||
@ -14,25 +19,44 @@ export default function Header({ className, ...props }: HeaderProps) {
|
||||
{...props}
|
||||
>
|
||||
<div className="flex items-center gap-4 md:gap-10">
|
||||
<div
|
||||
<Link
|
||||
to="/"
|
||||
className={cn('text-[20px] font-normal tracking-[-1.60px] text-black sm:text-[24px] md:text-[32px]')}
|
||||
style={{ fontFamily: "'Offside-Regular', Helvetica" }}
|
||||
>
|
||||
WorLabel
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{!isHomePage && (
|
||||
<nav className="hidden items-center gap-5 md:flex">
|
||||
<div className={cn('text-color-text-default-default', 'font-body-strong', 'text-sm sm:text-base md:text-lg')}>
|
||||
<Link
|
||||
to="/browse"
|
||||
className={cn('text-color-text-default-default', 'font-body-strong', 'text-sm sm:text-base md:text-lg')}
|
||||
>
|
||||
workspace
|
||||
</div>
|
||||
<div className={cn('text-color-text-default-default', 'font-body', 'text-sm sm:text-base md:text-lg')}>
|
||||
</Link>
|
||||
<Link
|
||||
to="/workspace"
|
||||
className={cn('text-color-text-default-default', 'font-body', 'text-sm sm:text-base md:text-lg')}
|
||||
>
|
||||
labeling
|
||||
</div>
|
||||
</Link>
|
||||
<Link
|
||||
to="/admin/1"
|
||||
className={cn('text-color-text-default-default', 'font-body', 'text-sm sm:text-base md:text-lg')}
|
||||
>
|
||||
admin
|
||||
</Link>
|
||||
</nav>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isHomePage && (
|
||||
<div className="flex items-center gap-4 md:gap-5">
|
||||
<Bell className="h-4 w-4 text-black sm:h-5 sm:w-5" />
|
||||
<User className="h-4 w-4 text-black sm:h-5 sm:w-5" />
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
35
frontend/src/components/Home/index.stories.tsx
Normal file
35
frontend/src/components/Home/index.stories.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import '@/index.css';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import Home from '.';
|
||||
import { useState } from 'react';
|
||||
|
||||
const HomeWrapper = ({ initialLoggedIn }: { initialLoggedIn: boolean }) => {
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(initialLoggedIn);
|
||||
|
||||
return (
|
||||
<Home
|
||||
isLoggedIn={isLoggedIn}
|
||||
setIsLoggedIn={setIsLoggedIn}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const meta: Meta<typeof Home> = {
|
||||
title: 'Components/Home',
|
||||
component: Home,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof Home>;
|
||||
|
||||
export const GoogleLogin: Story = {
|
||||
render: () => <HomeWrapper initialLoggedIn={false} />,
|
||||
};
|
||||
|
||||
export const SelectWorkspace: Story = {
|
||||
render: () => <HomeWrapper initialLoggedIn={true} />,
|
||||
};
|
91
frontend/src/components/Home/index.tsx
Normal file
91
frontend/src/components/Home/index.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue, SelectGroup } from '../ui/select';
|
||||
import GoogleLogo from '@/assets/icons/web_neutral_rd_ctn@1x.png';
|
||||
|
||||
interface HomeProps {
|
||||
isLoggedIn?: boolean;
|
||||
setIsLoggedIn?: (loggedIn: boolean) => void;
|
||||
}
|
||||
|
||||
export default function Home({ isLoggedIn = false, setIsLoggedIn }: HomeProps) {
|
||||
const [, setSelectedWorkspace] = useState('');
|
||||
const [loggedIn, setLoggedIn] = useState(isLoggedIn);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const workspaces = [
|
||||
{ id: 1, name: 'Workspace 1' },
|
||||
{ id: 2, name: 'Workspace 2' },
|
||||
{ id: 3, name: 'Workspace 3' },
|
||||
];
|
||||
|
||||
const handleGoogleSignIn = () => {
|
||||
console.log('구글로 계속하기');
|
||||
setLoggedIn(true);
|
||||
if (setIsLoggedIn) {
|
||||
setIsLoggedIn(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleWorkspaceSelect = (value: string) => {
|
||||
const selected = workspaces.find((workspace) => workspace.name === value);
|
||||
if (selected) {
|
||||
navigate(`/browse/${selected.id}`);
|
||||
}
|
||||
setSelectedWorkspace(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center bg-gray-50 p-8">
|
||||
<div className="mb-6 max-w-xl rounded-lg bg-white p-6 shadow-lg">
|
||||
<h2 className="mb-4 text-2xl font-bold text-gray-900">서비스 설명</h2>
|
||||
<p className="mb-4 text-base text-gray-700">
|
||||
본 서비스는 인공 지능(AI) 모델의 학습을 지원하기 위해 웹 기반의 자동 라벨링 도구를 개발하는 것을 목표로
|
||||
합니다. 이 도구는 이미지나 텍스트와 같은 비정형 데이터에 레이블을 자동으로 부여하는 기능을 제공합니다.
|
||||
</p>
|
||||
<p className="mb-4 text-base text-gray-700">
|
||||
기존의 수동적인 방법으로는 대량의 학습 데이터를 처리하는데 시간과 비용이 많이 소모되었습니다. 그러나 본
|
||||
서비스의 결과물인 Auto Labeler를 사용하면, 이러한 문제를 해결할 수 있을 것으로 기대됩니다.
|
||||
</p>
|
||||
<p className="mb-4 text-base text-gray-700">
|
||||
Auto Labeler는 웹 기반으로 동작하므로, 별도의 설치 과정 없이 인터넷 연결 환경에서 쉽게 사용할 수 있습니다.
|
||||
또한, 사용자 친화적인 인터페이스를 제공하여 비전문가도 손쉽게 이용할 수 있도록 설계될 예정입니다.
|
||||
</p>
|
||||
<p className="text-base text-gray-700">
|
||||
본 서비스는 특히 학습 데이터 구축 과정의 효율성과 정확도를 향상시키는 데 중점을 두고 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!loggedIn ? (
|
||||
<button
|
||||
onClick={handleGoogleSignIn}
|
||||
className="mb-4 transition hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-gray-300 active:opacity-80"
|
||||
>
|
||||
<img
|
||||
src={GoogleLogo}
|
||||
alt="Sign in with Google"
|
||||
className="h-auto w-full"
|
||||
/>
|
||||
</button>
|
||||
) : (
|
||||
<Select onValueChange={handleWorkspaceSelect}>
|
||||
<SelectTrigger className="mb-4 w-72">
|
||||
<SelectValue placeholder="워크스페이스를 선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{workspaces.map((workspace) => (
|
||||
<SelectItem
|
||||
key={workspace.id}
|
||||
value={workspace.name}
|
||||
>
|
||||
{workspace.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
19
frontend/src/components/PageLayout/index.stories.tsx
Normal file
19
frontend/src/components/PageLayout/index.stories.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import '@/index.css';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import PageLayout from '.';
|
||||
|
||||
const meta: Meta<typeof PageLayout> = {
|
||||
title: 'Layout/PageLayout',
|
||||
component: PageLayout,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof PageLayout>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <PageLayout></PageLayout>,
|
||||
};
|
@ -3,11 +3,11 @@ import ReviewItem from './ReviewItem';
|
||||
import ReviewSearchInput from './ReviewSearchInput';
|
||||
|
||||
interface ReviewListProps {
|
||||
acceptedCount: number;
|
||||
rejectedCount: number;
|
||||
pendingCount: number;
|
||||
totalCount: number;
|
||||
items: {
|
||||
acceptedCount?: number;
|
||||
rejectedCount?: number;
|
||||
pendingCount?: number;
|
||||
totalCount?: number;
|
||||
items?: {
|
||||
title: string;
|
||||
createdTime: string;
|
||||
creatorName: string;
|
||||
@ -24,18 +24,53 @@ const typeColors: Record<'Classification' | 'Detection' | 'Polygon' | 'Polyline'
|
||||
Polyline: '#c5f9d4',
|
||||
};
|
||||
|
||||
const defaultItems: ReviewListProps['items'] = [
|
||||
{
|
||||
title: '리뷰 항목 1',
|
||||
createdTime: '2024-09-09T10:00:00Z',
|
||||
creatorName: '사용자 1',
|
||||
project: '프로젝트 A',
|
||||
type: 'Classification',
|
||||
status: 'needs_review',
|
||||
},
|
||||
{
|
||||
title: '리뷰 항목 2',
|
||||
createdTime: '2024-09-08T14:30:00Z',
|
||||
creatorName: '사용자 2',
|
||||
project: '프로젝트 B',
|
||||
type: 'Detection',
|
||||
status: 'completed',
|
||||
},
|
||||
{
|
||||
title: '리뷰 항목 3',
|
||||
createdTime: '2024-09-07T08:45:00Z',
|
||||
creatorName: '사용자 3',
|
||||
project: '프로젝트 C',
|
||||
type: 'Polygon',
|
||||
status: 'in_progress',
|
||||
},
|
||||
{
|
||||
title: '리뷰 항목 4',
|
||||
createdTime: '2024-09-06T10:20:00Z',
|
||||
creatorName: '사용자 4',
|
||||
project: '프로젝트 D',
|
||||
type: 'Polyline',
|
||||
status: 'pending',
|
||||
},
|
||||
];
|
||||
|
||||
export default function ReviewList({
|
||||
acceptedCount,
|
||||
rejectedCount,
|
||||
pendingCount,
|
||||
totalCount,
|
||||
items,
|
||||
acceptedCount = 1,
|
||||
rejectedCount = 1,
|
||||
pendingCount = 1,
|
||||
totalCount = 3,
|
||||
items = defaultItems,
|
||||
}: ReviewListProps): JSX.Element {
|
||||
const [activeTab, setActiveTab] = useState('pending');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [sortValue, setSortValue] = useState('latest');
|
||||
|
||||
const filteredItems = items
|
||||
const filteredItems = (items ?? [])
|
||||
.filter((item) => {
|
||||
if (activeTab === 'pending') return item.status.toLowerCase() === 'needs_review';
|
||||
if (activeTab === 'accepted') return item.status.toLowerCase() === 'completed';
|
||||
|
@ -2,17 +2,9 @@ import { useParams } from 'react-router-dom';
|
||||
import ProjectCard from '@/components/ProjectCard';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Workspace } from '@/types';
|
||||
import { Plus, Smile, Users } from 'lucide-react';
|
||||
import { Plus, Smile } from 'lucide-react';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialogCustom';
|
||||
import WorkSpaceCreateForm from '../WorkSpaceCreateModal/WorkSpaceCreateForm';
|
||||
import MemberManageForm from '../MemberManageModal/MemberManageForm';
|
||||
|
||||
type Role = 'admin' | 'editor' | 'viewer';
|
||||
|
||||
interface Member {
|
||||
email: string;
|
||||
role: Role;
|
||||
}
|
||||
|
||||
export default function WorkspaceBrowseDetail() {
|
||||
const { workspaceId } = useParams<{ workspaceId: string }>();
|
||||
@ -27,78 +19,20 @@ export default function WorkspaceBrowseDetail() {
|
||||
id: numericWorkspaceId,
|
||||
name: `workspace-${workspaceId}`,
|
||||
projects: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'project1',
|
||||
type: 'Detection',
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'project2',
|
||||
type: 'Detection',
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'project3',
|
||||
type: 'Detection',
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'project4',
|
||||
type: 'Detection',
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'project5',
|
||||
type: 'Detection',
|
||||
children: [],
|
||||
},
|
||||
{ id: 1, name: 'project1', type: 'Detection', children: [] },
|
||||
{ id: 2, name: 'project2', type: 'Detection', children: [] },
|
||||
{ id: 3, name: 'project3', type: 'Detection', children: [] },
|
||||
{ id: 4, name: 'project4', type: 'Detection', children: [] },
|
||||
{ id: 5, name: 'project5', type: 'Detection', children: [] },
|
||||
],
|
||||
};
|
||||
|
||||
const members: Array<Member> = [
|
||||
{ email: 'admin1@example.com', role: 'admin' },
|
||||
{ email: 'admin2@example.com', role: 'admin' },
|
||||
{ email: 'viewer3@example.com', role: 'viewer' },
|
||||
{ email: 'editor1@example.com', role: 'editor' },
|
||||
{ email: 'editor2@example.com', role: 'editor' },
|
||||
{ email: 'editor3@example.com', role: 'editor' },
|
||||
{ email: 'editor4@example.com', role: 'editor' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col gap-8 px-6 py-4">
|
||||
<div className="flex items-center justify-center">
|
||||
<h1 className="small-title flex grow">{workspaceId ? workspace.name : ''}</h1>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex gap-3">
|
||||
{workspaceId ? (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<div className="body flex items-center gap-2">
|
||||
<Users size={16} />
|
||||
<span>멤버 관리</span>
|
||||
</div>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="lg:max-w-xl">
|
||||
<DialogHeader title="멤버 관리" />
|
||||
<MemberManageForm
|
||||
onSubmit={(data) => {
|
||||
console.log(data);
|
||||
}}
|
||||
members={members}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
|
@ -0,0 +1,23 @@
|
||||
import '@/index.css';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import WorkspaceBrowseLayout from '.';
|
||||
|
||||
const meta: Meta<typeof WorkspaceBrowseLayout> = {
|
||||
title: 'Layout/WorkspaceBrowseLayout',
|
||||
component: WorkspaceBrowseLayout,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof WorkspaceBrowseLayout>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <WorkspaceBrowseLayout />,
|
||||
};
|
||||
|
||||
export const Empty: Story = {
|
||||
render: () => <WorkspaceBrowseLayout />,
|
||||
};
|
@ -1,7 +1,6 @@
|
||||
import { Suspense } from 'react';
|
||||
import { NavLink, Outlet } from 'react-router-dom';
|
||||
import Header from '../Header';
|
||||
import Footer from '../Footer';
|
||||
import { Workspace } from '@/types';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialogCustom';
|
||||
import { Plus } from 'lucide-react';
|
||||
@ -60,7 +59,8 @@ export default function WorkspaceBrowseLayout() {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
{workspaces.map((workspace) => (
|
||||
{workspaces.length > 0 ? (
|
||||
workspaces.map((workspace) => (
|
||||
<NavLink
|
||||
to={`/browse/${workspace.id}`}
|
||||
key={workspace.id}
|
||||
@ -68,7 +68,10 @@ export default function WorkspaceBrowseLayout() {
|
||||
>
|
||||
{workspace.name}
|
||||
</NavLink>
|
||||
))}
|
||||
))
|
||||
) : (
|
||||
<p className="text-gray-500">워크스페이스가 없습니다.</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex w-[calc(100%-280px)] flex-col gap-24">
|
||||
<Suspense fallback={<div></div>}>
|
||||
@ -76,7 +79,6 @@ export default function WorkspaceBrowseLayout() {
|
||||
<Outlet />
|
||||
</main>
|
||||
</Suspense>
|
||||
<Footer className="mt-0" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,13 +1,19 @@
|
||||
import PageLayout from '@/components/PageLayout';
|
||||
import Home from '@/components/Home';
|
||||
import WorkspaceBrowseDetail from '@/components/WorkspaceBrowseDetail';
|
||||
import WorkspaceBrowseLayout from '@/components/WorkspaceBrowseLayout';
|
||||
import WorkspaceLayout from '@/components/WorkspaceLayout';
|
||||
import AdminLayout from '@/components/AdminLayout';
|
||||
import ReviewList from '@/components/ReviewList';
|
||||
import AdminMemberManage from '@/components/AdminMemberManage';
|
||||
import { createBrowserRouter } from 'react-router-dom';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
|
||||
export const webPath = {
|
||||
home: () => '/',
|
||||
browse: () => '/browse',
|
||||
workspace: () => '/workspace',
|
||||
admin: (id: string) => `/admin/${id}`,
|
||||
};
|
||||
|
||||
const router = createBrowserRouter([
|
||||
@ -17,7 +23,7 @@ const router = createBrowserRouter([
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <div>home</div>,
|
||||
element: <Home />,
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -45,6 +51,24 @@ const router = createBrowserRouter([
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: webPath.admin(':id'),
|
||||
element: <AdminLayout />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Navigate to="review" />,
|
||||
},
|
||||
{
|
||||
path: 'review',
|
||||
element: <ReviewList />,
|
||||
},
|
||||
{
|
||||
path: 'members',
|
||||
element: <AdminMemberManage />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
export default router;
|
||||
|
Loading…
Reference in New Issue
Block a user