diff --git a/docs/st/01.총평.md b/docs/st/01.총평.md new file mode 100644 index 000000000..bcd057151 --- /dev/null +++ b/docs/st/01.총평.md @@ -0,0 +1,60 @@ +# ST 앱 총평 (Architecture Overview) + +> 하수도 통계 관리 시스템 (ST: Statistics) 프론트엔드 앱의 전체 구조 및 기술 평가 리포트 + +--- + +## 1. 앱 개요 + +| 항목 | 내용 | +|------|------| +| **앱 이름** | `st` (Statistics) | +| **목적** | 하수도 통계 관리 시스템 - 통계 입력, 검토, 승인, 관리 기능 제공 | +| **프레임워크** | Next.js 15 (App Router) | +| **언어** | TypeScript | +| **패키지 관리** | pnpm Workspace (모노레포 구조) | + +--- + +## 2. 전체 평가 요약 + +### ✅ 강점 + +1. **FSD(Feature-Sliced Design) 아키텍처 도입** + `shared → entities → components → pages → app` 레이어 계층 구조를 적용하여 관심사 분리가 명확합니다. + +2. **서버 API 프록시 패턴 (Middleware)** + Next.js Middleware를 활용해 백엔드 API 호출을 프록시 처리하므로, 클라이언트에서 직접 API 서버를 노출하지 않는 보안 구조를 갖추고 있습니다. + +3. **멀티 백엔드 분리 구조** + 일반 서버 API(`/server/*`)와 ST 전용 서비스(`st-management-service`, `st-input-service`) 경로를 분리하여 URL 기반 라우팅으로 독립성을 확보하였습니다. + +4. **글로벌 상태 관리 일관성** + Zustand를 통한 전역 상태(사용자 정보, 메뉴, 알림, 탭)와 TanStack Query/SWR을 활용한 서버 상태 관리를 목적에 맞게 분리하였습니다. + +5. **재사용 가능한 공통 컴포넌트** + `DataGrid`, `SearchForm`, `ApproveButton`, `RejectButton` 등 업무 공통 컴포넌트를 `components/module`에 집중하여 중복 개발을 최소화하였습니다. + +### ⚠️ 개선 권고 사항 + +1. **데이터 페칭 라이브러리 혼용** + SWR과 TanStack Query를 동시에 사용하고 있습니다. 용도 기준이 명확하지 않으면 유지보수 시 혼란이 생길 수 있습니다. 가급적 하나의 라이브러리로 통일하거나, 사용 기준을 문서화하는 것을 권장합니다. + +2. **미구현 업무 함수** + 일부 컴포넌트에서 `rejectFunc`, `approveFunc` 등의 함수가 빈 상태로 남아 있습니다. 서비스 커버리지 확인이 필요합니다. + +3. **`src/store` 와 `src/shared/store` 이중 스토어 경로** + 탭 관련 스토어(`tab-store`)가 `src/store`에, 그 외 공통 스토어는 `src/shared/store`에 나뉘어 있습니다. FSD 원칙상 `src/shared/store`로 통일하는 것을 권장합니다. + +--- + +## 3. 문서 목차 + +| 문서 | 내용 | +|------|------| +| [02.디렉토리구조.md](./02.디렉토리구조.md) | 전체 소스 폴더 구조 설명 | +| [03.기술스택.md](./03.기술스택.md) | 사용 라이브러리 및 기술 상세 | +| [04.아키텍처패턴.md](./04.아키텍처패턴.md) | FSD 아키텍처 구조 및 데이터 흐름 | +| [05.상태관리.md](./05.상태관리.md) | Zustand 전역 상태 관리 패턴 | +| [06.데이터페칭.md](./06.데이터페칭.md) | SWR / TanStack Query 데이터 페칭 패턴 | +| [07.용어정리.md](./07.용어정리.md) | 프로젝트 주요 용어 정의 | diff --git a/docs/st/02.디렉토리구조.md b/docs/st/02.디렉토리구조.md new file mode 100644 index 000000000..479c974b5 --- /dev/null +++ b/docs/st/02.디렉토리구조.md @@ -0,0 +1,157 @@ +# 02. 디렉토리 구조 + +ST 앱의 전체 폴더 구조와 각 디렉토리의 역할을 설명합니다. + +--- + +## 루트 디렉토리 구조 + +``` +apps/st/ +├── app/ # Next.js App Router 라우팅 루트 +├── pages/ # Next.js Pages Router (일부 페이지) +├── src/ # 소스 코드 메인 디렉토리 +├── public/ # 정적 파일 (이미지, 아이콘 등) +├── docs/ # 앱 내부 문서 +├── middleware.ts # Next.js 미들웨어 (API 프록시, 인증) +├── next.config.ts # Next.js 설정 파일 +├── tailwind.config.ts # Tailwind CSS 설정 +├── tsconfig.json # TypeScript 설정 +└── package.json # 의존성 관리 +``` + +--- + +## `app/` - App Router 라우팅 + +``` +app/ +├── (with-base-layout)/ # 공통 레이아웃을 공유하는 라우트 그룹 +├── api/ # Next.js API Route Handler +├── auth/ # 인증 관련 페이지 (로그인 등) +└── layout.tsx # 앱 전체 루트 레이아웃 +``` + +> **참고**: Next.js의 라우트 그룹 `(with-base-layout)`은 URL에는 영향을 주지 않고 레이아웃만 공유하는 방식입니다. + +--- + +## `src/` - 소스 코드 (FSD 구조) + +FSD(Feature-Sliced Design) 아키텍처 원칙에 따라 레이어 계층이 구분되어 있습니다. + +``` +src/ +├── shared/ # 레이어 1: 공통 기반 (어디서나 재사용 가능) +├── entities/ # 레이어 2: 비즈니스 도메인 엔티티 +├── components/ # 레이어 3: UI 컴포넌트 (비즈니스 UI) +├── pages/ # 레이어 4: 페이지 컴포넌트 +├── store/ # 앱 레벨 전역 상태 (tab 관리) +└── app/ # 앱 초기화 / 프로바이더 구성 +``` + +--- + +## `src/shared/` - 공통 기반 레이어 + +``` +shared/ +├── api/ +│ └── client.ts # Axios HTTP 클라이언트 인스턴스 (일반/ST서비스 분리) +├── config/ +│ ├── api.ts # API 엔드포인트 URL 상수 +│ └── env.ts # 환경변수 로드 및 타입 관리 +├── lib/ +│ ├── use-mutation-wrapper.tsx # TanStack Query useMutation 래퍼 (에러 공통 처리) +│ ├── use-alert-store.ts # Alert 전역 상태 훅 +│ ├── use-user-store.ts # 사용자 정보 전역 상태 훅 +│ ├── use-menu-store.ts # 메뉴 전역 상태 훅 +│ ├── use-attachment-store.ts # 첨부파일 전역 상태 훅 +│ ├── use-mutation-wrapper.tsx # Mutation 공통 에러 처리 래퍼 +│ ├── use-tus.ts # 대용량 파일 업로드 (TUS 프로토콜) +│ ├── cookie.ts # 쿠키 유틸리티 +│ ├── match-query-status.ts # SWR/Query 상태 매핑 유틸 +│ └── csp-monitor.ts # CSP 보안 정책 모니터링 +├── model/ # 공통 타입 정의 +├── providers/ +│ ├── store-provider.tsx # Zustand 스토어 Provider +│ └── prompt-provider.tsx # 모달 프롬프트 Provider +├── store/ +│ ├── alert-store.ts # 알림(Alert) 전역 상태 +│ ├── user-store.ts # 사용자 정보 전역 상태 +│ ├── menu-store.ts # 메뉴/권한 전역 상태 +│ └── attachment-store.ts # 첨부파일 전역 상태 +└── ui/ # 공통 UI 원시 컴포넌트 +``` + +--- + +## `src/entities/` - 비즈니스 도메인 레이어 + +각 도메인 별로 `api/`(서비스 호출)과 `model/`(타입 정의)로 구성됩니다. + +``` +entities/ +├── auth/ # 인증 도메인 +├── code/ # 공통 코드(코드 관리) 도메인 +├── menu/ # 메뉴 도메인 +├── menu-authority/ # 메뉴 권한 도메인 +├── statistics/ # 통계 도메인 +│ ├── api/ +│ │ └── statistics-input-status-service.ts # 통계 입력 현황 API 서비스 +│ └── model/ +│ └── statistics-input-status-interface.ts # 요청/응답 타입 정의 +└── user/ # 사용자 도메인 +``` + +--- + +## `src/components/` - UI 컴포넌트 레이어 + +업무 화면을 구성하는 UI 컴포넌트들이 위치합니다. + +``` +components/ +├── module/ # 재사용 공통 업무 컴포넌트 +│ ├── DataGrid.tsx # RealGrid 기반 메인 데이터 그리드 (57KB, 핵심 컴포넌트) +│ ├── TabDataGrid.tsx # 탭 형태 데이터 그리드 +│ ├── CompactDataGrid.tsx # 압축형 데이터 그리드 +│ ├── SearchForm.tsx # 공통 검색 폼 +│ └── SplitDataGrid.tsx # 분할형 데이터 그리드 +├── button/ # 공통 버튼 컴포넌트 +│ ├── ApproveButton.tsx # 승인 버튼 +│ └── RejectButton.tsx # 반려 버튼 +├── statistics/ # 하수도 통계 관련 화면 컴포넌트 +│ ├── input/ # 통계 입력 관련 화면 +│ ├── management/ # 통계 관리 화면 +│ ├── review/ # 통계 검토 화면 +│ ├── total/ # 통계 합계 화면 +│ └── verification/ # 통계 검증 화면 +├── type/ # 컴포넌트 타입 정의 +├── util/ # 컴포넌트 유틸리티 +│ ├── column/ # DataGrid 컬럼 정의 모음 +│ └── grid/ # 그리드 관련 유틸 함수 +├── css/ # 컴포넌트 전용 CSS +└── Dashboard.tsx # 대시보드 컴포넌트 +``` + +--- + +## `src/store/` - 앱 레벨 전역 상태 + +``` +store/ +├── tab-store.ts # 탭 UI 관리 (탭 목록, 현재 활성 탭) +└── admin-store.ts # 관리자 관련 상태 +``` + +--- + +## 주요 개별 파일 + +| 파일 | 설명 | +|------|------| +| `middleware.ts` | API 프록시, JWT 토큰 헤더 주입, CSP 보안 헤더 설정 | +| `next.config.ts` | 서브 도메인 리라이트, CSP 헤더 설정 | +| `tailwind.config.ts` | Tailwind CSS 커스텀 테마 | +| `.env.example` | 환경변수 예시 파일 | diff --git a/docs/st/03.기술스택.md b/docs/st/03.기술스택.md new file mode 100644 index 000000000..f61a35618 --- /dev/null +++ b/docs/st/03.기술스택.md @@ -0,0 +1,134 @@ +# 03. 기술 스택 + +ST 앱에서 사용하는 라이브러리 및 기술 목록과 각각의 역할을 정리합니다. + +--- + +## 핵심 프레임워크 + +| 라이브러리 | 버전 | 역할 | +|-----------|------|------| +| **Next.js** | 15.3.8 | React 기반 풀스택 프레임워크. App Router + Pages Router 혼용. SSR, 라우팅, API Route, Middleware 제공 | +| **React** | (Next.js 내장) | UI 컴포넌트 렌더링 기반 | +| **TypeScript** | ^5 | 정적 타입 시스템으로 코드 안정성 확보 | + +--- + +## 상태 관리 + +| 라이브러리 | 버전 | 역할 | +|-----------|------|------| +| **Zustand** | ^5.0.3 | 전역 클라이언트 상태 관리 (사용자 정보, 메뉴, 탭, 알림 등) | +| **SWR** | ^2.3.7 | 서버 상태 데이터 페칭 및 캐싱 (주로 조회성 데이터) | +| **@tanstack/react-query** | ^5.67.1 | 서버 상태 mutation(생성/수정/삭제) 처리, devtools 포함 | + +--- + +## 폼 및 유효성 검증 + +| 라이브러리 | 버전 | 역할 | +|-----------|------|------| +| **react-hook-form** | ^7.54.2 | 성능 최적화된 폼 상태 관리 | +| **@hookform/resolvers** | ^4.1.3 | react-hook-form과 Zod 연결 어댑터 | +| **zod** | ^3.24.2 | 스키마 기반 런타임 유효성 검증 및 타입 추론 | + +--- + +## HTTP 통신 + +| 라이브러리 | 버전 | 역할 | +|-----------|------|------| +| **axios** | ^1.8.1 | Promise 기반 HTTP 클라이언트. 인터셉터를 통한 토큰 자동 주입 | +| **tus-js-client** | ^4.3.1 | 대용량 파일 업로드 (TUS 프로토콜 - 재개 가능한 업로드) | + +--- + +## UI 컴포넌트 & 스타일링 + +| 라이브러리 | 버전 | 역할 | +|-----------|------|------| +| **Tailwind CSS** | ^3.4.1 | 유틸리티 기반 CSS 프레임워크 | +| **tailwindcss-animate** | ^1.0.7 | Tailwind CSS 애니메이션 플러그인 | +| **@radix-ui/react-label** | ^2.1.2 | 접근성을 고려한 Label 원시 컴포넌트 | +| **@radix-ui/react-slot** | ^1.1.2 | Slot 기반 컴포넌트 합성 패턴 | +| **@radix-ui/react-tooltip** | ^1.1.8 | 접근성 있는 툴팁 컴포넌트 | +| **@radix-ui/react-icons** | ^1.3.2 | Radix UI 아이콘 세트 | +| **lucide-react** | ^0.474.0 | 오픈소스 SVG 아이콘 라이브러리 | +| **remixicon** | ^4.7.0 | 리믹스 아이콘 세트 | +| **next-themes** | ^0.4.6 | 다크모드/라이트모드 테마 전환 | +| **sonner** | ^2.0.1 | 경량 토스트(Toast) 알림 컴포넌트 | + +--- + +## 데이터 그리드 + +| 라이브러리 | 버전 | 역할 | +|-----------|------|------| +| **realgrid** | ^2.9.5 | 대용량 데이터 처리에 특화된 엔터프라이즈 그리드 라이브러리 | +| **realgrid-react** | ^0.9.7 | RealGrid React 바인딩 | +| **@tanstack/react-table** | ^8.21.2 | 헤드리스 테이블 라이브러리 (일부 보조 테이블에서 사용) | + +--- + +## 차트 & 시각화 + +| 라이브러리 | 버전 | 역할 | +|-----------|------|------| +| **chart.js** | ^4.4.8 | 기반 차트 라이브러리 | +| **react-chartjs-2** | ^5.3.0 | Chart.js의 React 컴포넌트 래퍼 | +| **chartjs-plugin-zoom** | ^2.2.0 | 차트 줌/팬 기능 플러그인 | + +--- + +## 날짜 & 유틸리티 + +| 라이브러리 | 버전 | 역할 | +|-----------|------|------| +| **dayjs** | ^1.11.13 | 경량 날짜 처리 라이브러리 | +| **js-cookie** | ^3.0.5 | 쿠키 읽기/쓰기 유틸리티 | +| **jszip** | ^3.10.1 | ZIP 파일 생성/압축 | +| **xlsx** | ^0.18.5 | 엑셀 파일 읽기/쓰기 | + +--- + +## 캘린더 & 드래그앤드롭 + +| 라이브러리 | 버전 | 역할 | +|-----------|------|------| +| **@fullcalendar/react** | ^6.1.17 | 풀칼린더 React 컴포넌트 (일정/달력 UI) | +| **@fullcalendar/daygrid** | ^6.1.17 | 월별 달력 뷰 | +| **@fullcalendar/timegrid** | ^6.1.17 | 시간별 달력 뷰 | +| **react-dnd** | ^16.0.1 | 드래그 앤 드롭 기능 | +| **@minoru/react-dnd-treeview** | 3.5.1-alpha.1 | 트리 뷰 DnD 컴포넌트 | + +--- + +## 에디터 & 기타 + +| 라이브러리 | 버전 | 역할 | +|-----------|------|------| +| **@tinymce/tinymce-react** | ^5.1.1 | 리치 텍스트 에디터 (게시판 등) | +| **next-qrcode** | ^2.5.1 | QR 코드 생성 | +| **@faker-js/faker** | ^9.9.0 | 테스트용 가짜 데이터 생성 | + +--- + +## 공유 패키지 (모노레포 내부) + +| 패키지 | 설명 | +|-------|------| +| `@common/assets` | 공통 이미지/아이콘 에셋 | +| `@common/components` | 공통 React 컴포넌트 (LoadingWithAnimation 등) | +| `@common/config` | 공통 설정값 | +| `@rsone/components` | 내부 공통 DS(Design System) 컴포넌트 | + +--- + +## 개발 도구 + +| 도구 | 역할 | +|------|------| +| **steiger** | FSD 아키텍처 규칙 검사 도구 | +| **prettier** | 코드 포맷터 | +| **ESLint** | 코드 품질 검사 | +| **@tanstack/react-query-devtools** | TanStack Query 개발자 도구 | diff --git a/docs/st/04.아키텍처패턴.md b/docs/st/04.아키텍처패턴.md new file mode 100644 index 000000000..3cdbf620b --- /dev/null +++ b/docs/st/04.아키텍처패턴.md @@ -0,0 +1,155 @@ +# 04. 아키텍처 패턴 + +ST 앱이 채택한 FSD(Feature-Sliced Design) 아키텍처 및 전체 데이터 흐름을 설명합니다. + +--- + +## 1. FSD(Feature-Sliced Design) 아키텍처 + +FSD는 레이어(Layer) 계층 간 단방향 의존성을 강제하는 프론트엔드 아키텍처 방법론입니다. +**"하위 레이어는 상위 레이어를 참조할 수 없다"** 는 규칙이 핵심입니다. + +``` +┌─────────────────────────────────────────┐ +│ app / pages (라우팅) │ ← 최상위 (조합) +├─────────────────────────────────────────┤ +│ components (업무 UI) │ +├─────────────────────────────────────────┤ +│ entities (도메인 엔티티) │ ← API 서비스 + 타입 +├─────────────────────────────────────────┤ +│ shared (공통 기반) │ ← 최하위 (재사용) +└─────────────────────────────────────────┘ +``` + +각 레이어는 내부적으로 슬라이스(Slice)와 세그먼트(Segment)로 세분화됩니다. + +``` +레이어/슬라이스/세그먼트 +예시: entities/statistics/api/ + entities/statistics/model/ +``` + +--- + +## 2. 레이어별 역할과 의존성 방향 + +### 🔷 `shared` - 공통 기반 레이어 +- **역할**: 모든 레이어에서 공유 가능한 순수 유틸리티, API 클라이언트, 타입, 훅, 스토어 정의 +- **의존성**: 다른 레이어를 참조하지 않음 (최하위) +- **주요 내용**: `client.ts`, `use-mutation-wrapper`, Zustand 스토어 정의, 환경변수 설정 + +### 🔶 `entities` - 비즈니스 도메인 레이어 +- **역할**: 하나의 비즈니스 도메인(auth, menu, statistics 등)을 API 서비스와 타입으로 캡슐화 +- **의존성**: `shared`만 참조 가능 +- **주요 내용**: API 서비스 함수, 요청/응답 인터페이스 타입 + +### 🟩 `components` - UI 컴포넌트 레이어 +- **역할**: 업무 화면을 구성하는 React 컴포넌트. 서버 데이터 페칭 로직 포함 +- **의존성**: `shared`, `entities` 참조 가능 +- **주요 내용**: 화면별 컴포넌트, module 공통 컴포넌트(DataGrid, SearchForm 등) + +### 🟦 `app / pages` - 라우팅 레이어 +- **역할**: Next.js 라우팅 연결. 컴포넌트를 조합하여 최종 페이지 구성 +- **의존성**: 모든 하위 레이어 참조 가능 + +--- + +## 3. 미들웨어 기반 API 프록시 아키텍처 + +클라이언트가 백엔드 API를 직접 호출하지 않고 **Next.js Middleware를 통해 프록시**합니다. + +``` +Browser + │ + │ 요청: /server/st-management-service/... + ▼ +Next.js Middleware (middleware.ts) + │ + ├─ /server/st-management-service/* → ST 전용 백엔드 서버 + ├─ /server/st-input-service/* → ST 입력 서비스 서버 + ├─ /server/* → 일반 백엔드 서버 (Spring Boot 등) + ├─ /attach-service/* → 파일 첨부 서비스 + └─ /api/link/* → 외부 링크 백엔드 API +``` + +**핵심 동작:** +1. 요청 URL 경로(`pathname`)를 패턴 매칭 +2. 쿠키에서 JWT 토큰(`CLAIM_NAME`)과 사용자 ID(`AUTH_USER_ID`) 추출 +3. 해당 값을 요청 헤더에 주입 후 실제 백엔드로 URL 재작성(`NextResponse.rewrite`) +4. 응답에 CSP 보안 헤더 추가 + +--- + +## 4. 멀티 백엔드 분리 구조 + +``` +┌─────────────────────────────────────────────────────────┐ +│ ST 프론트엔드 앱 │ +│ │ +│ src/shared/api/client.ts │ +│ ┌──────────────────┐ ┌──────────────────────────┐ │ +│ │ client │ │ stClient │ │ +│ │ (일반 API) │ │ (ST 서비스 전용 API) │ │ +│ └──────────────────┘ └──────────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ + │ │ + ▼ ▼ + 일반 백엔드 (Spring) ST 전용 백엔드 서비스 + BASE_URL ST_SERVICE_BASE_URL +``` + +--- + +## 5. 컴포넌트 데이터 흐름 + +통계 화면을 예시로 데이터 흐름을 설명합니다. + +``` +app/(with-base-layout)/... ← 라우팅 진입 + │ + ▼ +components/statistics/input/ + StatsInputStatusInp.tsx ← 페이지 컴포넌트 + │ + ├── [검색 이벤트] handleSearch() + │ │ + │ ▼ + │ entities/statistics/api/ + │ statistics-input-status-service.ts ← API 서비스 + │ │ + │ ▼ + │ shared/api/client.ts (stClient) ← Axios 인스턴스 + │ │ + │ ▼ + │ middleware.ts ← Next.js 프록시 + │ │ + │ ▼ + │ ST 백엔드 API 서버 + │ + ├── [상태 관리] useSWR / useTabStore / useAlertStore + │ + └── [렌더링] SearchForm + DataGrid(RealGrid) + 버튼 +``` + +--- + +## 6. 탭 기반 네비게이션 패턴 + +ST 앱은 메뉴를 클릭하면 새 페이지로 이동하는 것이 아니라, **탭(Tab)을 추가하는 방식**으로 내비게이션이 동작합니다. + +``` +메뉴 클릭 + │ + ▼ +useTabStore.addTab() + │ + ├── 이미 열린 탭이면 → 해당 탭 활성화 전환만 + └── 새 탭이면 → 탭 목록에 추가 (최대 10개 제한) + │ + ▼ + activeTabId 변경 + │ + ▼ + 각 화면 컴포넌트: + useEffect([activeTabId]) → 탭 포커스 감지 → mutate() 데이터 갱신 +``` diff --git a/docs/st/05.상태관리.md b/docs/st/05.상태관리.md new file mode 100644 index 000000000..4828e5be7 --- /dev/null +++ b/docs/st/05.상태관리.md @@ -0,0 +1,138 @@ +# 05. 상태 관리 + +ST 앱의 상태 관리 전략과 Zustand 기반 전역 상태 설계를 설명합니다. + +--- + +## 1. 상태 관리 전략 개요 + +ST 앱은 상태의 **성격(클라이언트 vs 서버)**에 따라 다른 도구를 사용합니다. + +``` +┌─────────────────────────────────────────────────────────┐ +│ 상태 관리 분리 │ +├────────────────────────┬────────────────────────────────┤ +│ 클라이언트 상태 │ 서버 상태 │ +│ (UI, 세션 데이터) │ (API 응답 데이터) │ +│ │ │ +│ ✅ Zustand │ ✅ SWR (조회) │ +│ │ ✅ TanStack Query (변경) │ +└────────────────────────┴────────────────────────────────┘ +``` + +--- + +## 2. Zustand - 전역 클라이언트 상태 + +Zustand는 보일러플레이트가 없는 경량 전역 상태 관리 라이브러리입니다. + +### 스토어 목록 + +| 스토어 파일 | 관리 상태 | 주요 위치 | +|------------|---------|---------| +| `user-store.ts` | 로그인 사용자 정보 (이름, ID 등) | `shared/store/` | +| `menu-store.ts` | 현재 메뉴 정보, BBS 설정, 페이지 권한 | `shared/store/` | +| `alert-store.ts` | 전역 알림(Alert) 메시지 | `shared/store/` | +| `attachment-store.ts` | 첨부파일 상태 | `shared/store/` | +| `tab-store.ts` | 탭 목록 및 현재 활성 탭 | `store/` | +| `admin-store.ts` | 관리자 관련 상태 | `store/` | + +--- + +### 2-1. 스토어 생성 패턴 + +**Vanilla 스토어 패턴** (`shared/store/` 내 스토어들): + +```typescript +// shared/store/menu-store.ts +import { createStore } from "zustand/vanilla"; + +export const createMenuStore = (initState = defaultInitState) => { + return createStore()((set) => ({ + ...initState, + setCurrentMenuInfo: (menu) => set({ currentMenuInfo: menu }), + // ... + })); +}; +``` + +- `zustand/vanilla`를 사용하여 React에 종속되지 않는 순수 스토어를 생성 +- `store-provider.tsx`에서 React Context와 연결하여 컴포넌트에서 사용 + +**React 스토어 패턴** (`store/tab-store.ts`): + +```typescript +// store/tab-store.ts +import { create } from "zustand"; + +const useTabStoreHook = create((set, get) => ({ + tabs: [], + activeTabId: null, + addTab: (menuItem) => { /* ... */ }, + removeTab: (tabId) => { /* ... */ }, +})); + +export const useTabStore = useTabStoreHook; +``` + +- `create`를 사용하여 직접 React Hook 형태로 사용 + + +--- + +### 2-2. 스토어 사용 패턴 + +```typescript +// 컴포넌트에서 사용 +const { setMessage: alert } = useAlertStore((state) => state); +const activeTabId = useTabStore((state) => state.activeTabId); +const userInfo = useUserStore((state) => state.userInfo); +``` + +> `(state) => state.xxx` 셀렉터 패턴을 사용하여 필요한 상태만 구독하고, 불필요한 리렌더링을 방지합니다. + +--- + +## 3. tab-store 설계 - 탭 UI 관리 + +탭 기반 내비게이션의 핵심 스토어입니다. + +### 주요 상태 + +| 상태 | 타입 | 설명 | +|------|------|------| +| `tabs` | `Tab[]` | 현재 열린 탭 목록 | +| `activeTabId` | `string \| null` | 현재 활성화된 탭 ID | +| `menuList` | `MenuItem[] \| null` | 전체 메뉴 목록 | + +### 주요 액션 + +| 액션 | 설명 | +|------|------| +| `addTab(menuItem)` | 탭 추가 (이미 있으면 활성화, 최대 10개 제한) | +| `removeTab(tabId)` | 탭 닫기 (이전/다음 탭으로 자동 전환) | +| `setActiveTab(tabId)` | 탭 활성화 전환 | +| `closeAllTabs()` | 모든 탭 닫기 | +| `closeOtherTabs(tabId)` | 현재 탭 외 모두 닫기 | + +--- + +## 4. 화면 탭 포커스 감지 패턴 + +각 화면 컴포넌트에서 탭 전환을 감지하여 데이터를 자동 갱신하는 패턴입니다. + +```typescript +const activeTabId = useTabStore((state) => state.activeTabId); + +useEffect(() => { + // 이 컴포넌트의 탭이 활성화되면 데이터 새로고침 + if (activeTabId === 'StatsInputStatusInp' && swrKey) { + mutate(swrKey); + } +}, [activeTabId]); +``` + +**동작 원리:** +1. `activeTabId`가 변경될 때마다 `useEffect`가 실행됨 +2. 현재 컴포넌트의 탭 ID와 일치하는지 확인 +3. 일치하면 SWR의 `mutate()`로 최신 데이터를 서버에서 다시 가져옴 diff --git a/docs/st/06.데이터페칭.md b/docs/st/06.데이터페칭.md new file mode 100644 index 000000000..b2bc15929 --- /dev/null +++ b/docs/st/06.데이터페칭.md @@ -0,0 +1,152 @@ +# 06. 데이터 페칭 + +ST 앱에서 서버 데이터를 가져오고 변경하는 패턴(SWR, TanStack Query)을 설명합니다. + +--- + +## 1. 개요 - 두 가지 라이브러리 사용 이유 + +| 구분 | 라이브러리 | 사용 목적 | +|------|-----------|---------| +| **조회 (Read)** | SWR | 데이터 조회, 캐싱, 자동 갱신 | +| **변경 (Write)** | TanStack Query (`useMutation`) | 생성, 수정, 삭제 후 성공/실패 처리 | + +--- + +## 2. SWR - 서버 데이터 조회 패턴 + +SWR은 `stale-while-revalidate` 전략을 구현한 데이터 페칭 라이브러리입니다. +캐시된 데이터를 즉시 반환하면서 백그라운드에서 최신 데이터로 갱신합니다. + +### 기본 사용 패턴 + +```typescript +// 1. Fetcher 함수 정의 (key 배열을 인자로 받음) +const getStatsInputStatusFetcher = async ( + key: [string, StatsInputStatusListRequestParams] +): Promise => { + return StatisticsInputStatusApi.getStatisticsInputStatusList(key[1]); +}; + +// 2. SWR Key 정의 (null이면 데이터 페칭 중지) +const swrKey = searchParams + ? (["stats-input-status", searchParams] as const) + : null; + +// 3. useSWR 훅 사용 +const { isLoading } = useSWR( + swrKey, // 키 (null이면 페칭 안 함) + getStatsInputStatusFetcher, + { + revalidateOnFocus: true, // 탭 포커스 시 자동 갱신 + onSuccess: (data) => { + if (data.status === "success" && data.data) { + setRowData(toGridRows(data.data)); + } else { + alert(data.message ?? "조회에 실패했습니다."); + } + }, + onError: () => { + alert("데이터 조회 중 오류가 발생했습니다."); + }, + } +); +``` + +### SWR Key 전략 + +SWR Key는 데이터의 고유 식별자입니다. + +```typescript +// 문자열 + 파라미터 배열 조합 +["stats-input-status", { ctpvNm: "서울", statsYr: "2024" }] +``` + +- **배열 형태**: Key의 첫 번째 요소는 식별 문자열, 두 번째는 파라미터 객체 +- **null Key**: 페칭 일시 정지 (조건부 페칭) +- **객체가 달라지면**: 새로운 API 호출 트리거 + +### `mutate` - 수동 캐시 무효화 + +```typescript +const { mutate } = useSWRConfig(); + +// 특정 key의 데이터를 서버에서 다시 가져옴 +mutate(["stats-input-status", params]); +``` + +주로 두 가지 상황에서 사용합니다. +1. **검색 버튼 클릭 시**: 새로운 파라미터로 즉시 조회 +2. **탭 포커스 시**: 화면으로 돌아왔을 때 데이터 갱신 + +--- + +## 3. TanStack Query - 데이터 변경 패턴 + +생성, 수정, 삭제 등 서버 상태를 변경하는 작업에는 `useMutation`을 사용합니다. + +### `useMutationWrapper` - 공통 에러 처리 래퍼 + +```typescript +// shared/lib/use-mutation-wrapper.tsx +export const useMutationWrapper = ( + mutationOptions: UseMutationOptions +) => { + const alert = useAlertStore((state) => state.setMessage); + + return useMutation({ + ...mutationOptions, + onError: (error) => { + // AxiosError 공통 에러 메시지 처리 + if (error instanceof AxiosError) { + if (error.response?.data?.message) errorMessage += `...`; + } + alert(`${errorMessage}`); + }, + }); +}; +``` + +**설계 목적**: `onError` 공통 처리를 래퍼에서 중앙화하여, 각 컴포넌트에서 Axios 에러를 반복해서 처리하지 않아도 됩니다. + +### `mutateAsync` vs `mutate` 비교 + +```typescript +const { mutateAsync: createPost } = useMutationWrapper({ + mutationFn: (data: PostFormData) => PostApi.createPost(data), + onSuccess: () => { + alert("등록되었습니다.", router.back); + }, +}); +``` + +| 구분 | `mutate` | `mutateAsync` | +|------|---------|---------------| +| **반환값** | `void` | `Promise` | +| **에러 처리** | `onError` 콜백만 가능 | `try/catch` 또는 `.catch()` 가능 | +| **await 사용** | ❌ 불가 | ✅ 가능 | +| **사용 시나리오** | 단순 저장 후 콜백 처리 | 자식 컴포넌트에 전달하거나 순서 제어 필요 시 | + +> `mutateAsync`를 쓰는 주된 이유: ``처럼 하위 컴포넌트에 실행 함수를 전달할 때, 하위 컴포넌트가 완료 시점을 `await`로 기다릴 수 있어야 하기 때문입니다. + +--- + +## 4. 패턴 요약 - 어떤 상황에서 무엇을 쓰나? + +``` +사용자 인터랙션 흐름 + │ + ├── [데이터 조회] 검색 버튼 클릭 + │ └── setSearchParams() → SWR Key 변경 → useSWR 자동 재조회 + │ 또는 + │ mutate(swrKey) → 강제 재조회 + │ + ├── [데이터 저장/수정/삭제] + │ └── useMutationWrapper({ mutationFn: ... }) + │ └── mutateAsync() / mutate() 호출 + │ ├── onSuccess: 성공 후처리 (Alert, 화면 이동 등) + │ └── onError: 자동 공통 에러 Alert 표시 + │ + └── [탭 전환 시 데이터 갱신] + └── useEffect([activeTabId]) → mutate(swrKey) +``` diff --git a/docs/st/07.용어정리.md b/docs/st/07.용어정리.md new file mode 100644 index 000000000..1e29336d0 --- /dev/null +++ b/docs/st/07.용어정리.md @@ -0,0 +1,107 @@ +# 07. 용어 정리 + +ST 앱 소스코드 및 문서에서 자주 등장하는 주요 용어를 알파벳·가나다 순으로 정리합니다. + +--- + +## 아키텍처 & 패턴 용어 + +| 용어 | 설명 | +|------|------| +| **FSD** (Feature-Sliced Design) | 레이어(Layer) 간 단방향 의존성을 강제하는 프론트엔드 아키텍처 방법론. `shared → entities → components → pages → app` 순으로 하위 레이어만 참조 가능 | +| **App Router** | Next.js 13+에서 도입된 파일시스템 기반 라우팅 방식. `app/` 디렉토리를 사용하며 Server Component, Layout 등 최신 기능을 지원 | +| **Pages Router** | Next.js의 기존 라우팅 방식. `pages/` 디렉토리를 사용 | +| **Route Group** | Next.js App Router에서 `(폴더명)` 형태로 URL 경로에는 영향 없이 레이아웃만 공유하는 폴더. 예: `(with-base-layout)` | +| **Middleware** | Next.js에서 요청이 실제 페이지/API에 도달하기 전에 실행되는 함수. `middleware.ts` 파일에 정의. 프록시, 인증 처리 등에 활용 | +| **CSP** (Content Security Policy) | XSS 공격을 방지하기 위한 HTTP 보안 헤더. 어떤 리소스(이미지, 스크립트 등)를 로드할 수 있는지 정의 | +| **Monorepo** | 여러 앱과 패키지를 하나의 저장소에서 관리하는 방식. pnpm Workspace를 사용하여 `@common/*`, `@rsone/*` 등 내부 패키지를 공유 | + +--- + +## 상태 관리 용어 + +| 용어 | 설명 | +|------|------| +| **Zustand** | 경량 전역 상태 관리 라이브러리. Redux보다 보일러플레이트가 적고, 훅 기반으로 사용이 간편함 | +| **Store** | Zustand에서 상태(State)와 액션(Action)을 담고 있는 단위. `create()` 또는 `createStore()`로 생성 | +| **Selector** | `useStore((state) => state.xxx)` 패턴으로 스토어에서 필요한 값만 구독하는 함수. 불필요한 리렌더링을 방지 | +| **Vanilla Store** | `zustand/vanilla`의 `createStore()`로 생성한 React에 무관한 순수 스토어. Provider 패턴과 함께 사용 | +| **activeTabId** | 현재 활성화(포커스)된 탭의 고유 ID. 각 화면 컴포넌트는 이 값을 감지하여 데이터를 자동 갱신함 | +| **Tab** | 탭 UI의 단위 객체. `id`(탭 ID), `label`(탭명), `urlPath`(URL), `menuItem`(메뉴 정보) 를 포함 | + +--- + +## 데이터 페칭 용어 + +| 용어 | 설명 | +|------|------| +| **SWR** (Stale-While-Revalidate) | 캐시된 데이터를 즉시 반환하면서 백그라운드로 서버 데이터를 갱신하는 전략. 데이터 조회에 사용 | +| **SWR Key** | SWR에서 데이터를 식별하고 캐싱하는 고유 키. `null`이면 페칭이 일시 중지됨. 보통 `[식별문자열, 파라미터]` 배열 형태 사용 | +| **mutate** (SWR) | SWR 캐시를 수동으로 무효화하고 서버에서 최신 데이터를 즉시 가져오는 함수 | +| **TanStack Query** | 서버 상태(비동기 데이터) 관리 라이브러리. 구 이름은 React Query. 캐싱, 리트라이, 데이터 동기화 등을 지원 | +| **useMutation** | TanStack Query에서 데이터 생성/수정/삭제(Write) 작업에 사용하는 훅. `mutate`, `mutateAsync` 함수를 반환 | +| **mutate** (TanStack Query) | `useMutation`이 반환하는 실행 함수. 반환값이 `void`이며, 결과는 콜백(`onSuccess`, `onError`)으로만 처리 가능 | +| **mutateAsync** | `useMutation`이 반환하는 비동기 실행 함수. `Promise`를 반환하므로 `await`로 결과를 기다리거나 `try/catch`로 에러 처리 가능 | +| **Fetcher** | SWR에서 데이터를 가져오는 비동기 함수. SWR Key를 인자로 받아 API를 호출하고 데이터를 반환 | +| **Revalidation** | 캐시가 오래된 경우 서버에서 최신 데이터를 다시 가져오는 과정. SWR의 핵심 동작 원리 | +| **invalidateQueries** | TanStack Query에서 특정 쿼리의 캐시를 무효화하여 자동 재조회를 유발하는 함수 | + +--- + +## HTTP & API 용어 + +| 용어 | 설명 | +|------|------| +| **Axios** | Promise 기반 HTTP 클라이언트. 인터셉터(Interceptor)를 통해 토큰 자동 주입, 에러 핸들링 등을 설정 가능 | +| **AxiosError** | Axios에서 HTTP 에러(4xx, 5xx 응답 등) 발생 시 던지는 에러 객체. `error.response.data`로 서버 에러 메시지에 접근 가능 | +| **API 프록시** | 클라이언트가 `/server/*` 경로로 요청하면 Middleware가 실제 백엔드 URL로 재작성(Rewrite)하는 패턴 | +| **Rewrite** | Next.js Middleware 또는 `next.config.ts`에서 요청 URL을 다른 URL로 투명하게 변환하는 기능 | +| **TUS 프로토콜** | 대용량 파일 업로드를 지원하는 개방형 프로토콜. 업로드 중 네트워크 오류가 발생해도 중단된 지점부터 재개(Resume) 가능 | +| **stClient** | ST 전용 백엔드 서비스(`st-management-service`, `st-input-service`)와 통신하는 Axios 인스턴스 | +| **client** | 일반 백엔드 API 서버와 통신하는 기본 Axios 인스턴스 | + +--- + +## UI 컴포넌트 용어 + +| 용어 | 설명 | +|------|------| +| **RealGrid** | 대용량 데이터 처리에 특화된 상용 엔터프라이즈 그리드 라이브러리. 인메모리 데이터 처리로 수십만 행도 빠르게 렌더링 가능 | +| **DataGrid** | RealGrid를 기반으로 프로젝트 사양에 맞게 구현한 공통 그리드 컴포넌트 (`components/module/DataGrid.tsx`) | +| **SearchForm** | 시도, 시군구, 연도, 사업장 등 공통 검색 조건을 가진 공유 폼 컴포넌트 | +| **FullCalendar** | 월별·주별·시간별 달력 UI를 제공하는 오픈소스 캘린더 라이브러리 | +| **TinyMCE** | 웹 기반 WYSIWYG 리치 텍스트 에디터. 게시판 본문 입력 등에 활용 | +| **Radix UI** | 접근성(a11y)을 준수하는 헤드리스(스타일이 없는) UI 원시 컴포넌트 라이브러리 | +| **Sonner** | 경량 토스트(Toast) 알림 컴포넌트 라이브러리 | + +--- + +## 폼 & 유효성 검증 용어 + +| 용어 | 설명 | +|------|------| +| **react-hook-form** | 비제어 컴포넌트(Uncontrolled Component) 방식으로 폼 상태를 관리하여 불필요한 리렌더링을 최소화하는 라이브러리 | +| **Zod** | TypeScript 우선 스키마 선언 및 런타임 유효성 검증 라이브러리. 스키마 정의에서 TypeScript 타입을 자동 추론 | +| **zodResolver** | react-hook-form과 Zod를 연결해주는 어댑터. 폼 제출 시 Zod 스키마로 자동 유효성 검증 | +| **Schema** | Zod에서 데이터 구조와 유효성 규칙을 정의한 것. `postFormSchema`, `loginSchema` 등 | + +--- + +## 업무 도메인 용어 + +| 약어/영문 | 설명 | +|---------|------| +| **ST** | Statistics - 통계 관리 시스템 | +| **ctpvNm** | 특별/광역시도명 (시도명) | +| **sggNm** | 시군구명 | +| **sewerBzmnCd** | 하수도 사업자 코드 | +| **statsYr** | 통계 연도 | +| **vrfcMthdCd** | 검증 방법 코드 | +| **mngAuthrtYn** | 관리 권한 여부 | +| **inqAuthrtYn** | 조회 권한 여부 | +| **regAuthrtYn** | 등록 권한 여부 | +| **mdfcnAuthrtYn** | 수정 권한 여부 | +| **delAuthrtYn** | 삭제 권한 여부 | +| **CLAIM_NAME** | JWT 토큰을 저장하는 쿠키/헤더 키 이름 | +| **AUTH_USER_ID** | 사용자 ID를 저장하는 쿠키/헤더 키 이름 | +| **SITE_ID** | 멀티 사이트 구분을 위한 사이트 식별자 | diff --git a/docs/st/08.리얼그리드활용.md b/docs/st/08.리얼그리드활용.md new file mode 100644 index 000000000..42dccf8f7 --- /dev/null +++ b/docs/st/08.리얼그리드활용.md @@ -0,0 +1,169 @@ +# 08. 리얼그리드(RealGrid) 활용 + +ST 앱에서 사용하는 엔터프라이즈급 그리드 라이브러리인 **RealGrid 2**의 구현 방식과 주요 함수, 속성을 정리합니다. + +--- + +## 1. 개요 및 구조 + +ST 앱은 `realgrid-react`를 사용하여 리액트 환경에서 그리드를 구현하고 있으며, 공통 모듈인 `DataGrid.tsx`에서 복잡한 설정을 추상화하여 제공합니다. + +### 주요 컴포넌트 +- **`DataGrid.tsx`**: 가장 핵심이 되는 공통 그리드 컴포넌트. +- **`TabDataGrid.tsx`**: 탭별로 독립적인 `DataGrid`를 렌더링. +- **`SplitDataGrid.tsx`**: 좌우 5:5 분할 화면에 두 개의 그리드를 배치. +- **`CompactDataGrid.tsx`**: 간소화된 형태의 그리드. + +--- + +## 2. 주요 설정 및 속성 (Props) + +`DataGridProps` 인터페이스를 통해 그리드의 동작을 제어합니다. + +| 속성명 | 타입 | 설명 | +|-------|------|------| +| `columnInfo` | `ColumnInfo` | 컬럼 정의(`columns`)와 레이아웃(`columnLayout`) 정보 | +| `rowData` | `Record[]` | 그리드에 표시할 실제 데이터 배열 | +| `fixedColumns` | `number` | 좌측에 고정할 컬럼 수 | +| `rowStartNum` | `number` | 엑셀 임포트 시 헤더가 시작되는 행 번호 | +| `isExcelImport` | `boolean` | 엑셀 가져오기 버튼 표시 여부 | +| `isExcelExport` | `boolean` | 엑셀 내보내기 버튼 표시 여부 | +| `showHeaderCheckbox`| `boolean` | 헤더 체크박스(CheckBar) 표시 여부 | +| `cellStyleCallback` | `Function` | 실시간 조건부 셀 스타일 적용 콜백 | +| `cellTooltipCallback`| `Function` | 셀 호버 시 표시할 툴팁 메시지 콜백 | +| `onRowClick` | `Function` | 행 클릭 시 실행될 이벤트 핸들러 | + +--- + +## 3. 컬럼 정의 (`ColumnConfig`) + +`src/components/type/DataGrid.type.ts`에 정의된 주요 컬럼 속성입니다. + +```typescript +export type ColumnConfig = { + name: string; // 컬럼 고유 이름 + fieldName: string; // 데이터 필드명 + header: { text: string }; // 헤더 표시 텍스트 + width?: number; // 컬럼 너비 (px) + editable?: boolean; // 편집 가능 여부 + visible?: boolean; // 숨김 여부 + styleName?: string; // 적용할 CSS 클래스 + mergeCells?: boolean; // 셀 병합 여부 (내용이 같을 때) + valueType?: ValueType; // 데이터 타입 (TEXT, NUMBER 등) + footer?: FooterConfig; // 푸터 설정 (합계 등) + computed?: Computed; // 계산 필드 설정 (합계/평균/비율 등) +}; +``` + +--- + +## 4. 핵심 기능 구현 상세 + +### 4-1. 커스텀 정렬 (`onSorting`) +기본 정렬 사용 시 "시군구" 등 병합된 셀의 그룹 순서가 깨지는 문제를 해결하기 위해 전용 핸들러를 사용합니다. +- **`createOnSortingHandler`**: 정렬 시 그룹(시군구) 내의 순서를 유지하며 그룹 간 정렬을 수행합니다. +- **`createNumberComparer`**: 숫자에 포함된 쉼표(,)를 제거하고 순수 숫자 값으로 비교하여 정확한 정렬을 보장합니다. + +### 4-2. 계산 필드 (`Computed Field`) +`dataProvider.setFields` 시 `valueCallback`을 통해 실시간 계산 값을 생성합니다. +- **지원 유형**: 단순 합계(`sum`), 평균(`avg`), 보급률(`pblswrgPopltnSuprtRate`) 등 특수 비즈니스 로직. +- **매커니즘**: `getComputedValueCallback` 유틸을 통해 여러 필드의 값을 조합하여 결과를 반환합니다. + +### 4-3. 엑셀 임포트/익스포트 (Smart Mapping) +일반적인 그리드와 달리, 엑셀 파일과 그리드 간의 정확한 필드 매칭을 위해 특수한 기법을 사용합니다. +- **익스포트 시**: 엑셀 헤더 텍스트 뒤에 `(fieldName)`을 임시로 붙여서 내보냅니다. (예: `인구수 (popltnCnt)`) +- **임포트 시**: 정규표현식 `/\(([a-zA-Z0-9_]+)\)$/`을 사용하여 헤더에서 `fieldName`을 추출, 데이터 파싱 시 정확한 객체 키로 매핑합니다. + +### 4-4. 푸터(Footer) 및 레이아웃 합병 +- **`footerUserSpans`**: `columnLayout` 설정을 통해 푸터 영역에서 여러 셀을 하나로 합치는 기능을 지원합니다. +- **멀티 푸터**: `gridView.setFooters`를 통해 2행 이상의 푸터를 설정하고, 각 행마다 다른 통계(합계, 평균 등)를 보여줍니다. + +--- + +## 5. 자주 사용하는 유틸리티 함수 + +### `toGridRows` +API 응답 객체 배열을 RealGrid가 처리하기 좋은 형태로 변환합니다. +- `null` 또는 `undefined` 값을 빈 문자열(`""`)로 치환하여 그리드 상에서 `null` 문구가 노출되지 않도록 합니다. + +--- + +## 6. 스타일 가이드 (`styleName`) + +그리드 셀에 적용되는 주요 CSS 클래스입니다 (프로젝트 관례). +- `rg-cell-readonly`: 읽기 전용 셀 배경색 처리 +- `rg-cell-total`: 합계 행/열 강조 +- `rg-cell-error`: 검증 오류 발생 셀 (빨간색 테두리 등) +- `rg-cell-required`: 필수 입력 셀 표시 + +--- + +## 7. RealGrid 원천 API 상세 (Native API) + +프로젝트 코드(`DataGrid.tsx`, `CompactDataGrid.tsx`)에서 실제로 호출하고 있는 RealGrid의 원천 함수와 속성 목록입니다. + +### 7-1. 전역 설정 (`realgrid`) +- **`RealGrid.setLicenseKey(key: string)`**: 라이선스 키를 등록합니다. +- **`ValueType`**: 필드 데이터 타입을 지정하는 열거형 (예: `ValueType.TEXT`, `ValueType.NUMBER`). +- **`GridFitStyle`**: 그리드 너비 맞춤 스타일 (예: `GridFitStyle.EVEN`). + +### 7-2. 그리드 뷰 (`GridView`) +그리드의 시각적 요소와 이벤트를 제어합니다. + +| API | 설명 | +|-----|-----| +| `setOptions(options)` | 그리드 전역 옵션 설정 (`checkBar.visible`, `summaryMode` 등) | +| `setDisplayOptions(options)` | 화면 표시 상세 설정 (`fitStyle`, `rowHeight` 등) | +| `setFixedOptions(options)` | 고정 컬럼 설정 (`colCount`) | +| `setFooters(heights)` | 멀티 푸터 행들의 높이를 배열로 설정 | +| `setColumns(columns)` | 컬럼 구성을 동적으로 재설정 | +| `setColumnLayout(layout)` | 컬럼의 시각적 배치(그룹 헤더 등) 적용 | +| `setColumnProperty(col, prop, val)` | 특정 컬럼의 속성(예: `renderer`)을 개별 변경 | +| `exportGrid(options)` | 엑셀 파일 내보내기 실행 | +| `setCurrent(index)` | 특정 행/셀로 포커스 이동 (`itemIndex` 사용) | +| `setFocus()` | 그리드 자체에 포커스 부여 | +| `refresh()` | 그리드 화면 강제 다시 그리기 (스타일 변경 시 유용) | +| `columnByName(name)` | 이름으로 특정 컬럼 객체 반환 | + +**주요 이벤트 핸들러:** +- `onSorting`: 정렬 시점 발생. (커스텀 그룹 정렬 구현 시 사용) +- `onCellClicked`: 셀 클릭 시 발생. (행 선택 로직 구현) +- `onShowTooltip`: 툴팁 표시 직전 발생. (동적 툴팁 내용 생성) + +### 7-3. 데이터 프로바이더 (`LocalDataProvider`) +그리드 내부의 실제 데이터를 관리합니다. + +| API | 설명 | +|-----|-----| +| `setFields(fields)` | 데이터 필드(데이터 모델) 구조 정의 | +| `setRows(rows)` | 전체 데이터 로드 | +| `getRowCount()` | 전체 데이터 행 수 반환 | +| `getValue(row, field)` | 특정 행/필드의 데이터 값 조회 | +| `setDataComparer(field, fn)` | 특정 필드의 정렬 로직을 사용자 정의 함수로 위임 | + +--- + +## 8. RealGrid-React 컴포넌트 (`realgrid-react`) + +### 8-1. `` (메인 컨테이너) +- **`ref`**: 내부 `gridView`와 `dataProvider` 객체에 접근하기 위해 사용. +- **`rows`**: 리액트 상태(`rowData`)를 그리드 데이터와 바인딩. +- **`layout`**: 정의된 `columnLayout`을 적용. +- **`autoGenerateField`**: `true` 설정 시 행 데이터를 바탕으로 필드를 자동 생성. +- **`onInitialized(grid)`**: 그리드 초기화 완료 후 실행되는 콜백. + +### 8-2. `` (컬럼 속성 정의) +JSX 형태로 각 컬럼의 세부 속성을 설정합니다. + +| 주요 Props | 설명 | +|-----------|-----| +| `name` / `fieldName` | 컬럼/필드 식별자 | +| `valueType` | 데이터 타입 (`TEXT`, `NUMBER`) | +| `numberFormat` | 숫자 표시 형식 (예: `#,##0.000`) | +| `header` | 헤더 설정 객체 (`{ text: "제목" }`) | +| `editable` | 해당 컬럼의 편집 가능 여부 | +| `styleName` | 해당 컬럼에 적용할 CSS 클래스명 | +| `renderer` | 셀 렌더러 설정 (예: `{ showTooltip: true }`) | +| `footer` | 푸터 합계/수식 설정 | +| `mergeRule` | 셀 병합 규칙 (`{ criteria: "value" }`) | +| `sortable` | 정렬 가능 여부 | diff --git a/docs/st/10.FSD_구조하에_CS_프로그램.md b/docs/st/10.FSD_구조하에_CS_프로그램.md new file mode 100644 index 000000000..36b402053 --- /dev/null +++ b/docs/st/10.FSD_구조하에_CS_프로그램.md @@ -0,0 +1,700 @@ +# 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 { + const response = await client.get( + `${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 ( + // React Query 설정 + // Zustand Store 초기화 + + + // entities/auth - 인증 처리 + // entities/menu - 메뉴 초기화 + + {children} + + + // shared/ui - 전역 다이얼로그 + + + + + + + + + + ); +}; +``` + +--- + +## 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`로 추출 | 레이어 역할 준수 |