https://www.youtube.com/watch?v=Av9C7xlV0fA
04:07:07
프로젝트 구조
src/
├── app/
│ ├── (dashboard)/
│ │ └── page.tsx # 메인 대시보드 페이지
│ └── api/
│ └── [[...route]]/
│ └── route.ts # API 라우트 통합 관리
├── config.ts # 환경변수 중앙 관리
├── features/
│ └── workspaces/
│ ├── api/ ### 클라이언트 API 호출
│ │ └── use-create-workspace.ts # 클라이언트 API 호출 훅
│ ├── component/ ### UI 컴포넌트
│ │ └── create-workspace-form.tsx # Workspace 생성 폼 UI
│ ├── schemas.ts ### 데이터 검증 스키마
│ └── server/
│ └── route.ts ### 서버사이드 API 라우트
└── lib/
├── rpc.ts # RPC 클라이언트 설정
└── session-middleware.ts # 세션 미들웨어
1. Appwrite 데이터베이스 설정 https://cloud.appwrite.io/
★ 데이터베이스 생성 과정
1. Appwrite 콘솔에서 jira-clone 데이터베이스 생성

.env.local
NEXT_PUBLIC_APP_URL=***
NEXT_PUBLIC_APPWRITE_ENDPOINT=***
NEXT_PUBLIC_APPWRITE_PROJECT=***
NEXT_PUBLIC_APPWRITE_DATABASE_ID=여기추가
NEXT_APPWRITE_KEY=***
2. workspaces 테이블 생성 (기존: Collection)

3. jira-clone Databases 에서 Create table -> workspaces table을 생성
Appwrite 데이터베이스 업데이트로 인해 용어가 변경되었다.
기존의 Collection이 Table로,
Document는 Row,
Attribute는 Column으로 변경되었다.
변경된 용어로 테이블(구: 컬렉션)을 만드는 현재 절차.
- 데이터베이스 페이지 이동: Appwrite 콘솔에서 Databases.
- 데이터베이스 선택: 테이블을 생성할 데이터베이스를 선택.
- 테이블 생성: Create table 버튼을 클릭.
- 컬럼(Columns) 생성: 테이블이 만들어지면, Columns 탭으로 이동하여 Create column 버튼을 클릭해 원하는 데이터 필드(구: 속성)를 추가.
- 권한(Permissions) 설정: Settings 탭의 Permissions에서 역할(Role)을 추가하고 CREATE, READ 등의 권한을 설정.

.env.local
NEXT_PUBLIC_APP_URL=***
NEXT_PUBLIC_APPWRITE_ENDPOINT=***
NEXT_PUBLIC_APPWRITE_PROJECT=***
NEXT_PUBLIC_APPWRITE_DATABASE_ID=***
NEXT_PUBLIC_APPWRITE_WORKSPACES_ID=workspaces // 이건뭐지 뭐가 바뀐거지
NEXT_APPWRITE_KEY=***
4. 필수 컬럼 추가:
- name (size: 256)
- userId (size: 100)

5. 권한 설정: Settings → Permissions에서 CREATE, READ 권한 설정

2. src/config.ts - 중앙집중식 설정 관리
왜 별도의 config 파일을 만드나?
- 중앙집중화: 모든 설정을 한 곳에서 관리
- 타입 안정성: TypeScript에서 환경변수의 존재를 보장
- 재사용성: 여러 파일에서 동일한 설정을 쉽게 사용
export const DATABASE_ID = process.env.NEXT_PUBLIC_APPWRITE_DATABASE_ID!;
export const WORKSPACES_ID = process.env.NEXT_PUBLIC_APPWRITE_WORKSPACES_ID!;
3. 데이터 검증 스키마
src/features/workspaces/schemas.ts
- Zod를 사용하여 클라이언트와 서버 양쪽에서 데이터 검증을 수행
import {z} from "zod"
export const createWorkspaceSchema = z.object({
name: z.string().trim().min(1, "Required"),
})
4. API 라우트 구조
API 라우트 통합관리 - src/app/api/[[...route]]/route.ts
- 모듈화: 기능별로 API 라우트를 분리하여 관리
- 확장성: 새로운 기능 추가 시 쉽게 라우트 추가 가능
- 타입 추론: AppType을 통해 클라이언트에서 API 타입 사용
import {Hono} from 'hono'
import {handle} from 'hono/vercel'
import auth from '@/features/auth/server/route'
import workspaces from '@/features/workspaces/server/route'
const app = new Hono().basePath("/api");
const routes = app
.route("/auth", auth)
.route("/workspaces", workspaces)
export const GET = handle(app)
export const POST = handle(app)
export const PUT = handle(app)
export const DELETE = handle(app)
export type AppType = typeof routes
Workspace API - src/features/workspaces/server/route.ts
Hono를 사용한 API 구조.
- 경량화: Express보다 빠르고 가벼운 프레임워크
- Edge Runtime 지원: Vercel Edge Functions에서 실행 가능
- 타입 안전성: 엔드 투 엔드 타입 안전성 제공
import { zValidator } from "@hono/zod-validator";
import { Hono } from "hono";
import { createWorkspaceSchema } from "../schemas";
import { sessionMiddleware } from "@/lib/session-middleware";
import { DATABASE_ID, WORKSPACES_ID } from "@/config";
import { ID } from "node-appwrite";
const app = new Hono()
.post(
"/",
zValidator("json", createWorkspaceSchema),
sessionMiddleware,
async (c) => {
const databases = c.get("databases")
const user = c.get("user")
const {name} = c.req.valid("json")
const workspace = await databases.createDocument(
DATABASE_ID,
WORKSPACES_ID,
ID.unique(),
{
name,
userId:user.$id
}
);
return c.json({data:workspace})
}
)
export default app
5. 클라이언트 API 호출
src/features/workspaces/api/use-create-workspace.ts
React Query(TanStack Query)를 사용하는 이유:
- 캐싱: API 응답을 자동으로 캐시하여 성능 향상
- 동기화: 데이터 변경 시 관련 쿼리를 자동으로 무효화
- 로딩 상태: 로딩, 에러, 성공 상태를 쉽게 관리
import {useMutation, useQueryClient} from '@tanstack/react-query'
import { InferRequestType, InferResponseType } from 'hono';
import {client} from '@/lib/rpc'
type ResponseType = InferResponseType<typeof client.api.workspaces['$post']>
type RequestType = InferRequestType<typeof client.api.workspaces['$post']>
export const useCreateWorkspace = () => {
const queryClient = useQueryClient()
const mutation = useMutation<
ResponseType,
Error,
RequestType
>({
mutationFn: async({json}) => {
const response = await client.api.workspaces["$post"]({json});
return response.json()
},
onSuccess: () => {
queryClient.invalidateQueries({queryKey : ['workspaces']})
}
})
return mutation
}
6. 사용자 인터페이스
create-workspace-form.tsx
'use client'
import { zodResolver } from "@hookform/resolvers/zod";
import { createWorkspaceSchema } from "../schemas";
import { useForm } from "react-hook-form";
import z from "zod";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import DottedSeparator from "@/components/dottedSeparator";
interface CreateWorkspaceFormProps{
onCancel?: () => void;
}
export const CreateWorkspaceForm = ({onCancel} : CreateWorkspaceFormProps) =>{
const form = useForm<z.infer<typeof createWorkspaceSchema>>({
resolver: zodResolver(createWorkspaceSchema),
defaultValues:{
name: ''
}
})
const onSubmit = (values: z.infer<typeof createWorkspaceSchema>) => {
console.log({values})
}
return(
<Card className="w-full h-full border-none shadow-none">
<CardHeader className="flex p-7">
<CardTitle className="text-xl font-bold">
Create a new Workspace
</CardTitle>
</CardHeader>
<div className="px=7">
<DottedSeparator/>
</div>
<CardContent className="p-7">
form 영역이 들어갈 자리
</CardContent>
</Card>
)
}
src/app/(dashboard)/page.tsx - 메인페이지
import { redirect } from "next/navigation";
import { getCurrent } from "@/features/auth/actions";
import { CreateWorkspaceForm } from "@/features/workspaces/component/create-workspace-form";
export default async function Home() {
const user = await getCurrent();
if(!user) redirect("/sign-in")
return (
<div className="bg-neutral-500 p-4 h-full">
<CreateWorkspaceForm/>
</div>
);
}

7. React Hook Form과 Zod를 결합하여 타입 안전한 폼을 구현:
src/features/workspaces/component/create-workspace-form.tsx
'use client'
import { zodResolver } from "@hookform/resolvers/zod";
import { createWorkspaceSchema } from "../schemas";
import { useForm } from "react-hook-form";
import z from "zod";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { useCreateWorkspace } from "../api/use-create-workspace";
import DottedSeparator from "@/components/dottedSeparator";
interface CreateWorkspaceFormProps{
onCancel?: () => void;
}
export const CreateWorkspaceForm = ({onCancel} : CreateWorkspaceFormProps) =>{
const {mutate, isPending} = useCreateWorkspace();
const form = useForm<z.infer<typeof createWorkspaceSchema>>({
resolver: zodResolver(createWorkspaceSchema),
defaultValues:{
name: ''
}
})
const onSubmit = (values: z.infer<typeof createWorkspaceSchema>) => {
mutate({json:values})
}
return(
<Card className="w-full h-full border-none shadow-none">
<CardHeader className="flex p-7">
<CardTitle className="text-xl font-bold">
Create a new Workspace
</CardTitle>
</CardHeader>
<div className="px=7">
<DottedSeparator/>
</div>
<CardContent className="p-7">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="flex flex-col gap-y-4">
<FormField
control={form.control}
name="name"
render={({field})=>(
<FormItem>
<FormLabel>Workspace Name</FormLabel>
<FormControl>
<Input
{...field}
placeholder="Enter Workstace name"
/>
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
</div>
<DottedSeparator className="py-7"/>
<div className="flex items-center justify-between">
<Button
type="button"
size="lg"
variant='secondary'
onClick={onCancel}
disabled={isPending}
>
Cancel
</Button>
<Button
type="submit"
size="lg"
disabled={isPending}
>
Create Workstace
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
)
}


'Clone Coding' 카테고리의 다른 글
| Jira-clone - image 업로드 (0) | 2025.09.29 |
|---|---|
| Jira-clone - Toaster 알림 라이브러리사용 (0) | 2025.09.27 |
| Jira-Clone - 사이드바 (0) | 2025.09.25 |
| Jira-Clone - 로그인, 로그아웃시 자동 리다이렉트 (0) | 2025.09.24 |
| Jira-Clone 사용자버튼 만들기 (0) | 2025.09.21 |