![[인프런 워밍업 클럽 Full Stack 3기] 2주차 Dropbox](https://cdn.inflearn.com/public/files/blogs/3c82581a-ad7c-4286-9411-1e9b091d86d7/inflearn.png)
[인프런 워밍업 클럽 Full Stack 3기] 2주차 Dropbox
16일 전
Part 1. Git Repository 생성 및 초기 설정 진행
초기 코드 생성
npx create-next-app@latest inflearn-supabase-dropbox-clone
cd inflearn-supabase-dropbox-clone
TODO List 코드 복사
config/*
app/layout.tsx
,app/middleware.tsx
,app/global.css
components/material-tailwind-theme-provider.tsx
utils/*
package.jsontailwind.config.ts, tsconfig.json , .env
Part 2. UI 작업
app/page.tsx
import UI from "./ui";
export const metadata = {
title: "Minibox",
description: "A minimalistic Dropbox clone",
};
export default function Home() {
return <UI />;
}
app/ui.tsx
"use client";
import DropboxImageList from "components/dropbox-image-list";
import FileDragDropZone from "components/file-dragdropzone";
import Logo from "components/logo";
import SearchComponent from "components/search-component";
import Image from "next/image";
import { useState } from "react";
export default function UI() {
const [searchInput, setSearchInput] = useState("");
return (
<main className="w-full p-2 flex flex-col gap-4">
{/* Logo */}
<Logo />
{/* Search Component */}
<SearchComponent
searchInput={searchInput}
setSearchInput={setSearchInput}
/>
{/* File Drag&Drop Zone */}
<FileDragDropZone />
{/* Dropbox Image List */}
<DropboxImageList />
</main>
);
}
components/dropbox-image-list.tsx
"use client";
import DropboxImage from "./dropbox-image";
export default function DropboxImageList() {
return (
<section className="grid md:grid-cols-3 lg:grid-cols-4 grid-cols-2">
<DropboxImage />
<DropboxImage />
<DropboxImage />
<DropboxImage />
</section>
);
}
components/dropbox-image.tsx
"use client";
import { IconButton } from "@material-tailwind/react";
export default function DropboxImage() {
return (
<div className="relative w-full flex flex-col gap-2 p-4 border border-gray-100 rounded-2xl shadow-md">
{/* Image */}
<div>
<img
src="/images/cutedog.jpeg"
className="w-full aspect-square rounded-2xl"
/>
</div>
{/* FileName */}
<div className="">cutedog.jpeg</div>
<div className="absolute top-4 right-4">
<IconButton onClick={() => {}} color="red">
<i className="fas fa-trash" />
</IconButton>
</div>
</div>
);
}
components/file-dragdropzone.tsx
"use client";
export default function FileDragDropZone() {
return (
<section className="w-full py-20 border-4 border-dotted border-indigo-700 flex flex-col items-center justify-center">
<input type="file" className="" />
<p>파일을 여기에 끌어다 놓거나 클릭하여 업로드하세요.</p>
</section>
);
}
components/logo.tsx
"use client";
import Image from "next/image";
export default function Logo() {
return (
<div className="flex items-center gap-1">
<Image
src="/images/dropbox_icon.png"
alt="Mini Dropbox Logo"
width={50}
height={30}
className="!w-8 !h-auto"
/>
<span className="text-xl font-bold">Minibox</span>
</div>
);
}
components/search-component.tsx
"use client";
import { Input } from "@material-tailwind/react";
import { useState } from "react";
export default function SearchComponent({ searchInput, setSearchInput }) {
return (
<Input
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
label="Search Images"
icon={<i className="fa-solid fa-magnifying-glass" />}
/>
);
}
Part 3. Supabase Storage 설명 & 파일 업로드 구현
Supabase Storage Bucket 생성 및 CRUD Policy 추가
액션 함수 구현
actions/storageActions.tsx
"use server";
import { createServerSupabaseClient } from "utils/supabase/server";
function handleError(error) {
if (error) {
console.error(error);
throw error;
}
}
export async function uploadFile(formData: FormData) {
const supabase = await createServerSupabaseClient();
const file = formData.get("file") as File;
const { data, error } = await supabase.storage
.from(process.env.NEXT_PUBLIC_STORAGE_BUCKET)
.upload(file.name, file, { upsert: true });
handleError(error);
return data;
}
export async function searchFiles(search: string = "") {
const supabase = await createServerSupabaseClient();
const { data, error } = await supabase.storage
.from(process.env.NEXT_PUBLIC_STORAGE_BUCKET)
.list(null, {
search,
});
handleError(error);
return data;
}
components/file-dragdropzone.tsx
"use client";
import { Button } from "@material-tailwind/react";
import { useMutation } from "@tanstack/react-query";
import { uploadFile } from "actions/storageActions";
import { queryClient } from "config/ReactQueryClientProvider";
import { useRef } from "react";
export default function FileDragDropZone() {
const fileRef = useRef(null);
const uploadImageMutation = useMutation({
mutationFn: uploadFile,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["images"],
});
},
});
return (
<form
onSubmit={async (e) => {
e.preventDefault();
const file = fileRef.current.files?.[0];
if (file) {
const formData = new FormData();
formData.append("file", file);
const result = await uploadImageMutation.mutate(formData);
console.log(result);
}
}}
className="w-full py-20 border-4 border-dotted border-indigo-700 flex flex-col items-center justify-center"
>
<input ref={fileRef} type="file" className="" />
<p>파일을 여기에 끌어다 놓거나 클릭하여 업로드하세요.</p>
<Button loading={uploadImageMutation.isPending} type="submit">
파일 업로드
</Button>
</form>
);
}
Part 4. 필수 라이브러리 설정 - React Query, Supabase
라이브러리 추가 설치
npm i --save @supabase/ssr @tanstack/react-query
React Query 설정
config/ReactQueryClientProvider.tsx
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
export const queryClient = new QueryClient({});
export default function ReactQueryClientProvider({
children,
}: React.PropsWithChildren) {
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}
app/layout.tsx
import ReactQueryClientProvider from "config/ReactQueryClientProvider";
export default function RootLayout({ children }) {
return (
<ReactQueryClientProvider>
...
</ReactQueryClientProvider>
)
}
Supabase 설정.env
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
NEXT_SUPABASE_SERVICE_ROLE=
NEXT_SUPABASE_DB_PASSWORD=
utils/supabase/client.ts
"use client";
import { createBrowserClient } from "@supabase/ssr";
export const createBrowserSupabaseClient = () =>
createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
utils/supabase/server.ts
"use server";
import { createServerClient, type CookieOptions } from "@supabase/ssr";
import { cookies } from "next/headers";
import { Database } from "types_db";
export const createServerSupabaseClient = async (
cookieStore: ReturnType<typeof cookies> = cookies(),
admin: boolean = false
) => {
return createServerClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
admin
? process.env.NEXT_SUPABASE_SERVICE_ROLE!
: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value;
},
set(name: string, value: string, options: CookieOptions) {
try {
cookieStore.set({ name, value, ...options });
} catch (error) {
// The `set` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
remove(name: string, options: CookieOptions) {
try {
cookieStore.set({ name, value: "", ...options });
} catch (error) {
// The `delete` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
},
}
);
};
export const createServerSupabaseAdminClient = async (
cookieStore: ReturnType<typeof cookies> = cookies()
) => {
return createServerSupabaseClient(cookieStore, true);
};
app/middleware.ts
import { createServerClient, type CookieOptions } from "@supabase/ssr";
import { type NextRequest, NextResponse } from "next/server";
export const applyMiddlewareSupabaseClient = async (request: NextRequest) => {
// Create an unmodified response
let response = NextResponse.next({
request: {
headers: request.headers,
},
});
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return request.cookies.get(name)?.value;
},
set(name: string, value: string, options: CookieOptions) {
// If the cookie is updated, update the cookies for the request and response
request.cookies.set({
name,
value,
...options,
});
response = NextResponse.next({
request: {
headers: request.headers,
},
});
response.cookies.set({
name,
value,
...options,
});
},
remove(name: string, options: CookieOptions) {
// If the cookie is removed, update the cookies for the request and response
request.cookies.set({
name,
value: "",
...options,
});
response = NextResponse.next({
request: {
headers: request.headers,
},
});
response.cookies.set({
name,
value: "",
...options,
});
},
},
}
);
// refreshing the auth token
await supabase.auth.getUser();
return response;
};
export async function middleware(request) {
return await applyMiddlewareSupabaseClient(request);
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* Feel free to modify this pattern to include more paths.
*/
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
],
};
Part 5. 할일 CRUD 기능 구현 (feat. Server Action)
"use server";
import { Database } from "types_db";
import { createServerSupabaseClient } from "utils/supabase/server";
export type TodoRow = Database["public"]["Tables"]["todo"]["Row"];
export type TodoRowInsert = Database["public"]["Tables"]["todo"]["Insert"];
export type TodoRowUpdate = Database["public"]["Tables"]["todo"]["Update"];
function handleError(error) {
console.error(error);
throw new Error(error.message);
}
export async function getTodos({ searchInput = "" }): Promise<TodoRow[]> {
const supabase = await createServerSupabaseClient();
const { data, error } = await supabase
.from("todo")
.select("*")
.like("title", `%${searchInput}%`)
.order("created_at", { ascending: true });
if (error) {
handleError(error);
}
return data;
}
export async function createTodo(todo: TodoRowInsert) {
const supabase = await createServerSupabaseClient();
const { data, error } = await supabase.from("todo").insert({
...todo,
created_at: new Date().toISOString(),
});
if (error) {
handleError(error);
}
return data;
}
export async function updateTodo(todo: TodoRowUpdate) {
const supabase = await createServerSupabaseClient();
console.log(todo);
const { data, error } = await supabase
.from("todo")
.update({
...todo,
updated_at: new Date().toISOString(),
})
.eq("id", todo.id);
if (error) {
handleError(error);
}
return data;
}
export async function deleteTodo(id: number) {
const supabase = await createServerSupabaseClient();
const { data, error } = await supabase.from("todo").delete().eq("id", id);
if (error) {
handleError(error);
}
return data;
}
댓글을 작성해보세요.