ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • AI Agent를 위한 Frontend 개발 가이드: AGETNTS.md 로 Next.js + shadcn/ui 프로젝트 구조 설계
    실제 경험과 인사이트를 AI와 함께 정리한 글 2025. 11. 24. 13:59

    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 개발 계획
Designed by Tistory.