본문 바로가기
Clone Coding

N8N & Zapier - Workflows Crud / 1

by zzuny-code 2026. 1. 12.
반응형

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

05:44:04

 

Update Workflow schema 워크플로 스키마 업데이트

Create Workflows API 워크플로 CRUD API 생성

 

 

1 - Workflows CRUD API 구현

1-1. User 모델에 Workflow 관계 추가

prisma/schema.prisma

  • One-to-Many 관계: 한 사용자는 여러 개의 워크플로우를 가질 수 있다.
  • Cascade Delete: 사용자가 삭제되면 해당 사용자의 모든 워크플로우도 함께 삭제
model User {
  ...
  workflows     Workflow[]
  ...
}


model Workflow{
  id String @id @default(cuid())
  name String

  // 기본 구조에 타임스탬프와 관계 필드를 추가
  // 타임스탬프
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  
  // user 관계
  userId String
  user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}

 

1-2 마이그레이션 실행

npx prisma migrate dev
? Enter a name for the new migration: › workflows-update

 

 

 

2 - 워크플로우 라우터 생성

 

2-1 워크플로우 이름을 자동으로 생성하기 위한 패키지를 설치

npm i random-word-slugs

 

2-2. 기본 Create API 생성

src/app/features/workflows/server/routers.ts

 

  • protectedProcedure: 인증된 사용자만 접근 가능
  • generateSlug(3): "happy-brown-fox" 형태의 이름 자동 생성
  • ctx.auth.user.id: 현재 로그인한 사용자의 ID 사용

 

import {generateSlug} from "random-word-slugs"
import prisma from "@/lib/db";
import { createTRPCRouter, protectedProcedure } from "@/trpc/init";

export const workflowsRouter = createTRPCRouter({
    create: protectedProcedure.mutation(({ctx}) => {
        return prisma.workflow.create({
            data : {
                name: generateSlug(3), // 3개 단어 조합으로 이름 생성
                userId: ctx.auth.user.id
            }
        })
    })
})

 

 

3 - tRPC 라우터 통합

3-1. App Router 리팩토링

src/trpc/routers/_app.ts 수정

 

  • 기능별로 라우터 분리 (모듈화)
  • 코드 가독성 향상
  • 유지보수 용이
  • 확장성 개선

 

// 변경전
import { inngest } from '@/inngest/client';
import { createTRPCRouter, premiumProcedure, protectedProcedure } from '../init';
import prisma from '@/lib/db';
import { TRPCError } from '@trpc/server';
import { workflowsRouter } from '@/app/features/workflows/server/routers';

export const appRouter = createTRPCRouter({

  testAi : premiumProcedure.mutation(async () => {
    await inngest.send({
      name: "execute/ai",
    })

    return {success: true, message: "작업 대기 중"}
  }),
  getWorkflows: protectedProcedure.query(({ctx}) => {    
      return prisma.workflow.findMany()
  }),
  createWorkflow : protectedProcedure.mutation(async()=>{
    await inngest.send({
      name: "test/hello.world",
      data: {
        email:"zzuny@zzuny.com"
      }
    }); 

    return {success: true, message: "작업 대기 중"}
  })
});
// export type definition of API
export type AppRouter = typeof appRouter;
// 변경 후
import { createTRPCRouter} from '../init';
import { workflowsRouter } from '@/app/features/workflows/server/routers';

export const appRouter = createTRPCRouter({
  workflows : workflowsRouter
});
// export type definition of API
export type AppRouter = typeof appRouter;

 

 

 

4 - 완전한 CRUD API 구현

src/app/features/workflows/server/routers.ts 

 

  • 모든 작업에 userId 체크 포함
  • 다른 사용자의 워크플로우에 접근 불가
  • Zod로 입력값 검증 (name은 최소 1글자 이상)

 

import z from 'zod'
import {generateSlug} from "random-word-slugs"
import prisma from "@/lib/db";
import { createTRPCRouter, protectedProcedure } from "@/trpc/init";

export const workflowsRouter = createTRPCRouter({
    create: protectedProcedure.mutation(({ctx}) => {
        return prisma.workflow.create({
            data : {
                // name: "TODO",
                name: generateSlug(3),
                userId: ctx.auth.user.id
            }
        })
    }),
    remove: protectedProcedure
        .input(z.object({id: z.string()}))
        .mutation(({ctx, input}) => {
        return prisma.workflow.delete({
            where: {
                id: input.id,
                userId: ctx.auth.user.id
            }
        })
    }),
    updateName : protectedProcedure
        .input(z.object({id: z.string(), name: z.string().min(1)}))
        .mutation(({ctx, input}) => {
            return prisma.workflow.update({
                where: {id: input.id, userId: ctx.auth.user.id},
                data: {name: input.name}
            })
    }),
    getOne : protectedProcedure
        .input(z.object({id: z.string()}))
        .query(({ctx, input}) => {
            return prisma.workflow.findUnique({
                where: { id: input.id, userId: ctx.auth.user.id}
            })
        }),
    getMany : protectedProcedure
        .query(({ctx}) => {
            return prisma.workflow.findMany({
                where: { userId: ctx.auth.user.id}
            })
        })
})

 

 

5 - React Query Hook 생성

src/app/features/workflows/hooks/use-workflows.ts

* useSuspenseQuery의 장점:

  • 데이터가 로드될 때까지 자동으로 Suspense 트리거
  • 로딩 상태를 컴포넌트에서 직접 처리할 필요 없음
  • React 18의 Concurrent Features 활용
import { useTRPC } from "@/trpc/client"
import { useSuspenseQuery } from "@tanstack/react-query";

/** 
    Hook to fetch all workflows using suspense
**/

export const useSuspenseWorkflows = () => {
    const trpc = useTRPC();

    return useSuspenseQuery(trpc.workflows.getMany.queryOptions());
}

 

 

6 - 클라이언트 컴포넌트 생성

src/app/features/workflows/components/workflows.tsx

  • 현재는 데이터를 JSON으로 출력하지만, 실제 프로젝트에서는 UI 컴포넌트로 렌더링
'use client'

import { useSuspenseWorkflows } from "../hooks/use-workflows"

export const WorkflowsList = () => {
    const workflows = useSuspenseWorkflows();

    return(
        <p>
            {JSON.stringify(workflows.data, null, 2)}
        </p>
    )
}

 

 

 

 

7 - 서버 사이드 Prefetch 설정

https://trpc.io/docs/client/tanstack-react-query/server-components

 

Set up with React Server Components | tRPC

This guide is an overview of how one may use tRPC with a React Server Components (RSC) framework such as Next.js App Router.

trpc.io

 

 

7-1. tRPC Server Utils 설정

 

src/trpc/server.tsx

  • prefetch와 HydrateClient를 추가
  • cache(): React의 캐싱 메커니즘으로 요청당 하나의 Query Client 보장
  • prefetch(): 서버에서 데이터를 미리 가져옴
  • HydrateClient: 서버에서 가져온 데이터를 클라이언트로 전달

 

 

import 'server-only'; // <-- ensure this file cannot be imported from the client
import { createTRPCOptionsProxy, TRPCQueryOptions } from '@trpc/tanstack-react-query';
import { cache } from 'react';
import { createTRPCContext } from './init';
import { makeQueryClient } from './query-client';
import { appRouter } from './routers/_app';
import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
// IMPORTANT: Create a stable getter for the query client that
//            will return the same client during the same request.
export const getQueryClient = cache(makeQueryClient);
export const caller = appRouter.createCaller(createTRPCContext);
export const trpc = createTRPCOptionsProxy({
  ctx: createTRPCContext,
  router: appRouter,
  queryClient: getQueryClient,
});

export function prefetch<T extends ReturnType<TRPCQueryOptions<any>>>(
  queryOptions: T,
) {
  const queryClient = getQueryClient();
  if (queryOptions.queryKey[1]?.type === 'infinite') {
    void queryClient.prefetchInfiniteQuery(queryOptions as any);
  } else {
    void queryClient.prefetchQuery(queryOptions);
  }
}

export function HydrateClient(props: { children: React.ReactNode }) {
  const queryClient = getQueryClient();
  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      {props.children}
    </HydrationBoundary>
  );
}

 

 

7-2. Prefetch 헬퍼 함수 생성

src/app/features/workflows/server/prefetch.ts

 

  • inferInput으로 입력 타입 자동 추론
  • TypeScript로 안전한 파라미터 전달 보장

 

import { prefetch, trpc } from "@/trpc/server";
import { inferInput } from "@trpc/tanstack-react-query";

type Input = inferInput<typeof trpc.workflows.getMany>

/* 
    prefetch all workflows
*/

export const prefetchWorkflows = (params: Input) => {
    return prefetch(trpc.workflows.getMany.queryOptions(params))
}

 

 

8 - Error Boundary 설정

8-1. 패키지 설치

react-error-boundary: React의 에러 경계를 쉽게 구현할 수 있는 라이브러리

npm i react-error-boundary

 

 

9 - 페이지 구성

src/app/(dashboard)/(rest)/workflows/page.tsx

 

  • requireAuth(): 비인증 사용자 리다이렉트
  • prefetchWorkflows(): 서버에서 데이터를 미리 가져옴
  • HydrateClient: 서버 데이터를 클라이언트로 전달
  • ErrorBoundary: 에러 발생 시 폴백 UI 표시
  • Suspense: 로딩 중 폴백 UI 표시
  • WorkflowsList: 실제 워크플로우 목록 렌더링

 

import { WorkflowsList } from "@/app/features/workflows/components/workflows";
import { prefetchWorkflows } from "@/app/features/workflows/server/prefetch";
import { requireAuth } from "@/lib/auth-utils";
import { HydrateClient } from "@/trpc/server";
import { Suspense } from "react";
import { ErrorBoundary } from "react-error-boundary";

const Page = async() => {
    await requireAuth()

    prefetchWorkflows()
    return(
        <HydrateClient>
            <ErrorBoundary fallback={<p>Error!</p>}>
                <Suspense fallback={<p>Loading...</p>}>
                    <WorkflowsList/>
                </Suspense>
            </ErrorBoundary>
        </HydrateClient>
    )
}

export default Page;

 

 

┌─────────────────────────────────────────────────┐
│  1. Server Component (Page)                     │
│     - requireAuth() 인증 확인                     │
│     - prefetchWorkflows() 데이터 미리 가져오기       │
└────────────────┬────────────────────────────────┘
                 │
                 ▼
┌─────────────────────────────────────────────────┐
│  2. HydrateClient                               │
│     - 서버 데이터를 클라이언트로 전달                   │
└────────────────┬────────────────────────────────┘
                 │
                 ▼
┌─────────────────────────────────────────────────┐
│  3. ErrorBoundary                               │
│     - 에러 발생 시 폴백 UI 표시                      │
└────────────────┬────────────────────────────────┘
                 │
                 ▼
┌─────────────────────────────────────────────────┐
│  4. Suspense                                    │
│     - 로딩 중 폴백 UI 표시                          │
└────────────────┬────────────────────────────────┘
                 │
                 ▼
┌─────────────────────────────────────────────────┐
│  5. WorkflowsList (Client Component)            │
│     - useSuspenseWorkflows() Hook 사용           │
│     - 캐시된 데이터로 즉시 렌더링                      │
└─────────────────────────────────────────────────┘

 

---- 이어서 WorkflowsList UI 컴포넌트 구현 할거임

반응형

'Clone Coding' 카테고리의 다른 글

N8N & Zapier - Workflows Pagination  (0) 2026.01.15
N8N & Zapier - Workflows Crud / 2  (0) 2026.01.13
N8N & Zapier - Payments Setup  (1) 2026.01.09
N8N & Zapier - Sidebar Layout  (0) 2026.01.07
N8N & Zapier - Error Tracking  (0) 2026.01.06