701 lines
33 KiB
Markdown
701 lines
33 KiB
Markdown
# 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`)**:
|
|
```typescript
|
|
// @/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`)**:
|
|
```typescript
|
|
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`)**:
|
|
```typescript
|
|
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 설정, 글로벌 스타일 적용
|
|
|
|
```typescript
|
|
// 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**만 합니다.
|
|
|
|
```typescript
|
|
// 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`로 추출 | 레이어 역할 준수 |
|