[as fr]pm2 통합방법

This commit is contained in:
2026-10-20 16:29:33 +09:00
parent cc4acb3916
commit 177f4653c8
3 changed files with 1327 additions and 0 deletions
@@ -0,0 +1,160 @@
# 개발(Dev) 및 운영(Production) 실행 가이드 & 환경변수 관리
본 문서는 `apps/as` 애플리케이션의 **개발(Dev) 모드** 및 **운영(Production) 모드** 실행 방법과 **환경변수(.env) 변경 시의 재빌드 규칙**을 상세히 정리한 가이드입니다.
---
## 1. Dev 모드 vs Production 모드 비교
| 구분 | 개발 모드 (Dev) | 운영 모드 (Production) |
|---|---|---|
| **실행 목적** | 로컬 개발, 디버깅, 실시간 코드 수정 | 실서버 운영, 성능 최적화, 안정성 보장 |
| **사전 빌드 필요 여부** | **불필요** (즉시 실행) | **필수** (`.next` 번들 생성 선행) |
| **코드 반영 방식** | 핫 리로딩(HMR, 저장 즉시 반영) | 빌드 시점의 번들링 코드 고정 서빙 |
| **참조 환경변수** | `.env.local` > `.env.development` | `.env.local` > `.env.production` |
| **프로세스 관리** | Node 단일 프로세스 (터미널) | **PM2 Cluster 모드** (2개 인스턴스 무중단) |
| **서비스 포트** | 기본 `3004` 포트 | 기본 `3004` 포트 |
---
## 2. 환경변수(.env) 변경 시 재빌드 필수 여부
### 2.1 결론: `.env.production` 변경 시 반드시 재빌드 필요
Next.js 환경에서는 `.env.production`을 수정했을 때 단순히 PM2 재시작(`pm2 reload` / `pm2 restart`)만으로는 변경 사항이 브라우저에 반영되지 않으며, **반드시 빌드(`next build`)를 다시 수행해야 합니다.**
### 2.2 이유: Next.js의 환경변수 인라이닝(Inlining) 원리
1. **`NEXT_PUBLIC_*` 변수의 빌드타임 주입**:
- `NEXT_PUBLIC_SERVER_API_URL`, `NEXT_PUBLIC_PROXY_HOST`, `NEXT_PUBLIC_WEBHYP_CTRL_URL` 등 브라우저에서 참조하는 변수들은 `next build` 시점에 생성되는 클라이언트 JavaScript 파일(`.next/static/...`) 안에 **문자열 값으로 직접 하드코딩(Inlining)**됩니다.
- 따라서 코드를 다시 빌드하지 않으면 예전 주소나 설정값이 그대로 유지됩니다.
2. **정적 페이지(SSG) 생성 시점의 값 고정**:
- 빌드 시 정적 생성된 페이지(`Generating static pages (22/22)`)는 빌드 당시 읽어들인 환경변수 값을 기준으로 생성되므로, 런타임에 환경변수를 바꿔도 페이지가 갱신되지 않습니다.
### 2.3 환경변수 우선순위 주의사항
Next.js의 환경변수 로딩 우선순위는 다음과 같습니다:
```text
[운영 환경: NODE_ENV=production]
1순위: .env.production.local
2순위: .env.local (NODE_ENV=test 제외 항상 최우선)
3순위: .env.production
4순위: .env
```
> [!WARNING]
> 실서버 환경에 `.env.local` 파일이 존재할 경우 `.env.production`보다 우선 적용됩니다. `.env.production`을 수정했는데도 반영되지 않는다면 같은 경로에 `.env.local`이 존재하는지 확인하십시오.
---
## 3. Dev (개발 모드) 실행 방법
사전 빌드 과정 없이 소스 코드를 실시간으로 수정하며 개발할 때 사용합니다.
### 3.1 프로젝트 루트 디렉터리에서 실행
```bash
# 루트 디렉터리: ~/frontend_as
pnpm dev:as
# 또는 pnpm filter 플래그 사용
pnpm --filter as dev
```
### 3.2 `apps/as` 디렉터리 내부에서 실행
```bash
cd ~/frontend_as/apps/as
pnpm dev
```
- **접속 주소**: [http://localhost:3004](http://localhost:3004)
- **종료**: 터미널에서 `Ctrl + C`
---
## 4. Production (운영 모드) 실행 방법
반드시 **빌드 산출물(`.next`)이 최신 상태로 생성되어 있어야 정상 실행**됩니다.
### 4.1 1단계: 프로덕션 빌드 (필수)
모노레포 전체를 빌드할 필요 없이 `as` 앱만 단독으로 빌드할 수 있습니다:
```bash
# 프로젝트 루트 디렉터리에서 실행 (약 20~30초 소요)
pnpm build:as
# 또는
pnpm --filter as build
```
---
### 4.2 2단계: 서버 프로세스 실행
#### 방법 A: 터미널에서 직접 실행 (로컬 프로덕션 점검용)
포그라운드 터미널에서 프로덕션 번들의 빌드 결과와 동작을 직접 테스트할 때 사용합니다:
```bash
# 루트 디렉터리에서 실행
pnpm --filter as start
# 또는 apps/as 디렉터리에서 실행
cd apps/as
pnpm start
```
---
#### 방법 B: PM2 프로세스 매니저로 실행 (실서버 운영 표준)
실제 운영 서버에서는 `ecosystem.config.js` 설정을 통해 무중단 클러스터(2개 인스턴스)로 실행합니다.
```bash
# 1. 프로세스가 아직 등록되지 않은 경우 (최초 기동)
pm2 start ecosystem.config.js --only as2-app
# 2. 이미 실행 중인 경우 (무중단 롤링 리로드 - 권장)
# 순차적으로 인스턴스를 재시작하므로 사용자 요청 끊김이 없습니다.
pm2 reload as2-app
# 3. 프로세스 완전 재시작이 필요한 경우
pm2 restart as2-app
```
---
## 5. PM2 운영 및 모니터링 치트시트
```bash
# 1. 전체 프로세스 상태 확인 (상태, 메모리, CPU 점유율 등)
pm2 status
# 또는
pm2 list
# 2. as2-app 실시간 로그 모니터링
pm2 logs as2-app
# 3. 최근 로그 100줄 출력
pm2 logs as2-app --lines 100
# 4. 프로세스 일시 중지 및 삭제
pm2 stop as2-app # 프로세스 일시 중지
pm2 delete as2-app # PM2 목록에서 제거
# 5. 서버 재부팅 시 자동 실행 등록
pm2 save # 현재 실행 중인 프로세스 목록 저장
pm2 startup # OS 부팅 스크립트 생성
```
---
## 6. 실무 배포 요약 워크플로우
`.env.production` 수정 또는 프론트엔드 코드 수정 후 배포 시 아래 3단계를 수행합니다:
```bash
# Step 1. 최신 코드 반영 (필요시)
git pull origin deploy
# Step 2. as 앱 단독 프로덕션 재빌드
pnpm build:as
# Step 3. PM2 무중단 롤링 리로드
pm2 reload as2-app
```
+551
View File
@@ -0,0 +1,551 @@
# AS(디지털 행정지원) 모듈 아키텍처, UI 구조 및 화면 데이터 흐름 분석 보고서
## 1. 개요 (Overview)
본 문서는 **디지털 행정지원 시스템(Administrative Support System, 이하 AS 모듈)** 프론트엔드 애플리케이션([`apps/as`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as))의 기술 스펙, UI 구조, 컴포넌트 아키텍처 및 화면 데이터 흐름을 상세히 분석하여 정리한 기술 명세서입니다.
AS 모듈은 하수도 기본계획, 재원협의, 설치인가, 설계자문 등 공공 행정 업무의 신청·접수·검토·보완·결재 절차를 전자화하고, 대시보드 및 각종 기준 정보(코드, 체크리스트, 필수 첨부파일 등)를 체계적으로 관리하는 핵심 업무 포털입니다.
---
## 2. 프로그램 기술 스펙 (Technology Stack & Specifications)
### 2.1 코어 런타임 및 프레임워크
| 구분 | 기술 / 라이브러리 | 버전 | 주요 역할 및 용도 |
| :--- | :--- | :--- | :--- |
| **Framework** | **Next.js** | `15.5.18` | App Router 기반 React 메타 프레임워크, SSR/CSR 하이브리드 지원, Standalone 빌드 배포 |
| **Core UI** | **React / React DOM** | `19.x` (`next` 내장) | 컴포넌트 기반 UI 렌더링 엔진 |
| **Language** | **TypeScript** | `^5.7.2` | 정적 타입 검사, DTO/인터페이스 정의 및 무결성 보장 |
| **Architecture** | **Feature-Sliced Design (FSD)** | `v2.1` | 계층형 프론트엔드 아키텍처(App, Pages, Widgets, Features, Entities, Shared) |
| **Architecture Lint** | **Steiger** | `^0.5.11` | FSD 아키텍처 규칙(레이어 간 Import 의존성 방향, Public API 준수 여부) 정적 검사 |
### 2.2 상태 관리 및 데이터 통신
| 구분 | 기술 / 라이브러리 | 버전 | 주요 역할 및 용도 |
| :--- | :--- | :--- | :--- |
| **Server State** | **@tanstack/react-query** | `^5.75.5` | 서버 데이터 페칭, 캐싱, 동기화, 자동 무효화(`invalidateQueries`), 쿼리 키 기반 상태 관리 |
| **DevTools** | **@tanstack/react-query-devtools** | `^5.75.5` | React Query 상태 디버깅 및 캐시 모니터링 |
| **Client State** | **Zustand** | (내장 / 공유) | 사용자 프로필(`user-store`), 메뉴/권한(`menu-store`), 공통 Alert(`alert-store`) 전역 클라이언트 상태 |
| **HTTP Client** | **Axios** | `^1.8.1` | REST API 클라이언트, 요청 헤더 주입(토큰, 사용자ID, 기기ID), 401 응답 인터셉터 |
| **File Transfer** | **tus-js-client** | `^4.3.1` | TUS 프로토콜 기반 대용량 첨부파일 청크 업로드 및 이어올리기 지원 |
| **Cookie Mgt** | **js-cookie** | `^3.0.5` | 브라우저 쿠키(인증 토큰, 사용자 ID, 사이트 ID, 메뉴 코드 등) 제어 |
### 2.3 폼 관리 및 유효성 검증
| 구분 | 기술 / 라이브러리 | 버전 | 주요 역할 및 용도 |
| :--- | :--- | :--- | :--- |
| **Form Engine** | **react-hook-form** | `^7.54.2` | 비제어 컴포넌트 기반 고성능 폼 상태 관리 (`FormProvider`, `useFormContext`) |
| **Schema Validation** | **zod** | `^3.24.2` | 선언형 스키마 정의 및 데이터 타입 런타임 유효성 검증 |
| **Form Resolver** | **@hookform/resolvers** | `^4.1.3` | React Hook Form과 Zod 스키마의 바인딩 어댑터 |
| **Form DevTools** | **@hookform/devtools** | `^4.4.0` | 폼 상태 및 에러 추적 디버깅 도구 |
### 2.4 데이터 테이블 및 UI 인터랙션
| 구분 | 기술 / 라이브러리 | 버전 | 주요 역할 및 용도 |
| :--- | :--- | :--- | :--- |
| **Data Grid** | **@tanstack/react-table** | `^8.21.2` | 헤드리스 데이터 테이블, 서버 사이드 페이징/정렬, 컬럼 가상화 기반 |
| **Drag & Drop** | **@dnd-kit/core**, **@dnd-kit/sortable** | `^6.3.1` / `^10.0.0` | 전자결재 결재선 지정 시 결재자 순서 변경 드래그 앤 드롭 |
| **UI Primitive** | **@radix-ui/react-tooltip** | `^1.2.8` | 웹 접근성을 준수하는 툴팁 컴포넌트 |
| **Icons** | **lucide-react** | `^0.474.0` | 현대적이고 통일된 SVG 벡터 아이콘 세트 |
| **Styling** | **Tailwind CSS** + **CVA** | `^3.4.14` / `^0.7.1` | 유틸리티 퍼스트 CSS 프레임워크 및 조건부 클래스 조합(`class-variance-authority`) |
| **Date Library** | **dayjs** | `^1.11.13` | 날짜/시간 포맷팅, 상대 시간 계산 |
| **Mock / Utilities** | **@faker-js/faker**, **@frontend-opensource/use-react-hooks** | `^9.9.0` / `^1.1.4` | 테스트 목업 데이터 생성 및 전역 Confirm 대화상자 Provider |
### 2.5 모노레포 내부 패키지 연동 (`packages/common`)
AS 모듈은 사내 공통 패키지 저장소와 긴밀하게 연계되어 있습니다:
- **`@common/auth`**: 통합 인증 규격(`AuthService`, `AuthConfig`), 토큰 수명 주기 계산(`getRemainingMs`), 디바이스 식별자 관리(`getOrCreateDeviceId`).
- **`@common/network`**: 공통 Axios 싱글톤 클라이언트([`Client.getInstance()`](file:///Users/changwng/workspace.ona/front/frontend_as/packages/common/network/client.ts)), 공통 인터셉터([`errorInterceptor`](file:///Users/changwng/workspace.ona/front/frontend_as/packages/common/network/error-interceptor.ts)), 401 발생 시 로그인 리다이렉트 처리.
- **`@common/components`**: 통합 GNB/LNB 기본 레이아웃([`BaseLayout`](file:///Users/changwng/workspace.ona/front/frontend_as/packages/common/components)), 공통 버튼, 인풋, 실렉트, 다이얼로그, 뱃지 등 UI 디자인 시스템.
- **`@common/assets`**: 전사 공통 로고, 이미지 에셋.
### 2.6 환경 변수 및 설정 (`.env.local`)
| 환경변수 키 | 값(로컬 기준) | 설명 |
| :--- | :--- | :--- |
| `NEXT_PUBLIC_PORT` | `3000` | 애플리케이션 기본 포트 |
| `NEXT_PUBLIC_PROXY_HOST` | `http://localhost:3003` | 프론트엔드 프록시 엔드포인트 호스트 |
| `NEXT_PUBLIC_SITE_ID` | `AS` | 행정지원 시스템 고유 사이트 식별자 |
| `SERVER_API_URL` | `http://localhost:8000` | 백엔드 API 게이트웨이 원본 주소 (미들웨어 프록시 대상) |
| `NEXT_PUBLIC_DC_SERVICE_API_URL` | `https://apigateway.apps.rsonesoft.com` | 데이터 연계/DC 서비스 엔드포인트 |
| `NEXT_PUBLIC_WEBHYP_CTRL_URL` | `http://hwp.onasys.co.kr/webhwpctrl` | 웹한글 기안기(WebHWP) 컨트롤 서버 주소 |
| `NEXT_PUBLIC_AUTH_LOGIN_REDIRECT_URL` | `/auth/login` | 인증 만료 시 리다이렉트될 로그인 경로 |
| `NEXT_PUBLIC_TINY_API_KEY` | *(Secret Key)* | 서식 작성용 TinyMCE 웹 에디터 라이선스 키 |
---
## 3. 아키텍처 및 UI 구조 (Architecture & UI Structure)
### 3.1 Next.js App Router와 FSD(v2.1)의 1:1 매핑 구조
AS 모듈은 **Feature-Sliced Design v2.1**과 Next.js App Router를 결합하여 설계되었습니다:
- **`app/` 디렉토리**: 라우팅 진입점, URL 파라미터 매핑, 루트/그룹 레이아웃, 미들웨어 프록시 및 API Route Handler만 담당합니다. 비즈니스 로직이나 UI 마크업을 직접 포함하지 않고 `src/pages`의 컴포넌트를 Re-export합니다.
- **`src/` 디렉토리**: FSD 6계층 구조에 따라 엄격히 분리된 실제 애플리케이션 구현체입니다.
```
apps/as/
├── app/ # [App Router] 라우팅 엔드포인트
│ ├── (auth)/ # 인증 전용 Route Group (독립 레이아웃)
│ │ ├── auth/login/page.tsx # -> re-export '@/pages/auth'
│ │ └── layout.tsx # 전역 Provider 없는 가벼운 HTML 구조
│ ├── (main)/ # 메인 업무 Route Group
│ │ ├── (with-base-layout)/ # 공통 GNB/사이드바 포함 레이아웃 그룹
│ │ │ ├── dashboard/page.tsx # 대시보드
│ │ │ ├── sewer-bsc-plan/ # 하수도기본계획 (목록/등록/상세/수정)
│ │ │ ├── fnrs-cnsltn/ # 재원협의 (목록/등록/상세/수정)
│ │ │ ├── instl-sncn/ # 설치인가 (목록/등록/상세/수정)
│ │ │ ├── design-cnstn/ # 설계자문 (목록/등록/상세/수정)
│ │ │ ├── my-pbadms-sprt/ # 나의 행정지원
│ │ │ ├── approval-line-management/ # 결재선 관리
│ │ │ ├── checklist-management/ # 체크리스트 관리
│ │ │ ├── code-management/ # 공통코드 관리
│ │ │ ├── esntl-atch-file-mng/ # 필수 첨부파일 관리
│ │ │ ├── rvw-mnpw-mng/ # 검토인력 관리
│ │ │ ├── nti-hstry/ # 알림톡 이력
│ │ │ ├── ntitlk-sndng/ # 알림톡 발송
│ │ │ └── layout.tsx # BaseLayoutWrapper 적용
│ │ └── layout.tsx # Providers(Query, Zustand, Auth, Menu) 적용
│ ├── api/ # Next.js Route Handlers
│ │ ├── auth/logout/route.ts # HttpOnly 쿠키 삭제 로그아웃
│ │ └── proxy/[...url]/route.ts # 로그인 & Refresh 토큰 교환 BFF 프록시
│ └── middleware.ts # 백엔드 API 및 TUS 업로드 리라이트 프록시
│
└── src/ # [FSD 아키텍처 코어]
├── app/ # 1. App Layer (전역 설정, Provider, Layout)
│ ├── layouts/base-layout.tsx # BaseLayoutWrapper (공통 UI Frame)
│ ├── providers/index.tsx # QueryClient, Store, Auth, Menu, Alert 통합
│ └── styles/globals.css # 글로벌 스타일시트
├── pages/ # 2. Pages Layer (화면 단위 조립 슬라이스)
│ ├── dashboard/ # 대시보드 화면
│ ├── sewer-bsc-plan/ # 하수도기본계획 화면군
│ ├── fnrs-cnsltn/ # 재원협의 화면군
│ ├── instl-sncn/ # 설치인가 화면군
│ ├── design-cnstn/ # 설계자문 화면군
│ ├── my-pbadms-sprt/ # 나의 행정지원
│ ├── approval-line-management/ # 결재선 관리
│ ├── checklist-management/ # 체크리스트 관리
│ ├── code-management/ # 코드 관리
│ ├── esntl-atch-file-mng/ # 필수 첨부파일 관리
│ ├── rvw-mnpw-mng/ # 검토인력 관리
│ ├── nti-hstry/ # 알림톡 이력
│ └── ntitlk-sndng/ # 알림톡 발송
├── widgets/ # 3. Widgets Layer (독립 복합 UI 블록)
│ ├── pbadms-sprt/ # 행정지원 공통 폼/뷰 섹션 위젯
│ ├── hangulgiangi/ # 웹한글 기안기 연동 위젯
│ ├── ognz-user-tree/ # 조직도/사용자 선택 트리 위젯
│ ├── bns-picker/ # 업무/사업 선택기
│ └── cnfm/ # 승인/확인 대화상자
├── features/ # 4. Features Layer (사용자 인터랙션 기능)
├── entities/ # 5. Entities Layer (비즈니스 도메인 모델/API/상태)
│ ├── sewer-bsc-plan/ # 하수도기본계획 엔티티
│ ├── fnrs-cnsltn/ # 재원협의 엔티티
│ ├── instl-sncn/ # 설치인가 엔티티
│ ├── design-cnstn/ # 설계자문 엔티티
│ ├── elapr/ # 전자결재 엔티티
│ ├── auth/ # 인증 및 토큰 관리 엔티티
│ ├── menu/ # 메뉴 및 권한 엔티티
│ ├── cklst/ # 체크리스트 엔티티
│ ├── esntl/ # 필수 첨부파일 엔티티
│ ├── tus/ # TUS 업로드 엔티티
│ └── ... # 기타 31개 도메인 엔티티
└── shared/ # 6. Shared Layer (공용 기반 인프라)
├── api/client.ts # Axios 기반 API 클라이언트
├── config/ # 엔드포인트 및 환경설정
├── lib/ # 훅, 테이블/쿼리 유틸리티
├── model/ # 전역 타입 및 공통 모델
├── providers/store-provider.tsx # Zustand Context Provider
├── store/ # Zustand Stores (User, Menu, Alert)
└── ui/ # 다이얼로그, 필드, 아코디언 등 UI 컴포넌트
```
### 3.2 레이아웃 계층 구조
```mermaid
graph TD
A["브라우저 요청"] --> B{"경로 구분"}
B -->|"/auth/login"| C["app/(auth)/layout.tsx"]
C --> C1["순수 HTML + Body (globals.css만 로드)"]
C1 --> C2["LoginPage (로그인 폼)"]
B -->|"업무 화면들"| D["app/(main)/layout.tsx"]
D --> D1["RootLayout (HTML data-theme 설정)"]
D1 --> D2["Providers (QueryClient, Store, Confirm)"]
D2 --> D3["AuthWrapper (세션 검증 & Silent Refresh)"]
D3 --> D4["MenuWrapper (URL 매칭 & 조회 권한 체크)"]
D4 --> E["app/(main)/(with-base-layout)/layout.tsx"]
E --> E1["BaseLayoutWrapper"]
E1 --> E2["@common/components BaseLayout (GNB, LNB, Footer)"]
E2 --> E3["PageLayout (공통 패딩 및 레이아웃 컨테이너)"]
E3 --> E4["각 도메인 Page 컴포넌트"]
```
1. **인증 전용 레이아웃 (`(auth)`)**:
- GNB나 복잡한 프로바이더 없이 최소한의 스타일만 로드하여 초기 구동 속도를 확보하고, 비로그인 상태의 화면을 렌더링합니다.
2. **메인 루트 레이아웃 (`(main)`)**:
- 쿠키 `site-theme-key`를 서버에서 읽어 `html data-theme`을 설정합니다.
- 전역 프로바이더([`Providers`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/app/providers/index.tsx))를 주입합니다.
- [`AuthWrapper`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/entities/auth/lib/auth-wrapper.tsx): 로그인 여부 확인, 사용자 정보(`userInfo`) 초기화, 주기적 백그라운드 Silent Refresh 타이머 가동.
- [`MenuWrapper`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/entities/menu/lib/menu-wrapper.tsx): 현재 접속 URL에 해당하는 메뉴 코드 및 권한(`inqAuthrtYn`) 검사.
3. **공통 프레임 레이아웃 (`(with-base-layout)`)**:
- 사내 공통 패키지의 [`BaseLayout`](file:///Users/changwng/workspace.ona/front/frontend_as/packages/common/components)을 래핑하여 표준 헤더, 사이드바, 푸터를 제공합니다.
- 메뉴 클릭 시 메뉴 상태 저장(`setCurrentMenuInfo`) 및 라우팅을 처리합니다.
---
## 4. 전체 화면 라우트 맵 및 도메인 화면 분석
| 라우트 경로 | 매핑된 Page 컴포넌트 | 주요 역할 및 기능 | 핵심 연계 엔티티 / 위젯 |
| :--- | :--- | :--- | :--- |
| `/auth/login` | [`LoginPage`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/pages/auth) | 시스템 로그인 화면 (ID/PW 인증, 디바이스 ID 전송) | `entities/auth`, `/api/proxy` |
| `/dashboard` | [`DashboardPage`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/pages/dashboard/ui/dashboard-page.tsx) | 메인 포털 대시보드 (전국 유역별 진행 현황 지도, 상태별 건수 카드, 알림/통보 목록, 공지사항) | `entities/dashboard`, `NationalAsMap` |
| `/sewer-bsc-plan` | [`SewerBscPlanListPage`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-list-page.tsx) | 하수도 기본계획 목록 조회 및 신규 신청 다이얼로그 호출 | `entities/sewer-bsc-plan`, `useDataTable` |
| `/sewer-bsc-plan/regist` | [`SewerBscPlanRegistPage`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-regist-page.tsx) | 하수도 기본계획 신규 신청 등록 폼 | `widgets/pbadms-sprt`, `useTus`, Zod |
| `/sewer-bsc-plan/[id]` | [`SewerBscPlanViewPage`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-view-page.tsx) | 하수도 기본계획 상세 조회, 검토 이력, 보완요구 처리 | `SewerBscPlanDetailGate`, `HangulGiangi` |
| `/sewer-bsc-plan/[id]/edit` | [`SewerBscPlanEditPage`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-edit-page.tsx) | 하수도 기본계획 수정 폼 (기존 데이터 프리필) | `widgets/pbadms-sprt`, `useTus` |
| `/fnrs-cnsltn` | [`FnrsListPage`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-list-page.tsx) | 재원협의 목록 조회 및 신규 신청 | `entities/fnrs-cnsltn`, `useDataTable` |
| `/fnrs-cnsltn/regist` | [`FnrsRegistPage`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-regist-page.tsx) | 재원협의 신규 등록 폼 (사업비/재원분담 매트릭스) | `widgets/pbadms-sprt`, `FnrsSharFormSection` |
| `/fnrs-cnsltn/[id]` | [`FnrsViewPage`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-view-page.tsx) | 재원협의 상세 조회 및 결재/검토 | `FnrsDetailGate`, `widgets/pbadms-sprt` |
| `/fnrs-cnsltn/[id]/edit` | [`FnrsEditPage`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-edit-page.tsx) | 재원협의 수정 폼 | `widgets/pbadms-sprt`, Zod |
| `/instl-sncn` | [`InstlListPage`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/pages/instl-sncn/ui/instl-list-page.tsx) | 설치인가 목록 조회 및 신청 | `entities/instl-sncn`, `useDataTable` |
| `/instl-sncn/regist` | [`InstlRegistPage`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/pages/instl-sncn/ui/instl-regist-page.tsx) | 설치인가 등록 폼 (사업비/시설규모/체크리스트) | `widgets/pbadms-sprt`, Zod |
| `/instl-sncn/[id]` | [`InstlViewPage`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/pages/instl-sncn/ui/instl-view-page.tsx) | 설치인가 상세 조회 및 보완 처리 | `InstlDetailGate`, `widgets/pbadms-sprt` |
| `/instl-sncn/[id]/edit` | [`InstlEditPage`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/pages/instl-sncn/ui/instl-edit-page.tsx) | 설치인가 수정 폼 | `widgets/pbadms-sprt`, Zod |
| `/design-cnstn` | [`DesignListPage`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/pages/design-cnstn/ui/design-list-page.tsx) | 설계자문 목록 조회 | `entities/design-cnstn`, `useDataTable` |
| `/design-cnstn/regist` | [`DesignRegistPage`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/pages/design-cnstn/ui/design-regist-page.tsx) | 설계자문 등록 폼 | `widgets/pbadms-sprt`, Zod |
| `/design-cnstn/[id]` | [`DesignViewPage`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/pages/design-cnstn/ui/design-view-page.tsx) | 설계자문 상세 및 자문의견 조회 | `DesignDetailGate`, `widgets/pbadms-sprt` |
| `/design-cnstn/[id]/edit` | [`DesignEditPage`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/pages/design-cnstn/ui/design-edit-page.tsx) | 설계자문 수정 폼 | `widgets/pbadms-sprt`, Zod |
| `/my-pbadms-sprt` | [`MyPbadmsSprtListPage`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/pages/my-pbadms-sprt/ui/my-pbadms-sprt-list-page.tsx) | 나의 행정지원 (로그인 사용자가 제출/담당한 전체 문서 통합 조회) | `entities/sewer-bsc-plan`, `useDataTable` |
| `/approval-line-management` | [`ApprovalLineManagementPage`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/pages/approval-line-management/ui/approval-line-management-page.tsx) | 전자결재 템플릿 관리 및 결재선 드래그 앤 드롭 설정 | `entities/elapr`, `@dnd-kit/core` |
| `/checklist-management` | [`ChecklistManagementPage`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/pages/checklist-management/ui/checklist-management-page.tsx) | 업무별 체크리스트 템플릿, 점검 항목 및 검토항목 관리 | `entities/cklst`, Tab View |
| `/code-management` | [`CodeManagementPage`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/pages/code-management/ui/code-management-page.tsx) | 기준 코드 관리 (회계구분, 공종구분, 재원분담구분 코드 트리) | `entities/code`, `entities/acntg`, `entities/cnstp` |
| `/esntl-atch-file-mng` | [`EsntlAtchFileMngPage`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/pages/esntl-atch-file-mng/ui/esntl-atch-file-mng-page.tsx) | 업무별 필수 첨부파일 마스터 관리 | `entities/esntl`, Dialog Form |
| `/rvw-mnpw-mng` | [`RvwMnpwMngPage`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/pages/rvw-mnpw-mng) | 검토 인력 배정 및 관리 | `entities/rvw-mnpw` |
| `/nti-hstry` | [`NtiHstryPage`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/pages/nti-hstry) | 알림톡 및 시스템 통보 발송 이력 조회 | `entities/noti`, `entities/ntftalk` |
| `/ntitlk-sndng` | [`NtitlkSndngPage`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/pages/ntitlk-sndng) | 알림톡 즉시/예약 발송 화면 | `entities/ntftalk`, 템플릿 바인딩 |
---
## 5. 공통 인프라 및 서브시스템 구조
### 5.1 인증 및 세션 수명 주기 (BFF + 쿠키 하이브리드 패턴)
AS 시스템은 토큰 탈취 방지와 유연한 백엔드 통신을 위해 **BFF(Backend For Frontend)와 브라우저 쿠키를 결합한 하이브리드 세션 관리**를 채택했습니다:
```mermaid
sequenceDiagram
autonumber
actor User as 사용자
participant Client as 브라우저 (AuthService)
participant RouteHandler as Next.js API (/api/proxy)
participant Backend as 백엔드 (USER_SERVICE)
participant Storage as 브라우저 Cookie / Store
User->>Client: ID / Password 로그인 요청
Client->>RouteHandler: POST /api/proxy/user-service/login (body: credentials, headers: X-Device-Id)
RouteHandler->>Backend: POST /user-service/login
Backend-->>RouteHandler: 200 OK (Response Headers: access-token, refresh-token, token-id)
Note over RouteHandler: refresh-token은 HttpOnly, Strict 쿠키로 Set-Cookie 생성
RouteHandler-->>Client: 200 OK (JSON: access-token, token-id)
Client->>Storage: document.cookie에 Authorization, token-id 저장
Client->>Client: getRemainingMs(access-token)으로 만료시간 계산 후 Silent Refresh 타이머 설정
opt 세션 만료 1분 전 자동 갱신 (Silent Refresh)
Client->>RouteHandler: PUT /api/proxy/user-service/api/v1/users/token/refresh
Note over RouteHandler: HttpOnly 쿠키의 refresh-token을 헤더에 주입하여 백엔드 호출
RouteHandler->>Backend: PUT /user-service/api/v1/users/token/refresh
Backend-->>RouteHandler: 새 access-token 및 refresh-token 발급
RouteHandler-->>Client: 새 access-token 반환 & 새 refresh-token 쿠키 갱신
Client->>Storage: 새 access-token 쿠키 업데이트 및 다음 타이머 가동
end
opt 로그아웃 (Logout)
Client->>RouteHandler: POST /api/auth/logout
RouteHandler-->>Client: refresh-token 쿠키 삭제
Client->>Storage: Authorization, token-id, Device-Id 쿠키 삭제
Client->>Client: window.dispatchEvent("auth:logout") 발생 -> /auth/login 리다이렉트
end
```
- **토큰 보관 전략**:
- `access-token`: 브라우저 JS가 접근 가능한 `Authorization` 쿠키에 저장되어 일반 Axios API 호출 시 즉시 헤더에 삽입됩니다.
- `refresh-token`: XSS 공격으로부터 보호하기 위해 Next.js Route Handler([`/api/proxy/[...url]`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/app/api/proxy/%5B...url%5D/route.ts))에서 `httpOnly: true`, `sameSite: "strict"` 쿠키로만 설정 및 관리됩니다.
- **백그라운드 세션 갱신**:
- JWT 페이로드의 `exp` 클레임을 파싱하여 만료 전 일정 시간에 자동으로 `silentRefresh()`가 트리거됩니다.
- **인터셉터 기반 401 감지**:
- 백엔드 통신 중 토큰 만료 또는 인증 실패(`401 UNAUTHORIZED`)가 감지되면 [`errorInterceptor`](file:///Users/changwng/workspace.ona/front/frontend_as/packages/common/network/error-interceptor.ts)가 동작하여 즉시 `/auth/login`으로 강제 이동시킵니다.
### 5.2 백엔드 프록시 및 미들웨어 라우팅 (`middleware.ts`)
Next.js 미들웨어([`middleware.ts`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/middleware.ts))는 클라이언트의 모든 API 요청을 백엔드 마이크로서비스로 동적 프록시(Rewrite)합니다:
1. **`/server/:path*` 프록시**:
- `pathname.includes("/dc-service")`인 경우: `DC_SERVICE_API_URL`로 리라이트.
- 그 외 일반 API 요청: `SERVER_API_URL`(예: `http://localhost:8000`)로 `/server` 접두사를 제거하여 리라이트.
2. **`/attach-service/:path*` 프록시 (TUS 대용량 업로드)**:
- `tus-js-client`가 파일 업로드 생성 후 백엔드의 `Location` 헤더로부터 받은 URL로 PATCH 요청을 직접 날릴 때(`/attach-service/api/v1/tus/upload/{id}`), 요청 쿠키의 인증 토큰(`CLAIM_NAME`)과 사용자 ID(`AUTH_USER_ID`)를 Request Headers에 명시적으로 주입하여 백엔드로 안전하게 바이패스합니다.
3. **루트 리다이렉트**:
- 사용자가 `/` 경로로 인입 시 기본 업무 페이지인 `/dashboard`로 307 리다이렉트 처리합니다.
4. **보안 헤더 삽입**:
- 모든 프록시 응답에 `Content-Security-Policy: img-src 'self' data: https: blob:;` 헤더를 주입합니다.
### 5.3 메뉴 동기화 및 페이지 접근 권한 검사 (`MenuWrapper`)
화면이 변경될 때마다 [`MenuWrapper`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/entities/menu/lib/menu-wrapper.tsx) 컴포넌트가 활성화되어 2단계 권한 검증을 수행합니다:
1. **메뉴 매핑**: 백엔드 CMS 서비스(`MenuApi.getMenuRoles(SITE_ID)`)로부터 인가된 전체 메뉴 트리를 가져와 현재 브라우저의 `pathname`과 일치하는 메뉴 항목을 추출하고, 전역 [`useMenuStore`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/shared/lib/use-menu-store.ts)에 저장합니다.
2. **페이지 단위 권한 검사**: 쿠키에 기록된 메뉴 코드(`menu-code-key`)를 바탕으로 `MenuAuthorityApi.getPageMenuAuthority`를 호출합니다.
3. **접근 통제**: 반환된 권한 객체의 `inqAuthrtYn`(조회 권한 여부)이 `"Y"`가 아닌 경우, 경고 팝업(`"페이지에 대한 조회 권한이 없습니다."`)을 띄우고 루트(`/`)로 강제 이동시킵니다.
### 5.4 행정지원 공통 업무 위젯 서브시스템 (`widgets/pbadms-sprt`)
하수도기본계획, 재원협의, 설치인가, 설계자문 등 4대 행정지원 업무는 고유한 서식과 더불어 **공통적인 행정 프로세스 서브시스템**을 공유합니다. 이를 위해 [`src/widgets/pbadms-sprt`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/widgets/pbadms-sprt) 패키지에 핵심 UI 및 비즈니스 로직이 모듈화되어 있습니다:
```
widgets/pbadms-sprt/
├── ui/
│ ├── attachments-form-section.tsx # 필수 첨부파일 등록 섹션 (TUS 업로드 연동)
│ ├── attachments-view-section.tsx # 첨부파일 조회 및 다운로드 뷰 섹션
│ ├── business-cost-form-section.tsx # 사업비 매트릭스 입력 및 합계 자동계산 섹션
│ ├── business-scale-form-section.tsx# 사업규모/시설용량 입력 섹션
│ ├── checklist-form-section.tsx # 업무별 체크리스트 탭 및 점검 항목 입력 섹션
│ ├── contact-info-form-section.tsx # 신청자 / 담당자 정보 입력 섹션
│ ├── fnrs-shar-form-section.tsx # 재원분담비율/금액 입력 섹션
│ ├── pbadms-detail-gate.tsx # 상세 화면 접근/수정 권한 검증 게이트
│ ├── pbadms-task-apply-dialog.tsx # 신규 신청 시 업무/행정구역 선택 다이얼로그
│ ├── privacy-consent-section.tsx # 개인정보 수집 및 이용 동의 체크 섹션
│ ├── progress-status-section.tsx # 진행상태 단계 표시기 (신청-접수-검토-보완-완료)
│ ├── splmnt-dmnd-dialog.tsx # 검토자의 보완요구 등록 대화상자
│ ├── splmnt-dmnd-form-section.tsx # 신청자의 보완요구 대응 내용 입력 섹션
│ └── supplement-demand-section.tsx # 보완요구 내역 조회 섹션
└── lib/
├── attachments-form-utils.ts # 파일 업로드 키 매핑 및 필수파일 누락 검증
├── business-cost-form-utils.ts # 공종별 사업비 총액/소계 재계산 엔진
├── business-scale-form-utils.ts # 시설규모 단위 및 합계 재계산
├── can-edit-pbadms-doc.ts # 문서 수정 가능 여부 판단 규칙
├── checklist-form-utils.ts # 체크리스트 트리와 폼 상태 동기화
└── fnrs-shar-form-utils.ts # 사업비 vs 재원분담 총액 일치성 검증 엔진
```
- **정합성 자동 검증 엔진 (`validateFnrsCstBalance`)**:
- 행정 문서 작성 시 공종별 사업비 총합계와 국비/지방비/원인자부담금 등 재원분담 합계가 정확히 일치하는지 클라이언트 폼 단에서 실시간 비교 검증하여 오입력을 원천 차단합니다.
- **수정 권한 가드 (`canEditPbadmsDoc`)**:
- 작성자 본인 여부, 현재 문서의 진행 상태 코드(임시저장 `TM`, 보완요청 `SL` 상태일 때만 수정 허용)를 평가하여 뷰 모드와 편집 모드를 제어합니다.
### 5.5 TUS 기반 대용량 파일 첨부 서브시스템 (`useTus`)
대용량 도면, 설계도서, 기본계획 보고서 PDF 등을 안정적으로 업로드하기 위해 TUS 프로토콜을 사용합니다:
- **확장자 화이트리스트 검증**: 문서(`.pdf`, `.hwp`, `.hwpx`, `.docx`, `.xlsx`, `.pptx`), 이미지(`.jpg`, `.png`), 압축파일(`.zip`) 등 사전 정의된 파일만 업로드 허용.
- **청크 분할 업로드**: 네트워크 불안정 시에도 처음부터 다시 올리지 않고 끊긴 지점부터 이어올리기 수행.
- **진행률 피드백**: 파일별 업로드 진행률(`progress`) 및 로딩 상태(`isUploading`)를 실시간 상태값으로 제공하여 프로그레스 바 렌더링.
- **메타데이터 바인딩**: 파일 업로드 완료 시 백엔드 첨부파일 식별자(`atflCd`, `atflId`)를 획득하여 폼 필드와 연계.
### 5.6 웹한글 기안기(WebHWP) 연동 아키텍처 (`widgets/hangulgiangi`)
행정 기관과의 공문서 및 결재 문서 호환을 위해 웹한글 기안기를 브라우저 내에 통합 지원합니다:
- **Iframe 브릿지 통신**: 웹한글 컨트롤은 Next.js가 제공하는 iframe 내에 격리되어 로드되며, `postMessage` 기반의 표준 메시징 프로토콜을 통해 부모 창과 통신합니다.
- **SOP(동일 출처 정책) 해결**: [`next.config.ts`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/next.config.ts)의 `rewrites` 설정을 통해 `/webhwpctrl/:path*` 경로를 웹한글 컨트롤 서버(`WEBHYP_CTRL_URL`)로 리라이트 프록시하여 브라우저의 크로스 오리진 제약을 완벽히 우회합니다.
- **문서 라이프사이클 관리**:
- 원격 첨부파일 다운로드 -> ArrayBuffer/Blob 변환 -> HwpCtrl에 바이트 스트림 로드 -> 웹 기안기에서 편집 -> HwpCtrl에서 Blob 추출 -> TUS 업로드 후 신규 문서 번호 갱신.
---
## 6. 화면별 상세 데이터 흐름 (Screen Data Flow)
### 6.1 목록 조회 및 검색 데이터 흐름 (List Pattern)
모든 목록 화면(하수도기본계획, 재원협의, 설치인가, 설계자문 등)은 URL 기반 상태 동기화 및 TanStack Query 캐싱 패턴을 일관되게 따릅니다:
```mermaid
sequenceDiagram
autonumber
actor User as 사용자
participant Page as ListPage
participant Hook as useSearchParamsState
participant ReactQuery as TanStack Query (usePageQuery)
participant Client as Axios Client
participant Server as Backend API (/server/...)
participant Table as TanStack Table (useDataTable)
User->>Page: 검색 조건 입력 (시도, 문서명 등) 및 검색 클릭
Page->>Hook: setParams({ sidoComCd, sewerBscPlanNm, page: 0 })
Hook->>Hook: 브라우저 URL QueryString 업데이트 (뒤로가기/새로고침 지원)
Hook-->>Page: 최신 params 상태 반환
Page->>ReactQuery: useSewerBscPlanPage(queryParams) 호출
alt 캐시 존재 및 유효한 경우
ReactQuery-->>Page: 캐시된 페이지 데이터 즉시 반환
else 캐시 만료 또는 첫 요청
ReactQuery->>Client: client.get("/as-service/api/v1/...", { params })
Client->>Server: HTTP GET 요청
Server-->>Client: 200 OK (PageResponseDto: content, totalElements, totalPages)
Client-->>ReactQuery: 데이터 캐싱
ReactQuery-->>Page: 최신 데이터 전달
end
Page->>Table: useDataTable({ query, columns, pagination })
Table-->>User: 데이터 그리드 및 페이징 컴포넌트 렌더링
opt 특정 행 클릭
User->>Table: Row Click
Table->>Page: handleRowClick(row)
Page->>Page: withListQs("/sewer-bsc-plan/" + docNo) 호출<br/>(현재 검색조건 쿼리스트링을 유지한 채 상세 페이지로 이동)
end
```
- **상태 보존 (`useSearchParamsState`)**: 검색 조건과 페이징 상태가 브라우저 URL 쿼리 파라미터와 양방향 동기화되어 사용자가 상세 화면을 조회한 뒤 브라우저 뒤로가기를 눌러도 이전 검색 목록과 페이지가 그대로 유지됩니다.
- **헤드리스 테이블 통합 (`useDataTable`)**: TanStack Table의 `state`, `getCoreRowModel`, `manualPagination`과 React Query의 `isPending`, `isError`, `data`를 바인딩하여 로딩 스켈레톤, 데이터 없음(`NoData`), 페이징네이션을 일관되게 렌더링합니다.
### 6.2 상세 조회 및 접근 제어 데이터 흐름 (Detail View Pattern)
상세 화면 진입 시에는 인가 검증과 상세 데이터 로딩이 병렬/순차적으로 처리됩니다:
```mermaid
sequenceDiagram
autonumber
actor User as 사용자
participant ViewPage as [Domain]ViewPage
participant Gate as DetailGate / canEditPbadmsDoc
participant ReactQuery as TanStack Query
participant Server as Backend API
User->>ViewPage: /sewer-bsc-plan/[id]?chgCycl=1 접근
ViewPage->>ReactQuery: useGetSewerBscPlanDetail(id, chgCycl)
ReactQuery->>Server: GET /as-service/api/v1/sewer-bsc-plans/{id}?chgCycl={chgCycl}
Server-->>ReactQuery: 200 OK (상세 DTO: 기본정보, 사업비, 체크리스트, 첨부파일, 진행상태)
ReactQuery-->>ViewPage: 상세 데이터 전달
ViewPage->>Gate: PbadmsDetailGate (doc, currentUser) 평가
alt 조회 권한 없음
Gate-->>User: "문서 열람 권한이 없습니다" 에러 안내 및 목록 이동
else 조회 권한 승인
Gate-->>ViewPage: 본문 렌더링 허용
ViewPage->>ViewPage: 기본정보, 사업비 매트릭스, 첨부파일 목록 렌더링
ViewPage->>ViewPage: canEditPbadmsDoc(doc) 평가 -> [수정], [보완요구], [결재신청] 버튼 노출 제어
end
```
### 6.3 폼 등록 및 수정 데이터 흐름 (Form Mutation Pattern)
등록 및 수정 화면에서는 복합 데이터 검증, TUS 파일 비동기 업로드, 트랜잭션 저장이 발생합니다:
```mermaid
sequenceDiagram
autonumber
actor User as 사용자
participant FormPage as [Domain]FormPage
participant RHF as React Hook Form + Zod
participant Tus as useTus (TUS 업로더)
participant AttachServer as ATTACH_SERVICE
participant Mutate as useMutation (Service API)
participant QueryClient as QueryClient
participant AsServer as AS_SERVICE
User->>FormPage: 페이지 진입 (create / edit)
alt 수정 모드 (Edit)
FormPage->>RHF: mapDetailToFormData(initialData)로 폼 기본값 프리필
else 등록 모드 (Create)
FormPage->>RHF: getCreateDefaultValues(userInfo, createDefaults) 세팅
end
User->>FormPage: 1. 파일 첨부 선택
FormPage->>Tus: uploadFileAsync(file)
Tus->>AttachServer: TUS 청크 업로드 (PATCH)
AttachServer-->>Tus: 업로드 완료 (atflCd, atflId 발급)
Tus-->>FormPage: atflCd, atflId 수신
FormPage->>RHF: setValue("essentialFiles", ...) 필드에 파일 ID 바인딩
User->>FormPage: 2. 저장 또는 신청 버튼 클릭
FormPage->>RHF: handleSubmit (Zod Schema 검증)
RHF->>RHF: zodResolver 검증 수행 (필수값, 자릿수, 포맷)
alt 유효성 오류
RHF-->>User: 입력 필드별 에러 메시지 표시
else 유효성 통과
FormPage->>FormPage: assertFnrsCstBalance() (사업비 vs 재원분담 총액 균형 검증)
FormPage->>FormPage: isFormChecklistComplete(), isFormEsntlAtchComplete() 검사
FormPage->>Mutate: sewerBscPlanService.apply(buildApplyPayload(formData))
Mutate->>AsServer: POST/PUT /as-service/api/v1/sewer-bsc-plans/...
AsServer-->>Mutate: 200 OK (성공 응답)
Mutate->>QueryClient: invalidateQueries({ queryKey: sewerBscPlanKeys.all })
QueryClient->>QueryClient: 이전 목록 및 상세 쿼리 캐시 만료 처리
Mutate-->>FormPage: 완료 콜백
FormPage-->>User: "정상적으로 신청되었습니다" 알림 후 목록으로 라우팅
end
```
### 6.4 대시보드 화면 데이터 흐름 (Dashboard Pattern)
대시보드([`DashboardPage`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/pages/dashboard/ui/dashboard-page.tsx))는 포털 초기 진입 속도를 극대화하기 위해 단일 통합 쿼리([`useGetMainDashboard`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/entities/dashboard))를 통해 화면 전체 데이터를 원샷으로 수신하여 자식 위젯들에 하향식(Props Drilling 최소화)으로 분배합니다:
```mermaid
graph TD
A["useGetMainDashboard() 쿼리 호출"] --> B["GET /as-service/api/v1/dashboard/main"]
B --> C["DashboardResponseDto"]
C --> D["data.basinDocPrgrsList"] --> D1["NationalAsMap (전국 4대강 유역별 행정지원 현황 지도 시각화)"]
C --> E["data.docPrgrsSttsList"] --> E1["DocPrgrsSttsCards (접수/검토/보완/완료 상태별 카운트 카드)"]
C --> F["data.notiList & notiStats"] --> F1["DashboardNotiList (실시간 업무 통보 및 알림톡 발송 현황)"]
G["독립 쿼리: useGetPosts()"] --> H["GET /board-service/api/v1/posts"]
H --> I["DashboardNoticeTable (시스템 공지사항 테이블)"]
```
---
## 7. 백엔드 마이크로서비스 연계 맵 (Backend Services Integration)
AS 모듈이 [`ENDPOINT`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/shared/config/api.ts)를 통해 통신하는 백엔드 마이크로서비스들의 상세 연계 정보는 다음과 같습니다:
```
+---------------------------------------------------------------------------------------------------+
| Next.js Middleware Proxy Layer |
| /server/* ==> SERVER_API_URL | /attach-service/* ==> SERVER_API_URL | /cms/* ==> SUB_DOMAIN |
+---------------------------------------------------------------------------------------------------+
| | | | |
v v v v v
[ AS_SERVICE ] [ ATTACH_SERVICE ] [ USER_SERVICE ] [ ELAPR_SERVICE ] [ CMS_SERVICE ]
- 하수도기본계획 - TUS 청크 업로드 - 사용자 로그인 - 전자결재 템플릿 - 공통코드 캐시
- 재원협의 - 첨부파일 메타데이터 - 토큰 갱신 - 결재선 관리 - 메뉴 및 권한
- 설치인가 - 파일 다운로드 - 내 정보 조회 - 결재 요청 - 조직도
- 설계자문 - 결재 진행상태 - 시스템 알림
- 체크리스트 마스터
- 공종/회계 마스터
- 대시보드 통계
```
| 서비스 명칭 | 기본 엔드포인트 URI | 담당 도메인 및 주요 기능 |
| :--- | :--- | :--- |
| **`AS_SERVICE`** | `/as-service/api/v1` | **행정지원 비즈니스 코어**<br/>- `/sewer-bsc-plans`: 하수도 기본계획 CRUD<br/>- `/fnrs-cnsltn-mng`: 재원협의 신청 및 분담 관리<br/>- `/instl-sncn`: 설치인가 관리<br/>- `/design-cnstn`: 설계자문 관리<br/>- `/cklst`, `/cklst-artcl`: 체크리스트 템플릿 및 평가<br/>- `/acntg-se`, `/cnstp-se`: 회계구분 및 공종구분 기준 코드<br/>- `/esntl-atch-file`: 필수 첨부파일 마스터<br/>- `/dashboard`: 대시보드 종합 지표 |
| **`ATTACH_SERVICE`** | `/attach-service/api/v1` | **파일 관리 서비스**<br/>- `/tus/upload`: TUS 프로토콜 파일 업로드 엔드포인트<br/>- `/atfl/attachments`: 첨부파일 메타데이터 조회<br/>- `/atflcd/download`, `/download`: 첨부파일 스트림 다운로드 |
| **`USER_SERVICE`** | `/user-service/api/v1` | **사용자 및 세션 서비스**<br/>- `/login`: 사용자 로그인 인증<br/>- `/users/token/refresh`: 토큰 재발급(Silent Refresh)<br/>- `/users/info/me`: 현재 로그인 사용자 상세 정보 |
| **`AS_ELAPR_SERVICE`** | `/as-elapr-service/api/v1` | **전자결재 서비스**<br/>- `/elapr-tmplt`: 결재 서식 템플릿<br/>- `/elapr-apln`: 결재선 지정 및 순서 관리<br/>- `/elapr-dmnd`: 결재 상신 및 승인/반려 요청<br/>- `/elapr-prgrs-stts`: 결재 진행 상태 조회 |
| **`CMS_SERVICE`** | `/cms-service/api/v1` | **공통 플랫폼 서비스**<br/>- `/code/cache`: 전사 공통코드 캐시 조회<br/>- `/menu-roles`, `/menu-authrts`: 사이트별 메뉴 및 권한 매핑<br/>- `/user-ognz`: 조직도 및 사용자 트리<br/>- `/noti`: 통보 및 알림 |
| **`NTFTALK_SERVICE`** | `/ntftalk-service/api/v1` | **알림톡 서비스**<br/>- `/ntftalk-temp`: 알림톡 템플릿 관리<br/>- `/ata/send`: 카카오 알림톡/SMS 발송 |
| **`BOARD_SERVICE`** | `/board-service/api/v1` | **게시판 서비스**<br/>- `/posts`: 공지사항 게시글 목록 및 상세 조회 |
---
## 8. 종합 검토 및 아키텍처적 시사점
### 8.1 아키텍처적 우수성
1. **엄격한 FSD v2.1 계층 원칙 준수**:
- 상위 레이어가 하위 레이어만 참조할 수 있는 단방향 의존성 규칙이 확립되어 있으며, `steiger` 린터를 통해 아키텍처 무결성을 상시 검증합니다.
2. **도메인 간 공통 프로세스의 위젯 캡슐화 (`widgets/pbadms-sprt`)**:
- 하수도기본계획, 재원협의, 설치인가, 설계자문 등 4대 행정지원 업무가 거의 동일한 행정 절차(신청->접수->검토->보완->결재)를 거치므로, 중복될 수 있는 폼 섹션과 계산 로직을 `widgets/pbadms-sprt`에 응집력 있게 격리하여 재사용성과 유지보수성을 극대화했습니다.
3. **견고한 폼 상태와 데이터 유효성 검증 체계**:
- 비제어 컴포넌트 기반 `react-hook-form`과 타입 세이프한 `zod` 스키마를 결합하여 복잡한 수치 매트릭스(사업비 합계, 재원분담 총액 균형)의 입력 성능과 무결성을 동시에 달성했습니다.
4. **BFF + 쿠키 기반의 안전한 인증 흐름**:
- XSS 공격에 취약한 Refresh Token을 브라우저 JS에 노출하지 않고 Next.js Route Handler의 HttpOnly 쿠키로 은닉 관리하면서, 브라우저에서는 만료 시점 기반 자동 갱신(Silent Refresh) 타이머를 가동하여 매끄러운 사용자 경험을 제공합니다.
5. **독립 iframe과 리라이트 프록시를 활용한 레거시 웹한글 기안기 통합**:
- 웹한글 컨트롤의 브라우저 SOP 문제를 Next.js 리라이트 프록시로 해결하고, iframe 브릿지 통신을 통해 최신 React 19 / Next.js 15 환경에서도 레거시 공문서 기안 기능을 안정적으로 구동시켰습니다.
### 8.2 개발 및 운영 시 주의점
1. **Next.js Standalone 배포 시 환경 변수 참조**:
- `SERVER_API_URL` 등 런타임에 필요한 서버 환경변수는 빌드 타임이 아닌 컨테이너 기동 시점에도 정확히 주입되어야 미들웨어의 API 프록시 리라이트가 정상 작동합니다.
2. **미들웨어 콘솔 로깅 부하**:
- 현재 [`middleware.ts`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/middleware.ts)에 모든 `/server` 요청 및 TUS 프록시 요청에 대해 `console.log`가 상세히 출력되도록 되어 있으므로, 운영 환경(Production) 배포 시에는 로깅 레벨 조정 또는 비활성화가 권장됩니다.
3. **대용량 파일 첨부 시 리버스 프록시 타임아웃/바디 제한**:
- Nginx 등 전면 웹서버를 통과할 때 `/attach-service` 및 `/server` 경로에 대한 `client_max_body_size` 및 타임아웃 설정이 TUS 업로드 크기에 맞추어 충분히 확보되어야 합니다.
@@ -0,0 +1,616 @@
# FSD(Feature-Sliced Design) v2.1과 Next.js App Router 결합 아키텍처 가이드
## 1. 개요 (Overview)
본 문서는 **디지털 행정지원 시스템([`apps/as`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as))**에 적용된 **Feature-Sliced Design(FSD v2.1)** 방법론과 **Next.js App Router**의 상호 결합 아키텍처를 상세히 설명하는 개발 가이드입니다.
현대 프론트엔드 개발에서 Next.js App Router는 파일 시스템 기반 라우팅과 서버 컴포넌트(RSC) 환경을 강력하게 제공하지만, 프로젝트 규모가 커질수록 `app/` 폴더 내부에 비즈니스 로직과 UI 컴포넌트가 무질서하게 혼재되는 "아키텍처 부패(Architecture Erosion)" 현상이 발생하기 쉽습니다.
AS 모듈은 이러한 문제를 방지하기 위해 **FSD v2.1 사양**과 **FSD 공식 Next.js 가이드**를 채택하여, **"Next.js App Router는 얇은 라우팅 진입점(Thin Routing Entry Point)만 담당하고, 실제 UI와 비즈니스 도메인은 `src/` 하위의 FSD 계층에 완전히 격리하는 1:1 매핑 결합 패턴"**을 구현했습니다.
---
## 2. Feature-Sliced Design (FSD) v2.1 핵심 개념
### 2.1 FSD란 무엇인가?
**Feature-Sliced Design(FSD)**은 프론트엔드 애플리케이션의 유지보수성, 확장성, 예측 가능성을 극대화하기 위해 고안된 **계층형 모듈식 프론트엔드 아키텍처 설계 방법론**입니다.
코드를 단순히 기술적 분류(components, hooks, utils, types 등)로 나누는 기존 방식과 달리, FSD는 **비즈니스 도메인과 책임의 범위(Scope of Responsibility)**에 따라 코드를 구조화합니다.
```
FSD 계층 구조 (Top-Down 의존성 방향)
┌────────────────────────────────────────────────────────┐
│ app (최상위: 전역 설정, 프로바이더, 글로벌 스타일) │ ▲ 가장 높은 책임
├────────────────────────────────────────────────────────┤ │ 가장 많은 의존성
│ pages (화면 슬라이스: 라우트에 대응하는 페이지 컴포넌트) │ │
├────────────────────────────────────────────────────────┤ │
│ widgets (복합 UI 블록: 헤더, 사이드바, 도메인 복합 섹션) │ │ 단방향 참조
├────────────────────────────────────────────────────────┤ │ (상위 -> 하위만 허용)
│ features (사용자 상호작용/액션: 로그인 폼, 필터, 검색 등) │ │
├────────────────────────────────────────────────────────┤ │
│ entities (비즈니스 도메인 모델: 하수도계획, 사용자, 결재 등)│ │
├────────────────────────────────────────────────────────┤ ▼ 가장 낮은 책임
│ shared (도메인 무관 기반 인프라: UI Kit, API 클라이언트)│ 의존성 없음
└────────────────────────────────────────────────────────┘
```
### 2.2 FSD의 3계층 구조 체계
FSD는 코드베이스를 세 가지 차원으로 체계화합니다:
1. **Layers (레이어 - 수평적 분리)**:
- 애플리케이션 내에서의 책임과 권한 수준에 따른 최상위 분류 (표준 6개 계층).
2. **Slices (슬라이스 - 수직적 비즈니스 분리)**:
- 레이어 내부를 특정 **비즈니스 도메인 단위**로 분할한 디렉토리.
- 예: `entities/sewer-bsc-plan`, `entities/auth`, `pages/dashboard`
- *주의: `app`과 `shared` 레이어는 도메인이 없으므로 슬라이스를 갖지 않습니다.*
3. **Segments (세그먼트 - 기술적 분리)**:
- 슬라이스 내부를 기술적 목적에 따라 나눈 폴더.
- 표준 세그먼트: `ui`(마크업/컴포넌트), `model`(타입/상태/스토어), `api`(서버 통신/쿼리), `lib`(유틸리티/헬퍼).
### 2.3 6개 표준 레이어의 상세 역할
| 레이어 (Layer) | 슬라이스 유무 | 책임 및 역할 | `apps/as` 실제 구현 사례 |
| :--- | :---: | :--- | :--- |
| **`app`** | ❌ (세그먼트만) | 앱 전역 설정, 전역 Context Providers, 글로벌 스타일, 최상위 레이아웃 | [`src/app/providers/index.tsx`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/app/providers/index.tsx)<br/>[`src/app/layouts/base-layout.tsx`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/app/layouts/base-layout.tsx)<br/>[`src/app/styles/globals.css`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/app/styles/globals.css) |
| **`pages`** | ⭕ | 라우트에 1:1로 대응되는 화면 조립 단위. 하위 레이어들을 조합하여 완성된 화면 UI 구성 | [`src/pages/dashboard`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/pages/dashboard)<br/>[`src/pages/sewer-bsc-plan`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/pages/sewer-bsc-plan)<br/>[`src/pages/approval-line-management`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/pages/approval-line-management) |
| **`widgets`** | ⭕ | 여러 feature/entity를 결합한 독립적인 대형 UI 블록. 페이지 간 재사용 가능 | [`src/widgets/pbadms-sprt`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/widgets/pbadms-sprt) (행정지원 공통 폼)<br/>[`src/widgets/hangulgiangi`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/widgets/hangulgiangi) (웹한글기안기)<br/>[`src/widgets/ognz-user-tree`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/widgets/ognz-user-tree) (조직도) |
| **`features`** | ⭕ | 사용자 중심의 핵심 상호작용 및 비즈니스 액션 시나리오 (버튼 클릭, 모달 오픈 등) | 검색 필터링, 신청 승인 액션 등 (프로젝트 규모에 따라 `pages`나 `widgets`에 통합 가능) |
| **`entities`** | ⭕ | 비즈니스 실세계 개념 및 데이터 모델(타입, API 호출, 캐싱 쿼리, 상태) | [`src/entities/sewer-bsc-plan`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/entities/sewer-bsc-plan)<br/>[`src/entities/auth`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/entities/auth)<br/>[`src/entities/elapr`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/entities/elapr)<br/>[`src/entities/tus`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/entities/tus) |
| **`shared`** | ❌ (세그먼트만) | 비즈니스 로직이 전혀 없는 재사용 가능한 기반 코드. UI 라이브러리, HTTP 클라이언트, 유틸 | [`src/shared/api/client.ts`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/shared/api/client.ts)<br/>[`src/shared/lib/use-tus.ts`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/shared/lib/use-tus.ts)<br/>[`src/shared/ui/confirm-dialog.tsx`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/shared/ui/confirm-dialog.tsx) |
### 2.4 FSD의 2대 절대 불변 규칙
#### 규칙 1: 단방향 하향 의존성 규칙 (Unidirectional Dependency Rule)
> **"모듈은 오직 자신보다 물리적으로 '하위 레이어'에 위치한 모듈만 import할 수 있다."**
- **허용**: `pages` ➔ `widgets`, `features`, `entities`, `shared`
- **허용**: `widgets` ➔ `features`, `entities`, `shared`
- **허용**: `entities` ➔ `shared`
- ❌ **절대 금지 (동일 레이어 교차 참조)**: `entities/user`가 `entities/product`를 직접 import 불가 (필요시 교차 슬라이스 `@x` 표기법 또는 상위 위젯에서 합성).
- ❌ **절대 금지 (상위 레이어 역참조)**: `entities`가 `widgets`나 `pages`를 import 불가.
```typescript
// ✅ 올바른 참조 (하위 레이어 import)
// src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-form-page.tsx (pages 레이어)
import { AttachmentsFormSection } from "@/widgets/pbadms-sprt"; // widgets (하위)
import { useSewerBscPlanPage } from "@/entities/sewer-bsc-plan"; // entities (하위)
import { useAlertStore } from "@/shared/lib"; // shared (하위)
// ❌ 잘못된 참조 (규칙 위반)
// src/entities/sewer-bsc-plan/api/service.ts (entities 레이어)
import { SewerBscPlanListPage } from "@/pages/sewer-bsc-plan"; // 🚫 상위 레이어 역참조!
import { AttachmentsFormSection } from "@/widgets/pbadms-sprt"; // 🚫 상위 레이어 역참조!
```
#### 규칙 2: Public API (index.ts) 캡슐화 규칙
> **"슬라이스 외부에서는 반드시 해당 슬라이스의 Public API(`index.ts`)를 통해서만 접근해야 한다."**
슬라이스 내부의 세부 구현 파일(예: `ui/form-dialog.tsx`, `model/internal-types.ts`)을 외부에서 직접 경로로 import하는 것을 엄격히 금지합니다.
```typescript
// ✅ 올바른 import (Public API 사용)
import { SewerBscPlanListPage } from "@/pages/sewer-bsc-plan";
import { authService } from "@/entities/auth";
// ❌ 잘못된 import (내부 파일 직접 침투)
import { SewerBscPlanListPage } from "@/pages/sewer-bsc-plan/ui/sewer-bsc-plan-list-page";
import { authService } from "@/entities/auth/api/auth-service";
```
### 2.5 아키텍처 린터: Steiger (`pnpm run lint:fsd`)
AS 모듈은 FSD 아키텍처 규칙이 코드베이스 전체에서 자동으로 강제되도록 **Steiger** 린터를 도입했습니다:
```bash
# FSD 규칙 위반 여부 정적 분석 실행
pnpm run lint:fsd
```
Steiger는 다음과 같은 오류를 감지하고 빌드를 차단합니다:
- 상위 레이어를 역참조하는 구문
- 같은 레이어 내에서 다른 슬라이스를 무단 참조하는 구문
- Public API(`index.ts`)를 우회하여 내부 깊은 경로를 직접 import하는 구문
---
## 3. Next.js App Router 핵심 개념
### 3.1 파일 시스템 기반 라우팅 메커니즘
Next.js App Router는 프로젝트의 `app/` 디렉토리 아래의 폴더 계층 구조가 그대로 웹 애플리케이션의 URL 경로(URL Pathname)로 1:1 매핑되는 파일 기반 라우팅 시스템입니다.
폴더가 URL 세그먼트를 정의하고, 해당 폴더 안의 특수 파일들이 화면과 동작을 정의합니다.
### 3.2 App Router 특수 파일 컨벤션
| 파일명 | 역할 및 렌더링 동작 |
| :--- | :--- |
| **`page.tsx`** | 해당 URL 경로의 고유한 화면(Leaf UI)을 렌더링하는 엔트리 파일. |
| **`layout.tsx`** | 여러 하위 페이지가 공유하는 공통 레이아웃. 페이지 이동 시 재렌더링되지 않고 상태(State)가 보존됨. |
| **`loading.tsx`** | 리액트 `Suspense`의 `fallback`으로 자동 래핑되는 로딩 스켈레톤 UI. |
| **`error.tsx`** | 하위 컴포넌트 렌더링 중 에러 발생 시 자동 포착하는 React `ErrorBoundary`. |
| **`not-found.tsx`** | `notFound()` 함수 호출 시 렌더링되는 404 전용 UI. |
| **`route.ts`** | 웹 페이지가 아닌 REST API 엔드포인트를 제공하는 서버 핸들러 (BFF 역할). |
### 3.3 라우트 그룹 (Route Groups: `(folder)`)
소괄호 `()`로 감싼 폴더는 **URL 경로 세그먼트에 포함되지 않으면서** 라우트들을 논리적으로 그룹화하고, **서로 다른 레이아웃을 독립적으로 적용**할 수 있도록 해주는 핵심 기능입니다.
```
app/
├── (auth)/ # URL에 '/auth'가 붙지 않음
│ ├── auth/login/page.tsx # 실제 URL: /auth/login
│ └── layout.tsx # (auth) 그룹 전용 가벼운 레이아웃 (GNB 없음)
│
└── (main)/ # URL에 '/main'이 붙지 않음
├── (with-base-layout)/ # URL에 '/with-base-layout'이 붙지 않음
│ ├── dashboard/page.tsx# 실제 URL: /dashboard
│ └── layout.tsx # BaseLayoutWrapper (GNB, LNB, Footer 포함)
└── layout.tsx # Providers, 테마, 세션체크가 포함된 루트 레이아웃
```
### 3.4 동적 세그먼트 (Dynamic Routes: `[id]`)
대괄호 `[]`로 감싼 폴더는 URL 경로의 해당 자리를 동적 매개변수(Parameter)로 캡처합니다.
- `app/(main)/(with-base-layout)/sewer-bsc-plan/[id]/page.tsx` ➔ `/sewer-bsc-plan/101` 접근 시 `id = "101"` 캡처.
- **Next.js 15 변경점**: `params`와 `searchParams`가 **비동기 Promise**로 전달되므로 반드시 `await params`로 언래핑해야 합니다.
---
## 4. FSD v2.1과 Next.js App Router의 충돌 지점과 해결 전략
### 4.1 근본적인 아키텍처적 충돌
Next.js App Router와 FSD는 코드베이스의 구조화 철학에서 본질적인 충돌을 겪습니다:
```
[ Next.js 철학 ] : "모든 것은 app/ 디렉토리 안에 파일 시스템 구조로 모여야 한다."
VS
[ FSD v2.1 철학 ] : "코드는 비즈니스 도메인과 책임 계층(Layer/Slice)에 따라 분리되어야 한다."
```
#### 발생할 수 있는 안티패턴 (Naive 접근법의 문제점):
만약 FSD를 무시하고 Next.js 권장대로 `app/` 폴더 안에 컴포넌트, 훅, 비즈니스 코드를 직접 넣는다면:
1. `app/(main)/(with-base-layout)/sewer-bsc-plan/_components/form.tsx`
2. `app/(main)/(with-base-layout)/sewer-bsc-plan/_hooks/use-sewer.ts`
3. 다른 라우트(예: `fnrs-cnsltn`)에서 `sewer-bsc-plan`의 컴포넌트나 훅을 가져다 쓰려고 할 때 상대 경로가 꼬이고 레이어 경계가 무너짐.
4. 재사용 가능한 비즈니스 엔티티가 특정 라우트 폴더 내부에 깊이 매몰되어 응집도가 파괴됨.
### 4.2 해결 전략: "얇은 라우팅 진입점 패턴" (Thin Routing Entry Point)
FSD 공식 가이드와 `apps/as` 프로젝트는 이 충돌을 **역할의 완벽한 분리**로 해결했습니다:
```mermaid
graph LR
subgraph NextJS["Next.js 영역 (app/)"]
direction TB
A["app/ (Route Groups)"]
A1["layout.tsx<br/>(URL 구조 정의)"]
A2["page.tsx<br/>(파라미터 추출 & Re-export)"]
end
subgraph FSD["FSD v2.1 아키텍처 영역 (src/)"]
direction TB
B["src/app/<br/>(Providers & Layout 구현체)"]
C["src/pages/<br/>(화면 컴포넌트 & 조립)"]
D["src/widgets/<br/>(복합 UI 블록)"]
E["src/entities/<br/>(비즈니스 도메인)"]
F["src/shared/<br/>(기반 인프라 & UI Kit)"]
end
A1 -.->|"import"| B
A2 -.->|"import & Re-export"| C
C --> D
C --> E
C --> F
D --> E
D --> F
E --> F
```
1. **`app/` 디렉토리의 역할 (Routing Only)**:
- 오직 Next.js가 요구하는 **라우팅 진입점, 레이아웃 트리 합성, URL 파라미터 추출**만 수행합니다.
- 내부 구현 코드(비즈니스 로직, 폼 마크업, CSS 스타일 등)는 **단 한 줄도 작성하지 않습니다.**
- `page.tsx`는 `src/pages`의 Public API에서 화면 컴포넌트를 가져와 re-export합니다.
2. **`src/` 디렉토리의 역할 (FSD Codebase Only)**:
- 모든 실제 컴포넌트, 상태, 훅, API는 FSD 표준 계층에 따라 `src/`에 존재합니다.
- `src/pages`는 Next.js에 종속되지 않은 순수한 React 컴포넌트로 작성되어 테스트 및 유지보수가 용이합니다.
### 4.3 루트의 빈 `pages/` 폴더 트릭
`apps/as` 프로젝트 루트를 보면 빈 [`pages/README.md`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/pages/README.md)만 들어있는 빈 `pages` 디렉토리가 존재합니다.
#### 왜 빈 `pages/` 폴더가 필요한가?
Next.js는 프로젝트 내에 `src/pages` 디렉토리가 존재하면, 기본적으로 이를 **Pages Router(레거시 라우팅 시스템)**의 라우트로 자동 인식하려고 시도합니다.
이로 인해 Next.js 빌드 시 다음과 같은 충돌이 발생할 수 있습니다:
- `src/pages/sewer-bsc-plan/ui/...` 파일들을 Pages Router의 페이지 경로로 오인하여 빌드 에러 유발.
#### FSD 공식 해결책:
프로젝트 루트에 **빈 `pages/` 폴더**를 생성해 두면, Next.js는 루트의 `pages/`를 우선 확인하고 아무 라우트도 없다고 판단하여 `src/pages`를 Pages Router로 스캔하지 않습니다.
결과적으로 `src/pages`는 순수한 **FSD의 Pages 레이어**로서만 안전하게 동작할 수 있게 됩니다.
---
## 5. `apps/as` 프로젝트의 실제 결합 구현 분석
### 5.1 레이아웃 계층의 FSD 합성 구조
`app/`의 다중 레이아웃과 `src/app`의 전역 설정이 어떻게 결합되는지 살펴보겠습니다:
#### 1) 메인 루트 레이아웃: `app/(main)/layout.tsx`
`app` 레이아웃은 Next.js 서버 기능(쿠키 읽기)을 처리하고, FSD의 `src/app/providers`를 불러와 감싸줍니다:
```tsx
// apps/as/app/(main)/layout.tsx
import type { Metadata } from "next";
import { cookies } from "next/headers";
import { Providers } from "@/app/providers"; // FSD App Layer에서 가져옴
import "@/app/styles/globals.css";
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const theme = (await cookies()).get("site-theme-key");
return (
<html lang="ko" data-theme={theme?.value}>
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
```
#### 2) 전역 프로바이더: `src/app/providers/index.tsx`
FSD App 레이어에 위치하며, React Query, Zustand Store, Confirm, Auth, Menu Provider를 결합합니다:
```tsx
// apps/as/src/app/providers/index.tsx
"use client";
import { ConfirmProvider } from "@frontend-opensource/use-react-hooks";
import { QueryClientProvider } from "@tanstack/react-query";
import { queryClient } from "@/shared/lib";
import { StoreProvider } from "@/shared/providers";
import { AuthWrapper } from "@/entities/auth";
import { MenuWrapper } from "@/entities/menu";
export function Providers({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<StoreProvider>
<ConfirmProvider>
<AuthWrapper>
<MenuWrapper>{children}</MenuWrapper>
</AuthWrapper>
</ConfirmProvider>
</StoreProvider>
</QueryClientProvider>
);
}
```
#### 3) 공통 프레임 레이아웃: `app/(main)/(with-base-layout)/layout.tsx`
GNB, LNB 사이드바, 푸터를 포함하는 레이아웃 역시 FSD App 레이어의 `BaseLayoutWrapper`를 re-export합니다:
```tsx
// apps/as/app/(main)/(with-base-layout)/layout.tsx
import { BaseLayoutWrapper } from "@/app/layouts/base-layout"; // FSD App Layer
export default function WithBaseLayout({ children }: { children: React.ReactNode }) {
return <BaseLayoutWrapper>{children}</BaseLayoutWrapper>;
}
```
### 5.2 화면 라우트 1:1 매핑 구현
#### 1) 단순 목록 화면: 하수도 기본계획 목록 (`sewer-bsc-plan/page.tsx`)
Next.js 라우트 파일은 단 11줄로 구성되며, FSD의 `src/pages/sewer-bsc-plan` 컴포넌트를 Suspense로 감싸기만 합니다:
```tsx
// apps/as/app/(main)/(with-base-layout)/sewer-bsc-plan/page.tsx
import { Suspense } from "react";
import { SewerBscPlanListPage } from "@/pages/sewer-bsc-plan"; // FSD Public API
export default function Page() {
return (
<Suspense>
<SewerBscPlanListPage />
</Suspense>
);
}
```
#### 2) 동적 파라미터 화면: 하수도 기본계획 상세 (`sewer-bsc-plan/[id]/page.tsx`)
Next.js 15의 비동기 `params`와 `searchParams`를 `await`로 추출하여, FSD 컴포넌트의 props로 주입합니다:
```tsx
// apps/as/app/(main)/(with-base-layout)/sewer-bsc-plan/[id]/page.tsx
import { SewerBscPlanDetailGate } from "@/pages/sewer-bsc-plan"; // FSD Public API
interface PageProps {
params: Promise<{ id: string }>;
searchParams: Promise<{ chgCycl?: string }>;
}
export default async function Page({ params, searchParams }: PageProps) {
// Next.js 15 Server Component에서 비동기 파라미터 언래핑
const { id } = await params;
const { chgCycl } = await searchParams;
const propChgCycl = chgCycl ? Number(chgCycl) : undefined;
// FSD Pages 컴포넌트에 순수 props로 전달
return <SewerBscPlanDetailGate id={id} propChgCycl={propChgCycl} />;
}
```
### 5.3 FSD Pages 레이어 내부 조립 (Composition Pattern)
실제 비즈니스 로직과 화면 UI는 [`src/pages/sewer-bsc-plan`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/pages/sewer-bsc-plan) 슬라이스 안에서 하위 위젯과 엔티티를 조합하여 구성됩니다:
```tsx
// apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-form-page.tsx
"use client";
// 1. 하위 shared 레이어 import
import { getListHref, useAlertStore, useUserStore } from "@/shared/lib";
import { SectionCollapsibleGroup } from "@/shared/ui";
// 2. 하위 entities 레이어 import
import { useAcntgSeMngTree } from "@/entities/acntg";
import { useEsntlAtchFileMngTree } from "@/entities/esntl";
import { sewerBscPlanService } from "@/entities/sewer-bsc-plan";
// 3. 하위 widgets 레이어 import (행정지원 공통 컴포넌트)
import {
AttachmentsFormSection,
ChecklistFormSection,
ContactInfoFormSection,
PrivacyConsentSection,
} from "@/widgets/pbadms-sprt";
export function SewerBscPlanFormPage({ mode, initialData }: Props) {
// react-hook-form 및 비즈니스 데이터 처리
// widgets과 entities에서 가져온 블록들을 선언적으로 배치
return (
<form onSubmit={...}>
<SectionCollapsibleGroup>
<ContactInfoFormSection />
<AttachmentsFormSection />
<ChecklistFormSection />
<PrivacyConsentSection />
</SectionCollapsibleGroup>
</form>
);
}
```
### 5.4 FSD Public API 작성 규칙 준수
모든 슬라이스는 최상단 `index.ts`를 통해 외부로 공개할 컴포넌트와 타입만 명시적으로 내보냅니다:
```typescript
// apps/as/src/pages/sewer-bsc-plan/index.ts
export { SewerBscPlanDetailGate } from "./ui/sewer-bsc-plan-detail-gate";
export { SewerBscPlanEditPage } from "./ui/sewer-bsc-plan-edit-page";
export { SewerBscPlanFormPage } from "./ui/sewer-bsc-plan-form-page";
export { SewerBscPlanListPage } from "./ui/sewer-bsc-plan-list-page";
export { SewerBscPlanRegistPage } from "./ui/sewer-bsc-plan-regist-page";
export { SewerBscPlanViewPage } from "./ui/sewer-bsc-plan-view-page";
```
슬라이스 내부의 세부 구현 파일(예: `lib/fnrs-cst-form-utils.ts`, `sections/basic-info-form-section.tsx`)은 외부로 노출되지 않고 슬라이스 내부에 안전하게 캡슐화됩니다.
---
## 6. 개발 가이드라인 및 의사결정 트리 (Best Practices)
### 6.1 신규 화면 추가 시 작업 순서 (Step-by-Step Workflow)
새로운 업무 화면(예: `water-quality-mng` 수질 관리)을 개발할 때 권장되는 작업 절차는 다음과 같습니다:
```
[1단계: Entity 정의]
src/entities/water-quality-mng/
├── api/ (Axios 요청 및 React Query Hooks 작성)
├── model/ (DTO 인터페이스 및 Zod 스키마 정의)
└── index.ts (Public API export)
↓
[2단계: 필요 시 Widget 작성]
src/widgets/water-quality-mng/ (여러 화면에서 재사용될 복합 UI인 경우만 생성)
↓
[3단계: Page 조립]
src/pages/water-quality-mng/
├── ui/ (목록, 등록, 상세 컴포넌트 작성)
└── index.ts (Public API export)
↓
[4단계: Next.js App Router 연결]
app/(main)/(with-base-layout)/water-quality-mng/
├── page.tsx (src/pages에서 컴포넌트 re-export)
└── [id]/page.tsx (동적 파라미터 언래핑 후 컴포넌트 re-export)
↓
[5단계: 아키텍처 린트 검증]
pnpm run lint:fsd 실행하여 의존성 규칙 준수 확인
```
### 6.2 컴포넌트 위치 결정 의사결정 트리
새로운 코드나 컴포넌트를 작성할 때 어떤 레이어에 배치해야 할지 판단하는 기준입니다:
```mermaid
graph TD
Q1{"Q1. 비즈니스 도메인(업무 개념)과 관련이 있는가?"}
Q1 -->|아니오| L_SHARED["shared/ 에 배치<br/>(UI Kit, 순수 유틸, 공통 클라이언트)"]
Q1 -->|예| Q2{"Q2. 특정 라우트 화면 하나에서만 쓰이는가?"}
Q2 -->|예| L_PAGES["src/pages/[slice]/ 에 배치<br/>(화면 전용 컴포넌트, 전용 폼)"]
Q2 -->|아니오| Q3{"Q3. 여러 화면에서 재사용되는 큰 규모의 UI 블록인가?"}
Q3 -->|예| L_WIDGETS["widgets/[slice]/ 에 배치<br/>(행정지원 공통 폼, 조직도, 기안기)"]
Q3 -->|아니오| Q4{"Q4. 사용자의 능동적인 인터랙션/액션 단위인가?"}
Q4 -->|예| L_FEATURES["features/[slice]/ 에 배치<br/>(로그인 액션, 검색 필터 버튼)"]
Q4 -->|아니오| L_ENTITIES["entities/[slice]/ 에 배치<br/>(데이터 모델, DTO, API 쿼리, 기본 카드 뷰)"]
```
### 6.3 Server Component와 Client Component 경계 관리
- **`app/` 디렉토리의 `page.tsx`**:
- 기본적으로 **Server Component**로 유지하는 것이 좋습니다.
- 비동기 `params`, `searchParams`를 서버에서 안전하게 수신하고, 필요한 경우 메타데이터(`generateMetadata`)를 구성할 수 있습니다.
- **`src/pages/` 디렉토리의 화면 컴포넌트**:
- 사용자 입력, 폼 상태(`react-hook-form`), 클라이언트 상태(`Zustand`), 데이터 페칭(`TanStack Query`)을 활발히 사용하므로 상단에 `"use client";`를 명시합니다.
- Next.js 서버 컴포넌트(`page.tsx`)가 클라이언트 컴포넌트(`SewerBscPlanListPage`)를 자식으로 임포트하는 자연스러운 **RSC 경계 분리**가 완성됩니다.
---
## 7. 결론 (Summary)
`apps/as` 모듈이 채택한 **FSD v2.1과 Next.js App Router의 결합 아키텍처**는 다음과 같은 결정적 장점을 제공합니다:
1. **프레임워크 종속성 격리**:
- Next.js의 프레임워크 전용 파일들(`layout.tsx`, `page.tsx`)을 얇은 라우팅 어댑터로 격리함으로써, 비즈니스 코드가 Next.js 버전에 종속되어 레거시화되는 것을 방지합니다.
2. **높은 도메인 응집도와 낮은 결합도**:
- 하수도기본계획, 재원협의 등 각 업무 도메인의 모델과 API가 `entities/`에 명확히 캡슐화되어 다른 화면이나 위젯에서 안전하게 재사용됩니다.
3. **엄격한 아키텍처 린팅 보장**:
- `steiger`를 통해 개발자의 실수로 인한 상위 레이어 역참조나 순환 참조를 사전에 차단하여 장기적인 유지보수 비용을 크게 낮춥니다.
---
## 8. Next.js 내부 소스 레벨의 라우트 그룹 (Route Groups) 인식 원리 및 구성 분석
Next.js에서 소괄호로 감싼 폴더인 **라우트 그룹(`(folder)`)**이 어떻게 URL 경로에서 투명하게 제외되고, 독립적인 레이아웃 경계를 형성하는지 Next.js 코어 소스코드와 빌드 엔진 관점에서 상세히 분석합니다.
### 8.1 Next.js 코어 소스코드 분석: 라우트 그룹 인식 메커니즘
Next.js의 내부 패키지([`next/dist`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/node_modules/next/dist))를 분석하면, 라우트 그룹을 감지하고 처리하는 핵심 함수들을 확인할 수 있습니다.
#### 1) 라우트 그룹 판별 함수: `isGroupSegment`
Next.js는 세그먼트 문자열이 라우트 그룹인지 판별할 때 정규식(RegExp) 대신 성능 최적화(Performance Purpose)를 위해 첫 글자와 끝 글자를 직접 검사합니다:
```typescript
// next/src/shared/lib/segment.ts (next/dist/shared/lib/segment.js)
export function isGroupSegment(segment: string) {
// O(1) 고속 연산을 위해 배열 인덱스 활용
return segment[0] === '(' && segment.endsWith(')')
}
```
- **판정 기준**: 폴더명 세그먼트의 첫 글자가 여는 소괄호 `(`이고, 마지막 글자가 닫는 소괄호 `)`이면 `GroupSegment`로 판단합니다.
- 예: `(auth)` ➔ `true`, `(main)` ➔ `true`, `(with-base-layout)` ➔ `true`, `[id]` ➔ `false`, `auth` ➔ `false`.
#### 2) URL 노출 여부 결정 함수: `doesStaticSegmentAppearInURL`
클라이언트 라우터가 라우트 트리에서 실제 브라우저 주소창(URL)에 노출될 경로를 조립할 때 호출하는 함수입니다:
```typescript
// next/src/client/route-params.ts (next/dist/client/route-params.js)
export function doesStaticSegmentAppearInURL(segment: string): boolean {
if (
segment === ROOT_SEGMENT_REQUEST_KEY ||
segment.startsWith(PAGE_SEGMENT_KEY) ||
// ★ 라우트 그룹은 URL에 나타나지 않으므로 false 반환 (생략)
(segment[0] === '(' && segment.endsWith(')')) ||
segment === DEFAULT_SEGMENT_KEY ||
segment === '/_not-found'
) {
return false
} else {
// 그 외 일반 세그먼트는 URL에 포함됨
return true
}
}
```
- 세그먼트가 라우트 그룹인 경우 `false`를 리턴하여 URL Pathname 생성 목록에서 완전히 제외(Skip)합니다.
#### 3) 클라이언트 라우터의 경로 정규화: `normalizeSegments`
페이지 이동 및 브라우저 히스토리 갱신 시 FlightRouterState 트리에서 실제 URL을 계산하는 로직입니다:
```typescript
// next/src/client/components/router-reducer/compute-changed-path.ts
function normalizeSegments(segments: string[]): string {
return (
segments.reduce((acc, segment) => {
segment = removeLeadingSlash(segment)
// ★ 빈 문자열이거나 라우트 그룹인 경우 누적 URL에 추가하지 않고 통과
if (segment === '' || isGroupSegment(segment)) {
return acc
}
return `${acc}/${segment}`
}, '') || '/'
)
}
```
- 경로 배열 `['(main)', '(with-base-layout)', 'dashboard']`가 전달되면 `isGroupSegment`가 `(main)`과 `(with-base-layout)`을 건너뛰어 최종적으로 `"/dashboard"`라는 정규화된 URL을 산출합니다.
#### 4) Webpack 빌드 타임의 로더 트리 생성: `next-app-loader`
Next.js 빌드 시 웹팩 로더([`next-app-loader`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/node_modules/next/dist/build/webpack/loaders/next-app-loader))는 파일 시스템을 스캔하여 계층적 트리(`FlightRouterState`)를 빌드합니다:
```typescript
// next/src/build/webpack/loaders/next-app-loader/index.ts
// 1. 첫 번째 레이어가 그룹 라우트인지 확인하여 루트 레이어로 취급
const isFirstLayerGroupRoute =
segments.length === 1 &&
subSegmentPath.filter((seg) => isGroupSegment(seg)).length === 1
// 2. 서로 다른 라우트 그룹이 동일한 URL 경로로 충돌하는지 정적 검사
if (!hasCurrentParallelPage && !isIncomingParallelPage) {
throw new Error(
`You cannot have two parallel pages that resolve to the same path. Please check ${existingChildrenPath} and ${appPath}. Refer to the route group docs for more information: https://nextjs.org/docs/app/building-your-application/routing/route-groups`
)
}
```
- 빌드 엔진은 라우트 그룹을 **"레이아웃과 에러 바운더리를 독립적으로 분기하는 가상 트리 노드"**로 유지하면서, 최종 URL 라우트 맵에서는 동일한 URL에 중복된 페이지가 매핑되지 않도록 엄격히 검증합니다.
---
### 8.2 세팅 및 구성(Configuration & Structure) 측면 분석
#### 1) 별도 환경설정 불필요 (Zero Configuration Convention)
Next.js의 라우트 그룹은 `next.config.ts`나 `tsconfig.json`에 특별한 플래그를 켤 필요가 없습니다.
Next.js 컴파일러와 런타임에 소괄호 `(name)` 패턴이 **예약된 명명 규칙(Reserved Naming Convention)**으로 내장되어 있기 때문입니다.
#### 2) `apps/as`의 다중 루트 레이아웃(Multiple Root Layouts) 구성 전략
`apps/as`는 라우트 그룹을 활용해 **서로 다른 HTML 셸을 가진 2개의 독립 루트 레이아웃**을 구성했습니다:
```
apps/as/app/
├── (auth)/ # 그룹 1: 인증 화면군
│ ├── auth/login/page.tsx # URL: /auth/login
│ └── layout.tsx # 독립 RootLayout: Provider 없이 가벼운 <html><body> 제공
│
└── (main)/ # 그룹 2: 메인 업무 포털 화면군
├── layout.tsx # 독립 RootLayout: 테마, QueryClient, Auth, Menu Provider 포함
└── (with-base-layout)/ # 중첩 그룹: 공통 프레임 레이아웃
├── layout.tsx # BaseLayoutWrapper: Header, Sidebar, Footer 포함
├── dashboard/page.tsx # URL: /dashboard
└── sewer-bsc-plan/... # URL: /sewer-bsc-plan
```
- **최상위 `app/layout.tsx` 부재의 의미**:
- `app/` 바로 아래에 `layout.tsx`를 두지 않고, `(auth)/layout.tsx`와 `(main)/layout.tsx`로 분리함으로써 **로그인 페이지 진입 시 불필요한 무거운 전역 Provider와 레이아웃 JS 번들이 로드되는 것을 원천 차단**했습니다.
- 각각의 레이아웃이 독자적인 `<html>`과 `<body>` 태그를 반환할 수 있습니다.
#### 3) 중첩 라우트 그룹 (Nested Route Groups) 구성
`app/(main)/(with-base-layout)`처럼 라우트 그룹 안에 또 다른 라우트 그룹을 중첩할 수 있습니다:
- `(main)`: 앱 전역 상태, 인증/세션 검사, 테마 컨텍스트 주입.
- `(with-base-layout)`: 시각적 GNB 헤더 및 LNB 사이드바 주입.
- 향후 기본 레이아웃이 필요 없는 팝업형 업무 창이 필요할 경우, `app/(main)/(without-base-layout)/popup/page.tsx` 형태로 확장할 수 있는 유연한 구조를 확보했습니다.
#### 4) 특수 폴더 명명 규칙 비교표
| 표기법 | Next.js 해석 | URL 포함 여부 | 주요 용도 |
| :--- | :--- | :---: | :--- |
| **`(folder)`** | **Route Group** | ❌ **제외됨** | 레이아웃 분리, 모듈 그룹화, 다중 루트 레이아웃 |
| **`_folder`** | **Private Folder** | ❌ **제외됨 (라우팅 불가)** | 라우트 대상에서 완전히 제외하고 내부 컴포넌트 격리 |
| **`[folder]`** | **Dynamic Segment** | ⭕ **포함됨 (가변값)** | 동적 URL 파라미터 매핑 (`[id]`, `[code]`) |
| **`@folder`** | **Parallel Route** | ❌ **제외됨** | 동일 레이아웃 내 독립 슬롯 렌더링 (병렬 라우트) |
| **`folder`** | **Static Segment** | ⭕ **포함됨 (고정값)** | 일반적인 정적 URL 세그먼트 |
#### 5) 라우트 그룹 구성 시 주의점 (Gotchas)
1. **경로 충돌(Collision)**:
- `app/(auth)/about/page.tsx`와 `app/(main)/about/page.tsx`를 동시에 생성하면 두 파일 모두 `/about`이라는 동일한 URL로 해석되므로 Next.js 빌드 단계에서 `You cannot have two parallel pages that resolve to the same path` 에러가 발생합니다.
2. **루트 레이아웃 간 이동 시 전체 리로드(Full Page Reload)**:
- 최상위 `<html>`, `<body>`를 각각 소유하는 라우트 그룹 간(예: `(auth)/auth/login` ➔ `(main)/dashboard`) 이동 시에는 React 상태가 보존되는 클라이언트 사이드 전환(Soft Navigation)이 아니라 브라우저 전체가 새로고침되는 하드 네비게이션(Hard Navigation)이 일어납니다. (로그인 후 세션 초기화 측면에서는 오히려 권장되는 동작입니다.)