` ~ ``
+
+| 영역 | JSP 식별 | 개요 |
+| ---- | ---------------------------------- | --------------------------------------------------------------- |
+| A | `gTitle1` | 페이지 타이틀·브레드크럼 |
+| B | `slide_10` | 기본계획변경 신청(마스터·연도·개요) |
+| C | `slide_15` | 신청자·설계자 정보 |
+| D | `slide_19` | 개인정보 동의 |
+| E | `slide_11` | 사업개요(단계별 인구·보급률) |
+| F | `slide_12` | 총사업비 — `BizCstMatrixFormSection` 전용 |
+| G | `slide_13` | 재원분담 |
+| H | `slide_14` | 처리시설별 하수도정비기본계획 (Checkbox + Tabs) |
+| I | `slide_16` | 체크리스트 + 필수첨부 + 기타첨부 |
+| J | JSP 상단 `saveBtn` / `presentnBtn` | React: 폼 하단 액션바 (`fnrs-form-page` 279~323과 동일 구성, API만 sewer) |
+
+### 1-2. 제외
+
+- `<%@ include %>`: top / sideMenu / footer / `bscPlnPopup.jsp` / `sndrdBzAmnt.jsp`
+- 레이아웃·메뉴·권한 셸 → 기존 `BaseLayout` / `PageLayout` / AUTH 체계
+- `mStep1` 진행 단계 표시 (미구현 · 마이그레이션 범위 밖)
+
+팝업(`이전기본계획조회`)은 **별도 Dialog 컴포넌트**로 재구현하며, include JSP 자체는 포팅하지 않는다.
+
+---
+
+## 2. 목표 아키텍처
+
+```
+app/(with-base-layout)/sewer-bsc-plan/regist/page.tsx
+ └─ pages/sewer-bsc-plan/ui/sewer-bsc-plan-regist-page.tsx
+ └─ sewer-bsc-plan-form-page.tsx # FormProvider + 섹션 조립
+ ├─ sections/*-form-section.tsx # 도메인 전용
+ └─ widgets/pbadms-sprt/* # 공통 재사용
+
+entities/sewer-bsc-plan/ # 이미 구현됨
+ saveApply / findDetails / submitSupplement …
+```
+
+**폼 스택 (fnrs-cnsltn과 동일)**
+
+- `react-hook-form` + `zod` (`zodResolver`, `mode: "onChange"`)
+- 섹션 래퍼: `SectionCollapsible` (Accordion 아님)
+- UI: `@common/components` (Input, Select, Textarea, Button, Radio, Tabs, Table 등)
+- 그리드가 필요한 곳만 `@tanstack/react-table` + `DataTable`
+- 데이터: React Query (`useSaveApplySewerBscPlan` 등) — 목데이터는 API 미연동 구간만 `@faker-js/faker`
+
+**Vercel React Best Practices (적용 요약)**
+
+- `client-swr-dedup`: React Query로 체크리스트/필수첨부/코드 조회 중복 제거
+- `bundle-dynamic-imports`: 처리시설 Tabs 패널·이전계획 Dialog는 `next/dynamic` 후보
+- `rerender-derived-state`: 단계 연도·합계·진행률은 render 중 파생, 불필요 effect 금지
+- `async-parallel`: 등록 진입 시 코드/체크리스트트리/필수첨부트리를 병렬 조회
+
+---
+
+## 3. 전체 레이아웃
+
+`fnrs-form-page.tsx`와 동일한 **FormProvider + 세로 섹션 스택** 구조를 따르고,
+하단 액션바는 **[`fnrs-form-page.tsx` 279~323](../../src/pages/fnrs-cnsltn/ui/fnrs-form-page.tsx)** 과 동일 구성(목록·저장·신청완료·삭제·보완완료)이며 **API/라우트만 sewer** 로 바꾼다.
+타이틀·브레드크럼은 폼 밖(`PageLayout` / regist·edit·view 셸)에서 처리한다.
+(상세 View의 sticky FAB·검토/결재 버튼은 본 등록 문서 범위 밖 — `fnrs-view-page` 대응 후속.)
+
+### 3-1. 컴포넌트 트리 (fnrs 대응)
+
+```
+PageLayout (title / breadcrumb)
+└─ SewerBscPlanRegistPage | SewerBscPlanEditPage | SewerBscPlanViewPage
+ └─ SewerBscPlanFormPage | SewerBscPlanViewPage 본문
+ FormProvider (등록/수정) / 읽기 섹션 (상세)
+ └─
│
+└─────────────────────────────────────────────────────────────────┘
+```
+
+### 3-3. 액션바 동작 (`fnrs-form-page` 279~323 동일, API만 sewer)
+
+구현 위치: `sewer-bsc-plan-form-page.tsx` 하단
+(`fnrs-form-page.tsx`의 `mx-4 flex flex-wrap justify-center gap-3` 블록을 그대로 옮기고 훅·path만 교체)
+
+| 버튼 | 조건 | 대표 동작 |
+| -------- | ---------------------------- | -------------------------------------------------------------------------- |
+| 목록 | 항상 | `router.push("/sewer-bsc-plan")` |
+| 저장 | `!isSplmntPrgrsStts` | `type="submit"` → `saveApply` + 임시저장(`SV`) |
+| 신청완료 | edit && `!isSplmntPrgrsStts` | 체크리스트 완료 + **필수 첨부 완료** 검증 → `saveApply` + `LE` |
+| 삭제 | edit && `!isSplmntPrgrsStts` | confirm → `delete` → 목록 |
+| 보완완료 | `isSplmntPrgrsStts` (SL) | `form.handleSubmit` → `useSubmitSupplementSewerBscPlan` |
+
+**신청완료 사전 검증** (fnrs `handleApplyClick`과 동일 정책)
+
+1. **체크리스트**: `isFormChecklistComplete(cklstResultList)` — 미완료 시 `"모든 체크리스트 항목을 체크해야 신청이 가능합니다."`
+2. **필수 첨부**: `esntlAtchYn === "Y"`인 필수첨부 슬롯마다 `esntlAtchFileList`에 `atchFileGroupId`가 있어야 함
+ - Form: `AttachmentsFormSection`의 트리 + 폼 `esntlAtchFileList`
+ - 미충족 시 예: `"필수 첨부파일을 모두 등록해야 신청이 가능합니다."`
+ - 헬퍼 후보: `isFormEsntlAtchComplete(requiredTree, esntlAtchFileList)` (`pbadms-sprt`에 추가)
+
+저장에는 위 검증을 적용하지 않는다 (임시저장 허용).
+
+저장/제출 버튼을 섹션 헤더(`gTitle2 gRt`)에 두지 않는다 — **폼 하단 액션바 단일 진입점** (§4-J).
+
+### 3-4. 라우트
+
+| 화면 | path | 셸 → FormPage |
+| ---- | --------------------------- | ------------------------------------------ |
+| 등록 | `/sewer-bsc-plan/regist` | `mode="create"` |
+| 수정 | `/sewer-bsc-plan/[id]/edit` | `mode="edit"` + `useSewerBscPlanDetails` |
+| 상세 | `/sewer-bsc-plan/[id]` | ViewPage (본 문서 범위 밖, fnrs-view 대응) |
+
+`docSeComCd` 후보: 백엔드 체크리스트·필수첨부 등록값 확인 후 **`SEWER`** (또는 레거시 `DMBP`) — **TBD, CMS 코드와 맞출 것**.
+
+---
+
+## 4. 섹션별 상세 구현
+
+### 4-0. Hidden / 세션 컨텍스트 (폼 루트)
+
+JSP hidden → React 폼 기본값·세션 스토어로 흡수.
+
+| JSP | 의미 | React |
+| ------------------------------- | --------------- | ------------------------------------------------------- |
+| `chgSe` / `chgSeSp` / `chgSeCd` | 변경구분 | `sewerBscPlan.sewerBscPlanSeComCd` + 표시용 코드명 |
+| `progrsSttus` | SAVE / PRESENTN | 저장·제출 시 `prgrsSttsComCd`로 매핑 (임시저장 vs 제출) |
+| `planSeCd` | 고정 `A` | 필요 시 상수 또는 API 필드 확인 |
+| `adstDstrCd` / `rgnCd` | 행정구역 | `sewerBscPlan.admdstCd` |
+| `sidoNm` / `gugunNm` | 표시용 | 읽기 전용 표시 (`detDto.sido`/`gugun`) |
+
+구현: `getCreateDefaultValues(userInfo)`에서 로그인 사용자·소속 지자체 주입.
+
+---
+
+### 4-A. 타이틀·브레드크럼
+
+| JSP | React |
+| ------------------------ | ------------------------------------------------------ |
+| `h3` 기본계획검토 | PageLayout title: **하수도기본계획 신청** (IIPBS 명명) |
+| 설치인가 및 제원협의 > … | 메뉴 메타 기반 breadcrumb |
+
+별도 섹션 컴포넌트 불필요. 라우트 `page.tsx` / layout metadata로 처리.
+
+---
+
+### 4-B. 기본계획변경 신청 (`slide_10`)
+
+**파일**: `ui/sections/basic-info-form-section.tsx`
+**래퍼**: `SectionCollapsible` title=`기본계획 신청`
+**필드 접두사**: `sewerBscPlan.*` + `sewerBscPlanDtl.*`
+
+| 표시명 | JSP | API 매핑 | UI | 필수 | 비고 |
+| ---------------- | ----------------- | ------------------------------- | ------------------ | ---- | ---------------------------------- |
+| 기본계획명 | `drngMtncPlanNm` | `sewerBscPlan.sewerBscPlanNm` | Input | Y | |
+| 계획년도 | `planYr` | `sewerBscPlanDtl.planYr` | Select (현재~2005) | Y | |
+| 기준년도 | `crtrYr` | `sewerBscPlanDtl.crtrYr` | Select | Y | |
+| 1~4단계 목표연도 | `goalYrStep1~4` | `frlvlGoalYr`~`folvlGoalYr` | Select (현재~+30) | Y | 1단계 변경 시 +5/+10/+15 자동 연동 |
+| 계획개요 | `bzCn` | `sewerBscPlanDtl.bizOtlnCn` | Textarea | Y | |
+| 구분 | `#chgSeNm` | `sewerBscPlanSeComCd` (+코드명) | 읽기 또는 Select | | 신규/변경 등 |
+| 신청 지자체 | `${lclgvrnmntNm}` | `admdstCd` | 읽기 전용 | | |
+
+**동작**
+
+- `goalYrStep1` onChange → 2·3·4단계 기본값 세팅 + E/F/G 헤더 연도 span 갱신 (`watch` 파생)
+- JSP 상단 `gRt` 저장/제출은 사용하지 않음 → FormPage 하단 액션바 (§3-3, §4-J)
+
+---
+
+### 4-C. 신청자 정보 (`slide_15`)
+
+**재사용**: `@/widgets/pbadms-sprt`의 `ContactInfoFormSection`
+**props**: `fieldPrefix="sewerBscPlanDtl"` (또는 dtl 필드 구조에 맞게)
+
+| 표시 | JSP | API |
+| ---------------------------- | --------------------------- | --------------------------------------------------------------------------------------- |
+| 신청자 이름/전화/부서/이메일 | `${UsrNm}` 등 서버렌더 | 로그인 사용자 읽기 전용 → `aplcntNm`, `aplcntMblTelno`, `aplcntOgdpNm`, `aplcntEmlAddr` |
+| 설계자 | `dsgnrNm/Telno/Psitn/Email` | `dsgnrNm`, `dsgnrTelno`, `dsgnrOgdpNm`, `dsgnrEmlAddr` |
+
+신청자 4필드는 세션에서 defaultValues에 채우고 Input `disabled`.
+
+---
+
+### 4-D. 개인정보 동의 (`slide_19`)
+
+**재사용**: `PrivacyConsentSection`
+
+| JSP | React |
+| ----------------------------- | ------------------------------------------------------------------ |
+| 고지 textarea + 보유기간 문구 | 동일 문구 상수/마크다운 |
+| `privacyAgree` Y/N | 폼 필드 `privacyAgree: "Y" \| "N"` (API 미전송, 클라이언트 검증만) |
+
+**검증**: 제출 시 `Y` 필수 (JSP `presentnBtn`과 동일). 저장(임시)은 동의 없이 허용할지 정책 TBD — 기본은 JSP와 같이 **제출만 강제**.
+
+---
+
+### 4-E. 사업개요 (`slide_11`)
+
+**파일**: `ui/sections/outline-form-section.tsx` (신규, 도메인 전용)
+**API**: `sewerBscPlanOtlnList: SewerBscPlanOtlnItem[]` (planStpNo 1~4)
+
+| 행(구분) | JSP name | Item 필드 |
+| ------------ | --------------- | ------------------- |
+| 계획인구 | `arrPlanPpltn` | `sewerPlanPpltnCnt` |
+| 처리인구 | `arrPrcsPpltn` | `sewerPrcsPpltnCnt` |
+| 하수도보급율 | `arrDrngSplyRt` | `sewerPsplyRt` |
+
+열: 1~4단계 (헤더에 `frlvlGoalYr` 등 표시).
+
+**버튼**: `이전기본계획조회` → Dialog (`PreviousPlanLookupDialog`)
+
+- 레거시: `fnbscPlnPopupToggle` + `bscPlnPopup.jsp`
+- React: Modal + 조회 API (**TBD**: 이전 계획 조회 엔드포인트; 없으면 1차는 Dialog UI만 + mock)
+
+숫자 입력: 콤마 제거 후 number 전송 (제출 시 JSP `replaceComma` 대체).
+
+---
+
+### 4-F. 총사업비 (`slide_12`)
+
+**파일**: `ui/sections/biz-cst-matrix-form-section.tsx` (**전용 컴포넌트** — fnrs `BusinessCostFormSection` 재사용 금지)
+**래퍼**: `SectionCollapsible` title=`총사업비` (단위 `백만원`은 헤더 우측)
+
+**JSP 현실**: 단계(1~4) × 원가 계층(총사업비, 공사비, 처리시설, 하수관로 신설/보수, 펌프장, 배수설비, 기타, 부대경비, 설계/감리/부대비). 일부 readonly·자동합산 JS.
+
+**신규 API**: `bizCstList: SewerBscPlanBizCstItem[]` = `{ planStpNo, acntgSeCd, cstrnAmt }`
+
+**구현 방향 (`BizCstMatrixFormSection` 전용)**
+
+- 행 = `useAcntgSeMngTree` → `buildAcntgCostItems` (fnrs `BusinessCostFormSection`과 동일 출처)
+- `costItems` 로드 시 `useEffect`로 `bizCstList`를 `mergeBizCstList(existing, costItems)`로 동기화
+- 열 = `planStpNo` 1~4 (헤더에 목표연도 표시)
+- readonly 행(`CostItemDef.readonly=true`)은 children 합산으로 자동계산; Input에 `readOnly` + `bg-muted`
+- 변경 시 `setAmt` → `recalculateBizCstMatrix(list, costItems)` → `setValue("bizCstList")`
+- 저장 시 섹션 onChange 합산을 신뢰하여 `bizCstList` 그대로 전송 (`toBizCstSaveList` identity)
+
+**표준사업비 조회** 버튼은 JSP에서 주석 처리됨 → **미구현**.
+
+---
+
+### 4-G. 재원분담 (`slide_13`)
+
+**주의**: fnrs의 `FnrsSharFormSection`(재원협의 공유구분 트리)과 **다름**.
+
+| 행 | JSP | `SewerBscPlanFnrsCstItem` |
+| ------------ | -------------- | ------------------------- |
+| 총사업비 | `arrTotAmnt` | `sumAmt` (또는 파생) |
+| 국고 | `arrNlepAmt` | `gvexpAmt` |
+| 지방비 | `arrLcepAmt` | `lcexpAmt` |
+| 원인자부담금 | `arrCusrAlotm` | `orgprBdchgAmt` |
+| 기타 | `arrEtcAmt` | `etcAmt` |
+
+**파일**: `ui/sections/fnrs-cst-form-section.tsx` (신규)
+데이터: `fnrsCstList` 길이 4 (`planStpNo` 1~4).
+
+검증(선택): 단계별 `gvexp+lcexp+orgpr+etc === sumAmt` (JSP `calculateTotalFunding` 참고).
+
+---
+
+### 4-H. 처리시설별 하수도정비기본계획 (`slide_14`) — 핵심 복잡도
+
+**JSP 동작**
+
+1. `BscFctList` 체크박스로 처리시설 다중 선택 (`arrBscFctCd`)
+2. 선택 시 `createFormSection(code, name)`로 **당초 4단계 | 현재 | 변경 4단계** 매트릭스 동적 삽입
+ → **React**: 세로 삽입 대신 `@common/components` `Tabs`로 시설별 패널 관리 (§아래 Tabs 규칙)
+3. 활성 `formFields`(축소판): 처리구역면적, 하수처리인구, 계획하수량, 시설용량, 분류식 오수/우수, 합류식, 차집, 배수설비, 오수중계/일반/우수 펌프
+4. 시설별 필수첨부 `createFileAttachmentSection`
+5. `fetchBasicPlanData`로 당초(이전) 데이터 바인딩
+
+**API 매핑**
+
+| UI | DTO |
+| -------------------- | ------------------------------------------------------------- |
+| 선택 시설 목록 | `sewerBscPlanTmplntList[].swtrmtFcltCd` (+ `atchFileGroupId`) |
+| 단계별 수치 (변경측) | `sewerBscPlanTmplntDtlList[]` → `SewerBscPlanTmplntDtlItem` |
+| 임시저장 | `tmplntTmprStrgList` (편집 중 임시, 필요 시) |
+
+필드 대응 (변경 단계 기준):
+
+| UI 라벨 | `SewerBscPlanTmplntDtlItem` |
+| -------------- | --------------------------- |
+| 처리구역면적 | `prcsZoneArea` |
+| 하수처리인구 | `swtrmtPpltnCnt` |
+| 계획하수량 | `planSwqty` |
+| 시설용량 | `fcltCpct` |
+| 분류식 오수 | `sprsysDrtwPplLen` |
+| 분류식 우수 | `sprsysExlnPplLen` |
+| 합류식 | `cbsysPplLen` |
+| 차집관로 | `icptgPplLen` |
+| 배수설비 | `drgCnt` |
+| 오수 중계 펌프 | `drtwRlyPumpCnt` |
+| 오수 일반 펌프 | `drtwPumpCnt` |
+| 우수 펌프 | `exlnPumpCnt` |
+
+**구현 구성**
+
+```
+ui/sections/tmplnt-form-section.tsx
+ ├─ TmplntFacilityPicker # BscFctList Checkbox 목록 (시설 마스터 API TBD)
+ └─ Tabs (@common/components Tabs) # 선택 시설별 동적 패널
+ ├─ TabsList / TabsTrigger # value=swtrmtFcltCd, 라벨=시설명
+ └─ TabsContent (시설 1개당)
+ ├─ TmplntMatrixTable # 당초|현재|변경 매트릭스 (useFieldArray)
+ └─ TmplntFacilityAttachments # 시설별 필수첨부 (선택)
+```
+
+**Tabs 관리 규칙** (`packages/common/components/src/ui/tabs`)
+
+- `BscFctList` 체크 **선택** → `useFieldArray` append + 해당 시설 `TabsTrigger`/`TabsContent` 추가 → `Tabs` `value`를 신규 시설로 전환
+- 체크 **해제** → confirm 후 remove + 해당 탭 제거 → 남은 탭 중 하나로 `value` 이동 (없으면 Tabs 영역 비표시)
+- 선택 시설이 0개면 Tabs UI 숨김; 1개 이상이면 Tabs로 시설별 패널 전환
+- 매트릭스·첨부는 각 `TabsContent` 안에만 렌더 (JSP의 세로 동적 삽입을 탭 패널로 대체)
+
+**매트릭스 열**
+
+- **당초 열**: 이전계획 API 조회 결과 읽기 전용 (없으면 빈 칸)
+- **현재 열**: 기준년도 시점 값 (API 제공 시)
+- **변경 열**: 편집 가능 → 저장 payload의 dtl만 전송
+
+시설 마스터 목록 API: 레거시 `BscFctList` 대체 — **TBD** (`swtrmtFclt` 계열).
+
+`bundle-dynamic-imports`: `TmplntFormSection`(또는 TabsContent 내부 매트릭스)을 `next/dynamic` 후보.
+
+---
+
+### 4-I. 체크리스트 + 첨부 (`slide_16`)
+
+#### I-1. 체크리스트
+
+**재사용**: `ChecklistFormSection`
+`docSeComCd="
"`
+`fieldPrefix`: 마스터에서 `taskSeComCd`를 읽는 경로 (예: `sewerBscPlan`)
+
+| JSP | React |
+| ----------------------- | ------------------------------------------- |
+| 업무탭 `ceckEssntlArr` | 코드 `taskSeComCd` 다중 → Tabs |
+| 진행률 / 전체 예·아니오 | 위젯 내장 로직 |
+| `exmntResult` Y/N | `cklstResultList[].selected` (+ `picRvwCn`) |
+
+저장: `cklstResultList: CklstResultItem[]`
+
+#### I-2. 필수 첨부 (탭별)
+
+**재사용**: `AttachmentsFormSection`
+`docSeComCd` 동일, `esntlAtchFileList`
+
+레거시: 체크리스트 탭마다 `essntlList` + `uploadFile` multipart.
+신규: TUS/`attach-service` + `esntlAtchFileId` / `atchFileGroupId` (fnrs와 동일).
+
+#### I-3. 기타 첨부
+
+`AttachmentsFormSection`의 etc 영역 또는 `atchFileGroupId` on `sewerBscPlanDtl`.
+JSP `uploadFileETC` multiple → Zip 안내 문구 유지.
+
+---
+
+### 4-J. 저장 / 제출
+
+**UI 구성**: [`fnrs-form-page.tsx` 279~323](../../src/pages/fnrs-cnsltn/ui/fnrs-form-page.tsx) 과 **동일**.
+마크업·분기(`isSplmntPrgrsStts` / `isEditMode`)·버튼 색상·`disabled={isSubmitting}` 패턴을 그대로 두고, **호출 API·목록 path만 sewer** 로 교체한다.
+
+```tsx
+
+
+ {!isSplmntPrgrsStts ? (
+ <>
+
+ {isEditMode && (
+ <>
+
+
+ >
+ )}
+ >
+ ) : (
+
+ )}
+
+```
+
+| 버튼 | fnrs 대응 | sewer API / 동작 |
+| -------- | ----------------------------- | ------------------------------------------------------------ |
+| 목록 | `router.push("/fnrs-cnsltn")` | `router.push("/sewer-bsc-plan")` |
+| 저장 | `saveApply` + 임시저장 | `useSaveApplySewerBscPlan` + `prgrsSttsComCd`=임시저장(`SV`) |
+| 신청완료 | `handleApplyClick` → 제출 | 체크리스트+필수첨부 검증 → `saveApply` + `LE` (상태코드 TBD) |
+| 삭제 | `handleDelClick` | `useDeleteSewerBscPlan`(또는 동등) → 목록 |
+| 보완완료 | `onSplmntCmptn` | `useSubmitSupplementSewerBscPlan` |
+
+| JSP | 진행값 | URL | React |
+| ---- | ---------- | ------------------------- | ---------------------------------------------------------- |
+| 저장 | `SAVE` | `insertFnrscDmbp.do` | `saveApply` + 임시저장 코드 |
+| 제출 | `PRESENTN` | `insertFnrscDscss.do` (!) | `saveApply` + 제출 상태코드 — **백엔드 상태코드 확인 TBD** |
+
+페이로드 조립 (`buildApplyPayload`):
+
+```ts
+SewerBscPlanApplySaveRequestDto {
+ sewerBscPlan, sewerBscPlanDtl,
+ sewerBscPlanOtlnList, bizCstList, fnrsCstList,
+ sewerBscPlanTmplntList, tmplntTmprStrgList?,
+ cklstResultList, esntlAtchFileList,
+ splmntDmndList?, rvwOpnnList? // 등록 화면에서는 보통 빈 배열
+}
+```
+
+성공 후: `invalidateQueries(sewerBscPlanBaseKey)` → `router.push('/sewer-bsc-plan')` (신청완료는 상세로 보낼 수 있음 — fnrs와 동일 정책).
+
+권한: JSP `AUTH3` → 기존 AS 권한 훅/역할로 버튼 `disabled`/`hidden`.
+
+---
+
+## 5. API / DTO 연결
+
+Base: `/as-service/api/v1/sewer-bsc-plans`
+엔티티: `@/entities/sewer-bsc-plan`
+
+| 화면 동작 | Hook / Service | 비고 |
+| -------------- | ---------------------------------------- | ---------------- |
+| 등록 저장·제출 | `useSaveApplySewerBscPlan` → `saveApply` | 본 문서 핵심 |
+| 수정 로드 | `useSewerBscPlanDetails(docNo, chgCycl)` | 편집 시 |
+| 마스터만 | `useSewerBscPlan` | 목록 행 클릭 등 |
+| 보완 제출 | `useSubmitSupplementSewerBscPlan` | edit + `prgrsSttsComCd=SL` 시 폼 하단 [보완완료] |
+| 기술검토 | `useTechReviewSewerBscPlan` | 등록 JSP 범위 밖 |
+
+등록 JSP에 없는 섹션(보완요구·검토의견)은 **상세/검토 화면**에서 `SplmntDmndFormSection` / 검토의견 섹션으로 후속 구현.
+
+---
+
+## 6. 권장 파일 배치
+
+```
+apps/as/
+├── app/(with-base-layout)/sewer-bsc-plan/
+│ └── regist/page.tsx
+├── src/pages/sewer-bsc-plan/
+│ ├── lib/
+│ │ ├── sewer-bsc-plan-regist-schema.ts # zod
+│ │ └── form-data-mapper.ts # Det ↔ Form, Apply payload
+│ ├── model/
+│ │ └── (필요 시 table columns)
+│ └── ui/
+│ ├── sewer-bsc-plan-regist-page.tsx
+│ ├── sewer-bsc-plan-form-page.tsx
+│ ├── components/
+│ │ └── previous-plan-lookup-dialog.tsx
+│ └── sections/
+│ ├── basic-info-form-section.tsx
+│ ├── outline-form-section.tsx # 사업개요
+│ ├── biz-cst-matrix-form-section.tsx # 총사업비 (전용)
+│ ├── fnrs-cst-form-section.tsx # 재원분담
+│ └── tmplnt-form-section.tsx # 처리시설 (Checkbox + Tabs)
+└── docs/sewer-bsc-plan/
+ └── dmbp-regist-migration.md # 본 문서
+```
+
+공통 재사용 (신규 작성 최소화):
+
+- `ContactInfoFormSection`, `PrivacyConsentSection`
+- `ChecklistFormSection`, `AttachmentsFormSection`
+- `SectionCollapsible`
+- `Tabs` / `TabsList` / `TabsTrigger` / `TabsContent` (`@common/components` — 처리시설 패널)
+
+도메인 전용 (재사용 금지):
+
+- `BizCstMatrixFormSection` — 총사업비 (fnrs `BusinessCostFormSection` 미사용)
+- 폼 하단 액션바 — `fnrs-form-page` 279~323 복제, API만 sewer
+
+---
+
+## 7. 화면 데이터 흐름
+
+```
+BEGIN
+
+ // 최초 진입 (등록)
+ CALL 로그인사용자조회
+ CALL 코드조회(SEWERBSCPLANSE, SEWER, …) // 병렬
+ CALL 체크리스트트리(docSeComCd) // 병렬
+ CALL 필수첨부트리(docSeComCd) // 병렬
+ CALL 처리시설마스터목록 // TBD (BscFctList)
+ DISPLAY 폼 defaultValues
+
+ ON 1단계목표연도 CHANGE DO
+ SET 2·3·4단계 연도 (+5/+10/+15)
+ DISPLAY E/F/G/H 헤더 연도
+ END ON
+
+ ON 처리시설 체크 DO
+ ADD tmplntList 항목
+ ADD TabsTrigger / TabsContent (시설 패널)
+ SET Tabs value = 신규 시설
+ CALL 이전기본계획수치(시설코드) // 당초열, TBD
+ END ON
+
+ ON 처리시설 체크해제 DO
+ CONFIRM 제거
+ REMOVE tmplntList + 해당 Tabs 패널
+ SET Tabs value = 남은 시설 (없으면 Tabs 숨김)
+ END ON
+
+ ON [이전기본계획조회] CLICK DO
+ OPEN PreviousPlanLookupDialog
+ END ON
+
+ ON [저장] CLICK DO
+ VALIDATE zod (동의 제외 가능)
+ CALL saveApply(payload, 임시저장 상태)
+ NAVIGATE 목록
+ END ON
+
+ ON [신청완료] CLICK DO
+ VALIDATE zod + privacyAgree=Y + 체크리스트 완료 + 필수첨부(esntlAtchYn=Y) 완료
+ CONFIRM 신청 확인
+ CALL saveApply(payload, 제출 상태 LE)
+ NAVIGATE 상세
+ END ON
+
+END
+```
+
+---
+
+## 8. JSP ↔ 신규 갭·리스크
+
+| # | 이슈 | 대응 |
+| --- | ---------------------------------------------------- | ------------------------------------------------- |
+| 1 | JSP 제출 URL이 `dscss`로 분기 (오타/구프로세스 가능) | 신규는 `saveApply` 단일 진입으로 통일 |
+| 2 | 총사업비 UI 계층 ≠ `BizCstItem` | `BizCstMatrixFormSection` 전용 + `acntgSeCd` 매핑 테이블 |
+| 3 | 재원분담 ≠ fnrs shar | 전용 `fnrs-cst-form-section` |
+| 4 | 처리시설 매트릭스(당초/현재/변경) | Checkbox 선택 → Tabs 패널; 저장은 변경 dtl 중심; 당초는 조회 전용 |
+| 5 | 시설·이전계획 API | TBD — 미정 시 체크박스 목록 mock(`faker`)만 |
+| 6 | `docSeComCd` 값 | CMS/체크리스트 마스터와 합의 |
+| 7 | 레거시 multipart vs TUS | fnrs 첨부 패턴 준수 |
+| 8 | 주석 처리된 상세 `formFields` (자연발생인구 등) | **축소된 formFields만** 이관; 주석 블록 복원 금지 |
+
+---
+
+## 9. 구현 순서 (권장)
+
+1. 라우트 + `FormPage` 골격 + zod 스키마(마스터·dtl·동의) + 폼 하단 액션바(§4-J)
+2. B·C·D 섹션 (공통 위젯 최대 활용)
+3. E 사업개요 + G 재원분담 (4열 매트릭스 패턴 공유)
+4. F 총사업비 (`BizCstMatrixFormSection` 전용)
+5. I 체크리스트·첨부
+6. H 처리시설 (`BscFctList` Checkbox + `@common/components` Tabs — 가장 복잡)
+7. 저장/신청완료/삭제/보완완료 API 연동 (§4-J)
+8. 이전계획 Dialog (API 확정 후)
+
+---
+
+## 10. 체크리스트 (jsp-migration-as)
+
+- [ ] contents만 이관 (include 제외)
+- [ ] vercel-react-best-practices 적용 (Query 병렬·파생상태·dynamic)
+- [ ] `@common/components` + 필요 시 DataTable
+- [ ] API·모델은 `@/entities/sewer-bsc-plan` 우선
+- [ ] 목데이터는 `@faker-js/faker`만 (시설 마스터 등 TBD 구간)
+- [ ] 로딩·빈·에러 상태
+- [ ] `router.push`로 목록 복귀
+
+---
+
+## 11. 후속 문서
+
+- 목록(`dmbpList.jsp`) / 상세(`dmbpView.jsp`) / 수정(`dmbpModify.jsp`)은 본 문서와 동일 섹션 체계로 별도 확장
+- 화면설계서 SPEC 형식이 필요하면 `docs/sewer-bsc-plan/SPEC.md`로 분리 (영역 A~J 표 + API 표)
diff --git a/apps/as/docs/approval-line-management/SPEC.md b/apps/as/docs/spec/approval-line-management/SPEC.md
similarity index 100%
rename from apps/as/docs/approval-line-management/SPEC.md
rename to apps/as/docs/spec/approval-line-management/SPEC.md
diff --git a/apps/as/docs/checklist-management/SPEC.md b/apps/as/docs/spec/checklist-management/SPEC.md
similarity index 100%
rename from apps/as/docs/checklist-management/SPEC.md
rename to apps/as/docs/spec/checklist-management/SPEC.md
diff --git a/apps/as/docs/code-management/SPEC.md b/apps/as/docs/spec/code-management/SPEC.md
similarity index 100%
rename from apps/as/docs/code-management/SPEC.md
rename to apps/as/docs/spec/code-management/SPEC.md
diff --git a/apps/as/docs/spec/dashboard/SPEC.md b/apps/as/docs/spec/dashboard/SPEC.md
new file mode 100644
index 000000000..3ce6f9800
--- /dev/null
+++ b/apps/as/docs/spec/dashboard/SPEC.md
@@ -0,0 +1,498 @@
+# 화면설계서: 전국 행정지원 지도 (대시보드 메인)
+
+## 1. 기본 정보
+
+| 항목 | 내용 |
+| ---------- | -------------------------------------------- |
+| 버전 | 1.2 |
+| 프로세스명 | UI설계 |
+| 화면명 | 전국 행정지원 지도 (대시보드 메인) |
+| 화면 ID | TBD |
+| 화면 유형 | [v] 조회 [ ] 입력 [ ] 수정 [ ] 삭제 [ ] 출력 |
+| 메뉴 경로 | 메인 |
+
+---
+
+## 2. 화면 목적
+
+로그인 사용자의 **기관 권한·당해 연도** 기준으로 문서별 진행상태 통계와 행정지원 알림을 한 화면에서 조회한다. 좌측 지도에서 전국 행정지원 현황을 보고, 우측에서 진행상황 카드·알림이력·공지사항을 확인한 뒤 알림 [처리하기]로 연계 업무 화면으로 이동한다.
+
+---
+
+## 3. 화면 구성
+
+### 3-1. 전체 레이아웃
+
+브레드크럼·페이지 타이틀은 스크린샷에 없음. 홈(`/`) 메인 콘텐츠로 구성한다.
+
+```
+┌────────────────────────────────┬─────────────────────────────────────────────────┐
+│ [영역 A] 전국 행정지원 지도 │ [영역 B] 문서별 진행상황 카드 (2×2) │
+│ 타이틀: 전국 행정지원 지도 │ ┌────────────────┐ ┌────────────────┐ │
+│ │ │ 재원협의 │ │ 설치인가 │ │
+│ (대한민국 시도·시군구 경계) │ │ 신청/진행/완료 │ │ 신청/진행/완료 │ │
+│ 마커: 서울 강남구, │ ├────────────────┤ ├────────────────┤ │
+│ 대전 유성구, │ │ 설계자문 │ │ 기본계획 │ │
+│ 부산 해운대구 │ │ 신청/진행/완료 │ │ 신청/진행/완료 │ │
+│ │ └────────────────┘ └────────────────┘ │
+│ ├─────────────────────────────────────────────────┤
+│ │ [영역 C] 알림이력 │
+│ │ 알림이력 업데이트 HH:mm [전체보기] │
+│ │ [오늘 N건] [지연 N건] [완료대기 N건] │
+│ │ [뱃지] [일자] 제목 [처리하기] │
+│ │ … (최대 5건) │
+│ ├─────────────────────────────────────────────────┤
+│ │ [영역 D] 공지사항 [더보기] │
+│ │ ┌────┬────────┬────────┬────────┬──────┐ │
+│ │ │번호│ 제목 │ 작성자 │ 작성일 │조회수│ │
+│ │ └────┴────────┴────────┴────────┴──────┘ │
+└────────────────────────────────┴─────────────────────────────────────────────────┘
+```
+
+### 3-2. 화면 데이터 흐름
+
+```
+BEGIN
+
+ // 최초 진입
+ // searchYear 미전달 → 서버가 당해 연도로 고정. 알림 목록은 최신 5건
+ CALL getMainDashboard(DashboardMainSearchDto) // Authorization은 client 인터셉터
+ DISPLAY 진행상황 카드 (영역 B) // docPrgrsSttsList, TOTAL 행 제외
+ DISPLAY 알림 통계 칩 · 알림 목록 (영역 C) // notiStats, notiList
+
+ CALL getUserPostList(UserPostSearchParams) // board-service, AS_DASHBOARD_BBS_ID
+ DISPLAY 공지사항 그리드 (영역 D)
+
+ CALL 지도 마커/음영 조회(…) // TBD — 대시보드 API에 없음
+ DISPLAY 전국 지도 (영역 A)
+
+ ON 영역 C [전체보기] 버튼 CLICK DO
+ NAVIGATE /nti-hstry
+ END ON
+
+ ON 영역 C [처리하기] 버튼 CLICK DO
+ CALL readNoti(notiId) // entities/noti
+ NAVIGATE notiUrl // 비어 있으면 이동하지 않음
+ CALL getMainDashboard(…) // → 영역 C 갱신 (invalidate)
+ END ON
+
+ ON 영역 D [더보기] 버튼 CLICK DO
+ NAVIGATE /cms/bbsnotice/{AS_DASHBOARD_BBS_ID} // as 앱 공지 목록 라우트 미구현. st/cm 관례
+ END ON
+
+ ON 영역 D 행 CLICK DO
+ NAVIGATE /cms/bbsnotice/{AS_DASHBOARD_BBS_ID}/{pstNo}
+ END ON
+
+ ON 영역 B 카드 CLICK DO
+ NAVIGATE 해당 문서 목록 // §5-3
+ END ON
+
+ ON 영역 A 마커 CLICK DO
+ // TBD — 팝업/필터/이동 여부 미확정
+ END ON
+
+END
+```
+
+---
+
+## 4. 영역 A — 전국 행정지원 지도
+
+스크린샷 타이틀: **전국 행정지원 지도**.
+
+`getMainDashboard` 응답에 지도·좌표·행정구역 음영 데이터는 **없다**. 마커·음영 소스 API는 **TBD**.
+
+### 4-1. 표시 요소
+
+| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
+| ----------- | --------- | ---- | ------ | ----------------- | ----------------------------------------------------------------- |
+| 섹션 타이틀 | Text | — | — | — | 고정 문구 `전국 행정지원 지도` |
+| 전국 지도 | Map | — | — | TBD | 시도·시군구 경계. 북·동부 음영(청) / 남서부 비음영(백) — 의미 TBD |
+| 마커 라벨 | Marker | — | — | TBD | 스크린샷 샘플: `서울 강남구`, `대전 유성구`, `부산 해운대구` |
+
+### 4-2. 상호작용
+
+| 동작 | 설명 |
+| ---------- | ----------------------------------------- |
+| 마커 클릭 | TBD (상세 팝업 / 해당 지자체 필터 / 이동) |
+| 지도 줌·팬 | TBD (스크린샷에 컨트롤 없음) |
+
+`entities/adstdstr`는 시도·구군 **콤보**만 제공하며 좌표를 주지 않는다. 지도 라이브러리도 as 앱에 없음 — 선정 TBD.
+
+---
+
+## 5. 영역 B — 문서별 진행상황 카드
+
+`DashboardMainResponseDto.docPrgrsSttsList`를 **`docSeComCd`로 매핑**해 4장 카드를 그린다. 배열 순서에 의존하지 않는다.
+
+스크린샷 배치(좌→우, 상→하):
+
+| 화면 카드 타이틀 | `docSeComCd` | API `docSeNm` |
+| ----------------- | ------------ | -------------- |
+| 재원협의 진행상황 | `FNRS` | 재원협의 |
+| 설치인가 진행상황 | `INSTL` | 설치인가 |
+| 설계자문 진행상황 | `CNFM` | 설계자문 |
+| 기본계획 진행상황 | `SEWER` | 하수도기본계획 |
+
+서버 SQL `ORDER BY`는 `INSTL → FNRS → SEWER → CNFM` 이며, `GROUP BY ROLLUP`으로 **`docSeComCd = TOTAL`** 합계 행이 추가된다. 카드에는 TOTAL 행을 **표시하지 않는다**.
+
+카드 타이틀은 스크린샷 문구(`… 진행상황`)를 쓴다. `SEWER`의 API명 `하수도기본계획`은 화면에서 `기본계획`으로 축약한다.
+
+### 5-1. 카드 내부 필드
+
+각 카드 공통 구조. `totCnt`는 화면에 없음.
+
+| 표시명 | Field | 비고 |
+| ------ | ---------- | ------------------ |
+| 신청 | `reqCnt` | 파란 아이콘 + 건수 |
+| 진행 | `prgrsCnt` | 초록 아이콘 + 건수 |
+| 완료 | `cmptnCnt` | 주황 아이콘 + 건수 |
+
+상태 그룹(서버): 임시저장(`SV`/`CV`) 제외. 문서별 `prgrs_stts_com_cd` → `REQ`/`PRG`/`CMP`.
+
+### 5-2. 검색 파라미터 (화면 미표시)
+
+영역 B에 연도·필터 UI는 없다. 서버 기본값을 따른다.
+
+| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
+| ------ | --------- | ---- | --------- | ----------------- | --------------------------------------------------------------------- |
+| (없음) | — | — | 당해 연도 | `searchYear` | 미전달 시 서버 `LocalDate.now().getYear()`. 연도 셀렉터 추가 여부 TBD |
+
+`docSeComCd` 쿼리는 **알림 목록 필터**에만 쓰이며, 진행상황 SQL에는 전달되지 않는다. 카드 4종은 항상 전원 집계한다.
+
+### 5-3. 상호작용
+
+카드 클릭 시 **해당 문서 목록 페이지**로 이동한다. `docSeComCd` → 라우트 고정 매핑.
+
+| 동작 | `docSeComCd` | 이동 경로 |
+| --------- | ------------ | ----------------- |
+| 카드 클릭 | `FNRS` | `/fnrs-cnsltn` |
+| 카드 클릭 | `INSTL` | `/instl-sncn` |
+| 카드 클릭 | `CNFM` | `/design-cnstn` |
+| 카드 클릭 | `SEWER` | `/sewer-bsc-plan` |
+
+---
+
+## 6. 영역 C — 알림이력
+
+서버가 cms-service 알림을 대시보드 응답에 합친다. 목록은 **page=0, size=5** (최신 5건). 페이지네이션 없음.
+
+### 6-1. 헤더
+
+| 표시명 | Field / 소스 | 비고 |
+| ------------- | ------------ | --------------------------------------------------------------------------------------- |
+| 섹션 타이틀 | — | 고정 문구 `알림이력` |
+| 업데이트 시각 | (client) | 스크린샷 `업데이트 10:32`. API 필드 없음. `getMainDashboard` 마지막 fetch 시각(`HH:mm`) |
+| 전체보기 | Button | `/nti-hstry`로 이동 |
+
+### 6-2. 통계 칩
+
+`NotiAsStatResponseDto`.
+
+| 표시명 | Field | 비고 |
+| ------------ | -------------- | -------------------------------------- |
+| 오늘 N건 | `todayCount` | 당일 00:00 이후 수신 |
+| 지연 N건 | `delayCount` | `notReadYn=N` 이고 수신 후 3일 이상 |
+| 완료대기 N건 | `pendingCount` | `notReadYn=Y` 이고 `notiPrcsCmptnYn=N` |
+
+### 6-3. 목록 행
+
+| 표시명 | Field | 비고 |
+| ------ | --------- | --------------------------------------------- |
+| 일자 | `regDt` | 표시 `YYYY-MM-DD`. 응답 `yyyy-MM-dd HH:mm:ss` |
+| 제목 | `notiTtl` | 알림 제목 |
+| 처리 | (action) | 스크린샷 라벨 `처리하기`. 클릭 §6-4 |
+
+응답에 있으나 이 목록에 **표시하지 않음**:
+
+| Field | 비고 |
+| ----------------- | ----------------------------------------- |
+| `notiId` | 행 키 · `readNoti` 파라미터 |
+| `recvUnqId` | 수신 회원 UUID (서버 로그인 사용자 고정) |
+| `senderUnqId` | 발신 회원 UUID |
+| `notiType` | `ADSS` |
+| `notiCn` | 본문 |
+| `notiUrl` | 처리 후 이동 URL. 컬럼 미표시 |
+| `notiLnkgUnqNo` | 알림연결고유번호 |
+| `docSeComCd` | 문서구분 코드 |
+| `docSeComNm` | 문서구분명 (알림이력 화면에서는 연계업무) |
+| `notReadYn` | 뱃지 판정 TBD에 사용 가능 |
+| `notiIdntyYn` | 확인 여부 |
+| `notiIdntyDt` | 확인 일시 |
+| `notiPrcsCmptnYn` | 처리 완료 여부 |
+| `notiPrcsCmptnDt` | 처리 완료 일시 |
+
+### 6-4. 행 상호작용
+
+| 동작 | 설명 |
+| --------------- | ----------------------------------------------------------------------------------------------------------------- |
+| [전체보기] 클릭 | `/nti-hstry` (알림이력 목록)로 이동 |
+| [처리하기] 클릭 | `readNoti(notiId)` 후 `notiUrl`로 이동. URL이 비면 이동하지 않음(읽음 처리는 수행). `dashboardBaseKey` invalidate |
+
+`DashboardController`에 읽음 API는 없다. **`entities/noti`의 `readNoti`** 를 쓴다. 알림이력 화면과 동일 (`PUT /cms-service/api/v1/noti/{notiId}/read`).
+
+### 6-5. 검색 파라미터 (화면 미표시)
+
+서버 `toNotiSearchDto()` 기본: `notiType = ADSS`. 화면 필터 없음.
+
+| DashboardMainSearchDto | 화면 | 비고 |
+| ---------------------- | ---- | --------------------------------- |
+| `notiType` | 없음 | 서버 기본 `ADSS` |
+| `notReadYn` | 없음 | 통계 칩 클릭 필터 시 후보 (TBD) |
+| `startDt` / `endDt` | 없음 | 대시보드 기본 미전달 |
+| `docSeComCd` | 없음 | 알림만 필터. 진행상황 카드와 무관 |
+
+---
+
+## 7. 영역 D — 공지사항
+
+`getMainDashboard`에 공지 필드는 없다. **board-service** `getUserPostList`로 별도 조회한다.
+
+- Base: `GET /board-service/api/v1/posts/user/{AS_DASHBOARD_BBS_ID}`
+- `AS_DASHBOARD_BBS_ID` = `NEXT_PUBLIC_AS_DASHBOARD_BBS_ID` (로컬 예: `7`)
+- 클라이언트: `@/entities/board` — `postApi.getUserPostList` / `useUserPostList`
+
+페이지네이션 컨트롤은 없다. 요약 목록만 표시한다 (`page=0`, `size=3`).
+
+### 7-1. 헤더·버튼
+
+| 버튼명 | 동작 |
+| ------ | ------------------------------------------------------------------------------------------- |
+| 더보기 | `/cms/bbsnotice/{AS_DASHBOARD_BBS_ID}` 로 이동. as 앱에 공지 목록 라우트는 없음(st/cm 관례) |
+
+### 7-2. 그리드 컬럼
+
+스크린샷 헤더 순서. `UserPost`.
+
+| 표시명 | Field | 비고 |
+| ------ | --------- | ---------------------------------------------------- |
+| 번호 | `rowNo` | `upndFixYn` = `Y` 이면 `[공지]` Badge, 그 외 `rowNo` |
+| 제목 | `pstTtl` | truncate |
+| 작성자 | `wrtrNm` | |
+| 작성일 | `regDt` | 표시 `YYYY-MM-DD` (응답 datetime) |
+| 조회수 | `inqNocs` | 숫자 |
+
+조회 파라미터 (`UserPostSearchParams`):
+
+| 필드 | 값 | 비고 |
+| ------------- | ----------- | ------------------------------------------ |
+| `page` | `0` | Spring 0-based |
+| `size` | `3` | 대시보드 요약. 스크린샷 3행 |
+| `keywordType` | `bbsTypeCd` | st 대시보드 공지 패널과 동일 |
+| `keyword` | `BBSNOTICE` | 게시판 유형. path의 bbsId와 함께 공지 한정 |
+
+응답에 있으나 그리드에 **표시하지 않음**: `bbsId`, `pstNo`(행 키·상세 이동), `pstCn`, `upndFixYn`(번호 열 판정만), `rgtrNm`, `atflCd` 등.
+
+### 7-3. 행 상호작용
+
+| 동작 | 설명 |
+| ------- | ------------------------------------------------------------------------------ |
+| 행 클릭 | `/cms/bbsnotice/{AS_DASHBOARD_BBS_ID}/{pstNo}` (as 앱 공지 상세 라우트 미구현) |
+
+---
+
+## 8. API/DTO 명세
+
+### 8-1. 공통
+
+| 항목 | 값 |
+| --------------- | ---------------------------------------------------------------------------------------------- |
+| Base prefix | `/as-service/api/v1/dashboard` (`ENDPOINT.AS_SERVICE.DASHBOARD`) |
+| 인증 | `Authorization` 헤더. 프론트는 `client` 인터셉터. 로그인 기관 권한은 서버가 사용자 캐시로 적용 |
+| 클라이언트 모듈 | `@/entities/dashboard` — `dashboardService` / `useGetMainDashboard` |
+| 알림 읽음 | `@/entities/noti` — `notiService.readNoti` / `useReadNoti` |
+| 공지사항 | `@/entities/board` — `postApi.getUserPostList` / `useUserPostList` |
+
+영역 A(지도)는 본 컨트롤러·board-service 범위 밖 → **TBD**.
+
+### 8-2. 대시보드 메인 조회 — `getMainDashboard`
+
+| 기능 | Method | URL | Request DTO | Response DTO | API |
+| -------------------------------------------- | ------ | ------------- | ------------------------------- | -------------------------- | ----------------------------------------------------------- |
+| 대시보드 메인 통합진행상태 및 알림 정보 조회 | GET | `{BASE}/main` | Query: `DashboardMainSearchDto` | `DashboardMainResponseDto` | `dashboardService.getMainDashboard` / `useGetMainDashboard` |
+
+**Request (`DashboardMainSearchDto`)**
+
+| 필드 | 필수 | 설명 |
+| ------------ | ---- | ---------------------------------------------------------------------- |
+| `searchYear` | N | 검색 연도. 미전달 시 서버 당해 연도. 영역 B 집계 기간 |
+| `notiType` | N | 알림 종류. 미전달 시 서버 `ADSS` |
+| `notReadYn` | N | 알림 확인 여부 `Y`/`N`. 본 화면 미사용 |
+| `startDt` | N | 알림 시작일 `YYYY-MM-DD`. 본 화면 미사용 |
+| `endDt` | N | 알림 종료일 `YYYY-MM-DD`. 본 화면 미사용 |
+| `docSeComCd` | N | 알림 문서구분 (`INSTL`/`FNRS`/`SEWER`/`CNFM` 등). 진행상황 집계와 무관 |
+
+`Authorization`은 service 시그니처에 넣지 않는다.
+
+**Response (`DashboardMainResponseDto`)**
+
+| 필드 | 타입 | 설명 |
+| ------------------ | --------------------------- | --------------------------- |
+| `docPrgrsSttsList` | `DocPrgrsSttsResponseDto[]` | 문서별 진행상태 + TOTAL 행 |
+| `notiList` | `NotiAsUserResponseDto[]` | 최신 최대 5건 |
+| `notiStats` | `NotiAsStatResponseDto` | 오늘 / 지연 / 완료대기 건수 |
+
+**`DocPrgrsSttsResponseDto`**
+
+| 필드 | 타입 | 설명 |
+| ------------ | ------ | --------------------------------------------- |
+| `docSeNm` | string | 문서 구분 명칭 |
+| `docSeComCd` | string | `INSTL` / `FNRS` / `SEWER` / `CNFM` / `TOTAL` |
+| `reqCnt` | number | 신청 건수 |
+| `prgrsCnt` | number | 진행 건수 |
+| `cmptnCnt` | number | 완료 건수 |
+| `totCnt` | number | 총 건수 (화면 미표시) |
+
+**`NotiAsStatResponseDto`**
+
+| 필드 | 타입 | 설명 |
+| -------------- | ------ | --------- |
+| `todayCount` | number | 오늘 알림 |
+| `delayCount` | number | 지연 알림 |
+| `pendingCount` | number | 완료대기 |
+
+**`NotiAsUserResponseDto`** — 필드 상세는 `docs/spec/nti-hstry` §6-2와 동일 (`notiId`, `notiTtl`, `regDt`, `notiUrl`, `notReadYn`, `notiPrcsCmptnYn` 등).
+
+### 8-3. 알림 읽음 처리 — `readNoti`
+
+대시보드 컨트롤러가 아니라 cms-service. 영역 C [처리하기]에서 호출.
+
+| 기능 | Method | URL | Request DTO | Response DTO | API |
+| -------------- | ------ | ---------------------------------------- | ----------------------- | ---------------- | -------------------------------------- |
+| 알림 읽음 처리 | PUT | `/cms-service/api/v1/noti/{notiId}/read` | Path: `notiId` (`Long`) | `Long` (알림 ID) | `notiService.readNoti` / `useReadNoti` |
+
+Body 없음. 성공 시 `dashboardBaseKey`(및 필요 시 `notiBaseKey`) invalidate.
+
+### 8-4. 전국 행정지원 지도 — TBD
+
+| 기능 | Method | URL | Request DTO | Response DTO | API |
+| ------------------- | ------ | --- | ----------- | ------------ | --- |
+| 지도 마커·음영 조회 | TBD | TBD | TBD | TBD | TBD |
+
+### 8-5. 공지사항 — `getUserPostList`
+
+board-service. bbsId는 환경변수 `AS_DASHBOARD_BBS_ID`로 path에 고정한다.
+
+| 기능 | Method | URL | Request DTO | Response DTO | API |
+| ----------------------- | ------ | ------------------------------------------ | ----------------------------- | ------------------------ | --------------------------------------------- |
+| 대시보드 공지 요약 목록 | GET | `/board-service/api/v1/posts/user/{bbsId}` | Query: `UserPostSearchParams` | `PageResponse` | `postApi.getUserPostList` / `useUserPostList` |
+
+**Request (`UserPostSearchParams`)** — 본 화면 고정값
+
+| 필드 | 필수 | 설명 |
+| ------------- | ---- | -------------- |
+| `page` | N | `0` |
+| `size` | N | `3` |
+| `keywordType` | N | `bbsTypeCd` |
+| `keyword` | N | `BBSNOTICE` |
+| `popupYn` 등 | N | 본 화면 미사용 |
+
+**Response (`UserPost`) — 화면 매핑 필드**
+
+| 필드 | 타입 | 설명 |
+| ----------- | ------ | --------------------------- |
+| `rowNo` | number | 번호. 고정글이 아니면 표시 |
+| `pstNo` | number | 게시 번호. 상세 이동 키 |
+| `bbsId` | number | 게시판 ID |
+| `pstTtl` | string | 제목 |
+| `wrtrNm` | string | 작성자 |
+| `regDt` | string | 작성일 |
+| `inqNocs` | number | 조회수 |
+| `upndFixYn` | string | 상단고정 `Y`/`N` → `[공지]` |
+
+---
+
+## 9. UI 구현 가이드
+
+### 9-1. 기술 스택·원칙
+
+- 라우트: 기존 홈 `app/(main)/(with-base-layout)/page.tsx` → `pages/home` (현재 `MainPage` 플레이스홀더)
+- UI: `@common/components` — Card, Badge, Button, Table(공지). 지도 라이브러리 TBD
+- 데이터: React Query `useGetMainDashboard()`. 처리하기는 `useReadNoti()`. 공지는 `useUserPostList()`
+- 진행상황 카드: `docSeComCd`로 4슬롯 매핑. `TOTAL` 제외
+- 알림 목록: 테이블이 아닌 행 리스트(뱃지 + 일자 + 제목 + 버튼)
+- 공지: `@tanstack/react-table` + `useDataTable`, 페이지네이션 없음
+- 검색 조건 URL 동기화: 본 화면 필터 없음. `searchYear` UI가 생기면 그때 검토
+
+### 9-2. 모듈·파일 배치
+
+| 구분 | 경로 |
+| ------------- | ------------------------------------------------------------ |
+| 라우트 | `app/(main)/(with-base-layout)/page.tsx` |
+| 페이지 | `pages/dashboard/ui/dashboard-page.tsx` |
+| 지도 | `pages/dashboard/ui/national-as-map.tsx` (API TBD, Skeleton) |
+| 진행상황 카드 | `pages/dashboard/ui/doc-prgrs-stts-cards.tsx` |
+| 알림이력 | `pages/dashboard/ui/dashboard-noti-list.tsx` |
+| 공지사항 | `pages/dashboard/ui/dashboard-notice-table.tsx` |
+| 공지 컬럼 | `pages/dashboard/model/dashboard-notice-table-columns.tsx` |
+| Entity API | `entities/dashboard/api/dashboard-service.ts` |
+| Entity Hook | `entities/dashboard/api/queries.ts` → `useGetMainDashboard` |
+| Entity Model | `entities/dashboard/model/dashboard-interface.ts` |
+| 알림 읽음 | `entities/noti` → `useReadNoti` |
+| 공지 Entity | `entities/board` → `useUserPostList` |
+
+Entity(`getMainDashboard`, `getUserPostList`) 구현됨. 영역 B·C·D 화면 구현. 영역 A는 Skeleton.
+
+### 9-3. 영역별 UI 매핑
+
+| 영역 | 컴포넌트 | 데이터 |
+| ---- | ------------------------ | ---------------------------------------------------------------------------- |
+| A | `national-as-map` | Skeleton (지도 API TBD) |
+| B | `doc-prgrs-stts-cards` | `useGetMainDashboard` → `docPrgrsSttsList` (`TOTAL` 제외, 코드 매핑) |
+| C | `dashboard-noti-list` | 동 훅 → `notiStats`, `notiList`. 처리 → `useReadNoti`. 전체보기 `/nti-hstry` |
+| D | `dashboard-notice-table` | `useUserPostList` → `content[]`. [더보기]·행 클릭 → `/cms/bbsnotice/...` |
+
+### 9-4. 다이얼로그·라우팅
+
+| 항목 | 내용 |
+| -------------- | -------------------------------------------------------------------------------------- |
+| 다이얼로그 | 없음 |
+| 본 화면 라우트 | `/` (홈) |
+| 알림 처리 이동 | [처리하기] → `readNoti` 후 `notiUrl` (알림이력과 동일, SEWER URL은 상세 세그먼트 제거) |
+| 알림 전체보기 | `/nti-hstry` |
+| 공지 더보기 | `/cms/bbsnotice/{AS_DASHBOARD_BBS_ID}` (as 앱 공지 라우트 미구현, st/cm 관례) |
+| 공지 행 클릭 | `/cms/bbsnotice/{AS_DASHBOARD_BBS_ID}/{pstNo}` |
+| 진행상황 카드 | §5-3 목록 라우트 (`/fnrs-cnsltn`, `/instl-sncn`, `/design-cnstn`, `/sewer-bsc-plan`) |
+
+### 9-5. 데이터 흐름
+
+```
+BEGIN
+
+ // 최초 진입 (홈)
+ CALL getMainDashboard({})
+ DISPLAY 영역 B // docPrgrsSttsList — FNRS/INSTL/CNFM/SEWER 슬롯
+ DISPLAY 영역 C // notiStats + notiList(≤5)
+
+ CALL getUserPostList({ page: 0, size: 3, keywordType: "bbsTypeCd", keyword: "BBSNOTICE" })
+ DISPLAY 영역 D
+
+ CALL 지도 데이터 // TBD → 영역 A
+
+ ON 영역 C [전체보기] CLICK DO
+ NAVIGATE /nti-hstry
+ END ON
+
+ ON 영역 C [처리하기] CLICK DO
+ CALL readNoti(notiId)
+ IF notiUrl THEN
+ NAVIGATE notiUrl
+ END IF
+ CALL getMainDashboard // invalidate dashboardBaseKey
+ END ON
+
+ ON 영역 B 카드 CLICK DO
+ NAVIGATE 문서 목록 // §5-3
+ END ON
+
+ ON 영역 D [더보기] CLICK DO
+ NAVIGATE /cms/bbsnotice/{AS_DASHBOARD_BBS_ID}
+ END ON
+
+ ON 영역 D 행 CLICK DO
+ NAVIGATE /cms/bbsnotice/{AS_DASHBOARD_BBS_ID}/{pstNo}
+ END ON
+
+END
+```
diff --git a/apps/as/docs/essential-attachment-file-management/SPEC.md b/apps/as/docs/spec/essential-attachment-file-management/SPEC.md
similarity index 100%
rename from apps/as/docs/essential-attachment-file-management/SPEC.md
rename to apps/as/docs/spec/essential-attachment-file-management/SPEC.md
diff --git a/apps/as/docs/spec/my-adss-doc/SPEC.md b/apps/as/docs/spec/my-adss-doc/SPEC.md
new file mode 100644
index 000000000..37ae2937d
--- /dev/null
+++ b/apps/as/docs/spec/my-adss-doc/SPEC.md
@@ -0,0 +1,285 @@
+# 화면설계서: 내 행정지원
+
+## 1. 기본 정보
+
+| 항목 | 내용 |
+| ---------- | -------------------------------------------- |
+| 버전 | 1.0 |
+| 프로세스명 | UI설계 |
+| 화면명 | 내 행정지원 |
+| 화면 ID | AS_SC0204011 |
+| 화면 유형 | [v] 조회 [ ] 입력 [ ] 수정 [ ] 삭제 [ ] 출력 |
+| 메뉴 경로 | 메인 > 행정지원서비스 > 내 행정지원 |
+
+---
+
+## 2. 화면 목적
+
+로그인한 사용자가 **본인이 신청한 행정지원 문서**를 기간·구분·진행상태로 조회한다. 목록에서 행을 선택하면 해당 문서 상세 화면으로 이동한다.
+
+---
+
+## 3. 화면 구성
+
+### 3-1. 전체 레이아웃
+
+```
+┌─────────────────────────────────────────────────────────────────────────────────┐
+│ 브레드크럼: 메인 > 행정지원서비스 > 내 행정지원 │
+│ 페이지 타이틀: 내 행정지원 │
+├─────────────────────────────────────────────────────────────────────────────────┤
+│ [영역 A] 검색 필터 │
+│ 기간 [시작일] ~ [종료일] 구분 ▼ 진행상태 ▼ [검색] │
+├─────────────────────────────────────────────────────────────────────────────────┤
+│ [영역 B] 내 신청 목록 │
+│ 타이틀: 내 신청 목록 N개 │
+│ ┌────┬──────┬────────┬──────────────┬────────────┬────────┬──────────────┐ │
+│ │ No │ 구분 │ 신청일 │ 진행상태 │ 완료(예정)일│ 담당자 │ 담당자연락처 │ │
+│ ├────┼──────┼────────┼──────────────┼────────────┼────────┼──────────────┤ │
+│ │ … │ … │ … │ prgrsSttsComNm│ … │ … │ … │ │
+│ └────┴──────┴────────┴──────────────┴────────────┴────────┴──────────────┘ │
+│ [페이지네이션] │
+└─────────────────────────────────────────────────────────────────────────────────┘
+```
+
+### 3-2. 화면 데이터 흐름
+
+```
+BEGIN
+
+ // 최초 진입
+ // 기간 기본값: 오늘 기준 최근 1개월 (startDate = today-1M, endDate = today)
+ CALL findMyElaperDocList(ElaprMyDocSearchDto + pageable)
+ DISPLAY 내 신청 목록 그리드 (영역 B)
+
+ ON [검색] 버튼 CLICK DO
+ CALL findMyElaperDocList(영역 A 조건 + page=0)
+ DISPLAY 내 신청 목록 그리드 (영역 B)
+ END ON
+
+ ON 페이지네이션 CHANGE DO
+ CALL findMyElaperDocList(영역 A 조건 + page/size)
+ DISPLAY 내 신청 목록 그리드 (영역 B)
+ END ON
+
+ ON 영역 B 행 CLICK DO
+ // docSeComCd·prgrsSttsComCd·rgtr → view/edit
+ NAVIGATE 문서 상세 또는 수정 화면
+ END ON
+
+END
+```
+
+---
+
+## 4. 영역 A — 검색 필터
+
+구성: **기간 · 구분 · 진행상태** (+ 검색).
+
+> UI설계 상세설명의 `docSecs` / `docStatusCd`는 레거시·설계 문서 명칭이다.
+> **API SoT**는 `ElaprMyDocSearchDto`의 `docSeComCd` / `prgrsSttsComCd`를 사용한다.
+
+### 4-1. 필드 정의
+
+| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
+| ---------- | ---------------- | ---- | --------------------------- | ----------------- | --------------------------------------------------------------------- |
+| 기간(시작) | DatePicker | N | 오늘 − 1개월 (`YYYY-MM-DD`) | `startDate` | 신청일(`reqDt`) 기준 검색으로 가정 |
+| 기간(종료) | DatePicker | N | 오늘 (`YYYY-MM-DD`) | `endDate` | |
+| 구분 | Select(공통코드) | N | 전체(미선택) | `docSeComCd` | 상위코드 `ASDOCSE`. UI설계 `docSecs` → API `docSeComCd` |
+| 진행상태 | Select(공통코드) | N | 전체(미선택) | `prgrsSttsComCd` | 상위코드 = `{docSeComCd}STTS` (예: `INSTL` → `INSTLSTTS`). UI설계 `docStatusCd` → API 필드명 |
+
+### 4-2. 버튼
+
+| 버튼명 | 동작 |
+| ------ | ----------------------------------------------------------------- |
+| 검색 | 영역 A 조건으로 `findMyElaperDocList` 호출 → 영역 B 갱신 (page=0) |
+
+### 4-3. 콤보 의존
+
+| 트리거 | 동작 |
+| ----------- | ------------------------------------------------------------------- |
+| 구분 변경 | 진행상태 옵션을 `upCd = docSeComCd + "STTS"`로 재조회. 진행상태 선택값 RESET |
+| 구분 미선택 | 진행상태 비활성(옵션 비움). `upCd` 조합 불가이므로 조회하지 않음 |
+
+---
+
+## 5. 영역 B — 내 신청 목록
+
+### 5-1. 그리드 컬럼
+
+| 표시명 | Field | 비고 |
+| ------------ | ---------------- | ------------------------------------------------- |
+| No | (row index) | 페이지 오프셋 반영 |
+| 구분 | `docSeComNm` | 코드 `docSeComCd` (`ASDOCSE`) |
+| 신청일 | `reqDt` | 표시 `YYYY-MM-DD` (응답은 `yyyy-MM-dd HH:mm:ss`) |
+| 진행상태 | `prgrsSttsComNm` | 텍스트 렌더링 (단계형 프로그레스 미사용) |
+| 완료(예정)일 | `cmptnExpctDt` | 표시 `YYYY-MM-DD` |
+| 담당자 | `picNm` | |
+| 담당자연락처 | `picTelno` | |
+
+응답에 있으나 그리드에 **표시하지 않음**:
+
+| Field | 비고 |
+| ---------------- | ----------------------------------------------------------------- |
+| `docNo` | 문서번호 — 행 클릭 라우팅 키 |
+| `docTtl` | 문서명/사업명 — 미표시 |
+| `chgCycl` | 변경차수 — 그리드 미표시. SEWER 이동 시 query 파라미터로 사용 |
+| `prgrsSttsComCd` | 진행상태 코드 — 컬럼 표시는 `prgrsSttsComNm` |
+| `rgtr` | 등록자 UUID — edit 판정용 (`rgtr === userUnqId`) |
+
+### 5-2. 목록 메타·페이지네이션
+
+| 항목 | 매핑 |
+| ------------- | ------------------------------------------- |
+| 건수 `N개` | `PageResponse.totalElements` |
+| page / size | `ElaprMyDocSearchDto`의 `PageableParams` |
+| 페이지 컨트롤 | Spring `Page` 표준 (`number`, `totalPages`) |
+
+### 5-3. 행 상호작용
+
+| 동작 | 설명 |
+| ------- | -------------------------------------------------------------------------------------------- |
+| 행 클릭 | `docSeComCd`·`prgrsSttsComCd`·`rgtr`로 view/edit 분기 후 이동 (`resolveMyPbadmsSprtDetailPath`) |
+
+**edit 조건** (기존 도메인 목록과 동일):
+
+| `docSeComCd` | edit 조건 |
+| ------------------------ | ---------------------------------------------------------------------- |
+| `INSTL` / `FNRS` / `SEWER` | `(prgrsSttsComCd === "SV" \|\| "SL") && rgtr === loggedInUserUnqId` |
+| `CNFM` | `prgrsSttsComCd === "CV" && rgtr === loggedInUserUnqId` |
+
+| `docSeComCd` | edit 경로 | view 경로 |
+| ------------ | -------------------------------------------- | --------------------------------------- |
+| `FNRS` | `/fnrs-cnsltn/{docNo}/edit` | `/fnrs-cnsltn/{docNo}` |
+| `INSTL` | `/instl-sncn/{docNo}/edit` | `/instl-sncn/{docNo}` |
+| `CNFM` | `/design-cnstn/{docNo}/edit` | `/design-cnstn/{docNo}` |
+| `SEWER` | `/sewer-bsc-plan/{docNo}/edit?chgCycl={n}` | `/sewer-bsc-plan/{docNo}?chgCycl={n}` |
+| 그 외 | 이동하지 않음 | |
+
+- SEWER `chgCycl`: `row.chgCycl ?? 1`
+- 조건 불충족 시 항상 view
+
+---
+
+## 6. API/DTO 명세
+
+### 6-1. 공통
+
+| 항목 | 값 |
+| --------------- | ------------------------------------------------------------------------------------------ |
+| Base prefix | `/as-elapr-service/api/v1/elapr-prgrs-stts` (`ENDPOINT.AS_ELAPR_SERVICE.ELAPR_PRGRS_STTS`) |
+| 인증 | 로그인 사용자 UUID — 서버 `SecurityUtils.getLoggedInUserUnqIdUUID()` (클라이언트 미전달) |
+| 클라이언트 모듈 | `@/entities/elapr` — `elaprPrgrsSttsService` / `useElaprPrgrsSttsMyDocPage` |
+
+### 6-2. 내 행정지원 목록 — `findMyElaperDocList`
+
+| 기능 | Method | URL | Request DTO | Response DTO | API |
+| ----------------- | ------ | --------------- | --------------------------------------- | ------------------------------------ | -------------------------------------------------------------------------- |
+| 내 신청 목록 조회 | GET | `{BASE}/my/doc` | Query: `ElaprMyDocSearchDto` + Pageable | `PageResponse` | `elaprPrgrsSttsService.findMyElaperDocList` / `useElaprPrgrsSttsMyDocPage` |
+
+**Request (`ElaprMyDocSearchDto` + Pageable)**
+
+| 필드 | 필수 | 설명 |
+| ------------------------ | ---- | ------------------------------------------------------------ |
+| `startDate` | N | 검색 시작일 `YYYY-MM-DD` |
+| `endDate` | N | 검색 종료일 `YYYY-MM-DD` |
+| `docSeComCd` | N | 문서구분 (`INSTL` / `FNRS` / `DESIGN` / `SEWER` / `PWRK` 등) |
+| `prgrsSttsComCd` | N | 문서진행상태공통코드 |
+| `page` / `size` / `sort` | N | Spring Pageable |
+
+**Response (`MyAdssDocResponseDto`)**
+
+| 필드 | 타입 | 설명 |
+| ---------------- | ------- | ------------------------------------------- |
+| `docNo` | string | 문서번호 |
+| `docTtl` | string? | 문서명/사업명 |
+| `docSeComCd` | string? | 문서구분 코드 |
+| `docSeComNm` | string? | 문서구분 코드명 (`ASDOCSE`) |
+| `reqDt` | string? | 신청일시 `yyyy-MM-dd HH:mm:ss` |
+| `prgrsSttsComCd` | string? | 문서진행상태 코드 |
+| `prgrsSttsComNm` | string? | 문서진행상태 코드명 |
+| `cmptnExpctDt` | string? | 완료(예정)일 `yyyy-MM-dd` |
+| `picNm` | string? | 담당자 성명 |
+| `picTelno` | string? | 담당자 연락처 |
+| `chgCycl` | number? | 변경차수 |
+| `rgtr` | string? | 등록자 UUID |
+
+### 6-3. 공통코드 (콤보)
+
+| 용도 | 상위코드 | API | 비고 |
+| -------- | ------------ | --------------------------- | ----------------------------- |
+| 구분 | `ASDOCSE` | `useCodeList("ASDOCSE")` 등 | 기존 관리 화면과 동일 |
+| 진행상태 | `{docSeComCd}STTS` | `useCodeList(docSeComCd + "STTS")` | 예: `INSTLSTTS`, `FNRSSTTS`, `SEWERSTTS`. 구분 미선택 시 미호출 |
+
+---
+
+## 7. UI 구현 가이드
+
+### 7-1. 기술 스택·원칙
+
+- UI: `@common/components` — DatePicker, Select/`CodeSelect`, Button, DataTable, Pagination
+- 목록: `@tanstack/react-table` + `useDataTable` (기존 `instl-list` / `fnrs-list` 패턴)
+- 데이터: React Query `useElaprPrgrsSttsMyDocPage(params)`
+- 검색 조건·페이지: URL `searchParams` 동기화 권장 (`useSearchParamsState` 스킬)
+- 진행상태 컬럼: `prgrsSttsComNm` 텍스트 렌더링 (단계형 프로그레스 미사용)
+- 행 클릭: `docSeComCd` + `prgrsSttsComCd` + `rgtr`로 view/edit 분기 (`SEWER`는 `chgCycl` query 포함)
+
+### 7-2. 앱·파일 배치
+
+| 구분 | 경로 |
+| ------------ | -------------------------------------------------------------- |
+| 라우트 | `app/(main)/(with-base-layout)/my-pbadms-sprt/page.tsx` |
+| 페이지 | `pages/my-pbadms-sprt/ui/my-pbadms-sprt-list-page.tsx` |
+| 목록 그리드 | `pages/my-pbadms-sprt/ui/my-pbadms-sprt-list.tsx` |
+| 경로 해석 | `pages/my-pbadms-sprt/lib/resolve-detail-path.ts` |
+| 테이블 컬럼 | `pages/my-pbadms-sprt/model/my-pbadms-sprt-table-columns.tsx` |
+| Entity API | `entities/elapr/api/elapr-prgrs-stts-service.ts` |
+| Entity Hook | `entities/elapr/api/queries.ts` → `useElaprPrgrsSttsMyDocPage` |
+| Entity Model | `entities/elapr/model/elapr-prgrs-stts-interface.ts` |
+
+### 7-3. 영역별 UI 매핑
+
+| 영역 | 컴포넌트 | 데이터 |
+| ---- | -------------------------- | ------------------------------------------ |
+| A | `my-pbadms-sprt-list-page` | 로컬 state → `ElaprMyDocSearchDto` |
+| B | DataTable + columns | `useElaprPrgrsSttsMyDocPage` → `content[]` |
+
+### 7-4. 다이얼로그·라우팅
+
+| 항목 | 내용 |
+| ----------- | -------------------------------------------------------------------- |
+| 다이얼로그 | 없음 (목록 전용) |
+| 목록 라우트 | `/my-pbadms-sprt` |
+| 상세 이동 | §5-3 표와 동일 (view/edit + `rgtr` 소유자 검사, SEWER `chgCycl`) |
+
+### 7-5. 데이터 흐름
+
+```
+BEGIN
+
+ // 최초 진입
+ SET 기간 = 최근 1개월
+ CALL findMyElaperDocList
+ DISPLAY 영역 B
+
+ ON [검색] CLICK DO
+ CALL findMyElaperDocList // → 영역 B 갱신
+ END ON
+
+ ON 페이지 CHANGE DO
+ CALL findMyElaperDocList
+ END ON
+
+ ON 영역 B 행 CLICK DO
+ IF NOT docNo OR unsupported docSeComCd THEN
+ // no-op
+ ELSE IF canEdit(prgrsSttsComCd, rgtr === userUnqId) THEN
+ NAVIGATE edit 경로 // SEWER면 ?chgCycl=
+ ELSE
+ NAVIGATE view 경로
+ END IF
+ END ON
+
+END
+```
+
diff --git a/apps/as/docs/spec/nti-hstry/SPEC.md b/apps/as/docs/spec/nti-hstry/SPEC.md
new file mode 100644
index 000000000..eb6fb5463
--- /dev/null
+++ b/apps/as/docs/spec/nti-hstry/SPEC.md
@@ -0,0 +1,352 @@
+# 화면설계서: 알림이력
+
+## 1. 기본 정보
+
+| 항목 | 내용 |
+| ----------- | -------------------------------------------- |
+| 버전 | 1.4 |
+| 프로세스명 | UI설계 |
+| 화면명 | 알림이력 |
+| 화면 ID | AS_SC0205011 |
+| 요구사항 ID | NSUR-001-001 |
+| 화면 유형 | [v] 조회 [ ] 입력 [ ] 수정 [ ] 삭제 [ ] 출력 |
+| 메뉴 경로 | 메인 > 행정지원서비스 > 알림이력 |
+
+---
+
+## 2. 화면 목적
+
+로그인한 사용자가 **본인에게 발송된 행정지원(ADSS) 알림**을 기간·구분으로 조회한다. 목록의 처리 버튼으로 해당 알림을 읽음 처리한 뒤, 연계된 업무 화면으로 이동한다.
+
+---
+
+## 3. 화면 구성
+
+### 3-1. 전체 레이아웃
+
+```
+┌─────────────────────────────────────────────────────────────────────────────────┐
+│ 브레드크럼: 메인 > 행정지원서비스 > 알림이력 │
+│ 페이지 타이틀: 알림이력 │
+├─────────────────────────────────────────────────────────────────────────────────┤
+│ [영역 A] 검색 필터 │
+│ 기간 [시작일] ~ [종료일] 구분 ▼ [검색] │
+├─────────────────────────────────────────────────────────────────────────────────┤
+│ [영역 B] 알림 이력 목록 │
+│ 타이틀: 알림 이력 목록 N개 ▼ │
+│ ┌────┬──────────────┬────────────────┬──────────┬────────┬────────┐ │
+│ │ No │ 알림일시 │ 알림내용 │ 연계업무 │ 상태 │ 처리 │ │
+│ ├────┼──────────────┼────────────────┼──────────┼────────┼────────┤ │
+│ │ … │ regDt │ notiTtl │ docSeComNm│ Badge │ 버튼 │ │
+│ └────┴──────────────┴────────────────┴──────────┴────────┴────────┘ │
+│ [페이지네이션] │
+└─────────────────────────────────────────────────────────────────────────────────┘
+```
+
+### 3-2. 화면 데이터 흐름
+
+```
+BEGIN
+
+ // 최초 진입
+ // 기간 기본값: 오늘 기준 최근 1개월 (시작일 = today-1M, 종료일 = today)
+ CALL findMyAsNotiList(NotiSearchDto + pageable)
+ DISPLAY 알림 이력 목록 그리드 (영역 B)
+
+ ON [검색] 버튼 CLICK DO
+ CALL findMyAsNotiList(영역 A 조건 + page=0)
+ DISPLAY 알림 이력 목록 그리드 (영역 B)
+ END ON
+
+ ON 페이지네이션 CHANGE DO
+ CALL findMyAsNotiList(영역 A 조건 + page/size)
+ DISPLAY 알림 이력 목록 그리드 (영역 B)
+ END ON
+
+ ON 영역 B [처리] 버튼 CLICK DO
+ CALL readNoti(notiId)
+ // UI설계: 해당 알림에 연계된 URL로 이동
+ NAVIGATE notiUrl
+ END ON
+
+END
+```
+
+---
+
+## 4. 영역 A — 검색 필터
+
+구성: **기간 · 구분** (+ 검색).
+
+서버 `findMyAsNotiList` 고정 정책 (클라이언트 미전달):
+
+- `recvUnqId` = 로그인 사용자 UUID
+- `delYn` = `N`
+- `notiType` = `ADSS` (행정지원 전용 API)
+
+### 4-1. 필드 정의
+
+| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
+| ---------- | ---------- | ---- | --------------------------- | ----------------- | ------------------------------------------------------------------------------------------------ |
+| 기간(시작) | DatePicker | N | 오늘 − 1개월 (`YYYY-MM-DD`) | `startDt` | `NotiSearchDto.startDt` |
+| 기간(종료) | DatePicker | N | 오늘 (`YYYY-MM-DD`) | `endDt` | `NotiSearchDto.endDt` |
+| 구분 | Select | N | 전체(미선택) | `docSeComCd` | `INSTL` / `FNRS` / `SEWER` / `CNFM` / `PWRK` (또는 단축 `I`/`F`/`S`/`C`/`P`). 공통코드 `ASDOCSE` |
+
+### 4-2. 버튼
+
+| 버튼명 | 동작 |
+| ------ | -------------------------------------------------------------- |
+| 검색 | 영역 A 조건으로 `findMyAsNotiList` 호출 → 영역 B 갱신 (page=0) |
+
+### 4-3. 콤보·기본값 주의
+
+| 항목 | 내용 |
+| -------------- | ---------------------------------------------------------- |
+| 구분 = 전체 | `docSeComCd` 미전달. `notiType`은 서버가 `ADSS`로 고정 |
+| 구분 옵션 소스 | 공통코드 `ASDOCSE` (`docSeComCd` ↔ `docSeComNm`) |
+| 기간 | UI설계 상세설명: 기본 최근 1개월. `startDt`/`endDt`로 전달 |
+
+---
+
+## 5. 영역 B — 알림 이력 목록
+
+### 5-1. 그리드 컬럼
+
+| 표시명 | Field | 비고 |
+| -------- | ------------ | --------------------------------------------------------------- |
+| No | (row index) | 페이지 오프셋 반영 |
+| 알림일시 | `regDt` | 표시 `YYYY-MM-DD HH:mm` (응답 `yyyy-MM-dd HH:mm:ss`) |
+| 알림내용 | `notiTtl` | 제목 문자열. UI 샘플은 `[YYYY-MM-DD] …` 접두 |
+| 연계업무 | `docSeComNm` | 문서구분 공통코드명 (`docSeComCd` + `@CodeName` `ASDOCSE`) |
+| 상태 | (derived) | `Badge` 렌더링. 판정 규칙은 §5-1-1 |
+| 처리 | (action) | 상태별 버튼 라벨. 클릭 시 `readNoti` 후 `notiUrl`로 이동 (§5-3) |
+
+#### 5-1-1. 상태 UI 케이스 (`@common/components` `Badge`)
+
+판정에 사용: `notiPrcsCmptnYn`, `notReadYn`, `regDt` (오늘 기준 경과일).
+
+| 조건 | 라벨 | Badge |
+| ------------------------------------------------------------------------------ | -------- | ----------------------------- |
+| `notiPrcsCmptnYn` = `Y` | 처리완료 | `variant="default"` (primary) |
+| `notiPrcsCmptnYn` = `N` 이고 `notReadYn` = `Y` 이고 `regDt`로부터 **7일 초과** | 처리지연 | `color="red"` |
+| `notiPrcsCmptnYn` = `N` 이고 `notReadYn` = `Y` 이고 `regDt`로부터 **7일 이내** | 처리중 | `color="green"` |
+| `notiPrcsCmptnYn` = `N` 이고 `notReadYn` = `N` 이고 `regDt`로부터 **3일 초과** | 지연 | `color="orange"` |
+| `notiPrcsCmptnYn` = `N` 이고 `notReadYn` = `N` 이고 `regDt`로부터 **3일 이내** | 미조회 | `color="yellow"` |
+
+```
+IF notiPrcsCmptnYn = Y THEN
+ DISPLAY Badge "처리완료" (primary / variant=default)
+ELSE IF notReadYn = Y THEN
+ IF daysSince(regDt) > 7 THEN
+ DISPLAY Badge "처리지연" (red)
+ ELSE
+ DISPLAY Badge "처리중" (green)
+ END IF
+ELSE // notReadYn = N
+ IF daysSince(regDt) > 3 THEN
+ DISPLAY Badge "지연" (orange)
+ ELSE
+ DISPLAY Badge "미조회" (yellow)
+ END IF
+END IF
+```
+
+- `daysSince(regDt)`: 오늘 날짜 − `regDt` 일자 (일 단위)
+- `regDt`가 없으면 상태 Badge 표시 전략 TBD (미표시 또는 기본값)
+
+응답에 있으나 그리드에 **표시하지 않음**:
+
+| Field | 비고 |
+| ----------------- | ---------------------------------------------------------------------- |
+| `notiId` | 알림 ID — 행 키 |
+| `recvUnqId` | 수신 회원 UUID (서버가 로그인 사용자로 고정) |
+| `senderUnqId` | 발신 회원 UUID |
+| `notiType` | 알림 종류 (`ADSS`) |
+| `notiCn` | 알림 내용 (목록에서는 서버가 제외할 수 있음) |
+| `notReadYn` | 확인 여부 `Y`/`N`. 상태 Badge 판정에 사용 (§5-1-1) |
+| `notiUrl` | 처리 버튼 이동 URL. 컬럼 미표시 |
+| `notiLnkgUnqNo` | 알림연결고유번호 |
+| `docSeComCd` | 문서구분 공통코드. 연계업무 표시는 `docSeComNm` 사용 |
+| `notiIdntyDt` | 알림 확인 일시 |
+| `notiIdntyYn` | 알림 확인 여부 (`notReadYn`과 동일 의미). 상태 판정은 `notReadYn` 사용 |
+| `notiPrcsCmptnYn` | 알림 처리 완료 여부. 상태 Badge 판정에 사용 (§5-1-1) |
+| `notiPrcsCmptnDt` | 알림 처리 완료 일시 |
+
+### 5-2. 목록 메타·페이지네이션
+
+| 항목 | 매핑 |
+| ------------- | ------------------------------------------- |
+| 건수 / size ▼ | 상단 우측 `N개` 셀렉트 → `Pageable.size` |
+| page / size | `NotiSearchDto`의 `PageableParams` |
+| 페이지 컨트롤 | Spring `Page` 표준 (`number`, `totalPages`) |
+
+### 5-3. 행 상호작용
+
+| 동작 | 설명 |
+| -------------- | --------------------------------------------------------------------------- |
+| 처리 버튼 클릭 | `readNoti(notiId)`로 읽음 처리 후, 해당 알림에 연계된 URL(`notiUrl`)로 이동 |
+| 행 클릭 | UI설계에 없음. 처리 버튼만 명시 |
+
+**처리 버튼 라벨** (상태 Badge §5-1-1 기준):
+
+| 상태 | 버튼명 |
+| -------- | -------- |
+| 미조회 | 처리하기 |
+| 지연 | 처리하기 |
+| 처리중 | 처리하기 |
+| 처리지연 | 처리하기 |
+| 처리완료 | 상세보기 |
+
+- `notiUrl`이 비어 있으면 이동하지 않음 (no-op). 읽음 처리는 `notiUrl` 유무와 관계없이 호출
+- 읽음 처리 성공 후 목록 쿼리(`notiBaseKey`)를 invalidate 하여 영역 B 상태를 갱신
+
+---
+
+## 6. API/DTO 명세
+
+### 6-1. 공통
+
+| 항목 | 값 |
+| --------------- | ---------------------------------------------------------------------------------------- |
+| Base prefix | `/cms-service/api/v1/noti` (`ENDPOINT.CMS_SERVICE.NOTI`) |
+| 인증 | 로그인 사용자 UUID — 서버 `SecurityUtils.getLoggedInUserUnqIdUUID()` (클라이언트 미전달) |
+| 클라이언트 모듈 | `@/entities/noti` — `notiService` / `useFindMyAsNotiList` / `useReadNoti` |
+
+### 6-2. 로그인한 사용자 행정지원 알림 목록 — `findMyAsNotiList`
+
+| 기능 | Method | URL | Request DTO | Response DTO | API |
+| ---------------------------------- | ------ | ------------------- | --------------------------------- | ------------------------------------- | ------------------------------------------------------ |
+| 로그인한 사용자 행정지원 알림 목록 | GET | `{BASE}/as/my/list` | Query: `NotiSearchDto` + Pageable | `PageResponse` | `notiService.findMyAsNotiList` / `useFindMyAsNotiList` |
+
+**Request (`NotiSearchDto` + Pageable)**
+
+| 필드 | 필수 | 설명 |
+| ------------------------ | ---- | --------------------------------------------------------------------------- |
+| `recvUnqId` | N | 수신 회원 UUID. 서버가 로그인 사용자로 덮어씀 |
+| `senderUnqId` | N | 발신 회원 UUID. 본 화면 필터에 없음 |
+| `notiType` | N | 알림 종류. 본 API는 서버가 `ADSS`로 고정 |
+| `notiTtlKeyword` | N | 알림 제목 키워드. 본 화면 필터에 없음 |
+| `notiCnKeyword` | N | 알림 내용 키워드. 본 화면 필터에 없음 |
+| `notReadYn` | N | 알림 확인 여부 (`Y`/`N`). 본 화면 검색 필터에 없음 |
+| `delYn` | N | 삭제 여부. 서버가 `N`으로 고정 |
+| `startDt` | N | 시작일자 `YYYY-MM-DD` — 영역 A 기간(시작) |
+| `endDt` | N | 종료일자 `YYYY-MM-DD` — 영역 A 기간(종료) |
+| `docSeComCd` | N | 문서 구분 공통 코드 (`INSTL`/`FNRS`/`SEWER`/`CNFM`/`PWRK` 등) — 영역 A 구분 |
+| `page` / `size` / `sort` | N | Spring Pageable |
+
+**Response (`NotiAsUserResponseDto`)**
+
+| 필드 | 타입 | 설명 |
+| ----------------- | ------- | ------------------------------------------------------- |
+| `notiId` | number | 알림 ID |
+| `recvUnqId` | string | 수신 회원 UUID |
+| `senderUnqId` | string | 발신 회원 UUID |
+| `notiType` | string | 알림 종류 (`ADSS`) |
+| `notiTtl` | string | 알림 제목 |
+| `notiCn` | string? | 알림 내용 (목록에서 제외될 수 있음) |
+| `notReadYn` | string? | 알림 확인 여부 (`Y`/`N`). 상태 Badge 판정 |
+| `regDt` | string? | 등록 일시 `yyyy-MM-dd HH:mm:ss`. 상태 Badge 경과일 기준 |
+| `notiUrl` | string? | 알림 URL 주소 |
+| `notiLnkgUnqNo` | string? | 알림연결고유번호 |
+| `docSeComCd` | string? | 문서구분 공통코드 |
+| `docSeComNm` | string? | 문서구분 공통코드명 (`ASDOCSE`) |
+| `notiIdntyDt` | string? | 알림 확인 일시 `yyyy-MM-dd HH:mm:ss` |
+| `notiIdntyYn` | string? | 알림 확인 여부 (`Y`/`N`) |
+| `notiPrcsCmptnYn` | string? | 알림 처리 완료 여부 (`Y`/`N`). 상태 Badge 판정 |
+| `notiPrcsCmptnDt` | string? | 알림 처리 완료 일시 `yyyy-MM-dd HH:mm:ss` |
+
+### 6-3. 알림 읽음 처리 — `readNoti`
+
+| 기능 | Method | URL | Request DTO | Response DTO | API |
+| -------------- | ------ | ---------------------- | ----------------------- | ---------------- | -------------------------------------- |
+| 알림 읽음 처리 | PUT | `{BASE}/{notiId}/read` | Path: `notiId` (`Long`) | `Long` (알림 ID) | `notiService.readNoti` / `useReadNoti` |
+
+**Request**
+
+| 필드 | 위치 | 필수 | 설명 |
+| -------- | ---- | ---- | ------- |
+| `notiId` | Path | Y | 알림 ID |
+
+Body 없음.
+
+**Response**
+
+| 타입 | 설명 |
+| ------ | ------------------------------ |
+| number | 읽음 처리된 알림 ID (`notiId`) |
+
+화면 매핑: 영역 B [처리] 버튼 클릭 시 호출. 성공 시 `notiBaseKey` invalidate → `findMyAsNotiList` 재조회.
+
+---
+
+## 7. UI 구현 가이드
+
+### 7-1. 기술 스택·원칙
+
+- UI: `@common/components` — DatePicker, Select, Button, DataTable, Pagination, **Badge**
+- 목록: `@tanstack/react-table` + `useDataTable` (기존 `my-pbadms-sprt-list` 패턴)
+- 데이터: React Query `useFindMyAsNotiList(params)`, `useReadNoti()`
+- 검색 조건·페이지: URL `searchParams` 동기화 권장 (`useSearchParamsState` 스킬)
+- 처리 컬럼: 행 클릭이 아니라 **버튼 클릭**으로 `readNoti` 후 `notiUrl` 이동
+- 상태 컬럼: §5-1-1 규칙으로 `Badge` 렌더링 (`notiPrcsCmptnYn` / `notReadYn` / `regDt`)
+- 연계업무: `docSeComNm` 표시
+
+### 7-2. 앱·파일 배치
+
+| 구분 | 경로 |
+| ------------ | --------------------------------------------------------------------- |
+| 라우트 | `app/(main)/(with-base-layout)/noti/page.tsx` |
+| 페이지 | `pages/noti/ui/noti-list-page.tsx` |
+| 목록 그리드 | `pages/noti/ui/noti-list.tsx` |
+| 테이블 컬럼 | `pages/noti/model/noti-table-columns.tsx` |
+| 상태 판정 | `pages/noti/lib/resolve-noti-status.ts` |
+| Entity API | `entities/noti/api/noti-service.ts` |
+| Entity Hook | `entities/noti/api/queries.ts` → `useFindMyAsNotiList`, `useReadNoti` |
+| Entity Model | `entities/noti/model/noti-interface.ts` |
+
+화면 레이어(`pages/noti`)·Entity(`findMyAsNotiList`, `readNoti`) 구현 완료.
+
+### 7-3. 영역별 UI 매핑
+
+| 영역 | 컴포넌트 | 데이터 |
+| ---- | ------------------- | -------------------------------------------------------------- |
+| A | `noti-list-page` | 로컬 state → `NotiSearchDto` |
+| B | DataTable + columns | `useFindMyAsNotiList` → `content[]`. 처리 버튼 → `useReadNoti` |
+
+### 7-4. 다이얼로그·라우팅
+
+| 항목 | 내용 |
+| ----------- | --------------------------------------------------------------------- |
+| 다이얼로그 | 없음 (목록 전용) |
+| 목록 라우트 | `/noti` |
+| 상세 이동 | 처리 버튼 → `readNoti(notiId)` 후 `notiUrl` (절대/상대 경로 형식 TBD) |
+
+### 7-5. 데이터 흐름
+
+```
+BEGIN
+
+ // 최초 진입
+ SET 기간 = 최근 1개월 // startDt / endDt 전달
+ CALL findMyAsNotiList
+ DISPLAY 영역 B
+
+ ON [검색] CLICK DO
+ CALL findMyAsNotiList // → 영역 B 갱신
+ END ON
+
+ ON 페이지 CHANGE DO
+ CALL findMyAsNotiList
+ END ON
+
+ ON 영역 B [처리] 버튼 CLICK DO
+ CALL readNoti(notiId) // → 영역 B 상태 갱신 (invalidate)
+ IF NOT notiUrl THEN
+ // no-op (읽음 처리는 수행됨)
+ ELSE
+ NAVIGATE notiUrl
+ END IF
+ END ON
+
+END
+```
diff --git a/apps/as/docs/spec/previous-plan-lookup/SPEC.md b/apps/as/docs/spec/previous-plan-lookup/SPEC.md
new file mode 100644
index 000000000..927121116
--- /dev/null
+++ b/apps/as/docs/spec/previous-plan-lookup/SPEC.md
@@ -0,0 +1,441 @@
+# 화면설계서: 이전기본계획조회 (기본 계획 찾기)
+
+## 1. 기본 정보
+
+| 항목 | 내용 |
+| ---------- | --------------------------------------------------------------- |
+| 버전 | 0.2 |
+| 프로세스명 | UI설계 |
+| 화면명 | 이전기본계획조회 (기본 계획 찾기) |
+| 화면 유형 | [v] 조회 [ ] 입력 [ ] 수정 [ ] 삭제 [ ] 출력 |
+| 메뉴 경로 | 하수도기본계획 등록·수정 > 사업개요 > [이전기본계획조회] Dialog |
+
+---
+
+## 2. 화면 목적
+
+하수도기본계획 등록·수정 화면에서 **이전(기수립) 기본계획** 수치와 관련 보고서를 **조회·다운로드**한다. 시도·구군·연도·처리시설을 선택한 뒤 총설·처리구역별 현황을 표시하고, 분류 트리에서 기본계획 파일을 내려받는다. **부모 사업개요 폼에는 수치를 반영하지 않는다**(레거시 `bscPlnPopup.jsp`와 동일).
+
+---
+
+## 3. 화면 구성
+
+### 3-1. 전체 레이아웃
+
+부모: 하수도기본계획 등록/수정 폼 → 사업개요 섹션의 `[이전기본계획조회]` 버튼으로 Dialog 오픈.
+
+```
+┌─────────────────────────────────────────────────────────────────────────────────┐
+│ Dialog 타이틀: 기본 계획 찾기 [닫기] │
+├─────────────────────────────────────────────────────────────────────────────────┤
+│ [영역 A] 검색 필터 │
+│ 시도 ▼ 구군 ▼ 연도 ▼ 처리시설 ▼ [조회] │
+├─────────────────────────────────────────────────────────────────────────────────┤
+│ [영역 B] 기본계획파일 │
+│ ┌──────────────────────┬────────────────────────────────────────────────────┐ │
+│ │ [영역 B-1] 분류 트리 │ [영역 B-2] 파일 목록 │ │
+│ │ (약 40%) │ (약 60%) │ │
+│ │ └ 노드… │ 파일명 / 다운로드 │ │
+│ └──────────────────────┴────────────────────────────────────────────────────┘ │
+├─────────────────────────────────────────────────────────────────────────────────┤
+│ [영역 C] 총설 │
+│ ┌────────┬──────────────당초──────────────┬──기준──┬──────────────변경──────────────┐ │
+│ │ 구분 │ 1단계 │ 2단계 │ 3단계 │ 4단계 │ 기준 │ 1단계 │ 2단계 │ 3단계 │ 4단계 │ │
+│ ├────────┼───────┼───────┼───────┼───────┼────────┼───────┼───────┼───────┼───────┤ │
+│ │계획목표│ 년 │ 년 │ 년 │ 년 │ 년 │ 년 │ 년 │ 년 │ 년 │ │
+│ │년도 │ │ │ │ │ │ │ │ │ │ │
+│ └────────┴───────┴───────┴───────┴───────┴────────┴───────┴───────┴───────┴───────┘ │
+├─────────────────────────────────────────────────────────────────────────────────┤
+│ [영역 D] 처리구역별 현황 │
+│ ┌────────┬──────────────당초──────────────┬──기준──┬──────────────변경──────────────┐ │
+│ │ 구분 │ 1~4단계(+연도헤더) │ 기준 │ 1~4단계(+연도헤더) │ │
+│ ├────────┼────────────────────────────────┼────────┼────────────────────────────────┤ │
+│ │ 하수처리인구 … 배제방식 (읽기 전용 행) │ │
+│ └────────┴────────────────────────────────┴────────┴────────────────────────────────┘ │
+└─────────────────────────────────────────────────────────────────────────────────┘
+```
+
+### 3-2. 화면 데이터 흐름
+
+```
+BEGIN
+
+ // 부모 폼 — 사업개요
+ ON [이전기본계획조회] 버튼 CLICK DO
+ OPEN PreviousPlanLookupDialog
+ // 프리필: 부모 폼 sewerBscPlan.sido / gugun / admdstCd
+ CALL findSidoCombo()
+ DISPLAY 시도 옵션 (영역 A)
+ IF 시도 프리필 THEN
+ CALL findGugunCombo(sidoCd)
+ DISPLAY 구군 옵션 (영역 A)
+ CALL getLvNowDmgMtncGnsvy({ level: "1", ctpvNm })
+ // TBD: useLvNowDmgMtncGnsvy enabled — level별 필수 파라미터만 검사하도록 조정
+ IF 구군 프리필 THEN
+ CALL getLvNowDmgMtncGnsvy({ level: "2", ctpvNm, sggNm })
+ DISPLAY 연도 옵션 (영역 A) // children 중 level=3 노드의 gnsvyYr
+ CALL getClsfSystm({ clsfSystmNm: gugunNm })
+ DISPLAY 분류 트리 (영역 B-1)
+ END IF
+ END IF
+ END ON
+
+ // 영역 A: 시도 변경
+ ON 영역 A 시도 CHANGE DO
+ RESET 구군 / 연도 / 처리시설
+ CALL findGugunCombo(sidoCd)
+ CALL getLvNowDmgMtncGnsvy({ level: "1", ctpvNm })
+ // 구군 미선택 → 연도·처리시설 옵션 비움, 트리 빈 안내
+ END ON
+
+ // 영역 A: 구군 변경
+ ON 영역 A 구군 CHANGE DO
+ RESET 연도 / 처리시설
+ CALL getLvNowDmgMtncGnsvy({ level: "2", ctpvNm, sggNm })
+ DISPLAY 연도 옵션 (영역 A) // level=3 노드 → gnsvyYr
+ CALL getClsfSystm({ clsfSystmNm: sggNm })
+ DISPLAY 분류 트리 (영역 B-1)
+ END ON
+
+ // 영역 A: 연도 변경
+ ON 영역 A 연도 CHANGE DO
+ RESET 처리시설
+ CALL getLvNowDmgMtncGnsvy({ level: "3", ctpvNm, sggNm, gnsvyYr })
+ DISPLAY 처리시설 옵션 (영역 A) // level=3 노드 children → swtrmtFcltCd / swtrmtFcltNm
+ END ON
+
+ // 영역 A: 조회
+ ON [조회] 버튼 CLICK DO
+ VALIDATE 시도, 구군, 연도(targetYear), 처리시설(swtrmtFcltCd)
+ CALL getNowDmgMtncGnsvy({ swtrmtFcltCd, ctpvNm, sggNm, targetYear })
+ DISPLAY 총설 (영역 C) // 응답[0]의 planYr / planYrDt* / planYrDc*
+ DISPLAY 처리구역별 현황 (영역 D) // 응답[0]의 지표 Dt1~4 / base / Dc1~4
+ END ON
+
+ // 영역 B: 트리 노드 선택
+ ON 영역 B-1 노드 CLICK DO
+ CALL getFile({ clsfSystmSn, sort, vetPrcsYn }) // Response DTO TBD
+ DISPLAY 파일 목록 (영역 B-2)
+ END ON
+
+ // 영역 B: 파일 다운로드
+ ON 영역 B-2 파일 CLICK DO
+ CALL getFilePrcs(fileSn) // 파일 메타(경로·제목) — Response TBD
+ CALL fileDownload({ filePath, fileName })
+ // 브라우저 다운로드 트리거
+ END ON
+
+ ON [닫기] DO
+ CLOSE PreviousPlanLookupDialog
+ // 부모 폼 값 변경 없음
+ END ON
+
+END
+```
+
+---
+
+## 4. 영역 A — 검색 필터
+
+구성: **시도 · 구군 · 연도 · 처리시설** (+ 조회).
+
+- 시도·구군: `adstdstr` 콤보 (기존 스펙 유지)
+- 연도·처리시설: 각각 `useLvNowDmgMtncGnsvy`를 **별도 호출**. `level`에 따라 반환 계층이 달라진다.
+
+### 4-1. 필드 정의
+
+| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
+| -------- | --------- | ---- | ------------------------- | ------------------------------------------------ | -------------------------------------------------------------- |
+| 시도 | Select | Y | 부모 `sewerBscPlan.sido` | `ctpvNm`(표시명), 코드는 `adstDstrCd` | `useFindSidoCombo` |
+| 구군 | Select | Y | 부모 `sewerBscPlan.gugun` | `sggNm`(표시명), 코드는 `adstDstrCd` | `useFindGugunCombo(sidoCd)` |
+| 연도 | Select | Y | 없음 | `gnsvyYr` / 조회 시 `targetYear` | `useLvNowDmgMtncGnsvy({ level: "2", ctpvNm, sggNm })` |
+| 처리시설 | Select | Y | 없음 | `swtrmtFcltCd` ← 옵션 value, 라벨 `swtrmtFcltNm` | `useLvNowDmgMtncGnsvy({ level: "3", ctpvNm, sggNm, gnsvyYr })` |
+
+> 처리시설은 `bns-if` `useFacilityList`를 **사용하지 않는다**.
+
+### 4-2. 버튼
+
+| 버튼명 | 동작 |
+| ------ | -------------------------------------------------------- |
+| 조회 | 필수값 검증 후 `getNowDmgMtncGnsvy` 호출 → 영역 C·D 갱신 |
+| 닫기 | Dialog 종료 (부모 미반영) |
+
+### 4-3. `lvNowDmgMtncGnsvy` level 캐스케이드
+
+| 트리거 | Request `level` | Request 파라미터 | UI에 쓰는 응답 | 옵션 추출 |
+| --------- | --------------- | ---------------------------- | --------------- | ------------------------------------------------------------------------------ |
+| 시도 선택 | `"1"` | `ctpvNm` | (하위 초기화용) | 연도·처리시설 Select 리셋. 구군은 `findGugunCombo` |
+| 구군 선택 | `"2"` | `ctpvNm`, `sggNm` | 연도 Select | 트리에서 `level === 3` 노드의 `gnsvyYr` |
+| 연도 선택 | `"3"` | `ctpvNm`, `sggNm`, `gnsvyYr` | 처리시설 Select | 선택 연도(`level === 3`) 노드의 `children[]` → `swtrmtFcltCd` / `swtrmtFcltNm` |
+
+상위 변경 시 하위 선택값·옵션을 RESET한다 (시도→구군·연도·시설 / 구군→연도·시설 / 연도→시설).
+
+---
+
+## 5. 영역 B — 기본계획파일
+
+### 5-1. 트리 (B-1)
+
+| 표시명 | Field | 비고 |
+| ------ | --------------- | ------------------------ |
+| 분류명 | `clsfSystmNm` | `ClsfSystmResponseDto` |
+| 노드키 | `clsfSystmSn` | 선택 시 파일 목록 조회 |
+| 상위키 | `upClsfSystmSn` | 클라이언트에서 트리 조립 |
+| 정렬 | `sortSeq` | |
+| 경로 | `path` | |
+
+- 조회 파라미터: `clsfSystmNm` = 선택 구군명(레거시와 동일). `upClsfSystmSn`은 하위 전개 시 TBD.
+- 시도·구군 미선택 시: 「시도와 구군을 선택하세요.」 안내.
+
+### 5-2. 파일 목록 (B-2)
+
+| 표시명 | Field | 비고 |
+| ------ | ----- | ------------------------------------------------------------------- |
+| 파일명 | TBD | `getFile` Response DTO가 `unknown` — 백엔드 스키마 확정 후 매핑 |
+| (기타) | TBD | 레거시: 트리 선택 후 목록 표시, 클릭 시 `fileprcs` → `fileDownload` |
+
+### 5-3. 버튼·상호작용
+
+| 동작 | 설명 |
+| -------------- | -------------------------------------------------------------- |
+| 트리 노드 클릭 | `getFile({ clsfSystmSn, … })` → B-2 목록 |
+| 파일 클릭 | `getFilePrcs(fileSn)` → `fileDownload({ filePath, fileName })` |
+| 빈 목록 | 「트리에서 항목을 선택하세요.」 / 「파일이 없습니다.」 |
+
+---
+
+## 6. 영역 C — 총설
+
+### 6-1. 테이블 구조
+
+| 행(구분) | 열 매핑 (당초1~~4 / 기준 / 변경1~~4) |
+| ------------ | ------------------------------------------------------------------------------ |
+| 계획목표년도 | `planYrDt1planYrDt4` ~~/~~ `planYr` ~~/~~ `planYrDc1planYrDc4` (+ `년` 접미사) |
+
+- 헤더: 당초(1~~4단계) · 기준 · 변경(1~~4단계)
+- 데이터 소스: `getNowDmgMtncGnsvy` 응답 배열의 **첫 번째** 요소
+- 읽기 전용
+
+---
+
+## 7. 영역 D — 처리구역별 현황
+
+### 7-1. 행 정의 (표시명 → Field 접두사)
+
+각 행은 `Dt1Dt4` ~~/ (접미사 없음=기준) /~~ `Dc1Dc4` 셀로 표시한다. 숫자는 천단위 콤마, 없으면 `-`.
+
+| 표시명 | Field 접두사 | 비고 |
+| ------------------------ | -------------------- | ---------------------------------------------- |
+| 하수처리인구(인) | `swtrmtPpltnCnt` | |
+| 자연적하수처리인구수(인) | `natrSwtrmtPpltnCnt` | |
+| 사회적하수처리인구수(인) | `scilSwtrmtPpltnCnt` | |
+| 하수처리구역면적 | `swtrmtZoneArea` | |
+| 물사용량원단위 | `wtrUsqtyOrgnUnit` | |
+| 오수전환율(%) | `drtwSwtcRt` | |
+| 변동부하율(%) | `flctnLodRt` | |
+| 오수원단위 | `drtwOrgnUnit` | |
+| 계획하수량합계(㎥/일) | `planSwageQntSum` | |
+| 생활오수(㎥/일) | `lvngDrtwSum` | |
+| 지하수(㎥/일) | `ugwtSum` | |
+| 공장폐수(㎥/일) | `fctryWwtSum` | |
+| 관광오수(㎥/일) | `toursmDrtwSum` | |
+| 군부대(㎥/일) | `mtbsSum` | |
+| 연계처리수(㎥/일) | `linkPrcsCntSum` | |
+| 기타오수(㎥/일) | `etcDrtwSum` | |
+| 지하수사용량(㎥/일) | `ugwtUseSum` | |
+| 시설용량(㎥/일) | `fcltCpctSum` | |
+| 배제방식 | `exclsMethExpln` | 문자열; 단계별 `*Dt*`/`*Dc*` 존재 시 동일 규칙 |
+
+헤더 3행째 연도: 영역 C와 동일하게 `planYrDt*` / `planYr` / `planYrDc*` 표시.
+
+---
+
+## 8. API/DTO 명세
+
+### 8-1. 공통
+
+| 항목 | 값 |
+| --------------- | ---------------------------------------------------- |
+| DC Base | `/dc-service/api/v1` (`ENDPOINT.DC_SERVICE`) |
+| AS adstdstr | `/as-service/api/v1/adstdstr` |
+| 클라이언트 모듈 | `@/entities/dc` (`dcService`), `@/entities/adstdstr` |
+
+> 영역 A 처리시설은 `bns-if`를 사용하지 않음.
+
+### 8-2. 시도·구군 콤보 (non-DC)
+
+| 기능 | Method | URL | Request DTO | Response DTO | API |
+| -------- | ------ | ----------------------------------- | ------------------------ | -------------------------------- | ---------------------------------------------------------- |
+| 시도목록 | GET | `/as-service/api/v1/adstdstr/sido` | — | `CmCdAdstDstrComboResponseDto[]` | `cmCdAdstDstrService.findSidoCombo` / `useFindSidoCombo` |
+| 구군목록 | GET | `/as-service/api/v1/adstdstr/gugun` | Query: `sido` (시도코드) | `CmCdAdstDstrComboResponseDto[]` | `cmCdAdstDstrService.findGugunCombo` / `useFindGugunCombo` |
+
+### 8-3. 연도·처리시설 계층 조회 — DC
+
+| 기능 | Method | URL | Request DTO | Response DTO | API |
+| --------------- | ------ | -------------------------------------- | ---------------------------- | -------------------------------- | --------------------------------------------------------- |
+| 계층형 계획목록 | GET | `/dc-service/api/v1/lvNowDmgMtncGnsvy` | `LvNowDmgMtncGnsvySearchDto` | `LvNowDmgMtncGnsvyResponseDto[]` | `dcService.getLvNowDmgMtncGnsvy` / `useLvNowDmgMtncGnsvy` |
+
+연도 Select·처리시설 Select는 **각각** `useLvNowDmgMtncGnsvy`를 호출한다 (`level`만 다르게).
+
+**Request (**`LvNowDmgMtncGnsvySearchDto`**)**
+
+| 필드 | 시도(`level=1`) | 구군→연도(`level=2`) | 연도→시설(`level=3`) |
+| --------- | --------------- | -------------------- | -------------------- |
+| `level` | `"1"` | `"2"` | `"3"` |
+| `ctpvNm` | 필수 | 필수 | 필수 |
+| `sggNm` | — | 필수 | 필수 |
+| `gnsvyYr` | — | — | 필수 |
+
+**Response 노드 필드 (**`LvNowDmgMtncGnsvyResponseDto`**, 부분 optional)**
+
+| 필드 | 등장 level | 설명 |
+| -------------- | ---------- | ------------------------ |
+| `level` | 1~3 | 계층 깊이 |
+| `ctpvNm` | 1 | 시도명 |
+| `sggNm` | 2 | 구군명 |
+| `gnsvyYr` | 3 | 연도 → 연도 Select value |
+| `swtrmtFcltCd` | 시설(리프) | 처리시설 코드 |
+| `swtrmtFcltNm` | 시설(리프) | 처리시설명 |
+| `children` | 1~3 | 하위 노드 배열 |
+
+> **TBD**: 현재 `useLvNowDmgMtncGnsvy` `enabled`가 `level && ctpvNm && sggNm && gnsvyYr`로 고정되어 `level=1`/`2` 호출이 막힘. level별 필수 파라미터만 검사하도록 수정 필요.
+>
+> 예: `level=1` → `ctpvNm` / `level=2` → `ctpvNm && sggNm` / `level=3` → `ctpvNm && sggNm && gnsvyYr`
+
+### 8-4. 기본계획 수치 조회 — DC
+
+| 기능 | Method | URL | Request DTO | Response DTO | API |
+| ---------------- | ------ | ------------------------------------ | -------------------------- | ------------------------------ | ----------------------------------------------------- |
+| 현재·단계별 수치 | GET | `/dc-service/api/v1/nowDmgMtncGnsvy` | `NowDmgMtncGnsvySearchDto` | `NowDmgMtncGnsvyResponseDto[]` | `dcService.getNowDmgMtncGnsvy` / `useNowDmgMtncGnsvy` |
+
+**Request (**`NowDmgMtncGnsvySearchDto`**)**
+
+| 필드 | 매핑 |
+| -------------- | ----------------------- |
+| `swtrmtFcltCd` | 영역 A 처리시설 |
+| `ctpvNm` | 영역 A 시도 표시명 |
+| `sggNm` | 영역 A 구군 표시명 |
+| `targetYear` | 영역 A 연도 (`gnsvyYr`) |
+
+**Response**: 지표 필드 + `Dt1Dt4` ~~/~~ `Dc1Dc4` 접미사 스테이지 필드 (`NowDmgMtncGnsvyResponseDto`). UI는 `data[0]` 사용.
+
+### 8-5. 분류체계 — DC
+
+| 기능 | Method | URL | Request DTO | Response DTO | API |
+| ------------ | ------ | ------------------------------ | -------------------- | ------------------------ | ----------------------------------------- |
+| 분류트리조회 | GET | `/dc-service/api/v1/clsfSystm` | `ClsfSystmParamsDto` | `ClsfSystmResponseDto[]` | `dcService.getClsfSystm` / `useClsfSystm` |
+
+**Request**: `clsfSystmNm?`, `upClsfSystmSn?`
+**Response**: `clsfSystmSn`, `upClsfSystmSn`, `clsfSystmNm`, `sortSeq`, `droppAble`, `path`
+
+> 레거시 경로 `/clsf` → 현재 클라이언트는 `/clsfSystm`.
+
+### 8-6. 파일 목록·처리·다운로드 — DC
+
+| 기능 | Method | URL | Request DTO / Params | Response DTO | API |
+| ------------ | ------ | --------------------------------- | ------------------------------------------------------- | ----------------- | -------------------------------------------- |
+| 파일목록 | GET | `/dc-service/api/v1/file` | `FileParamsDto` (`sort?`, `clsfSystmSn?`, `vetPrcsYn?`) | `unknown` **TBD** | `dcService.getFile` / `useFile` |
+| 파일처리정보 | GET | `/dc-service/api/v1/fileprcs` | Query: `fileSn` | `unknown` **TBD** | `dcService.getFilePrcs` / `useFilePrcs` |
+| 파일다운로드 | POST | `/dc-service/api/v1/fileDownload` | `FileDownloadParamsDto` (`filePath?`, `fileName?`) | `unknown` **TBD** | `dcService.fileDownload` / `useFileDownload` |
+
+> **TBD**: 파일 관련 Response 스키마·다운로드 blob/헤더 처리·`fileDownload` POST body vs query 전달 방식은 백엔드/게이트웨이 확인 후 확정.
+
+---
+
+## 9. UI 구현 가이드
+
+### 9-1. 기술 스택·원칙
+
+- UI: `@common/components` — `Dialog`, `Select`, `Button`, `Table`, `Skeleton`
+- 상태: Dialog 로컬 state + React Query (`entities/dc`, `adstdstr`)
+- 영역 A 연도·처리시설: `useLvNowDmgMtncGnsvy`를 level별 **두 번**(또는 시도 포함 시 세 번) 호출
+- 테이블: 읽기 전용 고정 행 → `@tanstack/react-table` / `DataTable` **불필요**
+- 트리: jsTree 미사용 — 재귀 들여쓰기 트리
+- 번들: `PreviousPlanLookupDialog`는 outline에서 `next/dynamic` 로드 권장
+- 부모 폼: **수치 미반영** (조회·다운로드 only)
+- 1차 구현 시 DC 미연동이면 `@faker-js/faker` mock + TODO → 이후 `dcService` 훅으로 교체
+
+### 9-2. 앱·파일 배치
+
+| 구분 | 경로 |
+| ------------ | ------------------------------------------------------------------------ |
+| 진입 버튼 | `pages/sewer-bsc-plan/ui/sections/outline-form-section.tsx` |
+| Dialog | `pages/sewer-bsc-plan/ui/previous-plan-lookup-dialog.tsx` |
+| 필터 | `pages/sewer-bsc-plan/ui/previous-plan/previous-plan-search-filters.tsx` |
+| 파일 | `pages/sewer-bsc-plan/ui/previous-plan/previous-plan-file-section.tsx` |
+| 총설 | `pages/sewer-bsc-plan/ui/previous-plan/previous-plan-summary-table.tsx` |
+| 처리구역 | `pages/sewer-bsc-plan/ui/previous-plan/previous-plan-area-table.tsx` |
+| 셀 매핑 유틸 | `pages/sewer-bsc-plan/lib/previous-plan-table-utils.ts` |
+| mock(선택) | `pages/sewer-bsc-plan/lib/previous-plan-mock.ts` |
+| DC entity | `entities/dc/api/dc-service.ts`, `queries.ts`, `model/dc-interface.ts` |
+| 레거시 참조 | `adssNewOld/.../cmmn/bscPlnPopup.jsp` |
+
+### 9-3. 영역별 UI 매핑
+
+| 영역 | 컴포넌트 | 데이터 |
+| ---- | ------------------------------ | -------------------------------------------------------------- |
+| A | `previous-plan-search-filters` | adstdstr + `useLvNowDmgMtncGnsvy`(level 1·2·3) |
+| B | `previous-plan-file-section` | `useClsfSystm` + `useFile` + `useFilePrcs` + `useFileDownload` |
+| C | `previous-plan-summary-table` | `useNowDmgMtncGnsvy` / 조회 결과 state |
+| D | `previous-plan-area-table` | 동일 |
+
+### 9-4. 다이얼로그·라우팅
+
+| 항목 | 내용 |
+| ------------ | ------------------------------------------------------------------------ |
+| 다이얼로그명 | `PreviousPlanLookupDialog` / 타이틀 `기본 계획 찾기` |
+| 오픈 | 사업개요 `[이전기본계획조회]` |
+| 라우트 | 없음 (모달). 부모: `/sewer-bsc-plan/regist`, `/sewer-bsc-plan/[id]/edit` |
+| 선택 반영 | 없음 |
+
+### 9-5. 데이터 흐름 (구현 요약)
+
+```
+BEGIN
+
+ ON [이전기본계획조회] CLICK DO
+ OPEN PreviousPlanLookupDialog (sido/gugun 프리필)
+ END ON
+
+ ON 시도 CHANGE DO
+ CALL findGugunCombo
+ CALL getLvNowDmgMtncGnsvy(level=1)
+ RESET 연도·처리시설
+ END ON
+
+ ON 구군 CHANGE DO
+ CALL getLvNowDmgMtncGnsvy(level=2) // → 연도 옵션
+ CALL getClsfSystm
+ RESET 처리시설
+ END ON
+
+ ON 연도 CHANGE DO
+ CALL getLvNowDmgMtncGnsvy(level=3) // → 처리시설 옵션
+ END ON
+
+ ON [조회] CLICK DO
+ VALIDATE
+ CALL getNowDmgMtncGnsvy
+ DISPLAY 영역 C · 영역 D
+ END ON
+
+ ON 트리 노드 / 파일 CLICK DO
+ CALL getFile → getFilePrcs → fileDownload // Response TBD
+ END ON
+
+END
+```
+
+---
+
+## 10. 미확정 (TBD) 추적
+
+| ID | 항목 | 비고 |
+| --- | -------------------------------------------------------- | -------------------------------------------------------------------- |
+| T1 | `useLvNowDmgMtncGnsvy` enabled을 level별 필수값으로 분기 | `1`: ctpvNm / `2`: +sggNm / `3`: +gnsvyYr |
+| T2 | `getFile` / `getFilePrcs` / `fileDownload` Response DTO | 현재 `unknown` |
+| T3 | 파일 다운로드 UX (blob, Content-Disposition, 게이트웨이) | 레거시 apigateway 직접 호출과 차이 |
+| T4 | `clsfSystm`의 `upClsfSystmSn` 점진 로드 여부 | flat 목록 vs 계층 API |
+| T5 | 1차 mock vs 즉시 `dcService` 실연동 | 마이그레이션 플랜은 mock 우선, entity는 준비됨 |
+| T6 | 부모 폼 수치 반영 | 요구 없음(레거시 동일). 요청 시 별도 스펙 |
+| T7 | `level=1` 응답 UI 활용 여부 | 시도 선택 시 호출하나 구군은 adstdstr — 응답은 캐시·검증용일 수 있음 |
diff --git a/apps/as/docs/review-manpower-management/SPEC.md b/apps/as/docs/spec/review-manpower-management/SPEC.md
similarity index 100%
rename from apps/as/docs/review-manpower-management/SPEC.md
rename to apps/as/docs/spec/review-manpower-management/SPEC.md
diff --git a/apps/as/middleware.ts b/apps/as/middleware.ts
index 94a1d0381..dbd2a53f7 100644
--- a/apps/as/middleware.ts
+++ b/apps/as/middleware.ts
@@ -1,8 +1,16 @@
import { NextRequest, NextResponse } from "next/server";
-import { AUTH_USER_ID, CLAIM_NAME, PORT, PROXY_HOST, SERVER_API_URL, SITE_ID } from "@/shared/config";
+import {
+ AUTH_USER_ID,
+ CLAIM_NAME,
+ PORT,
+ PROXY_HOST,
+ SERVER_API_URL,
+ SITE_ID,
+} from "@/shared/config";
const serverApiUrl = process.env.SERVER_API_URL;
+const dcServiceApiUrl = process.env.DC_SERVICE_API_URL;
// CSP 헤더 설정 함수 (img-src만)
function addSecurityHeaders(response: NextResponse): NextResponse {
@@ -22,7 +30,15 @@ export function middleware(req: NextRequest) {
if (pathname.startsWith("/server")) {
const queryParams = searchParams.toString();
- const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${
+
+ let serverUrl: string | undefined;
+ if (pathname.includes("/dc-service")) {
+ serverUrl = dcServiceApiUrl;
+ } else {
+ serverUrl = serverApiUrl;
+ }
+
+ const newUrl = `${serverUrl}${pathname.replace("/server", "")}${
queryParams ? `?${queryParams}` : ""
}`;
@@ -34,7 +50,6 @@ export function middleware(req: NextRequest) {
return addSecurityHeaders(response);
}
-
// tus-js-client PATCH 요청: Location 헤더로 /server 없이 반환된 경로 직접 처리
// (예: /attach-service/api/v1/tus/upload/{id})
if (pathname.startsWith("/attach-service")) {
@@ -56,10 +71,10 @@ export function middleware(req: NextRequest) {
return addSecurityHeaders(response);
}
- // if (pathname === "/") {
- // const response = NextResponse.redirect(new URL("/dashboard", req.url));
- // return addSecurityHeaders(response);
- // }
+ if (pathname === "/") {
+ const response = NextResponse.redirect(new URL("/dashboard", req.url));
+ return response;
+ }
logMessages.push("\n--------------------------------\n");
console.log(logMessages.join("\n"));
diff --git a/apps/as/next.config.ts b/apps/as/next.config.ts
index d3afd4d70..5d4f22bce 100644
--- a/apps/as/next.config.ts
+++ b/apps/as/next.config.ts
@@ -1,9 +1,53 @@
import type { NextConfig } from "next";
+const subDomain = process.env.NEXT_PUBLIC_SUB_DOMAIN || "http://localhost:13000";
+
+const webhwpCtrlBase = (
+ process.env.NEXT_PUBLIC_WEBHYP_CTRL_URL || "http://hwp.onasys.co.kr/webhwpctrl"
+).replace(/\/$/, "");
+
+const webhwpAssetPrefixes = ["js", "hwpsdk", "dist", "commonFrame", "wasm"] as const;
+
const nextConfig: NextConfig = {
output: "standalone",
reactStrictMode: true,
transpilePackages: ["@common/assets"],
+ experimental: {
+ globalNotFound: true,
+ },
+
+ async rewrites() {
+ return {
+ beforeFiles: [
+ {
+ source: "/_next/static/:path*",
+ destination: `${subDomain}/_next/static/:path*`,
+ has: [
+ {
+ type: "header",
+ key: "referer",
+ value: "(.*)/cms(.*)",
+ },
+ ],
+ },
+ ],
+ afterFiles: [
+ {
+ source: "/cms/:path*",
+ destination: `${subDomain}/cms/:path*`,
+ },
+ ...webhwpAssetPrefixes.map((prefix) => ({
+ source: `/webhwpctrl/${prefix}/:path*`,
+ destination: `${webhwpCtrlBase}/${prefix}/:path*`,
+ })),
+ {
+ source: "/webhwpctrl/file/:path*",
+ destination: `${webhwpCtrlBase}/file/:path*`,
+ },
+ ],
+ fallback: [],
+ };
+ },
};
export default nextConfig;
diff --git a/apps/as/package.json b/apps/as/package.json
index cebdf28ee..a90c4bdc1 100644
--- a/apps/as/package.json
+++ b/apps/as/package.json
@@ -32,11 +32,11 @@
"next": "15.5.18",
"react-hook-form": "^7.54.2",
"tus-js-client": "^4.3.1",
- "uuid": "^14.0.1",
"zod": "^3.24.2"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
+ "@hookform/devtools": "^4.4.0",
"@types/js-cookie": "^3.0.6",
"eslint": "^9.0.0",
"eslint-config-next": "15.3.8",
diff --git a/apps/as/public/images/help.png b/apps/as/public/images/help.png
new file mode 100644
index 000000000..e872802e4
Binary files /dev/null and b/apps/as/public/images/help.png differ
diff --git a/apps/as/public/images/magnifying-glass-with-warning.png b/apps/as/public/images/magnifying-glass-with-warning.png
new file mode 100644
index 000000000..b184da207
Binary files /dev/null and b/apps/as/public/images/magnifying-glass-with-warning.png differ
diff --git a/apps/as/public/images/region_map.svg b/apps/as/public/images/region_map.svg
new file mode 100644
index 000000000..f327bb9fc
--- /dev/null
+++ b/apps/as/public/images/region_map.svg
@@ -0,0 +1,63 @@
+
\ No newline at end of file
diff --git a/apps/as/public/images/region_map_bg.png b/apps/as/public/images/region_map_bg.png
new file mode 100644
index 000000000..6f59f58cc
Binary files /dev/null and b/apps/as/public/images/region_map_bg.png differ
diff --git a/apps/as/public/webhwpctrl/hwpsdkmain.html b/apps/as/public/webhwpctrl/hwpsdkmain.html
new file mode 100644
index 000000000..3f4bc1526
--- /dev/null
+++ b/apps/as/public/webhwpctrl/hwpsdkmain.html
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+ Hwp SDK Wasm Sample
+
+
+
+
+
+
+
+
+
+
+
+ 구성 중 ... 0%
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/as/public/webhwpctrl/index.html b/apps/as/public/webhwpctrl/index.html
new file mode 100644
index 000000000..dcb8c3e45
--- /dev/null
+++ b/apps/as/public/webhwpctrl/index.html
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+ 웹한글 기안기 브리지
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/as/src/app/styles/globals.css b/apps/as/src/app/styles/globals.css
index e557f947b..bf1ecad16 100644
--- a/apps/as/src/app/styles/globals.css
+++ b/apps/as/src/app/styles/globals.css
@@ -6,10 +6,19 @@
@tailwind components;
@tailwind utilities;
-html,
-body {
- color: var(--foreground);
- background: var(--background);
- font-family: "Noto Sans KR", sans-serif;
- font-size: 10px;
+@layer base {
+ :root {
+ --secondary-background: rgba(0, 0, 0, 0.04);
+ }
+ [data-theme="black"] {
+ --secondary-background: rgba(255, 255, 255, 0.09);
+ }
+
+ html,
+ body {
+ color: var(--foreground);
+ background: var(--background);
+ font-family: "Noto Sans KR", sans-serif;
+ font-size: 10px;
+ }
}
diff --git a/apps/as/src/entities/adstdstr/api/adstdstr-service.ts b/apps/as/src/entities/adstdstr/api/adstdstr-service.ts
index b4bb11fae..f282ea67b 100644
--- a/apps/as/src/entities/adstdstr/api/adstdstr-service.ts
+++ b/apps/as/src/entities/adstdstr/api/adstdstr-service.ts
@@ -13,6 +13,11 @@ async function findSidoCombo(): Promise {
return response.data;
}
+async function findUserSidoCombo(): Promise {
+ const response = await client.get(`${BASE}/user/sido`);
+ return response.data;
+}
+
async function findGugunCombo(sido: string): Promise {
const response = await client.get(`${BASE}/gugun`, {
params: { sido },
@@ -20,7 +25,16 @@ async function findGugunCombo(sido: string): Promise {
+ const response = await client.get(`${BASE}/user/gugun`, {
+ params: { sido },
+ });
+ return response.data;
+}
+
export const cmCdAdstDstrService = {
findSidoCombo,
+ findUserSidoCombo,
findGugunCombo,
+ findUserGugunCombo,
} satisfies CmCdAdstDstrService;
diff --git a/apps/as/src/entities/adstdstr/api/queries.ts b/apps/as/src/entities/adstdstr/api/queries.ts
index 353e25cfe..2556ec6db 100644
--- a/apps/as/src/entities/adstdstr/api/queries.ts
+++ b/apps/as/src/entities/adstdstr/api/queries.ts
@@ -1,6 +1,7 @@
import { useQuery } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
+import { IUserInfo } from "@/shared/store";
import { cmCdAdstDstrService } from "./adstdstr-service";
@@ -13,6 +14,16 @@ export function useFindSidoCombo() {
});
}
+export function useFindUserSidoCombo(userInfo?: IUserInfo | null) {
+ const adstDstrCdByUser = userInfo?.laeSeCd?.substring(2, 4);
+ return useQuery({
+ queryKey: [...cmCdAdstDstrBaseKey, "findUserSidoCombo", adstDstrCdByUser ?? "all"],
+ queryFn: () => cmCdAdstDstrService.findUserSidoCombo(),
+ select: (data) =>
+ adstDstrCdByUser ? data.filter((item) => item.adstDstrCd === adstDstrCdByUser) : data,
+ });
+}
+
export function useFindGugunCombo(sido: string) {
return useQuery({
queryKey: [...cmCdAdstDstrBaseKey, "findGugunCombo", sido],
@@ -20,3 +31,11 @@ export function useFindGugunCombo(sido: string) {
enabled: Boolean(sido),
});
}
+
+export function useFindUserGugunCombo(sido: string) {
+ return useQuery({
+ queryKey: [...cmCdAdstDstrBaseKey, "findUserGugunCombo", sido],
+ queryFn: () => cmCdAdstDstrService.findUserGugunCombo(sido),
+ enabled: Boolean(sido),
+ });
+}
diff --git a/apps/as/src/entities/adstdstr/index.ts b/apps/as/src/entities/adstdstr/index.ts
index da1dbd132..2fffe5a79 100644
--- a/apps/as/src/entities/adstdstr/index.ts
+++ b/apps/as/src/entities/adstdstr/index.ts
@@ -1,5 +1,10 @@
export { cmCdAdstDstrService } from "./api/adstdstr-service";
-export { useFindGugunCombo, useFindSidoCombo } from "./api/queries";
+export {
+ useFindGugunCombo,
+ useFindSidoCombo,
+ useFindUserGugunCombo,
+ useFindUserSidoCombo,
+} from "./api/queries";
export type {
CmCdAdstDstrComboResponseDto,
CmCdAdstDstrService,
diff --git a/apps/as/src/entities/adstdstr/model/adstdstr-interface.ts b/apps/as/src/entities/adstdstr/model/adstdstr-interface.ts
index 72cb673dc..1eaa14bff 100644
--- a/apps/as/src/entities/adstdstr/model/adstdstr-interface.ts
+++ b/apps/as/src/entities/adstdstr/model/adstdstr-interface.ts
@@ -7,5 +7,7 @@ export interface CmCdAdstDstrComboResponseDto {
/** Controller public 메서드명·시그니처와 동일 */
export interface CmCdAdstDstrService {
findSidoCombo(): Promise;
+ findUserSidoCombo(): Promise;
findGugunCombo(sido: string): Promise;
+ findUserGugunCombo(sido: string): Promise;
}
diff --git a/apps/as/src/entities/ai-auto-prcs-mng/api/ai-auto-prcs-mng-service.ts b/apps/as/src/entities/ai-auto-prcs-mng/api/ai-auto-prcs-mng-service.ts
new file mode 100644
index 000000000..124dde1dd
--- /dev/null
+++ b/apps/as/src/entities/ai-auto-prcs-mng/api/ai-auto-prcs-mng-service.ts
@@ -0,0 +1,24 @@
+import { client } from "@/shared/api";
+import { ENDPOINT } from "@/shared/config";
+
+import type {
+ AiAutoPrcsLatestResponseDto,
+ AiAutoPrcsMngService,
+} from "../model/ai-auto-prcs-mng-interface";
+
+const BASE = ENDPOINT.AS_SERVICE.AI_AUTO_PRCS_MNG;
+
+async function findLatestPerAutoSeByFnrsDocNo(
+ fnrsCnsltnDocNo: string,
+ atmtnSeComCd?: string,
+): Promise {
+ const response = await client.get(
+ `${BASE}/fnrs/${fnrsCnsltnDocNo}/latest`,
+ { params: atmtnSeComCd ? { atmtnSeComCd } : undefined },
+ );
+ return response.data;
+}
+
+export const aiAutoPrcsMngService = {
+ findLatestPerAutoSeByFnrsDocNo,
+} satisfies AiAutoPrcsMngService;
diff --git a/apps/as/src/entities/ai-auto-prcs-mng/api/queries.ts b/apps/as/src/entities/ai-auto-prcs-mng/api/queries.ts
new file mode 100644
index 000000000..fe97be2db
--- /dev/null
+++ b/apps/as/src/entities/ai-auto-prcs-mng/api/queries.ts
@@ -0,0 +1,30 @@
+import { useQuery } from "@tanstack/react-query";
+
+import { ENDPOINT } from "@/shared/config";
+
+import { aiAutoPrcsMngService } from "./ai-auto-prcs-mng-service";
+
+export const aiAutoPrcsMngKeys = {
+ all: [ENDPOINT.AS_SERVICE.AI_AUTO_PRCS_MNG] as const,
+ latest: (fnrsCnsltnDocNo: string, atmtnSeComCd?: string) =>
+ [...aiAutoPrcsMngKeys.all, "latest", fnrsCnsltnDocNo, atmtnSeComCd ?? "all"] as const,
+};
+
+interface UseAiAutoPrcsLatestOptions {
+ enabled?: boolean;
+ atmtnSeComCd?: string;
+}
+
+export function useAiAutoPrcsLatest(
+ fnrsCnsltnDocNo: string | null | undefined,
+ options: UseAiAutoPrcsLatestOptions = {},
+) {
+ const { enabled = true, atmtnSeComCd } = options;
+
+ return useQuery({
+ queryKey: aiAutoPrcsMngKeys.latest(fnrsCnsltnDocNo!, atmtnSeComCd),
+ queryFn: () =>
+ aiAutoPrcsMngService.findLatestPerAutoSeByFnrsDocNo(fnrsCnsltnDocNo!, atmtnSeComCd),
+ enabled: Boolean(fnrsCnsltnDocNo) && enabled,
+ });
+}
diff --git a/apps/as/src/entities/ai-auto-prcs-mng/index.ts b/apps/as/src/entities/ai-auto-prcs-mng/index.ts
new file mode 100644
index 000000000..0668a4e0c
--- /dev/null
+++ b/apps/as/src/entities/ai-auto-prcs-mng/index.ts
@@ -0,0 +1,8 @@
+export { aiAutoPrcsMngService } from "./api/ai-auto-prcs-mng-service";
+export { aiAutoPrcsMngKeys, useAiAutoPrcsLatest } from "./api/queries";
+export type {
+ AiAutoPrcsLatestResponseDto,
+ AiAutoPrcsMngService,
+ AiAutoSeComCd,
+} from "./model/ai-auto-prcs-mng-interface";
+export { AI_AUTO_SE } from "./model/ai-auto-prcs-mng-interface";
diff --git a/apps/as/src/entities/ai-auto-prcs-mng/model/ai-auto-prcs-mng-interface.ts b/apps/as/src/entities/ai-auto-prcs-mng/model/ai-auto-prcs-mng-interface.ts
new file mode 100644
index 000000000..ad833b3c6
--- /dev/null
+++ b/apps/as/src/entities/ai-auto-prcs-mng/model/ai-auto-prcs-mng-interface.ts
@@ -0,0 +1,44 @@
+/** 자동화구분 공통코드 (AIAUTOSE) */
+export const AI_AUTO_SE = {
+ FILE_VRFC: "FILE_VRFC",
+ DRAFT_GEN: "DRAFT_GEN",
+ COST_VRFC: "COST_VRFC",
+} as const;
+
+export type AiAutoSeComCd = (typeof AI_AUTO_SE)[keyof typeof AI_AUTO_SE];
+
+/** 백엔드 AiAutoPrcsLatestResponseDto */
+export interface AiAutoPrcsLatestResponseDto {
+ atmtnPrcsUuid?: string;
+ fnrsCnsltnDocNo?: string;
+ pwrkCnsltnDocNo?: string;
+ crtDmndUuid?: string;
+ atmtnSeComCd?: string;
+ atmtnSeComNm?: string;
+ bfrDocSttsComCd?: string;
+ bfrDocSttsComNm?: string;
+ aftrDocSttsComCd?: string;
+ aftrDocSttsComNm?: string;
+ atmtnPrcsSttsComCd?: string;
+ atmtnPrcsSttsComNm?: string;
+ dmndDt?: string;
+ dmndSysId?: string;
+ rplyDt?: string;
+ rplyRsltComCd?: string;
+ prcsErrCn?: string | null;
+ lastSttsInqDt?: string;
+ sttsInqRsltComCd?: string;
+ rtrsmCtpvNmtm?: number;
+ lastRtrsmDt?: string;
+ lastFrcSttsChgYn?: string;
+ isFailed?: boolean;
+ isRetryable?: boolean;
+}
+
+/** Controller public 메서드명·시그니처와 동일 */
+export interface AiAutoPrcsMngService {
+ findLatestPerAutoSeByFnrsDocNo(
+ fnrsCnsltnDocNo: string,
+ atmtnSeComCd?: string,
+ ): Promise;
+}
diff --git a/apps/as/src/entities/attach/api/attach-service.ts b/apps/as/src/entities/attach/api/attach-service.ts
index 0af0a916d..2cb1fc35d 100644
--- a/apps/as/src/entities/attach/api/attach-service.ts
+++ b/apps/as/src/entities/attach/api/attach-service.ts
@@ -3,7 +3,13 @@ import { AxiosResponse } from "axios";
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
-import type { AttachService } from "../model/attach-interface";
+import type { Attach, AttachService } from "../model/attach-interface";
+
+async function findByAtflCd(atflCd: string): Promise {
+ if (!atflCd) return [];
+ const response = await client.get(`${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}/${atflCd}`);
+ return response.data;
+}
async function deleteAttachmentByAtflId(atflId: string): Promise {
if (!atflId) return;
@@ -16,7 +22,15 @@ async function downloadAttachmentByAtflCd(atflCd: string): Promise {
+ return await client.get(`${ENDPOINT.ATTACH_SERVICE.DOWNLOAD_BY_ATFL_ID}/${atflId}`, {
+ responseType: "blob",
+ });
+}
+
export const attachService = {
+ findByAtflCd,
deleteAttachmentByAtflId,
downloadAttachmentByAtflCd,
+ downloadAttachmentByAtflId,
} satisfies AttachService;
diff --git a/apps/as/src/entities/attach/api/queries.ts b/apps/as/src/entities/attach/api/queries.ts
index ed9efefbf..09a1ab1d9 100644
--- a/apps/as/src/entities/attach/api/queries.ts
+++ b/apps/as/src/entities/attach/api/queries.ts
@@ -1,4 +1,5 @@
import { useMutation } from "@tanstack/react-query";
+import type { AxiosResponse } from "axios";
import { ENDPOINT } from "@/shared/config";
@@ -8,6 +9,22 @@ import { attachService } from "./attach-service";
const attachBaseKey = [ENDPOINT.ATTACH_SERVICE.ATTACHMENTS] as const;
const attachDownloadBaseKey = [ENDPOINT.ATTACH_SERVICE.DOWNLOAD] as const;
+const attachDownloadByAtflIdKey = [ENDPOINT.ATTACH_SERVICE.DOWNLOAD_BY_ATFL_ID] as const;
+
+function triggerBlobDownload(response: AxiosResponse) {
+ const filename = parseFilenameFromResponse(response);
+ const blob = new Blob([response.data]);
+ const url = window.URL.createObjectURL(blob);
+ const link = document.createElement("a");
+
+ link.href = url;
+ link.download = filename;
+ document.body.appendChild(link);
+ link.click();
+
+ window.URL.revokeObjectURL(url);
+ document.body.removeChild(link);
+}
export function useDeleteAttachment() {
return useMutation({
@@ -21,19 +38,17 @@ export function useDownloadAttachmentByAtflCd() {
mutationKey: [...attachDownloadBaseKey, "download"],
mutationFn: async (atflCd: string) => {
const response = await attachService.downloadAttachmentByAtflCd(atflCd);
- const filename = parseFilenameFromResponse(response);
-
- const blob = new Blob([response.data]);
- const url = window.URL.createObjectURL(blob);
- const link = document.createElement("a");
-
- link.href = url;
- link.download = filename;
- document.body.appendChild(link);
- link.click();
-
- window.URL.revokeObjectURL(url);
- document.body.removeChild(link);
+ triggerBlobDownload(response);
+ },
+ });
+}
+
+export function useDownloadAttachmentByAtflId() {
+ return useMutation({
+ mutationKey: [...attachDownloadByAtflIdKey, "download"],
+ mutationFn: async (atflId: string) => {
+ const response = await attachService.downloadAttachmentByAtflId(atflId);
+ triggerBlobDownload(response);
},
});
}
diff --git a/apps/as/src/entities/attach/index.ts b/apps/as/src/entities/attach/index.ts
index 37258e6e5..69b220102 100644
--- a/apps/as/src/entities/attach/index.ts
+++ b/apps/as/src/entities/attach/index.ts
@@ -1,3 +1,12 @@
export { attachService } from "./api/attach-service";
-export { useDeleteAttachment, useDownloadAttachmentByAtflCd } from "./api/queries";
+export {
+ useDeleteAttachment,
+ useDownloadAttachmentByAtflCd,
+ useDownloadAttachmentByAtflId,
+} from "./api/queries";
+export { parseFilenameFromResponse } from "./lib/parse-filename-from-response";
export type { Attach, AttachService } from "./model/attach-interface";
+export {
+ AttachmentDownloadLink,
+ type AttachmentDownloadLinkProps,
+} from "./ui/attachment-download-link";
diff --git a/apps/as/src/entities/attach/model/attach-interface.ts b/apps/as/src/entities/attach/model/attach-interface.ts
index d6b0a08d2..ef299a43c 100644
--- a/apps/as/src/entities/attach/model/attach-interface.ts
+++ b/apps/as/src/entities/attach/model/attach-interface.ts
@@ -17,6 +17,8 @@ export interface Attach {
}
export interface AttachService {
+ findByAtflCd: (atflCd: string) => Promise;
deleteAttachmentByAtflId: (atflId: string) => Promise;
downloadAttachmentByAtflCd: (atflCd: string) => Promise;
+ downloadAttachmentByAtflId: (atflId: string) => Promise;
}
diff --git a/apps/as/src/entities/attach/ui/attachment-download-link.tsx b/apps/as/src/entities/attach/ui/attachment-download-link.tsx
new file mode 100644
index 000000000..85aab6ec5
--- /dev/null
+++ b/apps/as/src/entities/attach/ui/attachment-download-link.tsx
@@ -0,0 +1,54 @@
+"use client";
+
+import { Paperclip } from "lucide-react";
+
+import { cn } from "@common/components";
+
+import { useDownloadAttachmentByAtflCd, useDownloadAttachmentByAtflId } from "../api/queries";
+
+interface AttachmentDownloadLinkProps {
+ atflCd?: string;
+ atflId?: string;
+ fileName?: string;
+ className?: string;
+}
+
+function AttachmentDownloadLink({
+ atflCd,
+ atflId,
+ fileName,
+ className,
+}: AttachmentDownloadLinkProps) {
+ const downloadByAtflCd = useDownloadAttachmentByAtflCd();
+ const downloadByAtflId = useDownloadAttachmentByAtflId();
+ const displayName = fileName || "첨부파일";
+ const canDownload = Boolean(atflId || atflCd);
+ const isPending = atflId ? downloadByAtflId.isPending : downloadByAtflCd.isPending;
+
+ function handleClick() {
+ if (atflId) {
+ downloadByAtflId.mutate(atflId);
+ return;
+ }
+ if (atflCd) downloadByAtflCd.mutate(atflCd);
+ }
+
+ return (
+
+ );
+}
+
+export { AttachmentDownloadLink };
+export type { AttachmentDownloadLinkProps };
diff --git a/apps/as/src/entities/auth/api/auth-service.ts b/apps/as/src/entities/auth/api/auth-service.ts
index 5615f55f8..d84f84776 100644
--- a/apps/as/src/entities/auth/api/auth-service.ts
+++ b/apps/as/src/entities/auth/api/auth-service.ts
@@ -1,8 +1,12 @@
import { ACCESS_TOKEN, AUTH_USER_ID, CLAIM_NAME, ENDPOINT } from "@/shared/config";
-import { type AuthPayload, type AuthService, getRemainingMs } from "@common/auth";
+import {
+ type AuthPayload,
+ type AuthService,
+ clearDeviceId,
+ getOrCreateDeviceId,
+} from "@common/auth";
-import { clearDeviceId, getOrCreateDeviceId } from "../lib/device-id";
-import { clearRefreshTimer, setToken, setupRefreshTimer, tokenManager } from "../lib/token-manager";
+import { clearRefreshTimer, setToken, tokenManager } from "../lib/token-manager";
async function login(payload: AuthPayload): Promise {
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, {
@@ -26,10 +30,10 @@ async function login(payload: AuthPayload): Promise {
setToken(CLAIM_NAME, accessToken);
setToken(AUTH_USER_ID, tokenId);
- const remainingMs = getRemainingMs(accessToken);
- if (remainingMs > 0) {
- setupRefreshTimer(silentRefresh, remainingMs);
- }
+ // const remainingMs = getRemainingMs(accessToken);
+ // if (remainingMs > 0) {
+ // setupRefreshTimer(silentRefresh, remainingMs);
+ // }
}
async function silentRefresh(): Promise {
@@ -58,10 +62,10 @@ async function silentRefresh(): Promise {
setToken(CLAIM_NAME, accessToken);
setToken(AUTH_USER_ID, tokenId);
- const remainingMs = getRemainingMs(accessToken);
- if (remainingMs > 0) {
- setupRefreshTimer(silentRefresh, remainingMs);
- }
+ // const remainingMs = getRemainingMs(accessToken);
+ // if (remainingMs > 0) {
+ // setupRefreshTimer(silentRefresh, remainingMs);
+ // }
} catch (error) {
console.error(`[silentRefresh] Failed at ${new Date().toISOString()}`, error);
tokenManager.clearAuthToken();
@@ -72,6 +76,7 @@ async function silentRefresh(): Promise {
}
async function logout(): Promise {
+ await fetch("/api/auth/logout", { method: "POST" });
tokenManager.clearAuthToken();
clearDeviceId();
diff --git a/apps/as/src/entities/auth/lib/device-id.ts b/apps/as/src/entities/auth/lib/device-id.ts
deleted file mode 100644
index ce173b141..000000000
--- a/apps/as/src/entities/auth/lib/device-id.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { v4 as uuidv4 } from "uuid";
-
-const DEVICE_ID_KEY = "x-device-id";
-
-export function getOrCreateDeviceId(): string {
- if (typeof window === "undefined") return "";
- const existing = localStorage.getItem(DEVICE_ID_KEY);
- if (existing) return existing;
- const id = uuidv4();
- localStorage.setItem(DEVICE_ID_KEY, id);
- return id;
-}
-
-export function clearDeviceId(): void {
- if (typeof window === "undefined") return;
- localStorage.removeItem(DEVICE_ID_KEY);
-}
diff --git a/apps/as/src/entities/bns-if/api/bns-if-service.ts b/apps/as/src/entities/bns-if/api/bns-if-service.ts
index 320a01efc..316ea041c 100644
--- a/apps/as/src/entities/bns-if/api/bns-if-service.ts
+++ b/apps/as/src/entities/bns-if/api/bns-if-service.ts
@@ -8,6 +8,8 @@ import type {
BnsListResponseWrapperDto,
BnsSearchDto,
EmCodeListResponseDto,
+ FacilityListResponseWrapperDto,
+ FacilitySearchDto,
} from "../model/bns-if-interface";
const BASE = ENDPOINT.AS_SERVICE.BNS_IF;
@@ -40,8 +42,18 @@ async function getEmCodeList(
return response.data;
}
+async function getFacilityList(
+ params: FacilitySearchDto,
+): Promise {
+ const response = await client.get(`${BASE}/facilityList`, {
+ params,
+ });
+ return response.data;
+}
+
export const bnsIfService = {
getBnsList,
getBnsDetail,
getEmCodeList,
+ getFacilityList,
} satisfies BnsIfService;
diff --git a/apps/as/src/entities/bns-if/api/queries.ts b/apps/as/src/entities/bns-if/api/queries.ts
index 7ab0b46a3..8edf93b0c 100644
--- a/apps/as/src/entities/bns-if/api/queries.ts
+++ b/apps/as/src/entities/bns-if/api/queries.ts
@@ -2,7 +2,7 @@ import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
-import type { BnsSearchDto } from "../model/bns-if-interface";
+import type { BnsSearchDto, FacilitySearchDto } from "../model/bns-if-interface";
import { bnsIfService } from "./bns-if-service";
@@ -31,3 +31,12 @@ export function useEmCodeList(dpmCd?: string, lclgvrnmntCd?: string) {
queryFn: () => bnsIfService.getEmCodeList(dpmCd, lclgvrnmntCd),
});
}
+
+export function useFacilityList(params: FacilitySearchDto, options?: { enabled?: boolean }) {
+ return useQuery({
+ queryKey: [...bnsIfBaseKey, "getFacilityList", params],
+ queryFn: () => bnsIfService.getFacilityList(params),
+ enabled: options?.enabled,
+ placeholderData: keepPreviousData,
+ });
+}
diff --git a/apps/as/src/entities/bns-if/index.ts b/apps/as/src/entities/bns-if/index.ts
index 430f4a611..d01b27189 100644
--- a/apps/as/src/entities/bns-if/index.ts
+++ b/apps/as/src/entities/bns-if/index.ts
@@ -1,5 +1,5 @@
export { bnsIfService } from "./api/bns-if-service";
-export { useBnsDetail, useBnsList, useEmCodeList } from "./api/queries";
+export { useBnsDetail, useBnsList, useEmCodeList, useFacilityList } from "./api/queries";
export { buildBnsSearchDto } from "./lib/build-bns-search-dto";
export type {
BnsDetailResponseDto,
@@ -10,4 +10,7 @@ export type {
BnsSearchDto,
EmCodeItemDto,
EmCodeListResponseDto,
+ FacilityListResponseWrapperDto,
+ FacilityResponseDto,
+ FacilitySearchDto,
} from "./model/bns-if-interface";
diff --git a/apps/as/src/entities/bns-if/model/bns-if-interface.ts b/apps/as/src/entities/bns-if/model/bns-if-interface.ts
index e13ab8500..833accca8 100644
--- a/apps/as/src/entities/bns-if/model/bns-if-interface.ts
+++ b/apps/as/src/entities/bns-if/model/bns-if-interface.ts
@@ -117,9 +117,89 @@ export interface EmCodeListResponseDto {
ddtlList: EmCodeItemDto[];
}
+/** 백엔드 FacilitySearchDto */
+export interface FacilitySearchDto {
+ sido?: string;
+ gugun?: string;
+}
+
+/** 백엔드 FacilityResponseDto */
+export interface FacilityResponseDto {
+ bscFctCd?: string;
+ bscFctNm?: string;
+ addr?: string;
+ fctCpc?: string;
+ fctCpc1?: string;
+ fctCpc2?: string;
+ fctCpc3?: string;
+ totInflAmntAve?: unknown;
+ pInflAmntAve?: unknown;
+ oInflAmntAve?: unknown;
+ aInflAmntAve?: unknown;
+ totTputAve?: unknown;
+ pTputAve?: unknown;
+ oTputAve?: unknown;
+ aTputAve?: unknown;
+ inflAssctBfrWqltBod?: unknown;
+ efflWqltBod?: unknown;
+ inflAssctBfrWqltCod?: unknown;
+ efflWqltCod?: unknown;
+ inflAssctBfrWqltSs?: unknown;
+ efflWqltSs?: unknown;
+ inflAssctBfrWqltTn?: unknown;
+ efflWqltTn?: unknown;
+ inflAssctBfrWqltTp?: unknown;
+ efflWqltTp?: unknown;
+ inflAssctBfrWqltClfgrpcnt?: unknown;
+ efflWqltClfgrpcnt?: unknown;
+ tmEfficiency?: unknown;
+ pcLoad?: unknown;
+ trtFrfHanNm?: string;
+ applctNtcheNm?: string;
+ excretionsTot?: unknown;
+ stkrsngWstwtrTot?: unknown;
+ lchtTot?: unknown;
+ etcTot?: unknown;
+ tel?: string;
+ rnDt?: string;
+ compDt?: string;
+ tot?: unknown;
+ oprClsNm?: string;
+ trstOprEtpNm?: string;
+ yrstpTrstCst?: string;
+ trstOprDt?: string;
+ trstAfr?: string;
+ dsfctNm?: string;
+ rvNm?: string;
+ mnstr?: string;
+ wbsnNm?: string;
+ efflWqltStndrdNm?: string;
+ elcpwAmnt?: unknown;
+ toeRt?: unknown;
+ toeSum?: unknown;
+ co2Emissions?: unknown;
+ toeQnty?: unknown;
+ toeRt2?: unknown;
+ useQnty?: unknown;
+ eqpPrmsDt?: string;
+ mdmbsn?: string;
+ lgbsn?: string;
+ lgbsnBod?: string;
+ cntrTrtFctNm?: string;
+ totWtrQrm?: unknown;
+ inflAssctBfrWqltToc?: unknown;
+ efflWqltToc?: unknown;
+}
+
+/** 백엔드 FacilityListResponseWrapperDto */
+export interface FacilityListResponseWrapperDto {
+ fctList: FacilityResponseDto[];
+}
+
/** Controller public 메서드명·시그니처와 동일 */
export interface BnsIfService {
getBnsList(params: BnsSearchDto): Promise;
getBnsDetail(gbn: string, yr: string, seqNo: string): Promise;
getEmCodeList(dpmCd?: string, lclgvrnmntCd?: string): Promise;
+ getFacilityList(params: FacilitySearchDto): Promise;
}
diff --git a/apps/as/src/entities/board/api/post-queries.ts b/apps/as/src/entities/board/api/post-queries.ts
new file mode 100644
index 000000000..4cf801e0a
--- /dev/null
+++ b/apps/as/src/entities/board/api/post-queries.ts
@@ -0,0 +1,15 @@
+import { useQuery } from "@tanstack/react-query";
+
+import { ENDPOINT } from "@/shared/config";
+
+import type { UserPostSearchParams } from "../model/post-interface";
+
+import { postApi } from "./post-service";
+
+export function useUserPostList(params?: UserPostSearchParams) {
+ return useQuery({
+ queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, params],
+ queryFn: () => postApi.getUserPostList(params),
+ refetchOnWindowFocus: false,
+ });
+}
diff --git a/apps/as/src/entities/board/api/post-service.ts b/apps/as/src/entities/board/api/post-service.ts
new file mode 100644
index 000000000..9cd25f976
--- /dev/null
+++ b/apps/as/src/entities/board/api/post-service.ts
@@ -0,0 +1,16 @@
+import { client } from "@/shared/api";
+import { AS_DASHBOARD_BBS_ID, ENDPOINT } from "@/shared/config";
+
+import { PostService, UserPostListResponse, UserPostSearchParams } from "../model/post-interface";
+
+async function getUserPostList(params?: UserPostSearchParams): Promise {
+ const response = await client.get(
+ `${ENDPOINT.BOARD_SERVICE.POSTS}/user/${AS_DASHBOARD_BBS_ID}`,
+ { params },
+ );
+ return response.data;
+}
+
+export const postApi: PostService = {
+ getUserPostList,
+};
diff --git a/apps/as/src/entities/board/index.ts b/apps/as/src/entities/board/index.ts
new file mode 100644
index 000000000..2bec23972
--- /dev/null
+++ b/apps/as/src/entities/board/index.ts
@@ -0,0 +1,8 @@
+export { useUserPostList } from "./api/post-queries";
+export { postApi } from "./api/post-service";
+export type {
+ PostService,
+ UserPost,
+ UserPostListResponse,
+ UserPostSearchParams,
+} from "./model/post-interface";
diff --git a/apps/as/src/entities/board/model/post-interface.ts b/apps/as/src/entities/board/model/post-interface.ts
new file mode 100644
index 000000000..31eca4e64
--- /dev/null
+++ b/apps/as/src/entities/board/model/post-interface.ts
@@ -0,0 +1,40 @@
+import type { PageableParams, PageResponse } from "@/shared/model";
+
+export interface UserPostSearchParams extends PageableParams {
+ keywordType?: string;
+ keyword?: string;
+ popupYn?: string;
+ upndFixYn?: string;
+ prdPstgYn?: string;
+ siteId?: string;
+ bbsTypeCd?: string;
+ ansUseYn?: string;
+}
+
+export interface UserPost {
+ rowNo: number;
+ bbsId: number;
+ pstNo: number;
+ pstTtl: string;
+ pstCn: string;
+ inqNocs: number;
+ rfrncUrl: string;
+ popupYn: string;
+ upndFixYn: string;
+ prdPstgYn: string;
+ rootPath: string;
+ pstPath: string;
+ ansPstn: number;
+ wrtrNm: string;
+ wrtrCplc: string;
+ atflCd: string;
+ regDt: string;
+ rgtrNm: string;
+ cmntCnt: number;
+}
+
+export type UserPostListResponse = PageResponse;
+
+export interface PostService {
+ getUserPostList(params?: UserPostSearchParams): Promise;
+}
diff --git a/apps/as/src/entities/code/ui/code-select.tsx b/apps/as/src/entities/code/ui/code-select.tsx
index e803e9285..74d011bcd 100644
--- a/apps/as/src/entities/code/ui/code-select.tsx
+++ b/apps/as/src/entities/code/ui/code-select.tsx
@@ -18,13 +18,7 @@ interface CodeSelectProps extends React.ComponentPropsWithoutRef
className?: string;
}
-function CodeSelect({
- upCd,
- useYn,
- placeholder = "선택",
- className,
- ...props
-}: CodeSelectProps) {
+function CodeSelect({ upCd, useYn, placeholder = "선택", className, ...props }: CodeSelectProps) {
const listQuery = useCodeList(useYn ? undefined : upCd);
const listByUseYnQuery = useCodeListByUseYn(useYn ? upCd : undefined, useYn ?? "");
@@ -34,7 +28,7 @@ function CodeSelect({
return (
- {aplnQuery.isPending && elaprTmpltId ? (
+ {isLoading ? (
결재서식을 선택하세요.
- ) : visibleRows.length === 0 ? (
-
등록된 결재선이 없습니다. [결재선 추가]로 결재자를 등록하세요.
) : (
@@ -161,10 +175,10 @@ function ElaprAplnPickerGrid({