Files
frontend/docs/st/10.FSD_구조하에_CS_프로그램.md
T

33 KiB

10. FSD 구조하에 CS 프로그램

FSD(Feature-Sliced Design): 레이어(Layer) 간 단방향 의존성을 강제하는 프론트엔드 아키텍처 방법론.
shared → entities → widgets → pages → app 순서로 하위 레이어만 참조 가능.


1. FSD란 무엇인가

FSD(Feature-Sliced Design)는 기능 중심으로 코드를 조직화하는 프론트엔드 아키텍처입니다.
가장 중요한 원칙은 "상위 레이어가 하위 레이어를 참조하고, 하위 레이어는 절대 상위 레이어를 참조할 수 없다" 입니다.

┌─────────────────────────────────────────────────────────┐
│                    app (애플리케이션 진입점)               │  ← 최상위: 모든 레이어 조합
├─────────────────────────────────────────────────────────┤
│                pages (Next.js 라우트 페이지)              │  ← 라우팅 연결 + 페이지 컴포넌트
├─────────────────────────────────────────────────────────┤
│                widgets (독립 UI 블록)                     │  ← 여러 entities 조합한 복합 UI
├─────────────────────────────────────────────────────────┤
│                entities (도메인 엔티티)                   │  ← 비즈니스 도메인 단위 캡슐화
├─────────────────────────────────────────────────────────┤
│                shared (공통 기반 인프라)                   │  ← 최하위: 재사용 가능한 순수 기능
└─────────────────────────────────────────────────────────┘

        ↑ 상위 레이어만 아래를 참조 (단방향)

레이어 내부 구조: 슬라이스(Slice) + 세그먼트(Segment)

각 레이어는 슬라이스(도메인/기능 단위) → 세그먼트(역할 단위)로 세분화됩니다.

레이어/슬라이스/세그먼트
예시: entities/user/api/
     entities/user/model/
     entities/user/ui/
     entities/user/index.ts   ← 공개 API (Public API)

2. CS 앱 실제 디렉터리 구조

전체 구조

apps/cs/
├── app/                          # Next.js App Router (라우팅 레이어)
│   ├── (auth)/                   # 인증 없는 공개 라우트
│   │   └── login/
│   ├── (main)/                   # 인증이 필요한 메인 라우트
│   │   ├── layout.tsx            # 메인 레이아웃 래퍼
│   │   ├── (with-base-layout)/   # 헤더/LNB 포함 레이아웃
│   │   │   ├── layout.tsx        # BaseLayout + PageLayout 조합
│   │   │   ├── users/            # 사용자 관리 라우트
│   │   │   │   ├── page.tsx      # → UsersPage 컴포넌트 연결
│   │   │   │   ├── register/
│   │   │   │   └── [userUnqId]/
│   │   │   ├── bbs/              # 게시판 관리 라우트
│   │   │   ├── dashboard/
│   │   │   ├── menu/
│   │   │   └── ...
│   │   └── (without-base-layout)/ # LNB 없는 전체화면
│   └── api/                      # Next.js API Routes
│
├── src/                          # FSD 소스 레이어
│   ├── app/                      # App 레이어 (글로벌 설정)
│   │   ├── index.tsx             # App 루트 컴포넌트
│   │   ├── providers/
│   │   │   └── app-provider.tsx  # 전역 Provider 조합
│   │   └── styles/               # 글로벌 CSS
│   │
│   ├── pages/                    # Pages 레이어 (페이지 컴포넌트)
│   │   ├── users/
│   │   │   ├── model/            # 테이블 컬럼 정의 등
│   │   │   ├── ui/               # 페이지 컴포넌트
│   │   │   └── index.ts          # Public API
│   │   ├── bbs/
│   │   ├── dashboard/
│   │   ├── menu/
│   │   ├── role/
│   │   ├── site/
│   │   └── ...
│   │
│   ├── widgets/                  # Widgets 레이어 (복합 UI 블록)
│   │   ├── dnd-tree/             # 드래그앤드롭 트리 위젯
│   │   ├── form-wrapper/         # 폼 래퍼 위젯
│   │   └── post/                 # 게시물 관련 복합 위젯
│   │
│   ├── entities/                 # Entities 레이어 (도메인 엔티티)
│   │   ├── auth/                 # 인증 도메인
│   │   ├── user/                 # 사용자 도메인
│   │   ├── board/                # 게시판 도메인
│   │   ├── code/                 # 공통코드 도메인
│   │   ├── menu/                 # 메뉴 도메인
│   │   ├── role/                 # 권한 도메인
│   │   ├── site/                 # 사이트 도메인
│   │   ├── group/                # 그룹 도메인
│   │   ├── organization/         # 조직 도메인
│   │   ├── attachment/           # 파일첨부 도메인
│   │   └── ...
│   │
│   └── shared/                   # Shared 레이어 (공통 기반)
│       ├── api/                  # API 클라이언트
│       ├── config/               # 환경변수, 엔드포인트 상수
│       ├── lib/                  # 공통 커스텀 훅
│       ├── model/                # 공통 타입 (PageableList 등)
│       ├── providers/            # 공통 Context Provider
│       ├── store/                # Zustand 스토어 정의
│       └── ui/                   # CS 앱 전용 공통 UI 컴포넌트
│
├── packages/common/              # 모노레포 공통 패키지
│   ├── components/               # @common/components - UI 컴포넌트 라이브러리
│   ├── business_components/      # 업무용 공통 컴포넌트
│   ├── auth/                     # @common/auth - 인증 유틸리티
│   ├── network/                  # @common/network - 네트워크 유틸리티
│   └── config/                   # 공통 설정
│
└── packages/rsone/               # 추가 공통 컴포넌트 패키지
    └── components/

3. 레이어별 상세 역할

🔵 shared - 공통 기반 레이어

위치: src/shared/
의존성: 다른 어떤 레이어도 참조하지 않음 (최하위)
역할: 앱 전체에서 재사용 가능한 순수 유틸리티, 인프라, 타입 정의

세그먼트 경로 내용
api src/shared/api/ Axios 인스턴스 (client.ts), 공통 request 설정
config src/shared/config/ 환경변수(env.ts), API 엔드포인트 상수(api.ts), 인증 설정
lib src/shared/lib/ 커스텀 훅 (useDataTable, useUserStore, useMutationWrapper 등)
model src/shared/model/ 공통 인터페이스 (PageableList, PageableParams)
store src/shared/store/ Zustand 스토어 정의 (userStore, menuStore, alertStore)
providers src/shared/providers/ Context Provider (StoreProvider, PromptProvider)
ui src/shared/ui/ 앱 전용 공통 UI (AlertDialog, ConfirmDialog, PageLayout, GridViewDataTable 등)

코드 예시 (src/shared/api/client.ts):

// @/shared/api 로 임포트
import { client } from "@/shared/api";

🟢 entities - 비즈니스 도메인 레이어

위치: src/entities/
의존성: shared만 참조 가능
역할: 하나의 비즈니스 도메인(user, board, auth 등)을 API 서비스와 타입으로 캡슐화

각 엔티티는 다음 세그먼트로 구성됩니다:

entities/user/
├── api/
│   └── user-service.ts     # API 서비스 클래스 (CRUD 메서드)
├── model/
│   └── user-interface.ts   # TypeScript 타입, Zod 스키마, 상수
├── ui/
│   └── password-reset-dialog.tsx  # 해당 도메인 전용 UI (선택적)
└── index.ts                # Public API - 외부에서 사용할 것만 export

코드 예시 (src/entities/user/api/user-service.ts):

import { client } from "@/shared/api";           // shared 참조 OK
import { ENDPOINT } from "@/shared/config";       // shared 참조 OK
import type { UserService, UserSearchParams } from "../model/user-interface";

export class UserApiService implements UserService {
  public async findPageUser(params: UserSearchParams): Promise<UserList> {
    const response = await client.get<UserList>(
      `${ENDPOINT.USER_SERVICE.USERS}`,
      { params },
    );
    return response.data;
  }
}

엔티티 간 참조 (@x 패턴):
동일 레이어(entities) 내 다른 슬라이스를 참조할 때는 @x 폴더를 통해 명시적으로 허용합니다.

entities/board/@x/
└── post.ts   # board 엔티티가 post 엔티티에서 필요한 것만 re-export

🟡 widgets - 독립 UI 블록 레이어

위치: src/widgets/
의존성: shared, entities 참조 가능
역할: 여러 entities를 조합하여 독립적으로 동작하는 복합 UI 블록

슬라이스 내용
dnd-tree 드래그앤드롭 트리 컴포넌트 (메뉴 관리 등에 활용)
form-wrapper 폼 공통 래퍼 (저장/취소 버튼 패턴 표준화)
post 게시물 관련 복합 UI 블록

🟠 pages - 페이지 컴포넌트 레이어

위치: src/pages/
의존성: shared, entities, widgets 참조 가능
역할: 실제 화면 UI 구현. Next.js App Router의 page.tsx에서 이 컴포넌트를 import하여 연결

각 페이지 슬라이스 구조:

pages/users/
├── model/
│   └── table-columns.tsx   # TanStack Table 컬럼 정의
├── ui/
│   ├── users-page.tsx      # 목록 페이지 메인 컴포넌트
│   ├── create-page.tsx     # 등록 페이지
│   ├── modify-page.tsx     # 수정 페이지
│   ├── user-detail.tsx     # 상세 컴포넌트
│   └── users-form.tsx      # 등록/수정 공통 폼
└── index.ts                # Public API

코드 예시 (src/pages/users/ui/users-page.tsx):

import { UserApi } from "@/entities/user";        // entities 참조 OK
import { CodeApi } from "@/entities/code";        // entities 참조 OK
import { ENDPOINT } from "@/shared/config";       // shared 참조 OK
import { useDataTable } from "@/shared/lib";      // shared 참조 OK
import { Button } from "@common/components";      // 공통 패키지 참조 OK
// ❌ import { something } from "@/app/...";      // 상위 레이어 참조 불가

🔴 app - 애플리케이션 레이어

위치: src/app/
의존성: 모든 하위 레이어 참조 가능
역할: 앱 초기화, 전역 Provider 설정, 글로벌 스타일 적용

// src/app/providers/app-provider.tsx
const AppProvider = ({ children }) => {
  return (
    <QueryClientProvider client={queryClient}>   // React Query 설정
      <StoreProvider>                            // Zustand Store 초기화
        <PromptProvider>
          <ConfirmProvider>
            <AuthWrapper>                        // entities/auth - 인증 처리
              <MenuWrapper>                      // entities/menu - 메뉴 초기화
                <DndProvider ...>
                  {children}
                </DndProvider>
              </MenuWrapper>
              <AlertDialog />                    // shared/ui - 전역 다이얼로그
              <PromptDialog />
              <ConfirmDialog />
              <Toaster />
            </AuthWrapper>
          </ConfirmProvider>
        </PromptProvider>
      </StoreProvider>
      <ReactQueryDevtools />
    </QueryClientProvider>
  );
};

4. Next.js App Router와 FSD 통합 방식

Next.js App Router의 라우팅 파일(app/ 디렉토리)과 FSD의 pages 레이어는 별도로 분리됩니다.

┌─────────────────────────────────────────────────────────┐
│  Next.js App Router (app/)                              │
│                                                         │
│  app/(main)/(with-base-layout)/users/page.tsx           │
│  ┌─────────────────────────────────────────────────┐   │
│  │  import { UsersPage } from "@/pages/users";     │   │
│  │  export default UsersPage;                      │   │
│  └─────────────────────────────────────────────────┘   │
│                    │                                    │
│                    │ (딱 1줄 연결)                       │
└────────────────────┼────────────────────────────────────┘
                     ▼
┌─────────────────────────────────────────────────────────┐
│  FSD pages 레이어 (src/pages/)                          │
│                                                         │
│  src/pages/users/ui/users-page.tsx                      │
│  → 실제 화면 로직과 UI 구현                               │
└─────────────────────────────────────────────────────────┘

핵심 패턴: app/ 폴더의 page.tsx는 단순히 FSD pages 레이어 컴포넌트를 re-export만 합니다.

// app/(main)/(with-base-layout)/users/page.tsx
import { UsersPage } from "@/pages/users";
export default UsersPage;  // 딱 1줄!

레이아웃 계층 구조

app/(main)/layout.tsx
  └── App (src/app/index.tsx)
       └── AppProvider (QueryClient, Auth, Menu, Store 등)

app/(main)/(with-base-layout)/layout.tsx
  └── BaseLayout (@common/components)     ← 헤더 + LNB
       └── PageLayout (src/shared/ui)     ← 콘텐츠 영역
            └── {children}               ← 각 page.tsx 컴포넌트

5. 모노레포 패키지와의 관계

CS 앱은 packages/ 아래의 공통 패키지를 shared 레이어의 하위 개념으로 활용합니다.

┌─────────────────────────────────────────────────────────┐
│                      apps/cs                            │
│                                                         │
│  ┌──────────────────────────────────────────────────┐  │
│  │  src/shared  (앱 전용 공통)                       │  │
│  │  - @/shared/api    → Axios 클라이언트            │  │
│  │  - @/shared/config → 엔드포인트 상수             │  │
│  │  - @/shared/lib    → 커스텀 훅                   │  │
│  │  - @/shared/ui     → 앱 전용 공통 UI             │  │
│  └──────────────────────────────────────────────────┘  │
│          ↑ 참조                                         │
│  ┌──────────────────────────────────────────────────┐  │
│  │  packages/common (모노레포 공통 패키지)            │  │
│  │  - @common/components → 기반 UI 컴포넌트 라이브러리 │  │
│  │  - @common/auth       → JWT, 쿠키 유틸리티       │  │
│  │  - @common/network    → 에러 인터셉터, 리다이렉트  │  │
│  └──────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘
패키지 Import 경로 역할
packages/common/components @common/components Button, Input, Table, Dialog 등 기반 UI
packages/common/auth @common/auth JWT 파싱, getRemainingMs, 쿠키 관리
packages/common/network @common/network 401 에러 인터셉터, isAtLoginRedirectUrl
packages/rsone/components @rsone/components 추가 UI 컴포넌트

6. 미들웨어 기반 API 프록시 아키텍처

CS 앱은 클라이언트가 백엔드를 직접 호출하지 않고 Next.js Middleware를 통해 프록시합니다.

Browser (React 컴포넌트)
   │
   │  axios.get('/server/user-service/users')
   ▼
Next.js Middleware (middleware.ts)
   │  - 쿠키에서 JWT 토큰 추출 → 요청 헤더에 주입
   │  - URL 패턴 매칭 → 실제 백엔드 URL로 rewrite
   │  - CSP 보안 헤더 추가
   ▼
실제 백엔드 API 서버 (Spring Boot)
   │
   └── 응답 반환

7. 의존성 규칙 요약

❌ 금지: 하위 레이어 → 상위 레이어 참조
✅ 허용: 상위 레이어 → 하위 레이어 참조

shared     ─────────────────────────── (다른 레이어 참조 불가)
entities   → shared
widgets    → shared, entities
pages      → shared, entities, widgets
app        → shared, entities, widgets, pages

동일 레이어 내 참조 규칙:

  • entities 내 다른 슬라이스 참조 시: @x 폴더를 통해 명시적으로 허용
  • pages 내 다른 슬라이스 참조: 원칙적으로 금지 (widgets로 추출해야 함)

8. UI 추가 시 프로그램 UI 및 액션 흐름

8-1. 신규 도메인 UI 추가 순서

Step 1: entities 레이어에 도메인 추가
  src/entities/{도메인}/
  ├── api/{도메인}-service.ts     ← API CRUD 클래스 작성
  ├── model/{도메인}-interface.ts ← TypeScript 타입/인터페이스/Zod 스키마 정의
  ├── ui/ (선택)                  ← 도메인 전용 UI (예: 검색 다이얼로그)
  └── index.ts                   ← Public API export

Step 2: pages 레이어에 페이지 컴포넌트 추가
  src/pages/{도메인}/
  ├── model/table-columns.tsx    ← 테이블 컬럼 정의 (TanStack Table)
  ├── ui/{도메인}-page.tsx       ← 목록 페이지
  ├── ui/create-page.tsx         ← 등록 페이지
  ├── ui/modify-page.tsx         ← 수정 페이지
  └── index.ts                   ← Public API export

Step 3: Next.js 라우트 연결
  app/(main)/(with-base-layout)/{도메인}/
  ├── page.tsx                   ← import { XxxPage } from "@/pages/{도메인}"; export default XxxPage;
  ├── register/page.tsx
  └── [id]/page.tsx

8-2. 목록 페이지 UI 액션 흐름

사용자 관리(/users) 페이지를 예시로 전체 액션 흐름을 설명합니다.

┌─────────────────────────────────────────────────────────────────┐
│                    UsersPage (src/pages/users/ui/users-page.tsx) │
│                                                                  │
│  [초기 렌더링]                                                    │
│  useQuery({ queryKey: [ENDPOINT.USER_SERVICE.USERS, ...] })      │
│    └── UserApi.findPageUser(params) → 사용자 목록 API 호출        │
│                                                                  │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │  검색 폼 영역                                              │   │
│  │  - 기간 선택: InputDateRangePicker (shared/ui)           │   │
│  │  - 상태 선택: Select + CodeApi.getCodeList() (entities)  │   │
│  │  - 키워드: Input (공통 컴포넌트)                           │   │
│  │  - [검색] 버튼 → handleSubmit() → setSearchParams()      │   │
│  │  - [초기화] 버튼 → handleReset() → 상태 초기화           │   │
│  └──────────────────────────────────────────────────────────┘   │
│                                                                  │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │  통계 현황 영역                                             │   │
│  │  - useQuery(userSttsStats) → UserApi.selectUserSttsStats() │  │
│  │  - 상태별 카운트 표시 (대기/정상/정지/탈퇴/삭제)            │   │
│  └──────────────────────────────────────────────────────────┘   │
│                                                                  │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │  데이터 테이블 영역 (useDataTable hook - shared/lib)       │   │
│  │  - DataTable (@common/components) 렌더링                 │   │
│  │  - 컬럼 정의: table-columns.tsx (pages/users/model/)     │   │
│  │  - 행 클릭 → router.push('/users/{userUnqId}')           │   │
│  │  - 페이지네이션 → setPagination() → 재쿼리               │   │
│  └──────────────────────────────────────────────────────────┘   │
│                                                                  │
│  [우상단 버튼]                                                    │
│  - [등록] 버튼 → router.push('/users/register')                  │
└─────────────────────────────────────────────────────────────────┘

액션 흐름 상세:

[검색 버튼 클릭]
      │
      ▼
handleSubmit(e)
  e.preventDefault()
  setPagination({ pageIndex: 0 })  ← 1페이지로 리셋
  setSearchParams({                ← 검색 파라미터 업데이트
    keywordType, keyword, userStateCode, ...
  })
      │
      ▼ (React 상태 변경 → 리렌더링)
useQuery 재실행 (queryKey에 searchParams 포함)
      │
      ▼
UserApi.findPageUser(params)
      │
      ▼
axios.get('/server/user-service/users?page=0&size=10&...')
      │
      ▼
middleware.ts → JWT 헤더 주입 → 백엔드로 rewrite
      │
      ▼
Spring Boot 백엔드 API
      │
      ▼ 응답
DataTable 리렌더링

8-3. 등록/수정 페이지 UI 액션 흐름

┌──────────────────────────────────────────────────────────────┐
│              CreatePage / ModifyPage                          │
│                                                               │
│  [폼 초기화]                                                  │
│  - react-hook-form useForm() 초기화                          │
│  - Zod resolver로 유효성 검사 스키마 연결                      │
│  - (수정 시) useQuery로 상세 데이터 로드 → form.reset()       │
│                                                               │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  입력 폼 (FormWrapper 위젯 or 직접 구성)              │    │
│  │  - FormField + Input, Select, DatePicker 등          │    │
│  │  - 유효성 에러 메시지 자동 표시                        │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                               │
│  [저장 버튼 클릭]                                             │
│  form.handleSubmit(onSubmit)                                  │
│    └── Zod 유효성 검사 통과 여부 확인                         │
│         │                                                     │
│         ├── 실패 → 에러 메시지 표시                           │
│         └── 성공 → useMutationWrapper 실행                    │
│                   → UserApi.saveUser(data) 호출               │
│                   → 성공 시 목록 페이지 이동                  │
│                   → 실패 시 AlertDialog 표시                  │
└──────────────────────────────────────────────────────────────┘

액션 흐름 상세:

[저장 버튼 클릭]
      │
      ▼
form.handleSubmit(onSubmit)
      │
      ├── Zod 유효성 검사 실패
      │     └── 필드별 에러 메시지 표시 (react-hook-form)
      │
      └── Zod 유효성 검사 성공
            │
            ▼
      useMutationWrapper / useMutation
            │
            ▼
      UserApi.saveUser(data)  [entities/user/api]
            │
            ▼
      axios.post('/server/user-service/users', data)
            │
            ▼
      middleware.ts → JWT 주입 → 백엔드로 rewrite
            │
            ├── 성공 응답
            │     ├── useAlertStore.showAlert('저장되었습니다')
            │     ├── queryClient.invalidateQueries(...)  ← 캐시 무효화
            │     └── router.push('/users')               ← 목록으로 이동
            │
            └── 실패 응답
                  └── useAlertStore.showAlert(에러 메시지)

8-4. 전체 데이터 흐름 다이어그램

사용자 인터랙션 (브라우저 UI)
        │
        ▼
app/(main)/.../(with-base-layout)/users/page.tsx
  [Next.js App Router - 라우팅 진입점]
        │ (re-export)
        ▼
src/pages/users/ui/users-page.tsx
  [FSD pages 레이어 - 화면 컴포넌트]
  - useState: 검색 파라미터, 페이지네이션 상태
  - useQuery: 데이터 조회 (React Query)
        │
        ├──(UI 조합)──────────────────────────────┐
        │                                         │
        ▼                                         ▼
@common/components                    src/shared/ui/
  Button, Input, Select, DataTable      GridViewDataTable, InputDateRangePicker
        │                                         │
        └──────────────┬──────────────────────────┘
                       │
                       ▼
              src/entities/user/api/user-service.ts
              src/entities/code/api/code-service.ts
              [FSD entities 레이어 - API 서비스]
                       │
                       ▼
              src/shared/api/client.ts
              [Axios 인스턴스]
                       │
                       ▼
              middleware.ts
              [Next.js Middleware - 프록시 + JWT 주입]
                       │
                       ▼
              Spring Boot 백엔드 API
                       │
                       ▼ 응답 반환
              React Query 캐시 업데이트
                       │
                       ▼
              UI 리렌더링

8-5. 인증 및 메뉴 초기화 흐름

앱 최초 로드
      │
      ▼
src/app/providers/app-provider.tsx (AppProvider)
      │
      ▼
entities/auth/lib/auth-wrapper.tsx (AuthWrapper)
      │
      ├── 쿠키에서 JWT 토큰 확인
      │     ├── 토큰 없음 → /login 리다이렉트
      │     └── 토큰 있음
      │           ├── getRemainingMs() → 남은 시간 계산
      │           └── setupRefreshTimer() → 자동 갱신 타이머 설정
      │
      ├── /users/info/me API 호출 → 로그인 사용자 정보 조회
      │     └── useUserStore.setUserInfo() → Zustand 전역 상태 저장
      │
      └── entities/menu/lib/menu-wrapper.tsx (MenuWrapper)
            │
            ├── 메뉴 목록 API 호출
            └── useMenuStore → Zustand에 메뉴 데이터 저장
                  │
                  ▼
            BaseLayout에 메뉴 렌더링 (LNB)
                  │
                  ▼
            메뉴 클릭 → router.push(item.urlPath)
            → 해당 page.tsx로 이동

9. 공통 패키지 (packages/common) 역할

packages/common/components → @common/components

모든 앱에서 공유하는 기반 UI 컴포넌트 라이브러리:

components/src/ui/
├── button/          # Button 컴포넌트 (variant: default, outline, ghost 등)
├── input/           # Input 텍스트 입력
├── select/          # Select 드롭다운
├── data-table/      # DataTable (TanStack Table 기반 페이지네이션 테이블)
├── dialog/          # Dialog 모달
├── date-picker/     # DatePicker 날짜 선택
├── sidebar/         # Sidebar (LNB)
├── header/          # Header 헤더
├── lnb/             # LNB 좌측 네비게이션
├── layout/          # BaseLayout (Header + LNB + Content 조합)
├── tree/            # Tree 트리 컴포넌트
├── pagination/      # Pagination 페이지네이션
├── tabs/            # Tabs 탭
├── badge/           # Badge 배지
└── ...

packages/common/business_components (업무용 공통 컴포넌트)

business_components/src/ui/
├── form/            # Form 공통 래퍼
├── table/           # 업무용 테이블
├── search_form/     # 검색 폼 레이아웃
├── date_picker/     # 업무용 날짜 선택
├── excel/           # 엑셀 다운로드 버튼
├── file-upload/     # 파일 업로드
├── map/             # 지도 컴포넌트
└── ...

10. FSD 준수 체크리스트

새로운 기능을 추가할 때 다음 사항을 확인합니다:

체크 항목 기준
✅ shared에 비즈니스 로직 없음 shared는 순수 유틸/인프라만 포함
✅ entities가 pages를 import 안 함 단방향 의존성 준수
✅ entities 간 참조는 @x 폴더 사용 명시적 교차 참조
✅ app/page.tsx는 re-export만 함 1줄 연결 패턴
✅ 각 슬라이스에 index.ts 존재 Public API 캡슐화
✅ 타입은 model/ 세그먼트에 위치 타입 중앙화
✅ API 호출은 entities/api/ 세그먼트에 위치 서비스 분리
✅ 전역 상태는 shared/store/에 정의 스토어 중앙화
✅ 복수 entities 조합 UI는 widgets로 추출 레이어 역할 준수