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 '@/index.css';
|
||||||
import { Meta, StoryObj } from '@storybook/react';
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
import AdminLayout from './index';
|
import AdminLayout from './index';
|
||||||
import { BrowserRouter as Router } from 'react-router-dom';
|
|
||||||
import { Workspace } from '@/types';
|
import { Workspace } from '@/types';
|
||||||
|
|
||||||
const meta: Meta<typeof AdminLayout> = {
|
const meta: Meta<typeof AdminLayout> = {
|
||||||
@ -36,9 +35,5 @@ const workspace: Workspace = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const Default: Story = {
|
export const Default: Story = {
|
||||||
render: () => (
|
render: () => <AdminLayout workspace={workspace} />,
|
||||||
<Router>
|
|
||||||
<AdminLayout workspace={workspace} />
|
|
||||||
</Router>
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet } from 'react-router-dom';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
import Header from '../Header';
|
import Header from '../Header';
|
||||||
import { ResizablePanelGroup, ResizablePanel } from '../ui/resizable';
|
import { ResizablePanelGroup, ResizablePanel } from '../ui/resizable';
|
||||||
import AdminProjectSidebar from '../AdminProjectSidebar';
|
import AdminProjectSidebar from '../AdminProjectSidebar';
|
||||||
@ -6,18 +7,59 @@ import AdminMenuSidebar from '../AdminMenuSidebar';
|
|||||||
import { Workspace } from '@/types';
|
import { Workspace } from '@/types';
|
||||||
|
|
||||||
interface AdminLayoutProps {
|
interface AdminLayoutProps {
|
||||||
workspace: Workspace;
|
workspace?: Workspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AdminLayout({ workspace }: AdminLayoutProps) {
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header className="fixed left-0 top-0" />
|
<Header className="fixed left-0 top-0" />
|
||||||
<div className="mt-16 h-[calc(100vh-64px)] w-screen">
|
<div className="mt-16 h-[calc(100vh-64px)] w-screen">
|
||||||
<ResizablePanelGroup direction="horizontal">
|
<ResizablePanelGroup direction="horizontal">
|
||||||
<AdminProjectSidebar
|
<AdminProjectSidebar
|
||||||
workspaceName={workspace.name}
|
workspaceName={effectiveWorkspace.name}
|
||||||
projects={workspace.projects}
|
projects={effectiveWorkspace.projects}
|
||||||
/>
|
/>
|
||||||
<ResizablePanel className="flex w-full items-center">
|
<ResizablePanel className="flex w-full items-center">
|
||||||
<main className="h-full grow">
|
<main className="h-full grow">
|
||||||
|
@ -16,18 +16,24 @@ interface Project {
|
|||||||
|
|
||||||
export default function AdminMemberManage({
|
export default function AdminMemberManage({
|
||||||
title = '멤버 관리',
|
title = '멤버 관리',
|
||||||
projects,
|
projects = [
|
||||||
onProjectChange,
|
{ id: 'project-1', name: '프로젝트 A' },
|
||||||
onSubmit,
|
{ id: 'project-2', name: '프로젝트 B' },
|
||||||
members,
|
],
|
||||||
onMemberInvite,
|
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;
|
title?: string;
|
||||||
projects: Project[];
|
projects?: Project[];
|
||||||
onProjectChange: (projectId: string) => void;
|
onProjectChange?: (projectId: string) => void;
|
||||||
onSubmit: (data: MemberManageFormValues) => void;
|
onSubmit?: (data: MemberManageFormValues) => void;
|
||||||
members: Member[];
|
members?: Member[];
|
||||||
onMemberInvite: () => void;
|
onMemberInvite?: () => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col gap-6 border-b-[0.67px] border-[#dcdcde] bg-[#fbfafd] p-6">
|
<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';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
export default function AdminMenuSidebar() {
|
export default function AdminMenuSidebar() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ label: '작업', path: '/admin/tasks' },
|
{ label: '리뷰', path: `/admin/${id}/review` },
|
||||||
{ label: '리뷰', path: '/admin/reviews' },
|
{ label: '멤버 관리', path: `/admin/${id}/members` },
|
||||||
{ label: '멤버 관리', path: '/admin/members' },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Bell, User } from 'lucide-react';
|
import { Bell, User } from 'lucide-react';
|
||||||
|
import { useLocation, Link } from 'react-router-dom';
|
||||||
|
|
||||||
export interface HeaderProps extends React.HTMLAttributes<HTMLDivElement> {}
|
export interface HeaderProps extends React.HTMLAttributes<HTMLDivElement> {}
|
||||||
|
|
||||||
export default function Header({ className, ...props }: HeaderProps) {
|
export default function Header({ className, ...props }: HeaderProps) {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const isHomePage = location.pathname === '/';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -14,25 +19,44 @@ export default function Header({ className, ...props }: HeaderProps) {
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-4 md:gap-10">
|
<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]')}
|
className={cn('text-[20px] font-normal tracking-[-1.60px] text-black sm:text-[24px] md:text-[32px]')}
|
||||||
style={{ fontFamily: "'Offside-Regular', Helvetica" }}
|
style={{ fontFamily: "'Offside-Regular', Helvetica" }}
|
||||||
>
|
>
|
||||||
WorLabel
|
WorLabel
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{!isHomePage && (
|
||||||
|
<nav className="hidden items-center gap-5 md:flex">
|
||||||
|
<Link
|
||||||
|
to="/browse"
|
||||||
|
className={cn('text-color-text-default-default', 'font-body-strong', 'text-sm sm:text-base md:text-lg')}
|
||||||
|
>
|
||||||
|
workspace
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/workspace"
|
||||||
|
className={cn('text-color-text-default-default', 'font-body', 'text-sm sm:text-base md:text-lg')}
|
||||||
|
>
|
||||||
|
labeling
|
||||||
|
</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>
|
</div>
|
||||||
<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')}>
|
|
||||||
workspace
|
|
||||||
</div>
|
|
||||||
<div className={cn('text-color-text-default-default', 'font-body', 'text-sm sm:text-base md:text-lg')}>
|
|
||||||
labeling
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
<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>
|
</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';
|
import ReviewSearchInput from './ReviewSearchInput';
|
||||||
|
|
||||||
interface ReviewListProps {
|
interface ReviewListProps {
|
||||||
acceptedCount: number;
|
acceptedCount?: number;
|
||||||
rejectedCount: number;
|
rejectedCount?: number;
|
||||||
pendingCount: number;
|
pendingCount?: number;
|
||||||
totalCount: number;
|
totalCount?: number;
|
||||||
items: {
|
items?: {
|
||||||
title: string;
|
title: string;
|
||||||
createdTime: string;
|
createdTime: string;
|
||||||
creatorName: string;
|
creatorName: string;
|
||||||
@ -24,18 +24,53 @@ const typeColors: Record<'Classification' | 'Detection' | 'Polygon' | 'Polyline'
|
|||||||
Polyline: '#c5f9d4',
|
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({
|
export default function ReviewList({
|
||||||
acceptedCount,
|
acceptedCount = 1,
|
||||||
rejectedCount,
|
rejectedCount = 1,
|
||||||
pendingCount,
|
pendingCount = 1,
|
||||||
totalCount,
|
totalCount = 3,
|
||||||
items,
|
items = defaultItems,
|
||||||
}: ReviewListProps): JSX.Element {
|
}: ReviewListProps): JSX.Element {
|
||||||
const [activeTab, setActiveTab] = useState('pending');
|
const [activeTab, setActiveTab] = useState('pending');
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [sortValue, setSortValue] = useState('latest');
|
const [sortValue, setSortValue] = useState('latest');
|
||||||
|
|
||||||
const filteredItems = items
|
const filteredItems = (items ?? [])
|
||||||
.filter((item) => {
|
.filter((item) => {
|
||||||
if (activeTab === 'pending') return item.status.toLowerCase() === 'needs_review';
|
if (activeTab === 'pending') return item.status.toLowerCase() === 'needs_review';
|
||||||
if (activeTab === 'accepted') return item.status.toLowerCase() === 'completed';
|
if (activeTab === 'accepted') return item.status.toLowerCase() === 'completed';
|
||||||
|
@ -2,17 +2,9 @@ import { useParams } from 'react-router-dom';
|
|||||||
import ProjectCard from '@/components/ProjectCard';
|
import ProjectCard from '@/components/ProjectCard';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Workspace } from '@/types';
|
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 { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialogCustom';
|
||||||
import WorkSpaceCreateForm from '../WorkSpaceCreateModal/WorkSpaceCreateForm';
|
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() {
|
export default function WorkspaceBrowseDetail() {
|
||||||
const { workspaceId } = useParams<{ workspaceId: string }>();
|
const { workspaceId } = useParams<{ workspaceId: string }>();
|
||||||
@ -27,78 +19,20 @@ export default function WorkspaceBrowseDetail() {
|
|||||||
id: numericWorkspaceId,
|
id: numericWorkspaceId,
|
||||||
name: `workspace-${workspaceId}`,
|
name: `workspace-${workspaceId}`,
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{ id: 1, name: 'project1', type: 'Detection', children: [] },
|
||||||
id: 1,
|
{ id: 2, name: 'project2', type: 'Detection', children: [] },
|
||||||
name: 'project1',
|
{ id: 3, name: 'project3', type: 'Detection', children: [] },
|
||||||
type: 'Detection',
|
{ id: 4, name: 'project4', type: 'Detection', children: [] },
|
||||||
children: [],
|
{ id: 5, name: 'project5', 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 (
|
return (
|
||||||
<div className="flex h-full w-full flex-col gap-8 px-6 py-4">
|
<div className="flex h-full w-full flex-col gap-8 px-6 py-4">
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
<h1 className="small-title flex grow">{workspaceId ? workspace.name : ''}</h1>
|
<h1 className="small-title flex grow">{workspaceId ? workspace.name : ''}</h1>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex gap-3">
|
<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>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button
|
<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 { Suspense } from 'react';
|
||||||
import { NavLink, Outlet } from 'react-router-dom';
|
import { NavLink, Outlet } from 'react-router-dom';
|
||||||
import Header from '../Header';
|
import Header from '../Header';
|
||||||
import Footer from '../Footer';
|
|
||||||
import { Workspace } from '@/types';
|
import { Workspace } from '@/types';
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialogCustom';
|
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialogCustom';
|
||||||
import { Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
@ -60,15 +59,19 @@ export default function WorkspaceBrowseLayout() {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
{workspaces.map((workspace) => (
|
{workspaces.length > 0 ? (
|
||||||
<NavLink
|
workspaces.map((workspace) => (
|
||||||
to={`/browse/${workspace.id}`}
|
<NavLink
|
||||||
key={workspace.id}
|
to={`/browse/${workspace.id}`}
|
||||||
className={({ isActive }) => (isActive ? 'body-strong' : 'body') + ' cursor-pointer'}
|
key={workspace.id}
|
||||||
>
|
className={({ isActive }) => (isActive ? 'body-strong' : 'body') + ' cursor-pointer'}
|
||||||
{workspace.name}
|
>
|
||||||
</NavLink>
|
{workspace.name}
|
||||||
))}
|
</NavLink>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-500">워크스페이스가 없습니다.</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-[calc(100%-280px)] flex-col gap-24">
|
<div className="flex w-[calc(100%-280px)] flex-col gap-24">
|
||||||
<Suspense fallback={<div></div>}>
|
<Suspense fallback={<div></div>}>
|
||||||
@ -76,7 +79,6 @@ export default function WorkspaceBrowseLayout() {
|
|||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<Footer className="mt-0" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,13 +1,19 @@
|
|||||||
import PageLayout from '@/components/PageLayout';
|
import PageLayout from '@/components/PageLayout';
|
||||||
|
import Home from '@/components/Home';
|
||||||
import WorkspaceBrowseDetail from '@/components/WorkspaceBrowseDetail';
|
import WorkspaceBrowseDetail from '@/components/WorkspaceBrowseDetail';
|
||||||
import WorkspaceBrowseLayout from '@/components/WorkspaceBrowseLayout';
|
import WorkspaceBrowseLayout from '@/components/WorkspaceBrowseLayout';
|
||||||
import WorkspaceLayout from '@/components/WorkspaceLayout';
|
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 { createBrowserRouter } from 'react-router-dom';
|
||||||
|
import { Navigate } from 'react-router-dom';
|
||||||
|
|
||||||
export const webPath = {
|
export const webPath = {
|
||||||
home: () => '/',
|
home: () => '/',
|
||||||
browse: () => '/browse',
|
browse: () => '/browse',
|
||||||
workspace: () => '/workspace',
|
workspace: () => '/workspace',
|
||||||
|
admin: (id: string) => `/admin/${id}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
@ -17,7 +23,7 @@ const router = createBrowserRouter([
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
index: true,
|
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;
|
export default router;
|
||||||
|
Loading…
Reference in New Issue
Block a user