티스토리 뷰

AI Agent를 위한 Frontend 개발 가이드: AGENTS.md로 Next.js + shadcn/ui 프로젝트 구조 설계

작성일: 2025년 11월 24일
카테고리: Frontend, AI, Architecture
키워드: Next.js, React, AI Agent, AGENTS.md, CLAUDE.md, shadcn/ui, TanStack Query, Zustand

요약

AI 코딩 어시스턴트(Claude, Copilot 등)와 협업할 때 프로젝트 구조를 명확히 문서화하면 AI가 일관된 코드를 생성합니다. 이 글에서는 Next.js 16 + shadcn/ui 기반 프로젝트에서 AI Agent가 따라야 할 아키텍처 패턴, 네이밍 컨벤션, 데이터 흐름을 정의하는 방법을 공유합니다. 실제 B2B API Gateway 콘솔 프로젝트에서 사용 중인 가이드 문서입니다.

문제 상황

배경

AI 코딩 어시스턴트와 함께 프론트엔드를 개발할 때 다음 문제들이 발생합니다:

  • AI가 매번 다른 파일 구조와 네이밍 컨벤션을 사용
  • API 호출 로직이 컴포넌트, 훅, 서비스 등 여기저기 흩어짐
  • 상태 관리 방식이 일관되지 않음 (useState, Zustand, Context 혼용)
  • 새 기능 추가 시 어디에 코드를 배치해야 할지 AI가 판단하지 못함

환경 구성

  • Framework: Next.js 16.0.3 (App Router)
  • React: 19.0.0
  • TypeScript: 5.7.2
  • Styling: Tailwind CSS 4.0.0
  • UI Components: shadcn/ui (new-york style)
  • State Management: Zustand 5.0.2
  • Server State: TanStack Query 5.x
  • Forms: React Hook Form + Zod
  • Auth: Keycloak (OIDC PKCE)

해결 과정: AI Agent 가이드 문서 설계

1. 계층 아키텍처 정의

AI가 코드를 배치할 위치를 명확히 알 수 있도록 계층 구조를 정의합니다:

src/
├── types/              # TypeScript interfaces & types only
├── services/           # API Layer (object-based services)
├── hooks/              # React Query hooks (domain-specific)
├── store/              # Zustand stores (global UI state)
├── features/           # Feature-Sliced Design modules
├── components/         # Shared UI components
├── lib/                # Utilities (NOT API calls)
└── app/                # Pages (UI composition only)

2. 데이터 흐름 시각화

AI가 데이터 흐름을 이해할 수 있도록 다이어그램으로 표현합니다:

┌─────────────────────────────────────────────────────────────┐
│                         Page (app/)                         │
│  - UI composition only                                      │
│  - Imports from features/ and hooks/                        │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                    Features (features/)                     │
│  - Domain-specific components                               │
│  - Uses hooks/ for data, components/ for UI primitives      │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                      Hooks (hooks/)                         │
│  - React Query hooks (useQuery, useMutation)                │
│  - Calls services/ for API operations                       │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                    Services (services/)                     │
│  - API layer (object-based)                                 │
│  - Uses lib/api/client.ts for HTTP requests                 │
└─────────────────────────────────────────────────────────────┘

3. 네이밍 컨벤션 명시

일관된 네이밍을 위해 규칙을 표로 정리합니다:

Item Convention Example
Hooks use-*.ts use-gateways.ts, use-products.ts
Services *.ts gateway.ts, product.ts
Types *.ts gateway.ts, product.ts
Components *.tsx gateway-card.tsx, product-form.tsx
Utilities *.ts format-date.ts, cn.ts
Feature folders kebab-case api-services/, client-apps/

핵심 원칙: 모든 파일은 kebab-case를 사용합니다.

4. Service 패턴 표준화

API 호출은 모두 service 객체를 통해 수행합니다:

// services/gateway.ts
import { apiClient } from '@/lib/api/client';
import type { Gateway, CreateGatewayRequest } from '@/types/gateway';

const BASE_PATH = '/v1/provider/gateways';

export const gatewayService = {
  /**
   * GET /v1/provider/gateways - List all gateways
   */
  list: async (): Promise<Gateway[]> => {
    const { data } = await apiClient.get<{ gateways: Gateway[] }>(BASE_PATH);
    return data.gateways || [];
  },

  /**
   * GET /v1/provider/gateways/:id - Get single gateway
   */
  get: async (id: string): Promise<Gateway> => {
    const { data } = await apiClient.get<{ gateway: Gateway }>(`${BASE_PATH}/${id}`);
    return data.gateway;
  },

  /**
   * POST /v1/provider/gateways - Create gateway
   */
  create: async (request: CreateGatewayRequest): Promise<Gateway> => {
    const { data } = await apiClient.post<{ gateway: Gateway }>(BASE_PATH, request);
    return data.gateway;
  },

  /**
   * DELETE /v1/provider/gateways/:id - Delete gateway
   */
  delete: async (id: string): Promise<void> => {
    await apiClient.delete(`${BASE_PATH}/${id}`);
  },
};

5. React Query Hook 패턴 표준화

서비스를 호출하는 React Query 훅을 표준화합니다:

// hooks/use-gateways.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { gatewayService } from '@/services';
import type { CreateGatewayRequest } from '@/types/gateway';

// Query keys for cache management
export const gatewayKeys = {
  all: ['gateways'] as const,
  list: () => [...gatewayKeys.all, 'list'] as const,
  detail: (id: string) => [...gatewayKeys.all, 'detail', id] as const,
};

/**
 * Hook to fetch all gateways
 */
export function useGateways() {
  return useQuery({
    queryKey: gatewayKeys.list(),
    queryFn: () => gatewayService.list(),
  });
}

/**
 * Hook to create gateway
 */
export function useCreateGateway() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (request: CreateGatewayRequest) => gatewayService.create(request),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: gatewayKeys.all });
    },
  });
}

6. Feature-Sliced Design 패턴

각 도메인별로 자체 컴포넌트를 가진 feature 모듈을 구성합니다:

features/<feature-name>/
├── components/
│   ├── <feature>-card.tsx           # Display component
│   ├── <feature>-form.tsx           # Form component
│   ├── <feature>-table.tsx          # Table component
│   ├── create-<feature>-dialog.tsx  # Create dialog
│   └── edit-<feature>-dialog.tsx    # Edit dialog
└── index.ts                         # Public exports only
// features/gateways/index.ts
export { GatewayCard } from './components/gateway-card';
export { GatewayForm } from './components/gateway-form';
export { GatewayTable } from './components/gateway-table';
export { CreateGatewayDialog } from './components/create-gateway-dialog';

7. 상태 관리 가이드라인

상태 종류별로 어떤 도구를 사용할지 명확히 정의합니다:

상태 종류 도구 용도
Server State TanStack Query API 데이터 (자동 캐싱, 리페칭)
Global UI State Zustand 사이드바 상태 등 전역 UI 상태
URL State nuqs 필터, 검색, 페이지네이션
Local State useState 다이얼로그 open/close 등
// Server State
const { data, isLoading } = useGateways();

// Global UI State (Zustand)
const { isOpen, toggle } = useSidebarStore();

// URL State (nuqs)
const [search, setSearch] = useQueryState('q');

// Local State
const [isDialogOpen, setIsDialogOpen] = useState(false);

8. 새 기능 추가 체크리스트

AI가 새 기능을 추가할 때 따라야 할 순서를 정의합니다:

  1. Create Types (types/my-feature.ts)
  2. Create Service (services/my-feature.ts)
  3. Create Hooks (hooks/use-my-feature.ts)
  4. Create Feature Components (features/my-feature/)
  5. Create Page (app/(admin)/my-feature/page.tsx)
  6. Add to Sidebar (components/layout/app-sidebar.tsx)

프로젝트 구조 전체 예시

frontend/
├── src/
│   ├── app/                          # Next.js App Router pages
│   │   ├── layout.tsx                # Root layout (providers, theme)
│   │   ├── (auth)/                   # Auth route group (no sidebar)
│   │   │   ├── login/page.tsx
│   │   │   └── callback/page.tsx
│   │   ├── (admin)/                  # Admin/Provider route group
│   │   │   ├── layout.tsx            # AppSidebar + Header + auth
│   │   │   ├── dashboard/
│   │   │   ├── gateways/
│   │   │   └── products/
│   │   └── (portal)/                 # Customer DevPortal route group
│   │       ├── layout.tsx            # PortalSidebar + Header + auth
│   │       ├── home/
│   │       └── market/
│   │
│   ├── features/                     # Feature-Sliced Design modules
│   │   ├── gateways/
│   │   │   ├── components/
│   │   │   └── index.ts
│   │   └── products/
│   │       ├── components/
│   │       └── index.ts
│   │
│   ├── hooks/                        # React Query hooks
│   │   ├── index.ts
│   │   ├── use-gateways.ts
│   │   └── use-products.ts
│   │
│   ├── services/                     # API services (object-based)
│   │   ├── index.ts
│   │   ├── gateway.ts
│   │   └── product.ts
│   │
│   ├── types/                        # TypeScript interfaces
│   │   ├── index.ts
│   │   ├── gateway.ts
│   │   └── product.ts
│   │
│   ├── components/                   # Shared UI components
│   │   ├── ui/                       # shadcn/ui primitives
│   │   ├── layout/                   # App layout components
│   │   └── forms/                    # Form field wrappers
│   │
│   ├── store/                        # Zustand stores
│   │   └── sidebar.ts
│   │
│   └── lib/                          # Utilities
│       ├── utils.ts
│       ├── api/client.ts
│       └── auth/keycloak.ts
│
├── components.json                   # shadcn/ui configuration
├── tailwind.config.js
└── next.config.ts

교훈

1. 명시적 규칙이 일관된 AI 출력을 만든다

"API 호출은 services/에서만", "상태 관리는 용도별로 분리" 등 명시적인 규칙을 문서화하면 AI가 일관된 코드를 생성합니다.

2. 코드 예시가 설명보다 효과적

"Service는 object-based 패턴을 사용합니다"라는 설명보다 실제 코드 예시가 AI에게 더 명확한 가이드가 됩니다.

3. 계층 구조 다이어그램이 데이터 흐름을 명확히 한다

텍스트 설명만으로는 복잡한 계층 구조를 전달하기 어렵습니다. ASCII 다이어그램으로 시각화하면 AI가 더 잘 이해합니다.

4. 새 기능 추가 체크리스트가 실수를 방지한다

파일 생성 순서와 필요한 파일 목록을 체크리스트로 제공하면 AI가 필요한 파일을 빠뜨리지 않습니다.

5. 네이밍 컨벤션 표가 혼란을 방지한다

kebab-case vs camelCase vs PascalCase 등 네이밍 규칙을 표로 정리하면 AI가 일관된 네이밍을 사용합니다.

참고 자료

관련 문서

기술 스택

부록: 전체 AGENTS.md 원본

에이전트별 설정 방법: Claude Code, Gemini CLI, Codex CLI에서 AGENTS.md를 인식하도록 설정하는 방법은 Claude, Gemini, Codex에서 AGENTS.md 설정하기를 참조하세요.

아래는 실제 프로젝트에서 사용 중인 AI Agent 가이드 문서 전체입니다 (752줄):

# Frontend v2 - AI Agent Development Guide

This document provides codebase documentation for the imp-gateway frontend v2 (web/).

## Tech Stack

| Category | Technology | Version |
|----------|------------|---------|
| Framework | Next.js (App Router) | 16.x |
| React | React | 19.x |
| Language | TypeScript | 5.x |
| Styling | Tailwind CSS | 4.x |
| UI Components | shadcn/ui (new-york style) | - |
| State Management | Zustand | 5.x |
| Server State | TanStack Query | 5.x |
| Forms | React Hook Form + Zod | - |
| Auth | Better Auth + Keycloak (Confidential Client) | - |
| Icons | Lucide React | - |
| Bundler | Turbopack (via Next.js) | - |

---

## Architecture Overview

### Feature-Sliced Design (FSD)

Frontend v2는 FSD 아키텍처를 따릅니다.

상위 레이어 → 하위 레이어 의존 가능
하위 레이어 → 상위 레이어 의존 불가

app → pages → widgets → features → entities → shared


### FSD Standard Layers

| Layer | Purpose | Next.js 통합 |
|-------|---------|-------------|
| **App** | 라우팅, 엔트리포인트, 글로벌 설정 | `app/` - Next.js App Router |
| **Pages** | 페이지 단위 UI 조합 | `pages/` - 페이지별 컴포넌트 |
| **Widgets** | 독립적인 대규모 UI 블록 | `widgets/` |
| **Features** | 비즈니스 기능 (사용자 액션) | `features/` |
| **Entities** | 비즈니스 엔티티 (도메인 모델) | `entities/` |
| **Shared** | 재사용 가능한 공용 코드 | `shared/` |

### Layer Structure

src/
├── app/ # [App] Next.js App Router - 라우팅 엔트리포인트
├── pages/ # [Pages] 페이지 단위 UI 조합 컴포넌트
├── widgets/ # [Widgets] 독립적인 복합 UI 블록
├── features/ # [Features] 비즈니스 기능 단위
├── entities/ # [Entities] 도메인 모델 + API
└── shared/ # [Shared] 공용 UI/유틸리티


### Data Flow

┌─────────────────────────────────────────────────────────────┐
│ App (app/) │
│ - Next.js App Router (라우팅 엔트리포인트) │
│ - layout.tsx, page.tsx (minimal - pages/ 컴포넌트 호출) │
│ - Providers, global styles │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ Pages (pages/) │
│ - 페이지 단위 UI 조합 │
│ - widgets/, features/, entities/ 조합 │
│ - 예: ClustersPage, DashboardPage │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ Widgets (widgets/) │
│ - 독립적인 대규모 UI 블록 │
│ - 완전한 유스케이스 전달 (Sidebar, DataTable, Wizard) │
│ - Uses features/, entities/, shared/ │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ Features (features/) │
│ - 비즈니스 기능 (사용자에게 가치를 제공하는 액션) │
│ - 예: CreateCluster, DeleteProduct, PublishAPI │
│ - Uses entities/, shared/ │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ Entities (entities/) │
│ - 비즈니스 엔티티 (도메인 모델) │
│ - Types, API services, React Query hooks │
│ - 예: Cluster, Product, Subscription │
│ - Uses shared/ only │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ Shared (shared/) │
│ - UI primitives (shadcn/ui) │
│ - Utilities (cn, formatDate) │
│ - API client, Auth config │
│ - 프로젝트/비즈니스와 무관한 재사용 코드 │
└─────────────────────────────────────────────────────────────┘


---

## App vs Pages 구분

### App Layer (`app/`)
Next.js App Router의 라우팅 엔트리포인트. **최소한의 코드만** 포함.

```tsx
// app/operator/clusters/page.tsx
import { ClustersPage } from "@/pages/operator";

export default function Page() {
  return <ClustersPage />;
}

Pages Layer (pages/)

실제 페이지 UI 조합 로직. widgets, features, entities를 조합.

// pages/operator/ClustersPage.tsx
"use client";

import { ClusterTable, CreateClusterDialog } from "@/features/clusters";
import { useClusters } from "@/entities/cluster";
import { PageContainer } from "@/widgets/layout";

export function ClustersPage() {
  const { data: clusters, isLoading } = useClusters();

  return (
    <PageContainer title="Clusters">
      <div className="flex justify-between mb-4">
        <h1 className="text-2xl font-bold">Clusters</h1>
        <CreateClusterDialog />
      </div>
      <ClusterTable data={clusters} isLoading={isLoading} />
    </PageContainer>
  );
}

Naming Convention

All files use kebab-case for consistency.

Item Convention Example
App Router page.tsx, layout.tsx app/operator/clusters/page.tsx
Page Components PascalCase.tsx pages/operator/ClustersPage.tsx
Components PascalCase.tsx ClusterCard.tsx, ClusterForm.tsx
Hooks use-*.ts use-clusters.ts, use-products.ts
Services *.service.ts cluster.service.ts
Types *.types.ts or types.ts cluster.types.ts
Utilities kebab-case.ts format-date.ts, cn.ts

Project Structure

web/
├── src/
│   ├── app/                          # [App] Next.js App Router
│   │   ├── layout.tsx                # Root layout (providers, theme)
│   │   ├── page.tsx                  # Landing (redirect)
│   │   │
│   │   ├── (auth)/                   # Auth route group
│   │   │   └── sign-in/
│   │   │       └── page.tsx          # → SignInPage
│   │   │
│   │   ├── api/auth/[...all]/        # Better Auth API routes
│   │   │   └── route.ts
│   │   │
│   │   ├── operator/                 # Operator Portal routes
│   │   │   ├── layout.tsx            # OperatorLayout
│   │   │   ├── page.tsx              # → DashboardPage
│   │   │   ├── clusters/page.tsx     # → ClustersPage
│   │   │   ├── agents/page.tsx       # → AgentsPage
│   │   │   └── ...
│   │   │
│   │   ├── provider/                 # Provider Portal routes
│   │   │   ├── layout.tsx            # ProviderLayout
│   │   │   ├── page.tsx              # → DashboardPage
│   │   │   └── ...
│   │   │
│   │   └── consumer/                 # Consumer Portal routes
│   │       ├── layout.tsx            # ConsumerLayout
│   │       ├── page.tsx              # → DashboardPage
│   │       └── ...
│   │
│   ├── pages/                        # [Pages] 페이지 UI 조합
│   │   ├── operator/
│   │   │   ├── index.ts              # Public exports
│   │   │   ├── DashboardPage.tsx
│   │   │   ├── ClustersPage.tsx
│   │   │   └── AgentsPage.tsx
│   │   │
│   │   ├── provider/
│   │   │   ├── index.ts
│   │   │   ├── DashboardPage.tsx
│   │   │   └── ApiServicesPage.tsx
│   │   │
│   │   ├── consumer/
│   │   │   ├── index.ts
│   │   │   └── DashboardPage.tsx
│   │   │
│   │   └── auth/
│   │       ├── index.ts
│   │       └── SignInPage.tsx
│   │
│   ├── widgets/                      # [Widgets] 독립적 UI 블록
│   │   ├── layout/
│   │   │   ├── index.ts
│   │   │   ├── Sidebar.tsx
│   │   │   ├── OperatorSidebar.tsx
│   │   │   ├── ProviderSidebar.tsx
│   │   │   ├── ConsumerSidebar.tsx
│   │   │   ├── TenantSwitcher.tsx
│   │   │   ├── PortalHeader.tsx
│   │   │   └── PageContainer.tsx
│   │   │
│   │   └── data-table/
│   │       ├── index.ts
│   │       └── DataTable.tsx
│   │
│   ├── features/                     # [Features] 비즈니스 기능
│   │   ├── clusters/
│   │   │   ├── ui/
│   │   │   │   ├── ClusterCard.tsx
│   │   │   │   ├── ClusterTable.tsx
│   │   │   │   └── CreateClusterDialog.tsx
│   │   │   └── index.ts
│   │   │
│   │   └── ... (other features)
│   │
│   ├── entities/                     # [Entities] 도메인 모델 + API
│   │   ├── cluster/
│   │   │   ├── model/
│   │   │   │   └── types.ts
│   │   │   ├── api/
│   │   │   │   ├── cluster.service.ts
│   │   │   │   └── use-clusters.ts
│   │   │   └── index.ts
│   │   │
│   │   └── ... (other entities)
│   │
│   └── shared/                       # [Shared] 공용 코드
│       ├── api/
│       │   └── client.ts
│       ├── components/
│       │   └── ui/                   # shadcn/ui
│       ├── hooks/
│       ├── lib/
│       │   ├── auth/
│       │   │   ├── server.ts
│       │   │   ├── client.ts
│       │   │   └── index.ts
│       │   └── utils.ts
│       └── config/
│           └── env.ts
│
├── public/
├── .env
├── components.json
├── tailwind.config.ts
├── tsconfig.json
└── next.config.ts

3-Portal Architecture

Portal Overview

Portal Target User Core Features
Operator SRE/운영자 Cluster/Agent 관리, Fleet 모니터링
Provider API 제공자 API Service 정의, Product 생성, 배포
Consumer API 소비자 Marketplace 탐색, 구독, Credential 관리

Route Groups

Route Layout Auth Required
(auth)/sign-in Minimal No
operator/* OperatorSidebar Yes
provider/* ProviderSidebar + TenantSwitcher Yes
consumer/* ConsumerSidebar Yes

Sidebar Menu Structure

Operator Portal:

  • Overview: Dashboard, Fleet Monitor
  • Infrastructure: Clusters, Agents
  • Organization: Tenants, Users
  • Audit & Logs
  • Settings

Provider Portal (with TenantSwitcher):

  • Overview: Dashboard
  • API Management: API Services, Gateways
  • Product Management: Products, Publishes, Auth Providers
  • Consumer Management: Subscriptions
  • Settings: Team, Settings

Consumer Portal:

  • Overview: Dashboard
  • Marketplace: Explore APIs
  • My Subscriptions
  • Credentials: Applications, API Keys
  • Analytics: Usage
  • Settings

Entity Pattern (Domain + API)

Each entity contains types, API service, and React Query hooks.

Entity Structure

entities/<entity>/
├── model/
│   └── types.ts              # TypeScript interfaces
├── api/
│   ├── <entity>.service.ts   # API service (object-based)
│   └── use-<entity>.ts       # React Query hooks
└── index.ts                  # Public exports

Types Example

// entities/cluster/model/types.ts
export interface Cluster {
  id: string;
  name: string;
  region: string;
  status: "active" | "inactive" | "pending";
  createdAt: string;
  updatedAt: string;
}

export interface CreateClusterRequest {
  name: string;
  region: string;
}

export interface UpdateClusterRequest {
  name?: string;
  region?: string;
}

Service Example

// entities/cluster/api/cluster.service.ts
import { apiClient } from "@/shared/api/client";
import type { Cluster, CreateClusterRequest, UpdateClusterRequest } from "../model/types";

const BASE_PATH = "/v1/operator/clusters";

export const clusterService = {
  list: async (): Promise<Cluster[]> => {
    const { data } = await apiClient.get<{ clusters: Cluster[] }>(BASE_PATH);
    return data.clusters || [];
  },

  get: async (id: string): Promise<Cluster> => {
    const { data } = await apiClient.get<{ cluster: Cluster }>(`${BASE_PATH}/${id}`);
    return data.cluster;
  },

  create: async (request: CreateClusterRequest): Promise<Cluster> => {
    const { data } = await apiClient.post<{ cluster: Cluster }>(BASE_PATH, request);
    return data.cluster;
  },

  update: async (id: string, request: UpdateClusterRequest): Promise<Cluster> => {
    const { data } = await apiClient.put<{ cluster: Cluster }>(`${BASE_PATH}/${id}`, request);
    return data.cluster;
  },

  delete: async (id: string): Promise<void> => {
    await apiClient.delete(`${BASE_PATH}/${id}`);
  },
};

React Query Hooks Example

// entities/cluster/api/use-clusters.ts
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { clusterService } from "./cluster.service";
import type { CreateClusterRequest, UpdateClusterRequest } from "../model/types";

export const clusterKeys = {
  all: ["clusters"] as const,
  list: () => [...clusterKeys.all, "list"] as const,
  detail: (id: string) => [...clusterKeys.all, "detail", id] as const,
};

export function useClusters() {
  return useQuery({
    queryKey: clusterKeys.list(),
    queryFn: () => clusterService.list(),
  });
}

export function useCluster(id: string) {
  return useQuery({
    queryKey: clusterKeys.detail(id),
    queryFn: () => clusterService.get(id),
    enabled: !!id,
  });
}

export function useCreateCluster() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: (request: CreateClusterRequest) => clusterService.create(request),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: clusterKeys.all });
    },
  });
}

export function useUpdateCluster(id: string) {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: (request: UpdateClusterRequest) => clusterService.update(id, request),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: clusterKeys.all });
    },
  });
}

export function useDeleteCluster() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: (id: string) => clusterService.delete(id),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: clusterKeys.all });
    },
  });
}

Entity Index

// entities/cluster/index.ts
export type { Cluster, CreateClusterRequest, UpdateClusterRequest } from "./model/types";
export { clusterService } from "./api/cluster.service";
export {
  useClusters,
  useCluster,
  useCreateCluster,
  useUpdateCluster,
  useDeleteCluster,
  clusterKeys,
} from "./api/use-clusters";

Feature Pattern

Features contain business actions (사용자에게 비즈니스 가치를 제공하는 기능).

Feature Structure

features/<feature>/
├── ui/
│   ├── <Feature>Card.tsx
│   ├── <Feature>Table.tsx
│   └── Create<Feature>Dialog.tsx
├── model/                    # (optional) feature-specific types
│   └── types.ts
└── index.ts

Feature Index

// features/clusters/index.ts
export { ClusterCard } from "./ui/ClusterCard";
export { ClusterTable } from "./ui/ClusterTable";
export { CreateClusterDialog } from "./ui/CreateClusterDialog";

Page Pattern

Pages 레이어는 widgets, features, entities를 조합하여 완전한 페이지 UI를 구성.

Pages Structure

pages/<portal>/
├── index.ts                  # Public exports
├── DashboardPage.tsx
├── ClustersPage.tsx
└── ClusterDetailPage.tsx

Page Example

// pages/operator/ClustersPage.tsx
"use client";

import { ClusterTable, CreateClusterDialog } from "@/features/clusters";
import { useClusters } from "@/entities/cluster";
import { PageContainer } from "@/widgets/layout";
import { Button } from "@/shared/components/ui/button";

export function ClustersPage() {
  const { data: clusters, isLoading } = useClusters();

  return (
    <PageContainer>
      <div className="flex items-center justify-between mb-6">
        <div>
          <h1 className="text-2xl font-bold">Clusters</h1>
          <p className="text-muted-foreground">Manage your Kubernetes clusters</p>
        </div>
        <CreateClusterDialog />
      </div>
      <ClusterTable data={clusters ?? []} isLoading={isLoading} />
    </PageContainer>
  );
}

App Router Integration

// app/operator/clusters/page.tsx
import { ClustersPage } from "@/pages/operator";

export default function Page() {
  return <ClustersPage />;
}

Authentication (Better Auth + Keycloak)

Configuration Files

File Purpose
shared/lib/auth/server.ts Better Auth 서버 설정 (genericOAuth)
shared/lib/auth/client.ts Better Auth 클라이언트 훅
app/api/auth/[...all]/route.ts API Route Handler

Environment Variables

# .env
NEXT_PUBLIC_APP_URL=http://localhost:3000
AUTH_SECRET=<32+ chars secret>

# Keycloak (Confidential Client)
KC_ISSUER=https://keycloak.admin.imprun.dev/realms/imprun
KC_CLIENT_ID=imprun-console
KC_CLIENT_SECRET=<client secret>

Usage

// Client-side: Sign in with Keycloak
import { signInWithKeycloak } from "@/shared/lib/auth/client";
await signInWithKeycloak("/dashboard");

// Client-side: Get session
import { useSession } from "@/shared/lib/auth/client";
const { data: session, isPending } = useSession();

// Server-side: Get session
import { auth } from "@/shared/lib/auth/server";
import { headers } from "next/headers";
const session = await auth.api.getSession({ headers: await headers() });

Component Patterns

Server vs Client Components

// Server Component (default)
export async function PortalHeader({ title }: Props) {
  const session = await auth.api.getSession({ headers: await headers() });
  return <header>...</header>;
}

// Client Component
"use client";
export function Sidebar({ menuGroups }: Props) {
  const pathname = usePathname();
  // ...
}

Key Rules

  1. Server Components: 기본값, 데이터 fetching, 세션 조회
  2. Client Components: 상호작용, hooks, state, browser APIs
  3. 경계 주의: Server → Client로 함수 직접 전달 불가

Form Pattern

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/shared/components/ui/form";

const schema = z.object({
  name: z.string().min(2, "Name must be at least 2 characters"),
  region: z.string().min(1, "Region is required"),
});

type FormData = z.infer<typeof schema>;

export function ClusterForm({ onSubmit }: { onSubmit: (data: FormData) => void }) {
  const form = useForm<FormData>({
    resolver: zodResolver(schema),
    defaultValues: { name: "", region: "" },
  });

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
        <FormField
          control={form.control}
          name="name"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Name</FormLabel>
              <FormControl>
                <Input {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit">Create</Button>
      </form>
    </Form>
  );
}

State Management

Server State (TanStack Query)

const { data, isLoading, error, refetch } = useClusters();

Global UI State (Zustand)

// shared/store/sidebar.ts
import { create } from "zustand";

interface SidebarStore {
  isOpen: boolean;
  toggle: () => void;
}

export const useSidebarStore = create<SidebarStore>((set) => ({
  isOpen: true,
  toggle: () => set((state) => ({ isOpen: !state.isOpen })),
}));

Local Component State (useState)

const [isDialogOpen, setIsDialogOpen] = useState(false);

Adding New Features

1. Create Entity

src/entities/<entity>/
├── model/types.ts
├── api/<entity>.service.ts
├── api/use-<entity>.ts
└── index.ts

2. Create Feature

src/features/<feature>/
├── ui/<Feature>Table.tsx
├── ui/Create<Feature>Dialog.tsx
└── index.ts

3. Create Page

src/pages/<portal>/<Feature>Page.tsx

4. Add App Router Entry

// app/<portal>/<feature>/page.tsx
import { FeaturePage } from "@/pages/<portal>";
export default function Page() {
  return <FeaturePage />;
}

5. Add to Sidebar

Update widgets/layout/<Portal>Sidebar.tsx.


Development Commands

pnpm install          # Install dependencies
pnpm dev              # Dev server (port 3000)
pnpm build            # Production build
pnpm lint             # ESLint
pnpm format           # Prettier

shadcn/ui Configuration

# Add component (installs to src/shared/components/ui/)
pnpm dlx shadcn@latest add button card dialog form input

FSD-compatible paths in components.json:

  • utils@/shared/lib/utils
  • ui@/shared/components/ui
  • hooks@/shared/hooks

Code Style Summary

Category Convention
File naming kebab-case for hooks/utils, PascalCase for components
Component naming PascalCase
Hook naming use-*.ts files, useMyHook in code
Service naming Object-based, myService.method()
TypeScript Strict mode, explicit types
Exports Named exports preferred
State React Query for server, Zustand for UI

Key Files

File Purpose
src/app/layout.tsx Root layout with providers
src/app/operator/layout.tsx Operator portal layout
src/pages/operator/index.ts Operator page exports
src/widgets/layout/index.ts Layout widget exports
src/shared/api/client.ts API client with auth
src/shared/lib/auth/server.ts Better Auth server config

Key Documentation

Document Content
docs/epics/EPIC-003-frontend-v2.md Frontend v2 요구사항 및 스토리
docs/user-flows.md 사용자 여정별 플로우
docs/frontend-plan-v2.md 개발 계획
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/02   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
글 보관함