diff --git a/apps/as/docs/pm2/03.dev-and-production-execution-guide.md b/apps/as/docs/pm2/03.dev-and-production-execution-guide.md new file mode 100644 index 000000000..c398a1689 --- /dev/null +++ b/apps/as/docs/pm2/03.dev-and-production-execution-guide.md @@ -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 +``` diff --git a/docs/as/01.as-architecture-and-data-flow.md b/docs/as/01.as-architecture-and-data-flow.md new file mode 100644 index 000000000..3b5363016 --- /dev/null +++ b/docs/as/01.as-architecture-and-data-flow.md @@ -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) 호출
(현재 검색조건 쿼리스트링을 유지한 채 상세 페이지로 이동) + 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` | **행정지원 비즈니스 코어**
- `/sewer-bsc-plans`: 하수도 기본계획 CRUD
- `/fnrs-cnsltn-mng`: 재원협의 신청 및 분담 관리
- `/instl-sncn`: 설치인가 관리
- `/design-cnstn`: 설계자문 관리
- `/cklst`, `/cklst-artcl`: 체크리스트 템플릿 및 평가
- `/acntg-se`, `/cnstp-se`: 회계구분 및 공종구분 기준 코드
- `/esntl-atch-file`: 필수 첨부파일 마스터
- `/dashboard`: 대시보드 종합 지표 | +| **`ATTACH_SERVICE`** | `/attach-service/api/v1` | **파일 관리 서비스**
- `/tus/upload`: TUS 프로토콜 파일 업로드 엔드포인트
- `/atfl/attachments`: 첨부파일 메타데이터 조회
- `/atflcd/download`, `/download`: 첨부파일 스트림 다운로드 | +| **`USER_SERVICE`** | `/user-service/api/v1` | **사용자 및 세션 서비스**
- `/login`: 사용자 로그인 인증
- `/users/token/refresh`: 토큰 재발급(Silent Refresh)
- `/users/info/me`: 현재 로그인 사용자 상세 정보 | +| **`AS_ELAPR_SERVICE`** | `/as-elapr-service/api/v1` | **전자결재 서비스**
- `/elapr-tmplt`: 결재 서식 템플릿
- `/elapr-apln`: 결재선 지정 및 순서 관리
- `/elapr-dmnd`: 결재 상신 및 승인/반려 요청
- `/elapr-prgrs-stts`: 결재 진행 상태 조회 | +| **`CMS_SERVICE`** | `/cms-service/api/v1` | **공통 플랫폼 서비스**
- `/code/cache`: 전사 공통코드 캐시 조회
- `/menu-roles`, `/menu-authrts`: 사이트별 메뉴 및 권한 매핑
- `/user-ognz`: 조직도 및 사용자 트리
- `/noti`: 통보 및 알림 | +| **`NTFTALK_SERVICE`** | `/ntftalk-service/api/v1` | **알림톡 서비스**
- `/ntftalk-temp`: 알림톡 템플릿 관리
- `/ata/send`: 카카오 알림톡/SMS 발송 | +| **`BOARD_SERVICE`** | `/board-service/api/v1` | **게시판 서비스**
- `/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 업로드 크기에 맞추어 충분히 확보되어야 합니다. diff --git a/docs/as/02.fsd-and-nextjs-app-router-integration.md b/docs/as/02.fsd-and-nextjs-app-router-integration.md new file mode 100644 index 000000000..feb27158f --- /dev/null +++ b/docs/as/02.fsd-and-nextjs-app-router-integration.md @@ -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)
[`src/app/layouts/base-layout.tsx`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/app/layouts/base-layout.tsx)
[`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)
[`src/pages/sewer-bsc-plan`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/pages/sewer-bsc-plan)
[`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) (행정지원 공통 폼)
[`src/widgets/hangulgiangi`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/widgets/hangulgiangi) (웹한글기안기)
[`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)
[`src/entities/auth`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/entities/auth)
[`src/entities/elapr`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/entities/elapr)
[`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)
[`src/shared/lib/use-tus.ts`](file:///Users/changwng/workspace.ona/front/frontend_as/apps/as/src/shared/lib/use-tus.ts)
[`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
(URL 구조 정의)"] + A2["page.tsx
(파라미터 추출 & Re-export)"] + end + + subgraph FSD["FSD v2.1 아키텍처 영역 (src/)"] + direction TB + B["src/app/
(Providers & Layout 구현체)"] + C["src/pages/
(화면 컴포넌트 & 조립)"] + D["src/widgets/
(복합 UI 블록)"] + E["src/entities/
(비즈니스 도메인)"] + F["src/shared/
(기반 인프라 & 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 ( + + + {children} + + + ); +} +``` + +#### 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 ( + + + + + {children} + + + + + ); +} +``` + +#### 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 {children}; +} +``` + +### 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 ( + + + + ); +} +``` + +#### 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 ; +} +``` + +### 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 ( +
+ + + + + + +
+ ); +} +``` + +### 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/ 에 배치
(UI Kit, 순수 유틸, 공통 클라이언트)"] + Q1 -->|예| Q2{"Q2. 특정 라우트 화면 하나에서만 쓰이는가?"} + + Q2 -->|예| L_PAGES["src/pages/[slice]/ 에 배치
(화면 전용 컴포넌트, 전용 폼)"] + Q2 -->|아니오| Q3{"Q3. 여러 화면에서 재사용되는 큰 규모의 UI 블록인가?"} + + Q3 -->|예| L_WIDGETS["widgets/[slice]/ 에 배치
(행정지원 공통 폼, 조직도, 기안기)"] + Q3 -->|아니오| Q4{"Q4. 사용자의 능동적인 인터랙션/액션 단위인가?"} + + Q4 -->|예| L_FEATURES["features/[slice]/ 에 배치
(로그인 액션, 검색 필터 버튼)"] + Q4 -->|아니오| L_ENTITIES["entities/[slice]/ 에 배치
(데이터 모델, 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 없이 가벼운 제공 +│ +└── (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 번들이 로드되는 것을 원천 차단**했습니다. + - 각각의 레이아웃이 독자적인 ``과 `` 태그를 반환할 수 있습니다. + +#### 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)**: + - 최상위 ``, ``를 각각 소유하는 라우트 그룹 간(예: `(auth)/auth/login` ➔ `(main)/dashboard`) 이동 시에는 React 상태가 보존되는 클라이언트 사이드 전환(Soft Navigation)이 아니라 브라우저 전체가 새로고침되는 하드 네비게이션(Hard Navigation)이 일어납니다. (로그인 후 세션 초기화 측면에서는 오히려 권장되는 동작입니다.) +