-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Feat] 프로젝트 생성 기능 구현 #94
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import { useForm } from 'react-hook-form'; | ||
import { zodResolver } from '@hookform/resolvers/zod'; | ||
import { z } from 'zod'; | ||
import { | ||
Dialog, | ||
DialogContent, | ||
DialogDescription, | ||
DialogHeader, | ||
DialogTitle, | ||
} from '@/components/ui/dialog'; | ||
import { Input } from '@/components/ui/input'; | ||
import { Button } from '@/components/ui/button'; | ||
import { CreateProjectRequestDTO } from '@/types/project'; | ||
|
||
const formSchema = z.object({ | ||
title: z | ||
.string() | ||
.min(1, 'Title is required.') | ||
.regex(/^[a-zA-Z0-9 ]*$/, 'Only English letters and numbers are allowed.'), | ||
}); | ||
|
||
interface CreateProjectDialogProps { | ||
isOpen: boolean; | ||
onOpenChange: (isOpen: boolean) => void; | ||
onSubmit: (data: CreateProjectRequestDTO) => void; | ||
isPending: boolean; | ||
} | ||
|
||
function CreateProjectDialog({ | ||
isOpen, | ||
onOpenChange, | ||
onSubmit, | ||
isPending, | ||
}: CreateProjectDialogProps) { | ||
const { | ||
register, | ||
handleSubmit, | ||
formState: { errors }, | ||
} = useForm<CreateProjectRequestDTO>({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟢 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ㅋㅋㅋ고민하다가 이렇게 했는데, 뭐로 맞출까용 |
||
resolver: zodResolver(formSchema), | ||
}); | ||
|
||
return ( | ||
<Dialog open={isOpen} onOpenChange={onOpenChange}> | ||
<DialogContent className="bg-white"> | ||
<DialogHeader> | ||
<DialogTitle>Create a New Project</DialogTitle> | ||
<DialogDescription>Enter a title to create a new project.</DialogDescription> | ||
</DialogHeader> | ||
<form onSubmit={handleSubmit(onSubmit)}> | ||
<div className="flex flex-col gap-2"> | ||
<label htmlFor="title"> | ||
<Input id="title" type="text" placeholder="Project Title" {...register('title')} /> | ||
{errors.title && <span className="text-sm text-red-500">{errors.title.message}</span>} | ||
</label> | ||
<Button | ||
type="submit" | ||
className="bg-black text-white hover:bg-black/80" | ||
disabled={isPending} | ||
> | ||
{isPending ? 'Creating...' : 'Create'} | ||
</Button> | ||
</div> | ||
</form> | ||
</DialogContent> | ||
</Dialog> | ||
); | ||
} | ||
|
||
export default CreateProjectDialog; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
import * as React from 'react'; | ||
import * as DialogPrimitive from '@radix-ui/react-dialog'; | ||
import { Cross2Icon } from '@radix-ui/react-icons'; | ||
import { cn } from '@/lib/utils'; | ||
|
||
const Dialog = DialogPrimitive.Root; | ||
|
||
const DialogTrigger = DialogPrimitive.Trigger; | ||
|
||
const DialogPortal = DialogPrimitive.Portal; | ||
|
||
const DialogClose = DialogPrimitive.Close; | ||
|
||
const DialogOverlay = React.forwardRef< | ||
React.ElementRef<typeof DialogPrimitive.Overlay>, | ||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> | ||
>(({ className, ...props }, ref) => ( | ||
<DialogPrimitive.Overlay | ||
ref={ref} | ||
className={cn( | ||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/30', | ||
className | ||
)} | ||
{...props} | ||
/> | ||
)); | ||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; | ||
|
||
const DialogContent = React.forwardRef< | ||
React.ElementRef<typeof DialogPrimitive.Content>, | ||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> | ||
>(({ className, children, ...props }, ref) => ( | ||
<DialogPortal> | ||
<DialogOverlay /> | ||
<DialogPrimitive.Content | ||
ref={ref} | ||
className={cn( | ||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg', | ||
className | ||
)} | ||
{...props} | ||
> | ||
{children} | ||
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none"> | ||
<Cross2Icon className="h-4 w-4" /> | ||
<span className="sr-only">Close</span> | ||
</DialogPrimitive.Close> | ||
</DialogPrimitive.Content> | ||
</DialogPortal> | ||
)); | ||
DialogContent.displayName = DialogPrimitive.Content.displayName; | ||
|
||
function DialogHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) { | ||
return ( | ||
<div | ||
className={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)} | ||
{...props} | ||
/> | ||
); | ||
} | ||
DialogHeader.displayName = 'DialogHeader'; | ||
|
||
function DialogFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) { | ||
return ( | ||
<div | ||
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)} | ||
{...props} | ||
/> | ||
); | ||
} | ||
DialogFooter.displayName = 'DialogFooter'; | ||
|
||
const DialogTitle = React.forwardRef< | ||
React.ElementRef<typeof DialogPrimitive.Title>, | ||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title> | ||
>(({ className, ...props }, ref) => ( | ||
<DialogPrimitive.Title | ||
ref={ref} | ||
className={cn('text-lg font-semibold leading-none tracking-tight', className)} | ||
{...props} | ||
/> | ||
)); | ||
DialogTitle.displayName = DialogPrimitive.Title.displayName; | ||
|
||
const DialogDescription = React.forwardRef< | ||
React.ElementRef<typeof DialogPrimitive.Description>, | ||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description> | ||
>(({ className, ...props }, ref) => ( | ||
<DialogPrimitive.Description | ||
ref={ref} | ||
className={cn('text-muted-foreground text-sm', className)} | ||
{...props} | ||
/> | ||
)); | ||
DialogDescription.displayName = DialogPrimitive.Description.displayName; | ||
|
||
export { | ||
Dialog, | ||
DialogPortal, | ||
DialogOverlay, | ||
DialogTrigger, | ||
DialogClose, | ||
DialogContent, | ||
DialogHeader, | ||
DialogFooter, | ||
DialogTitle, | ||
DialogDescription, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import axios from 'axios'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 라우트에서 쿼리 동작을 분리하신 이유가 궁금해요~ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 뭔가 걸려서 다시보니, 양쪽다 있네요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. https://tanstack.com/router/latest/docs/framework/react/guide/external-data-loading#a-more-realistic-example-using-tanstack-query 요것들을 참고했습니다. |
||
import { useState } from 'react'; | ||
import { useMutation, useQueryClient, useSuspenseQuery } from '@tanstack/react-query'; | ||
import { Button } from '@/components/ui/button'; | ||
import { useAuth } from '@/contexts/authContext'; | ||
import TabView from '@/components/TabView'; | ||
import ProjectCard from '@/components/ProjectCard'; | ||
import CreateProjectDialog from '@/components/dialog/CreateProjectDialog'; | ||
import { CreateProjectRequestDTO, GetProjectsResponseDTO } from '@/types/project'; | ||
|
||
function AccountOverview() { | ||
const auth = useAuth(); | ||
const queryClient = useQueryClient(); | ||
const [isDialogOpen, setIsDialogOpen] = useState(false); | ||
const { data: projects } = useSuspenseQuery({ | ||
queryKey: ['projects'], | ||
queryFn: async () => { | ||
try { | ||
const projects = await axios.get<GetProjectsResponseDTO>('/api/projects', { | ||
headers: { | ||
'Content-Type': 'application/json', | ||
Authorization: `Bearer ${auth.accessToken}`, | ||
}, | ||
}); | ||
return projects.data.result; | ||
} catch { | ||
throw new Error('Failed to fetch projects'); | ||
} | ||
}, | ||
}); | ||
const { isPending, mutate } = useMutation({ | ||
mutationFn: async (data: CreateProjectRequestDTO) => { | ||
await axios.post('/api/project', data, { | ||
headers: { | ||
'Content-Type': 'application/json', | ||
Authorization: `Bearer ${auth.accessToken}`, | ||
}, | ||
}); | ||
}, | ||
onSuccess: () => { | ||
queryClient.invalidateQueries({ queryKey: ['projects'] }); | ||
setIsDialogOpen(false); | ||
}, | ||
onError: (error) => { | ||
console.log('Failed to create project', error); | ||
}, | ||
}); | ||
|
||
return ( | ||
<TabView> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❓ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 리뷰가 누락되어있는 것 같은데요!? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 그러네요 서스펜스에 대해 언급했었습니다. |
||
<TabView.Title>Account Overview</TabView.Title> | ||
<TabView.Content> | ||
<div className="mb-6 flex items-center justify-between"> | ||
<div> | ||
<h2 className="text-xl font-medium">Projects</h2> | ||
<p className="mt-1 text-sm text-gray-500"> | ||
The Projects that are associated with your Harmony account. | ||
</p> | ||
</div> | ||
<Button | ||
className="bg-black text-white hover:bg-black/80" | ||
onClick={() => setIsDialogOpen(true)} | ||
> | ||
Create a Project | ||
</Button> | ||
<CreateProjectDialog | ||
isOpen={isDialogOpen} | ||
onOpenChange={setIsDialogOpen} | ||
onSubmit={mutate} | ||
isPending={isPending} | ||
/> | ||
</div> | ||
<div className="flex flex-col gap-2"> | ||
{projects.map((project) => ( | ||
<ProjectCard key={project.id} project={project} /> | ||
))} | ||
</div> | ||
</TabView.Content> | ||
</TabView> | ||
); | ||
} | ||
|
||
export default AccountOverview; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,84 +1,27 @@ | ||
import axios from 'axios'; | ||
import { createFileRoute } from '@tanstack/react-router'; | ||
import { useSuspenseQuery } from '@tanstack/react-query'; | ||
import { Button } from '@/components/ui/button'; | ||
import { useAuth } from '@/contexts/authContext'; | ||
import TabView from '@/components/TabView'; | ||
import ProjectCard from '@/components/ProjectCard'; | ||
|
||
type Project = { | ||
id: number; | ||
title: string; | ||
createdAt: string; | ||
role: string; | ||
}; | ||
|
||
type ProjectResponse = { | ||
status: number; | ||
message: string; | ||
result: Project[]; | ||
}; | ||
import { GetProjectsResponseDTO } from '@/types/project'; | ||
import AccountOverview from '@/pages/AccountOverview'; | ||
|
||
export const Route = createFileRoute('/_auth/account/')({ | ||
loader: ({ context: { auth, queryClient } }) => { | ||
queryClient.ensureQueryData({ | ||
queryKey: ['projects'], | ||
queryFn: async () => { | ||
try { | ||
const projects = await axios.get<ProjectResponse>('/api/projects', { | ||
const projects = await axios.get<GetProjectsResponseDTO>('/api/projects', { | ||
headers: { | ||
'Content-Type': 'application/json', | ||
Authorization: `Bearer ${auth.accessToken}`, | ||
}, | ||
}); | ||
return projects.data.result; | ||
} catch (error) { | ||
} catch { | ||
throw new Error('Failed to fetch projects'); | ||
} | ||
}, | ||
}); | ||
}, | ||
errorComponent: () => <div>Failed to load projects</div>, | ||
component: AccountIndex, | ||
component: AccountOverview, | ||
}); | ||
|
||
function AccountIndex() { | ||
const auth = useAuth(); | ||
const { data: projects } = useSuspenseQuery({ | ||
queryKey: ['projects'], | ||
queryFn: async () => { | ||
try { | ||
const projects = await axios.get<ProjectResponse>('/api/projects', { | ||
headers: { | ||
'Content-Type': 'application/json', | ||
Authorization: `Bearer ${auth.accessToken}`, | ||
}, | ||
}); | ||
return projects.data.result; | ||
} catch (error) { | ||
throw new Error('Failed to fetch projects'); | ||
} | ||
}, | ||
}); | ||
return ( | ||
<TabView> | ||
<TabView.Title>Account Overview</TabView.Title> | ||
<TabView.Content> | ||
<div className="mb-6 flex items-center justify-between"> | ||
<div> | ||
<h2 className="text-xl font-medium">Projects</h2> | ||
<p className="mt-1 text-sm text-gray-500"> | ||
The Projects that are associated with your Harmony account. | ||
</p> | ||
</div> | ||
<Button className="bg-black text-white hover:bg-black/80">Create a Project</Button> | ||
</div> | ||
<div className="flex flex-col gap-2"> | ||
{projects.map((project) => ( | ||
<ProjectCard key={project.id} project={project} /> | ||
))} | ||
</div> | ||
</TabView.Content> | ||
</TabView> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
export interface Project { | ||
id: number; | ||
title: string; | ||
createdAt: string; | ||
role: string; | ||
} | ||
|
||
export interface GetProjectsResponseDTO { | ||
status: number; | ||
message: string; | ||
result: Project[]; | ||
} | ||
|
||
export interface CreateProjectRequestDTO { | ||
title: string; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
영어만 가능! 적용하셨군요!