본문 바로가기
Clone Coding

Jira-clone - image 업로드

by zzuny-code 2025. 9. 29.
반응형

https://www.youtube.com/watch?v=Av9C7xlV0fA

4:37:27

 

핵심 개념

  1. Storage: 실제 파일(이미지) 저장소
  2. Database: 파일의 "위치"(ID)를 기록
  3. FormData: File 객체를 서버로 전송하는 유일한 방법

이미지를 보여줄 때는 imageUrl(파일 ID)로 Storage에서 실제 이미지를 가져오면 됨!

 

 

💡 전체 흐름 요약

1. 사용자가 이미지 선택
   ↓
2. FormData로 서버에 전송
   ↓
3. Zod 스키마 검증 (File 또는 string)
   ↓
4. Storage에 이미지 업로드
   ↓
5. 생성된 파일 ID 반환
   ↓
6. Database에 workspace 생성 (imageUrl 포함)

 

* 스키마 정의 (src/features/workspaces/schemas.ts)

  • File 객체 (새로운 이미지 업로드) 또는
  • string (기존 이미지 URL)을 받을 수 있도록 설정
  • 빈 문자열은 undefined로 변환
import {z} from "zod"

export const createWorkspaceSchema = z.object({
    name: z.string().trim().min(1, "Required"), 
    image : z.union([
        z.instanceof(File),
        z.string().transform((value) => value === "" ? undefined : value),
    ]).optional(),
})

 

src/features/workspaces/server/route.ts

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, image} = 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

 

* Appwrite 설정

Storage Bucket 생성 (이미지 파일들을 저장할 공간)

  • Appwrite Console → Storage → images bucket 생성

Database Column 추가

  • Workspaces collection에 imageUrl 컬럼 추가
  • 업로드된 이미지의 ID를 저장할 필드

 

환경변수 설정

.env.local

...
NEXT_PUBLIC_APPWRITE_IMAGES_ID=여기넣어주기

 

src/config.ts 

export const DATABASE_ID = process.env.NEXT_PUBLIC_APPWRITE_DATABASE_ID!;
export const WORKSPACES_ID = process.env.NEXT_PUBLIC_APPWRITE_WORKSPACES_ID!;
export const IMAGES_BUCKET_ID = process.env.NEXT_PUBLIC_APPWRITE_IMAGES_ID!;

 

 

* 서버 라우트 수정 (src/features/workspaces/server/route.ts)

① JSON → Form 데이터로 변경

  • File 객체는 JSON으로 전송할 수 없고, FormData로만 전송 가능

② Storage 가져오기

  • Appwrite Storage API를 사용하여 파일 업로드

 

동작 순서:

  1. image가 File 객체인지 확인
  2. Storage에 파일 업로드 → 고유 ID 생성됨
  3. 생성된 파일 ID(file.$id)를 uploadedImageUrl에 저장
  4. Database에 workspace 생성 시 imageUrl 필드에 저장
import { zValidator } from "@hono/zod-validator";
import { Hono } from "hono";
import { createWorkspaceSchema } from "../schemas";
import { sessionMiddleware } from "@/lib/session-middleware";
import { DATABASE_ID, IMAGES_BUCKET_ID, WORKSPACES_ID } from "@/config";
import { ID } from "node-appwrite";

const app = new Hono()
    .post(
        "/",
        zValidator("form", createWorkspaceSchema),  ①
        sessionMiddleware,
        async (c) => {
            const databases = c.get("databases")
            const storage = c.get("storage")  ② 
            const user = c.get("user")
            
            const {name, image} = c.req.valid("form") ①

            let uploadedImageUrl: string | undefined;

            console.log('data', image);

            if(image instanceof File){

                // 안토니오 방법
                // const file = await storage.createFile(
                //     IMAGES_BUCKET_ID,
                //     ID.unique(),
                //     image
                // );

                // const arrayBuffer = await storage.getFilePreview(
                //     IMAGES_BUCKET_ID,
                //     file.$id
                // )

                // uploadedImageUrl = `data:image/png;base64,${Buffer.from(arrayBuffer).toString('base64')}`

                
                // 수정된 코드
                if(image instanceof File){
                    // 1. Storage에 파일 업로드
                    const file = await storage.createFile(
                        IMAGES_BUCKET_ID,  // 어디에 저장할지
                        ID.unique(),       // 고유한 파일 ID 생성
                        image              // 업로드할 파일
                    );

                    // 2. 파일 ID를 URL로 저장
                    uploadedImageUrl = file.$id
                }              
            }

            const workspace = await databases.createDocument(
                DATABASE_ID,
                WORKSPACES_ID,
                ID.unique(),
                {
                    name,
                    userId:user.$id,
                    imageUrl: uploadedImageUrl
                }
            );

            
            

            return c.json({data:workspace})
        }
    )

export default app

 

 

⚠️ 주의사항

안토니오 방법 vs 수정된 코드

 
// 안토니오: Base64로 인코딩하여 저장 (무거움)
uploadedImageUrl = `data:image/png;base64,${...}`

// 수정: 파일 ID만 저장 (가벼움, 권장)
uploadedImageUrl = file.$id

 

왜 수정했는가?

  • Base64는 이미지를 문자열로 변환 → 용량 33% 증가
  • 파일 ID만 저장하면 나중에 storage.getFilePreview()로 가져올 수 있음
반응형