본문 바로가기
Clone Coding

Jira-clone - Workspace 목록 불러오기

by zzuny-code 2025. 10. 1.
반응형

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

5:04:39

 

전체 데이터 흐름

1. 컴포넌트 렌더링
   <WorkspaceSwitcher />
   
   ↓

2. React Query Hook 호출
   useGetWorkspaces()
   - 캐시 확인
   - 없으면 API 호출
   
   ↓

3. HTTP 요청
   GET /api/workspaces
   (RPC 클라이언트 through Hono)
   
   ↓

4. 서버 라우트 처리
   route.ts - GET "/"
   databases.listDocuments()
   
   ↓

5. Appwrite Database
   WORKSPACES_ID 컬렉션 조회
   
   ↓

6. 응답 반환
   {data: {documents: [...], total: N}}
   
   ↓

7. React Query 캐싱 & 상태 업데이트
   - data에 저장
   - 컴포넌트 리렌더링
   
   ↓

8. UI 렌더링
   {JSON.stringify(data)}

 

서버 라우트 (src/features/workspaces/server/route.ts) - 데이터 제공자

  • Appwrite Database에서 모든 workspace 문서들을 조회
  • listDocuments(): 특정 컬렉션의 모든 문서를 가져오는 메서드
  • JSON 형태로 클라이언트에 반환
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()
    .get(
        "/",
        sessionMiddleware,
        async (c) => {
            const databases = c.get("databases")

            const workspace = await databases.listDocuments(
                DATABASE_ID,
                WORKSPACES_ID,
            );

            return c.json({data:workspace})
        }
    )
    .post(
    ...생략
    )

export default app

 

 

React Query Hook(src/features/workspaces/api/use-get-workspaces.ts) - 데이터 관리자

  • React Query의 역할:
    • 자동 캐싱: 한 번 불러온 데이터는 메모리에 저장
    • 자동 리페칭: 페이지 포커스 시 자동으로 최신 데이터 가져옴
    • 로딩 상태 관리: isLoading, isFetching 자동 제공
    • 에러 처리: error 객체 자동 제공
queryKey: ["workspaces"]
  • queryKey의 중요성:
    • React Query가 데이터를 식별하는 고유 키
    • 같은 키로 여러 곳에서 호출해도 중복 요청 안 함
    • 캐시 무효화할 때도 이 키 사용
 
import {useQuery} from "@tanstack/react-query"

import {client} from "@/lib/rpc"

export const useGetWorkspaces = () => {
    const query = useQuery({
        queryKey:["workspaces"],
        queryFn: async () => {
            const response = await client.api.workspaces.$get();

            if(!response.ok){
                throw new Error("Failted to fetch workspaces")
            }

            const {data} = await response.json();

            return data;
        }
    })

    return query
}

 

 

 

UI 컴포넌트(src/components/workspace-switcher.tsx) - 데이터 표시자 

"use client"

import { useGetWorkspaces } from "@/features/workspaces/api/use-get-workspace"

export const WorkspaceSwitcher = () => {
    // hook 호출 → {data, isLoading, error} 반환
    const {data} = useGetWorkspaces();
    
    return(
        <div>
            {JSON.stringify(data)}  // 임시로 데이터 확인
        </div>
    )
}

 

 

 

Sidebar 통합(src/features/auth/components/sidebar.tsx) - 레이아웃 구성

<aside className='h-full bg-neutral-100 p-4 w-full'>
    <Link href={"/"}>
        <Image src={"logo.svg"} alt='logo' width={164} height={48}/>
    </Link>
    <DottedSeparator className='my-4'/>
    
    <WorkspaceSwitcher/>  {/* workspace 목록 표시 */}
    
    <DottedSeparator className='my-4'/>
    <Navigation/>
</aside>

 

 

 

Appwrite 이미지를 Next.js에서 표시하기

! 문제 상황

  • Appwrite Storage에서 이미지 파일 ID만 받아옴 (예: 689e7d5d002ef0b5bcb2)
  • 실제 이미지를 보려면 완전한 URL이 필요함
  • Next.js Image 컴포넌트 사용 시 외부 호스트 설정 필요
  • 401 Unauthorized 오류 발생

 

 

1. Next.js 이미지 호스트 설정(next.config.mjs)

  • Next.js가 외부 이미지를 최적화하려면 remotePatterns에 도메인 등록 필수
/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'fra.cloud.appwrite.io',
        port: '',
        pathname: '/v1/storage/buckets/**',
      },
    ],
  },
};

export default nextConfig;

 

 

2.  WorkspaceAvatar 컴포넌트 구현(src/features/workspaces/component/workspace-avatar.tsx)

  • mode=admin 제거함 (클라이언트에서 직접 접근하므로)
import Image from "next/image"
import {cn} from "@/lib/utils"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"

import {
  APPWRITE_ENDPOINT, // 너의 appwrite 엔드포인트 (ex: fra.cloud.appwrite.io)
  PROJECT_ID,         // 너의 프로젝트 ID
  IMAGES_BUCKET_ID    // images Storage 버킷 ID
} from "@/config" 


interface WorkspaceAvatarProps {
    name: string,
    image?: string,
    className?:string
}

export const WorkspaceAvatar = ({name, image, className}:WorkspaceAvatarProps) => {
    

    let imageUrl = ''; 

    if(image && typeof image === 'string') {

        // baseEndpoint에서 '/v1' 경로 추가
        const baseEndpoint = APPWRITE_ENDPOINT.startsWith('https://') 
            ? APPWRITE_ENDPOINT 
            : `https://${APPWRITE_ENDPOINT}`;

        // '/v1'이 이미 포함되어 있는지 확인
        const endpoint = baseEndpoint.includes('/v1') 
            ? baseEndpoint 
            : `${baseEndpoint}/v1`;

        imageUrl = `${endpoint}/storage/buckets/${IMAGES_BUCKET_ID}/files/${image}/view?project=${PROJECT_ID}&mode=admin`;

        // https://fra.cloud.appwrite.io/v1/storage/buckets/68d8d717003253abebae/files/68da5074000291bf93ab/view?project=68ca4e5d002612399c40&mode=admin
        // 만약 'mode=admin' 이 꼭 필요하다면 &mode=admin 을 붙여도 되지만 보통은 필요 없음.

        return(
            <div className={cn(
                "size-10 relative rounded-md overflow-hidden",
                className
            )}>
                <Image src={imageUrl} alt={name} fill className="object-cover"/>
            </div>
        )
    }

    
    return(
        <Avatar className={cn("size-10", className)}>
            <AvatarFallback className="text-white bg-blue-600 font-semibold text-lg uppercase">
                {name[0]}
            </AvatarFallback>
        </Avatar>
    )
}

 

 

3.  WorkspaceSwitcher에서 사용(src/components/workspace-switcher.tsx

bunx --bun shadcn@latest add select
"use client"

import {Select, SelectTrigger, SelectValue, SelectContent, SelectItem} from '@/components/ui/select'
import { useGetWorkspaces } from "@/features/workspaces/api/use-get-workspace"
import { WorkspaceAvatar } from '@/features/workspaces/component/workspace-avatar';

export const WorkspaceSwitcher = () => {
    // hook 호출 → {data, isLoading, error} 반환
    const {data:workspace} = useGetWorkspaces();
    
    return(
        <div>
            <Select>
                <SelectTrigger className="w-[180px]">
                    <SelectValue placeholder="No workspace selected"/>
                </SelectTrigger>
                <SelectContent>
                    {workspace?.documents.map((item) => (
                        <SelectItem key={item.$id} value={item.$id}>
                            <div className="flex justify-start items-center gap-3 font-medium">                                
                                <WorkspaceAvatar name={item.name} image={item.imageUrl}/>
                                <span className="truncate">{item.name}</span>
                            </div>
                        </SelectItem>
                    ))}
                    
                </SelectContent>
                </Select>
        </div>
    )
}

 

Appwrite Console에서:

  1. Storage → 해당 버킷 선택
  2. Settings
  3. Permissions에서 다음 중 하나 설정:
    • Any 역할에 read 권한 추가 (공개 읽기)
    • 또는 특정 사용자/팀에게만 권한 부여

권한 설정 없으면 401 Unauthorized 오류 발생!

 

일단.. 해결 :) 

반응형