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.
Files changed (141) hide show
  1. package/.eslintrc.js +10 -0
  2. package/.vscode/settings.json +3 -0
  3. package/README.md +92 -0
  4. package/apps/web/README.md +11 -0
  5. package/apps/web/api/auth/keys.ts +3 -0
  6. package/apps/web/api/auth/types.ts +25 -0
  7. package/apps/web/api/auth/use-query-profile.ts +19 -0
  8. package/apps/web/api/auth/use-sign-in.ts +24 -0
  9. package/apps/web/api/axios.ts +122 -0
  10. package/apps/web/api/error-code.ts +36 -0
  11. package/apps/web/api/event/keys.ts +14 -0
  12. package/apps/web/api/event/types.ts +91 -0
  13. package/apps/web/api/event/use-mutation-event-assignee.ts +103 -0
  14. package/apps/web/api/event/use-mutation-event-priority.ts +97 -0
  15. package/apps/web/api/event/use-mutation-event-status.ts +198 -0
  16. package/apps/web/api/event/use-query-event-detail.ts +25 -0
  17. package/apps/web/api/event/use-query-event-list.ts +42 -0
  18. package/apps/web/api/health-check/index.ts +21 -0
  19. package/apps/web/api/project/keys.ts +4 -0
  20. package/apps/web/api/project/types.ts +28 -0
  21. package/apps/web/api/project/use-create-project.ts +30 -0
  22. package/apps/web/api/project/use-query-project-list.ts +19 -0
  23. package/apps/web/api/project/use-query-project-users.ts +23 -0
  24. package/apps/web/api/types.ts +44 -0
  25. package/apps/web/app/(auth)/layout.tsx +5 -0
  26. package/apps/web/app/(auth)/sign-in/page.tsx +20 -0
  27. package/apps/web/app/(auth)/sign-up/page.tsx +5 -0
  28. package/apps/web/app/(project)/project/[project_id]/issues/[issue_id]/page.tsx +17 -0
  29. package/apps/web/app/(project)/project/[project_id]/issues/page.tsx +15 -0
  30. package/apps/web/app/(project)/project/[project_id]/page.tsx +10 -0
  31. package/apps/web/app/(project)/project/page.tsx +10 -0
  32. package/apps/web/app/(report)/error-list/page.tsx +7 -0
  33. package/apps/web/app/favicon.ico +0 -0
  34. package/apps/web/app/layout.tsx +35 -0
  35. package/apps/web/app/page.tsx +3 -0
  36. package/apps/web/components/.gitkeep +0 -0
  37. package/apps/web/components/event-list/event-detail.tsx +242 -0
  38. package/apps/web/components/event-list/event-list.tsx +376 -0
  39. package/apps/web/components/event-list/preview.tsx +573 -0
  40. package/apps/web/components/layouts/default-layout.tsx +59 -0
  41. package/apps/web/components/login-form.tsx +124 -0
  42. package/apps/web/components/project/create-project.tsx +130 -0
  43. package/apps/web/components/project/hooks/use-get-event-params.ts +9 -0
  44. package/apps/web/components/project/hooks/use-get-project-params.ts +10 -0
  45. package/apps/web/components/project/project-detail.tsx +240 -0
  46. package/apps/web/components/project/project-list.tsx +137 -0
  47. package/apps/web/components/providers.tsx +25 -0
  48. package/apps/web/components/ui/assignee-dropdown.tsx +176 -0
  49. package/apps/web/components/ui/event-status-dropdown.tsx +104 -0
  50. package/apps/web/components/ui/priority-dropdown.tsx +123 -0
  51. package/apps/web/components/widget/app-sidebar.tsx +225 -0
  52. package/apps/web/components/widget/nav-main.tsx +73 -0
  53. package/apps/web/components/widget/nav-projects.tsx +84 -0
  54. package/apps/web/components/widget/nav-user.tsx +113 -0
  55. package/apps/web/components.json +20 -0
  56. package/apps/web/constants/routes.ts +12 -0
  57. package/apps/web/eslint.config.js +4 -0
  58. package/apps/web/hooks/use-boolean-state.ts +13 -0
  59. package/apps/web/lib/.gitkeep +0 -0
  60. package/apps/web/next-env.d.ts +5 -0
  61. package/apps/web/next.config.mjs +6 -0
  62. package/apps/web/package.json +60 -0
  63. package/apps/web/postcss.config.mjs +1 -0
  64. package/apps/web/providers/flag-provider.tsx +35 -0
  65. package/apps/web/providers/query-client-provider.tsx +17 -0
  66. package/apps/web/providers/telemetry-provider.tsx +12 -0
  67. package/apps/web/tsconfig.json +24 -0
  68. package/apps/web/utils/avatar.ts +26 -0
  69. package/apps/web/utils/date.ts +26 -0
  70. package/apps/web/utils/front-end-tracer.ts +119 -0
  71. package/apps/web/utils/schema/project.schema.ts +12 -0
  72. package/apps/web/utils/span-processor.ts +36 -0
  73. package/package.json +21 -0
  74. package/packages/eslint-config/README.md +3 -0
  75. package/packages/eslint-config/base.js +32 -0
  76. package/packages/eslint-config/next.js +51 -0
  77. package/packages/eslint-config/package.json +25 -0
  78. package/packages/eslint-config/react-internal.js +41 -0
  79. package/packages/rusty-replay/README.md +165 -0
  80. package/packages/rusty-replay/package.json +67 -0
  81. package/packages/rusty-replay/src/environment.ts +27 -0
  82. package/packages/rusty-replay/src/error-batcher.ts +75 -0
  83. package/packages/rusty-replay/src/front-end-tracer.ts +86 -0
  84. package/packages/rusty-replay/src/handler.ts +37 -0
  85. package/packages/rusty-replay/src/index.ts +8 -0
  86. package/packages/rusty-replay/src/recorder.ts +71 -0
  87. package/packages/rusty-replay/src/reporter.ts +115 -0
  88. package/packages/rusty-replay/src/utils.ts +13 -0
  89. package/packages/rusty-replay/tsconfig.build.json +13 -0
  90. package/packages/rusty-replay/tsconfig.json +27 -0
  91. package/packages/rusty-replay/tsup.config.ts +39 -0
  92. package/packages/typescript-config/README.md +3 -0
  93. package/packages/typescript-config/base.json +20 -0
  94. package/packages/typescript-config/nextjs.json +13 -0
  95. package/packages/typescript-config/package.json +9 -0
  96. package/packages/typescript-config/react-library.json +8 -0
  97. package/packages/ui/components.json +20 -0
  98. package/packages/ui/eslint.config.js +4 -0
  99. package/packages/ui/package.json +60 -0
  100. package/packages/ui/postcss.config.mjs +6 -0
  101. package/packages/ui/src/components/.gitkeep +0 -0
  102. package/packages/ui/src/components/avatar.tsx +53 -0
  103. package/packages/ui/src/components/badge.tsx +46 -0
  104. package/packages/ui/src/components/breadcrumb.tsx +109 -0
  105. package/packages/ui/src/components/button.tsx +59 -0
  106. package/packages/ui/src/components/calendar.tsx +75 -0
  107. package/packages/ui/src/components/calendars/date-picker.tsx +43 -0
  108. package/packages/ui/src/components/calendars/date-range-picker.tsx +79 -0
  109. package/packages/ui/src/components/card.tsx +92 -0
  110. package/packages/ui/src/components/checkbox.tsx +32 -0
  111. package/packages/ui/src/components/collapsible.tsx +33 -0
  112. package/packages/ui/src/components/dialog.tsx +135 -0
  113. package/packages/ui/src/components/dialogs/confirmation-modal.tsx +216 -0
  114. package/packages/ui/src/components/dropdown-menu.tsx +261 -0
  115. package/packages/ui/src/components/input.tsx +30 -0
  116. package/packages/ui/src/components/label.tsx +24 -0
  117. package/packages/ui/src/components/login-form.tsx +68 -0
  118. package/packages/ui/src/components/mode-switcher.tsx +34 -0
  119. package/packages/ui/src/components/popover.tsx +48 -0
  120. package/packages/ui/src/components/scroll-area.tsx +58 -0
  121. package/packages/ui/src/components/select.tsx +185 -0
  122. package/packages/ui/src/components/separator.tsx +28 -0
  123. package/packages/ui/src/components/sheet.tsx +139 -0
  124. package/packages/ui/src/components/sidebar.tsx +726 -0
  125. package/packages/ui/src/components/skeleton.tsx +13 -0
  126. package/packages/ui/src/components/sonner.tsx +25 -0
  127. package/packages/ui/src/components/table.tsx +116 -0
  128. package/packages/ui/src/components/tabs.tsx +66 -0
  129. package/packages/ui/src/components/team-switcher.tsx +91 -0
  130. package/packages/ui/src/components/textarea.tsx +18 -0
  131. package/packages/ui/src/components/tooltip.tsx +61 -0
  132. package/packages/ui/src/hooks/.gitkeep +0 -0
  133. package/packages/ui/src/hooks/use-meta-color.ts +28 -0
  134. package/packages/ui/src/hooks/use-mobile.ts +19 -0
  135. package/packages/ui/src/lib/utils.ts +6 -0
  136. package/packages/ui/src/styles/globals.css +138 -0
  137. package/packages/ui/tsconfig.json +13 -0
  138. package/packages/ui/tsconfig.lint.json +8 -0
  139. package/pnpm-workspace.yaml +4 -0
  140. package/tsconfig.json +4 -0
  141. 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&apos;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,9 @@
1
+ import { useParams } from 'next/navigation';
2
+
3
+ export function useGetEventParams() {
4
+ const { issue_id: eventId } = useParams<{
5
+ issue_id: string;
6
+ }>();
7
+
8
+ return { eventId: eventId ? parseInt(eventId) : undefined };
9
+ }
@@ -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
+ }