rusty-replay 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +10 -0
- package/.vscode/settings.json +3 -0
- package/README.md +92 -0
- package/apps/web/README.md +11 -0
- package/apps/web/api/auth/keys.ts +3 -0
- package/apps/web/api/auth/types.ts +25 -0
- package/apps/web/api/auth/use-query-profile.ts +19 -0
- package/apps/web/api/auth/use-sign-in.ts +24 -0
- package/apps/web/api/axios.ts +122 -0
- package/apps/web/api/error-code.ts +36 -0
- package/apps/web/api/event/keys.ts +14 -0
- package/apps/web/api/event/types.ts +91 -0
- package/apps/web/api/event/use-mutation-event-assignee.ts +103 -0
- package/apps/web/api/event/use-mutation-event-priority.ts +97 -0
- package/apps/web/api/event/use-mutation-event-status.ts +198 -0
- package/apps/web/api/event/use-query-event-detail.ts +25 -0
- package/apps/web/api/event/use-query-event-list.ts +42 -0
- package/apps/web/api/health-check/index.ts +21 -0
- package/apps/web/api/project/keys.ts +4 -0
- package/apps/web/api/project/types.ts +28 -0
- package/apps/web/api/project/use-create-project.ts +30 -0
- package/apps/web/api/project/use-query-project-list.ts +19 -0
- package/apps/web/api/project/use-query-project-users.ts +23 -0
- package/apps/web/api/types.ts +44 -0
- package/apps/web/app/(auth)/layout.tsx +5 -0
- package/apps/web/app/(auth)/sign-in/page.tsx +20 -0
- package/apps/web/app/(auth)/sign-up/page.tsx +5 -0
- package/apps/web/app/(project)/project/[project_id]/issues/[issue_id]/page.tsx +17 -0
- package/apps/web/app/(project)/project/[project_id]/issues/page.tsx +15 -0
- package/apps/web/app/(project)/project/[project_id]/page.tsx +10 -0
- package/apps/web/app/(project)/project/page.tsx +10 -0
- package/apps/web/app/(report)/error-list/page.tsx +7 -0
- package/apps/web/app/favicon.ico +0 -0
- package/apps/web/app/layout.tsx +35 -0
- package/apps/web/app/page.tsx +3 -0
- package/apps/web/components/.gitkeep +0 -0
- package/apps/web/components/event-list/event-detail.tsx +242 -0
- package/apps/web/components/event-list/event-list.tsx +376 -0
- package/apps/web/components/event-list/preview.tsx +573 -0
- package/apps/web/components/layouts/default-layout.tsx +59 -0
- package/apps/web/components/login-form.tsx +124 -0
- package/apps/web/components/project/create-project.tsx +130 -0
- package/apps/web/components/project/hooks/use-get-event-params.ts +9 -0
- package/apps/web/components/project/hooks/use-get-project-params.ts +10 -0
- package/apps/web/components/project/project-detail.tsx +240 -0
- package/apps/web/components/project/project-list.tsx +137 -0
- package/apps/web/components/providers.tsx +25 -0
- package/apps/web/components/ui/assignee-dropdown.tsx +176 -0
- package/apps/web/components/ui/event-status-dropdown.tsx +104 -0
- package/apps/web/components/ui/priority-dropdown.tsx +123 -0
- package/apps/web/components/widget/app-sidebar.tsx +225 -0
- package/apps/web/components/widget/nav-main.tsx +73 -0
- package/apps/web/components/widget/nav-projects.tsx +84 -0
- package/apps/web/components/widget/nav-user.tsx +113 -0
- package/apps/web/components.json +20 -0
- package/apps/web/constants/routes.ts +12 -0
- package/apps/web/eslint.config.js +4 -0
- package/apps/web/hooks/use-boolean-state.ts +13 -0
- package/apps/web/lib/.gitkeep +0 -0
- package/apps/web/next-env.d.ts +5 -0
- package/apps/web/next.config.mjs +6 -0
- package/apps/web/package.json +60 -0
- package/apps/web/postcss.config.mjs +1 -0
- package/apps/web/providers/flag-provider.tsx +35 -0
- package/apps/web/providers/query-client-provider.tsx +17 -0
- package/apps/web/providers/telemetry-provider.tsx +12 -0
- package/apps/web/tsconfig.json +24 -0
- package/apps/web/utils/avatar.ts +26 -0
- package/apps/web/utils/date.ts +26 -0
- package/apps/web/utils/front-end-tracer.ts +119 -0
- package/apps/web/utils/schema/project.schema.ts +12 -0
- package/apps/web/utils/span-processor.ts +36 -0
- package/package.json +21 -0
- package/packages/eslint-config/README.md +3 -0
- package/packages/eslint-config/base.js +32 -0
- package/packages/eslint-config/next.js +51 -0
- package/packages/eslint-config/package.json +25 -0
- package/packages/eslint-config/react-internal.js +41 -0
- package/packages/rusty-replay/README.md +165 -0
- package/packages/rusty-replay/package.json +67 -0
- package/packages/rusty-replay/src/environment.ts +27 -0
- package/packages/rusty-replay/src/error-batcher.ts +75 -0
- package/packages/rusty-replay/src/front-end-tracer.ts +86 -0
- package/packages/rusty-replay/src/handler.ts +37 -0
- package/packages/rusty-replay/src/index.ts +8 -0
- package/packages/rusty-replay/src/recorder.ts +71 -0
- package/packages/rusty-replay/src/reporter.ts +115 -0
- package/packages/rusty-replay/src/utils.ts +13 -0
- package/packages/rusty-replay/tsconfig.build.json +13 -0
- package/packages/rusty-replay/tsconfig.json +27 -0
- package/packages/rusty-replay/tsup.config.ts +39 -0
- package/packages/typescript-config/README.md +3 -0
- package/packages/typescript-config/base.json +20 -0
- package/packages/typescript-config/nextjs.json +13 -0
- package/packages/typescript-config/package.json +9 -0
- package/packages/typescript-config/react-library.json +8 -0
- package/packages/ui/components.json +20 -0
- package/packages/ui/eslint.config.js +4 -0
- package/packages/ui/package.json +60 -0
- package/packages/ui/postcss.config.mjs +6 -0
- package/packages/ui/src/components/.gitkeep +0 -0
- package/packages/ui/src/components/avatar.tsx +53 -0
- package/packages/ui/src/components/badge.tsx +46 -0
- package/packages/ui/src/components/breadcrumb.tsx +109 -0
- package/packages/ui/src/components/button.tsx +59 -0
- package/packages/ui/src/components/calendar.tsx +75 -0
- package/packages/ui/src/components/calendars/date-picker.tsx +43 -0
- package/packages/ui/src/components/calendars/date-range-picker.tsx +79 -0
- package/packages/ui/src/components/card.tsx +92 -0
- package/packages/ui/src/components/checkbox.tsx +32 -0
- package/packages/ui/src/components/collapsible.tsx +33 -0
- package/packages/ui/src/components/dialog.tsx +135 -0
- package/packages/ui/src/components/dialogs/confirmation-modal.tsx +216 -0
- package/packages/ui/src/components/dropdown-menu.tsx +261 -0
- package/packages/ui/src/components/input.tsx +30 -0
- package/packages/ui/src/components/label.tsx +24 -0
- package/packages/ui/src/components/login-form.tsx +68 -0
- package/packages/ui/src/components/mode-switcher.tsx +34 -0
- package/packages/ui/src/components/popover.tsx +48 -0
- package/packages/ui/src/components/scroll-area.tsx +58 -0
- package/packages/ui/src/components/select.tsx +185 -0
- package/packages/ui/src/components/separator.tsx +28 -0
- package/packages/ui/src/components/sheet.tsx +139 -0
- package/packages/ui/src/components/sidebar.tsx +726 -0
- package/packages/ui/src/components/skeleton.tsx +13 -0
- package/packages/ui/src/components/sonner.tsx +25 -0
- package/packages/ui/src/components/table.tsx +116 -0
- package/packages/ui/src/components/tabs.tsx +66 -0
- package/packages/ui/src/components/team-switcher.tsx +91 -0
- package/packages/ui/src/components/textarea.tsx +18 -0
- package/packages/ui/src/components/tooltip.tsx +61 -0
- package/packages/ui/src/hooks/.gitkeep +0 -0
- package/packages/ui/src/hooks/use-meta-color.ts +28 -0
- package/packages/ui/src/hooks/use-mobile.ts +19 -0
- package/packages/ui/src/lib/utils.ts +6 -0
- package/packages/ui/src/styles/globals.css +138 -0
- package/packages/ui/tsconfig.json +13 -0
- package/packages/ui/tsconfig.lint.json +8 -0
- package/pnpm-workspace.yaml +4 -0
- package/tsconfig.json +4 -0
- package/turbo.json +21 -0
@@ -0,0 +1,124 @@
|
|
1
|
+
import { cn } from '@workspace/ui/lib/utils';
|
2
|
+
import { Button } from '@workspace/ui/components/button';
|
3
|
+
import {
|
4
|
+
Card,
|
5
|
+
CardContent,
|
6
|
+
CardDescription,
|
7
|
+
CardHeader,
|
8
|
+
CardTitle,
|
9
|
+
} from '@workspace/ui/components/card';
|
10
|
+
import { Input } from '@workspace/ui/components/input';
|
11
|
+
import { Label } from '@workspace/ui/components/label';
|
12
|
+
import { useSignIn } from '@/api/auth/use-sign-in';
|
13
|
+
import { useState } from 'react';
|
14
|
+
import { useRouter } from 'next/navigation';
|
15
|
+
import { routes } from '@/constants/routes';
|
16
|
+
|
17
|
+
export function LoginForm({
|
18
|
+
className,
|
19
|
+
...props
|
20
|
+
}: React.ComponentPropsWithoutRef<'div'>) {
|
21
|
+
const router = useRouter();
|
22
|
+
|
23
|
+
const [loginForm, setLoginForm] = useState({
|
24
|
+
email: '',
|
25
|
+
password: '',
|
26
|
+
});
|
27
|
+
|
28
|
+
const { mutateAsync: signIn, isPending: isLoading } = useSignIn();
|
29
|
+
const handleSignIn = async () => {
|
30
|
+
await signIn(
|
31
|
+
{
|
32
|
+
email: loginForm.email,
|
33
|
+
password: loginForm.password,
|
34
|
+
},
|
35
|
+
{
|
36
|
+
onSuccess: (data) => {
|
37
|
+
console.log('Login successful', data);
|
38
|
+
router.push(routes.home());
|
39
|
+
},
|
40
|
+
onError: (error) => {
|
41
|
+
console.error('Login failed', error);
|
42
|
+
},
|
43
|
+
}
|
44
|
+
);
|
45
|
+
};
|
46
|
+
|
47
|
+
return (
|
48
|
+
<div className={cn('flex flex-col gap-6', className)} {...props}>
|
49
|
+
<Card>
|
50
|
+
<CardHeader>
|
51
|
+
<CardTitle className="text-2xl">Login</CardTitle>
|
52
|
+
<CardDescription>
|
53
|
+
Enter your email below to login to your account
|
54
|
+
</CardDescription>
|
55
|
+
</CardHeader>
|
56
|
+
<CardContent>
|
57
|
+
<form
|
58
|
+
onSubmit={(e) => {
|
59
|
+
e.preventDefault();
|
60
|
+
handleSignIn();
|
61
|
+
}}
|
62
|
+
>
|
63
|
+
<div className="flex flex-col gap-6">
|
64
|
+
<div className="grid gap-2">
|
65
|
+
<Label htmlFor="email">Email</Label>
|
66
|
+
<Input
|
67
|
+
id="email"
|
68
|
+
type="email"
|
69
|
+
placeholder="m@example.com"
|
70
|
+
required
|
71
|
+
onChange={(e) => {
|
72
|
+
setLoginForm({
|
73
|
+
...loginForm,
|
74
|
+
email: e.target.value,
|
75
|
+
});
|
76
|
+
}}
|
77
|
+
/>
|
78
|
+
</div>
|
79
|
+
<div className="grid gap-2">
|
80
|
+
<div className="flex items-center">
|
81
|
+
<Label htmlFor="password">Password</Label>
|
82
|
+
<a
|
83
|
+
href="#"
|
84
|
+
className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
|
85
|
+
>
|
86
|
+
Forgot your password?
|
87
|
+
</a>
|
88
|
+
</div>
|
89
|
+
<Input
|
90
|
+
id="password"
|
91
|
+
type="password"
|
92
|
+
required
|
93
|
+
onChange={(e) => {
|
94
|
+
setLoginForm({
|
95
|
+
...loginForm,
|
96
|
+
password: e.target.value,
|
97
|
+
});
|
98
|
+
}}
|
99
|
+
/>
|
100
|
+
</div>
|
101
|
+
<Button
|
102
|
+
type="submit"
|
103
|
+
className="w-full"
|
104
|
+
// onClick={handleSignIn}
|
105
|
+
disabled={isLoading}
|
106
|
+
>
|
107
|
+
Login
|
108
|
+
</Button>
|
109
|
+
<Button variant="outline" className="w-full">
|
110
|
+
Login with Google
|
111
|
+
</Button>
|
112
|
+
</div>
|
113
|
+
<div className="mt-4 text-center text-sm">
|
114
|
+
Don't have an account?{' '}
|
115
|
+
<a href="#" className="underline underline-offset-4">
|
116
|
+
Sign up
|
117
|
+
</a>
|
118
|
+
</div>
|
119
|
+
</form>
|
120
|
+
</CardContent>
|
121
|
+
</Card>
|
122
|
+
</div>
|
123
|
+
);
|
124
|
+
}
|
@@ -0,0 +1,130 @@
|
|
1
|
+
import { useBooleanState } from '@/hooks/use-boolean-state';
|
2
|
+
import { Button } from '@workspace/ui/components/button';
|
3
|
+
import { Input } from '@workspace/ui/components/input';
|
4
|
+
import { Textarea } from '@workspace/ui/components/textarea';
|
5
|
+
import { Label } from '@workspace/ui/components/label';
|
6
|
+
import { toast } from '@workspace/ui/components/sonner';
|
7
|
+
import { Plus } from 'lucide-react';
|
8
|
+
import React, { useEffect } from 'react';
|
9
|
+
import { Controller, useForm } from 'react-hook-form';
|
10
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
11
|
+
import {
|
12
|
+
CreateProjectSchema,
|
13
|
+
CreateProjectSchemaType,
|
14
|
+
} from '@/utils/schema/project.schema';
|
15
|
+
import { useCreateProject } from '@/api/project/use-create-project';
|
16
|
+
import { ConfirmationModal } from '@workspace/ui/components/dialogs/confirmation-modal';
|
17
|
+
|
18
|
+
export function CreateProject() {
|
19
|
+
const createState = useBooleanState();
|
20
|
+
const { mutateAsync: createProject, isPending: isCreating } =
|
21
|
+
useCreateProject();
|
22
|
+
|
23
|
+
const form = useForm<CreateProjectSchemaType>({
|
24
|
+
resolver: zodResolver(CreateProjectSchema),
|
25
|
+
defaultValues: {
|
26
|
+
name: undefined,
|
27
|
+
description: undefined,
|
28
|
+
},
|
29
|
+
});
|
30
|
+
|
31
|
+
useEffect(() => {
|
32
|
+
if (createState.isOpen) {
|
33
|
+
form.clearErrors();
|
34
|
+
}
|
35
|
+
}, [createState.isOpen]);
|
36
|
+
|
37
|
+
console.log('form.formState', form.formState.errors);
|
38
|
+
|
39
|
+
const handleCreateProject = async (data: CreateProjectSchemaType) => {
|
40
|
+
await createProject(data, {
|
41
|
+
onSuccess: () => {
|
42
|
+
toast.success('프로젝트가 생성되었습니다!');
|
43
|
+
createState.close();
|
44
|
+
form.reset();
|
45
|
+
},
|
46
|
+
onError: (error) => {
|
47
|
+
toast.error('프로젝트 생성에 실패했습니다.');
|
48
|
+
},
|
49
|
+
});
|
50
|
+
};
|
51
|
+
|
52
|
+
return (
|
53
|
+
<>
|
54
|
+
<Button
|
55
|
+
onClick={() => {
|
56
|
+
createState.open();
|
57
|
+
}}
|
58
|
+
size={'sm'}
|
59
|
+
className="flex items-center gap-2"
|
60
|
+
type="button"
|
61
|
+
>
|
62
|
+
<Plus size={16} />새 프로젝트
|
63
|
+
</Button>
|
64
|
+
|
65
|
+
<ConfirmationModal
|
66
|
+
visible={createState.isOpen}
|
67
|
+
title="새 프로젝트 생성"
|
68
|
+
confirmLabel="저장하기"
|
69
|
+
cancelLabel="취소"
|
70
|
+
onConfirm={form.handleSubmit(handleCreateProject)}
|
71
|
+
onCancel={createState.close}
|
72
|
+
loading={isCreating}
|
73
|
+
size="md"
|
74
|
+
className="sm:max-w-[425px]"
|
75
|
+
>
|
76
|
+
<div>
|
77
|
+
<p className="text-sm text-gray-500 mb-4">
|
78
|
+
새 프로젝트 정보를 입력하세요. 완료되면 저장 버튼을 클릭하세요.
|
79
|
+
</p>
|
80
|
+
<div className="grid gap-4 py-2">
|
81
|
+
<div className="grid grid-cols-4 items-center gap-4">
|
82
|
+
<Label htmlFor="name" className="text-right">
|
83
|
+
프로젝트명
|
84
|
+
</Label>
|
85
|
+
<div className="col-span-3">
|
86
|
+
<Controller
|
87
|
+
control={form.control}
|
88
|
+
name="name"
|
89
|
+
render={({ field }) => {
|
90
|
+
return (
|
91
|
+
<Input
|
92
|
+
{...field}
|
93
|
+
id={field.name}
|
94
|
+
placeholder="프로젝트 이름을 입력하세요"
|
95
|
+
autoFocus
|
96
|
+
aria-invalid={!!form.formState.errors.name}
|
97
|
+
error={form.formState.errors.name?.message}
|
98
|
+
/>
|
99
|
+
);
|
100
|
+
}}
|
101
|
+
/>
|
102
|
+
</div>
|
103
|
+
</div>
|
104
|
+
<div className="grid grid-cols-4 items-center gap-4">
|
105
|
+
<Label htmlFor="description" className="text-right">
|
106
|
+
설명
|
107
|
+
</Label>
|
108
|
+
<div className="col-span-3">
|
109
|
+
<Controller
|
110
|
+
name="description"
|
111
|
+
control={form.control}
|
112
|
+
render={({ field }) => {
|
113
|
+
return (
|
114
|
+
<Textarea
|
115
|
+
id={field.name}
|
116
|
+
{...field}
|
117
|
+
value={field.value ?? undefined}
|
118
|
+
placeholder="프로젝트 설명을 입력하세요"
|
119
|
+
/>
|
120
|
+
);
|
121
|
+
}}
|
122
|
+
/>
|
123
|
+
</div>
|
124
|
+
</div>
|
125
|
+
</div>
|
126
|
+
</div>
|
127
|
+
</ConfirmationModal>
|
128
|
+
</>
|
129
|
+
);
|
130
|
+
}
|
@@ -0,0 +1,10 @@
|
|
1
|
+
import { useParams } from 'next/navigation';
|
2
|
+
import React from 'react';
|
3
|
+
|
4
|
+
export function useGetProjectParams() {
|
5
|
+
const { project_id: projectId } = useParams<{
|
6
|
+
project_id: string;
|
7
|
+
}>();
|
8
|
+
|
9
|
+
return { projectId: projectId ? parseInt(projectId) : undefined };
|
10
|
+
}
|
@@ -0,0 +1,240 @@
|
|
1
|
+
'use client';
|
2
|
+
|
3
|
+
import React, { useEffect } from 'react';
|
4
|
+
import { useGetProjectParams } from './hooks/use-get-project-params';
|
5
|
+
import { useQueryProjectList } from '@/api/project/use-query-project-list';
|
6
|
+
import {
|
7
|
+
Card,
|
8
|
+
CardContent,
|
9
|
+
CardDescription,
|
10
|
+
CardFooter,
|
11
|
+
CardHeader,
|
12
|
+
CardTitle,
|
13
|
+
} from '@workspace/ui/components/card';
|
14
|
+
import {
|
15
|
+
Tabs,
|
16
|
+
TabsContent,
|
17
|
+
TabsList,
|
18
|
+
TabsTrigger,
|
19
|
+
} from '@workspace/ui/components/tabs';
|
20
|
+
import { Badge } from '@workspace/ui/components/badge';
|
21
|
+
import { Button } from '@workspace/ui/components/button';
|
22
|
+
import { Skeleton } from '@workspace/ui/components/skeleton';
|
23
|
+
import { Separator } from '@workspace/ui/components/separator';
|
24
|
+
import {
|
25
|
+
ArrowLeft,
|
26
|
+
Calendar,
|
27
|
+
Edit,
|
28
|
+
Trash2,
|
29
|
+
RefreshCw,
|
30
|
+
KeyRound,
|
31
|
+
Shield,
|
32
|
+
} from 'lucide-react';
|
33
|
+
import { Project } from '@/api/project/types';
|
34
|
+
import dayjs from 'dayjs';
|
35
|
+
import 'dayjs/locale/ko';
|
36
|
+
import ProjectList from './project-list';
|
37
|
+
import { useRouter } from 'next/navigation';
|
38
|
+
import { formatDateFromNow } from '@/utils/date';
|
39
|
+
import { routes } from '@/constants/routes';
|
40
|
+
|
41
|
+
dayjs.locale('ko');
|
42
|
+
|
43
|
+
export default function ProjectDetail() {
|
44
|
+
const router = useRouter();
|
45
|
+
const { projectId } = useGetProjectParams();
|
46
|
+
const { data: projectList, isLoading } = useQueryProjectList();
|
47
|
+
const [currentProject, setCurrentProject] = React.useState<Project | null>(
|
48
|
+
null
|
49
|
+
);
|
50
|
+
|
51
|
+
useEffect(() => {
|
52
|
+
if (projectList && projectId) {
|
53
|
+
const project = projectList.find((p) => p.id === Number(projectId));
|
54
|
+
setCurrentProject(project || null);
|
55
|
+
}
|
56
|
+
}, [projectList, projectId]);
|
57
|
+
|
58
|
+
if (!projectId) {
|
59
|
+
return <ProjectList />;
|
60
|
+
}
|
61
|
+
|
62
|
+
return (
|
63
|
+
<div className="space-y-6">
|
64
|
+
<div className="flex items-center gap-2">
|
65
|
+
<Button
|
66
|
+
variant="outline"
|
67
|
+
size="sm"
|
68
|
+
onClick={() => {
|
69
|
+
router.push(routes.project.list());
|
70
|
+
}}
|
71
|
+
>
|
72
|
+
<ArrowLeft size={16} className="mr-2" />
|
73
|
+
프로젝트 목록
|
74
|
+
</Button>
|
75
|
+
|
76
|
+
<Button
|
77
|
+
variant="outline"
|
78
|
+
size="sm"
|
79
|
+
onClick={() => {
|
80
|
+
router.push(routes.event.list(projectId));
|
81
|
+
}}
|
82
|
+
>
|
83
|
+
<Shield size={16} className="mr-2" />
|
84
|
+
모든 이슈
|
85
|
+
</Button>
|
86
|
+
|
87
|
+
{!isLoading && !currentProject && (
|
88
|
+
<Badge variant="destructive">존재하지 않는 프로젝트입니다</Badge>
|
89
|
+
)}
|
90
|
+
</div>
|
91
|
+
|
92
|
+
{isLoading ? (
|
93
|
+
<Card>
|
94
|
+
<CardHeader>
|
95
|
+
<Skeleton className="h-8 w-1/3" />
|
96
|
+
<Skeleton className="h-4 w-1/4" />
|
97
|
+
</CardHeader>
|
98
|
+
<CardContent>
|
99
|
+
<div className="space-y-4">
|
100
|
+
<Skeleton className="h-4 w-full" />
|
101
|
+
<Skeleton className="h-4 w-full" />
|
102
|
+
<Skeleton className="h-4 w-3/4" />
|
103
|
+
</div>
|
104
|
+
</CardContent>
|
105
|
+
</Card>
|
106
|
+
) : currentProject ? (
|
107
|
+
<Tabs defaultValue="overview" className="w-full">
|
108
|
+
<TabsList className="mb-4">
|
109
|
+
<TabsTrigger value="overview">개요</TabsTrigger>
|
110
|
+
<TabsTrigger value="settings">설정</TabsTrigger>
|
111
|
+
<TabsTrigger value="api">API</TabsTrigger>
|
112
|
+
</TabsList>
|
113
|
+
|
114
|
+
<TabsContent value="overview">
|
115
|
+
<Card>
|
116
|
+
<CardHeader className="flex flex-row items-start justify-between">
|
117
|
+
<div>
|
118
|
+
<CardTitle className="text-2xl font-bold">
|
119
|
+
{currentProject.name}
|
120
|
+
</CardTitle>
|
121
|
+
<CardDescription>
|
122
|
+
프로젝트 ID: {currentProject.id}
|
123
|
+
</CardDescription>
|
124
|
+
</div>
|
125
|
+
<div className="flex gap-2">
|
126
|
+
<Button
|
127
|
+
variant="outline"
|
128
|
+
size="sm"
|
129
|
+
className="flex items-center gap-1"
|
130
|
+
>
|
131
|
+
<Edit size={14} />
|
132
|
+
편집
|
133
|
+
</Button>
|
134
|
+
<Button
|
135
|
+
variant="destructive"
|
136
|
+
size="sm"
|
137
|
+
className="flex items-center gap-1"
|
138
|
+
>
|
139
|
+
<Trash2 size={14} />
|
140
|
+
삭제
|
141
|
+
</Button>
|
142
|
+
</div>
|
143
|
+
</CardHeader>
|
144
|
+
<CardContent className="space-y-6">
|
145
|
+
<div>
|
146
|
+
<h3 className="text-lg font-semibold mb-2">설명</h3>
|
147
|
+
<p className="text-muted-foreground">
|
148
|
+
{currentProject.description || '설명이 없습니다.'}
|
149
|
+
</p>
|
150
|
+
</div>
|
151
|
+
|
152
|
+
<Separator />
|
153
|
+
|
154
|
+
<div className="grid grid-cols-2 gap-4">
|
155
|
+
<div>
|
156
|
+
<h3 className="text-sm font-medium mb-1 text-muted-foreground">
|
157
|
+
API 키
|
158
|
+
</h3>
|
159
|
+
<div className="flex items-center gap-2">
|
160
|
+
<KeyRound size={16} />
|
161
|
+
<Badge variant="outline" className="font-mono">
|
162
|
+
{currentProject.apiKey}
|
163
|
+
</Badge>
|
164
|
+
</div>
|
165
|
+
</div>
|
166
|
+
<div>
|
167
|
+
<h3 className="text-sm font-medium mb-1 text-muted-foreground">
|
168
|
+
생성일
|
169
|
+
</h3>
|
170
|
+
<div className="flex items-center gap-2">
|
171
|
+
<Calendar size={16} />
|
172
|
+
{formatDateFromNow(currentProject.createdAt)}
|
173
|
+
</div>
|
174
|
+
</div>
|
175
|
+
<div>
|
176
|
+
<h3 className="text-sm font-medium mb-1 text-muted-foreground">
|
177
|
+
마지막 수정일
|
178
|
+
</h3>
|
179
|
+
<div className="flex items-center gap-2">
|
180
|
+
<Calendar size={16} />
|
181
|
+
{formatDateFromNow(currentProject.updatedAt)}
|
182
|
+
</div>
|
183
|
+
</div>
|
184
|
+
</div>
|
185
|
+
</CardContent>
|
186
|
+
<CardFooter className="flex justify-end">
|
187
|
+
<Button
|
188
|
+
variant="outline"
|
189
|
+
size="sm"
|
190
|
+
className="flex items-center gap-2"
|
191
|
+
onClick={() => {
|
192
|
+
router.refresh();
|
193
|
+
}}
|
194
|
+
>
|
195
|
+
<RefreshCw size={14} />
|
196
|
+
새로고침
|
197
|
+
</Button>
|
198
|
+
</CardFooter>
|
199
|
+
</Card>
|
200
|
+
</TabsContent>
|
201
|
+
|
202
|
+
<TabsContent value="settings">
|
203
|
+
<Card>
|
204
|
+
<CardHeader>
|
205
|
+
<CardTitle>프로젝트 설정</CardTitle>
|
206
|
+
<CardDescription>프로젝트 설정을 관리합니다</CardDescription>
|
207
|
+
</CardHeader>
|
208
|
+
<CardContent>
|
209
|
+
<p>프로젝트 설정 컨텐츠가 여기에 표시됩니다.</p>
|
210
|
+
</CardContent>
|
211
|
+
</Card>
|
212
|
+
</TabsContent>
|
213
|
+
|
214
|
+
<TabsContent value="api">
|
215
|
+
<Card>
|
216
|
+
<CardHeader>
|
217
|
+
<CardTitle>API 설정</CardTitle>
|
218
|
+
<CardDescription>
|
219
|
+
API 키 및 관련 설정을 관리합니다
|
220
|
+
</CardDescription>
|
221
|
+
</CardHeader>
|
222
|
+
<CardContent>
|
223
|
+
<p>API 설정 컨텐츠가 여기에 표시됩니다.</p>
|
224
|
+
</CardContent>
|
225
|
+
</Card>
|
226
|
+
</TabsContent>
|
227
|
+
</Tabs>
|
228
|
+
) : (
|
229
|
+
<Card>
|
230
|
+
<CardContent className="p-6 text-center">
|
231
|
+
<p className="text-lg font-medium">프로젝트를 찾을 수 없습니다</p>
|
232
|
+
<p className="text-muted-foreground mt-2">
|
233
|
+
요청하신 ID: {projectId}에 해당하는 프로젝트가 존재하지 않습니다.
|
234
|
+
</p>
|
235
|
+
</CardContent>
|
236
|
+
</Card>
|
237
|
+
)}
|
238
|
+
</div>
|
239
|
+
);
|
240
|
+
}
|
@@ -0,0 +1,137 @@
|
|
1
|
+
'use client';
|
2
|
+
|
3
|
+
import React from 'react';
|
4
|
+
import { useQueryProjectList } from '@/api/project/use-query-project-list';
|
5
|
+
import {
|
6
|
+
Card,
|
7
|
+
CardContent,
|
8
|
+
CardDescription,
|
9
|
+
CardFooter,
|
10
|
+
CardHeader,
|
11
|
+
CardTitle,
|
12
|
+
} from '@workspace/ui/components/card';
|
13
|
+
import {
|
14
|
+
Table,
|
15
|
+
TableBody,
|
16
|
+
TableCell,
|
17
|
+
TableHead,
|
18
|
+
TableHeader,
|
19
|
+
TableRow,
|
20
|
+
} from '@workspace/ui/components/table';
|
21
|
+
import { Badge } from '@workspace/ui/components/badge';
|
22
|
+
import { Button } from '@workspace/ui/components/button';
|
23
|
+
import { Skeleton } from '@workspace/ui/components/skeleton';
|
24
|
+
import dayjs from 'dayjs';
|
25
|
+
import relativeTime from 'dayjs/plugin/relativeTime';
|
26
|
+
import 'dayjs/locale/ko';
|
27
|
+
import { RefreshCw, ArrowRight, Calendar, Plus } from 'lucide-react';
|
28
|
+
import { useRouter } from 'next/navigation';
|
29
|
+
import { formatDateFromNow } from '@/utils/date';
|
30
|
+
import { CreateProject } from './create-project';
|
31
|
+
import { routes } from '@/constants/routes';
|
32
|
+
|
33
|
+
dayjs.extend(relativeTime);
|
34
|
+
dayjs.locale('ko');
|
35
|
+
|
36
|
+
export default function ProjectList() {
|
37
|
+
const router = useRouter();
|
38
|
+
const { data: projectList, isLoading, refetch } = useQueryProjectList();
|
39
|
+
|
40
|
+
return (
|
41
|
+
<Card className="w-full">
|
42
|
+
<CardHeader className="flex flex-row items-center justify-between">
|
43
|
+
<div>
|
44
|
+
<CardTitle className="text-2xl font-bold">프로젝트 목록</CardTitle>
|
45
|
+
<CardDescription>
|
46
|
+
전체 {projectList?.length || 0}개의 프로젝트가 있습니다
|
47
|
+
</CardDescription>
|
48
|
+
</div>
|
49
|
+
|
50
|
+
<CreateProject />
|
51
|
+
</CardHeader>
|
52
|
+
<CardContent>
|
53
|
+
{isLoading ? (
|
54
|
+
<div className="space-y-4">
|
55
|
+
<Skeleton className="h-10 w-full" />
|
56
|
+
<Skeleton className="h-10 w-full" />
|
57
|
+
<Skeleton className="h-10 w-full" />
|
58
|
+
<Skeleton className="h-10 w-full" />
|
59
|
+
</div>
|
60
|
+
) : (
|
61
|
+
<Table>
|
62
|
+
<TableHeader>
|
63
|
+
<TableRow>
|
64
|
+
<TableHead className="w-16">ID</TableHead>
|
65
|
+
<TableHead>프로젝트명</TableHead>
|
66
|
+
{/* <TableHead>API 키</TableHead> */}
|
67
|
+
<TableHead>설명</TableHead>
|
68
|
+
<TableHead>생성일</TableHead>
|
69
|
+
<TableHead>수정일</TableHead>
|
70
|
+
<TableHead className="text-right">작업</TableHead>
|
71
|
+
</TableRow>
|
72
|
+
</TableHeader>
|
73
|
+
<TableBody>
|
74
|
+
{projectList?.map((project) => (
|
75
|
+
<TableRow key={project.id}>
|
76
|
+
<TableCell className="font-medium">{project.id}</TableCell>
|
77
|
+
<TableCell className="font-medium">{project.name}</TableCell>
|
78
|
+
{/* <TableCell>
|
79
|
+
<Badge variant="outline" className="font-mono">
|
80
|
+
{project.apiKey}
|
81
|
+
</Badge>
|
82
|
+
</TableCell> */}
|
83
|
+
<TableCell className="max-w-xs truncate">
|
84
|
+
{project.description || '설명 없음'}
|
85
|
+
</TableCell>
|
86
|
+
<TableCell className="text-sm text-muted-foreground">
|
87
|
+
<div className="flex items-center gap-2">
|
88
|
+
<Calendar size={14} />
|
89
|
+
{formatDateFromNow(project.createdAt)}
|
90
|
+
</div>
|
91
|
+
</TableCell>
|
92
|
+
<TableCell className="text-sm text-muted-foreground">
|
93
|
+
<div className="flex items-center gap-2">
|
94
|
+
<Calendar size={14} />
|
95
|
+
{formatDateFromNow(project.updatedAt)}
|
96
|
+
</div>
|
97
|
+
</TableCell>
|
98
|
+
<TableCell className="flex justify-end">
|
99
|
+
<Button
|
100
|
+
variant="outline"
|
101
|
+
size="sm"
|
102
|
+
className="flex items-center gap-1"
|
103
|
+
onClick={() => {
|
104
|
+
router.push(routes.project.detail(project.id));
|
105
|
+
}}
|
106
|
+
>
|
107
|
+
상세보기
|
108
|
+
<ArrowRight size={14} />
|
109
|
+
</Button>
|
110
|
+
</TableCell>
|
111
|
+
</TableRow>
|
112
|
+
))}
|
113
|
+
{(!projectList || projectList.length === 0) && (
|
114
|
+
<TableRow>
|
115
|
+
<TableCell colSpan={7} className="h-24 text-center">
|
116
|
+
프로젝트가 없습니다. 새 프로젝트를 생성해보세요.
|
117
|
+
</TableCell>
|
118
|
+
</TableRow>
|
119
|
+
)}
|
120
|
+
</TableBody>
|
121
|
+
</Table>
|
122
|
+
)}
|
123
|
+
</CardContent>
|
124
|
+
<CardFooter className="flex justify-end">
|
125
|
+
<Button
|
126
|
+
variant="outline"
|
127
|
+
size="sm"
|
128
|
+
className="flex items-center gap-2"
|
129
|
+
onClick={() => refetch()}
|
130
|
+
>
|
131
|
+
<RefreshCw size={14} />
|
132
|
+
새로고침
|
133
|
+
</Button>
|
134
|
+
</CardFooter>
|
135
|
+
</Card>
|
136
|
+
);
|
137
|
+
}
|
@@ -0,0 +1,25 @@
|
|
1
|
+
'use client';
|
2
|
+
|
3
|
+
import * as React from 'react';
|
4
|
+
import { ThemeProvider as NextThemesProvider } from 'next-themes';
|
5
|
+
import QueryClientProvider from '@/providers/query-client-provider';
|
6
|
+
import TelemetryProvider from '@/providers/telemetry-provider';
|
7
|
+
import FlagProvider from '@/providers/flag-provider';
|
8
|
+
|
9
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
10
|
+
return (
|
11
|
+
<NextThemesProvider
|
12
|
+
attribute="class"
|
13
|
+
defaultTheme="system"
|
14
|
+
enableSystem
|
15
|
+
disableTransitionOnChange
|
16
|
+
enableColorScheme
|
17
|
+
>
|
18
|
+
<TelemetryProvider>
|
19
|
+
{/* <FlagProvider> */}
|
20
|
+
<QueryClientProvider>{children}</QueryClientProvider>
|
21
|
+
{/* </FlagProvider> */}
|
22
|
+
</TelemetryProvider>
|
23
|
+
</NextThemesProvider>
|
24
|
+
);
|
25
|
+
}
|