Merge branch 'refs/heads/main' into feature/0612_sentry_fr_merge
This commit is contained in:
@@ -19,11 +19,15 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@common/components": ["../../packages/common/components/dist/"],
|
||||
"@common/network": ["../../packages/common/network/"]
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
"../../packages/common/types/**/*.d.ts"
|
||||
],
|
||||
"exclude": ["node_modules", ".next", "dist", "coverage", "storybook-static", "public"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import "@/app/styles/globals.css";
|
||||
|
||||
export const metadata = {
|
||||
title: "디지털 행정지원 시스템",
|
||||
};
|
||||
|
||||
export default function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="ko">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { DesignRegistPage } from "@/pages/design-cnstn";
|
||||
import { parseTaskSeComCdFromApi } from "@/pages/design-cnstn/lib/design-cnstn-regist-schema";
|
||||
|
||||
interface PageProps {
|
||||
searchParams: Promise<{
|
||||
taskSeComCd?: string;
|
||||
admdstCd?: string;
|
||||
sido?: string;
|
||||
gugun?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export default async function Page({ searchParams }: PageProps) {
|
||||
const params = await searchParams;
|
||||
const taskSeComCd = parseTaskSeComCdFromApi(params.taskSeComCd);
|
||||
|
||||
return (
|
||||
<DesignRegistPage
|
||||
defaults={{
|
||||
taskSeComCd: taskSeComCd.length ? taskSeComCd : undefined,
|
||||
admdstCd: params.admdstCd,
|
||||
sido: params.sido,
|
||||
gugun: params.gugun,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { FnrsRegistPage } from "@/pages/fnrs-cnsltn";
|
||||
import { parseTaskSeComCdFromApi } from "@/pages/fnrs-cnsltn/lib/fnrs-cnsltn-regist-schema";
|
||||
|
||||
interface PageProps {
|
||||
searchParams: Promise<{
|
||||
taskSeComCd?: string;
|
||||
admdstCd?: string;
|
||||
sido?: string;
|
||||
gugun?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export default async function Page({ searchParams }: PageProps) {
|
||||
const params = await searchParams;
|
||||
const taskSeComCd = parseTaskSeComCdFromApi(params.taskSeComCd);
|
||||
|
||||
return (
|
||||
<FnrsRegistPage
|
||||
defaults={{
|
||||
taskSeComCd: taskSeComCd.length ? taskSeComCd : undefined,
|
||||
admdstCd: params.admdstCd,
|
||||
sido: params.sido,
|
||||
gugun: params.gugun,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { InstlRegistPage } from "@/pages/instl-sncn";
|
||||
|
||||
interface InstlSncnRegistRouteProps {
|
||||
searchParams: Promise<{
|
||||
taskSeComCd?: string;
|
||||
admdstCd?: string;
|
||||
sido?: string;
|
||||
gugun?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export default async function Page({ searchParams }: InstlSncnRegistRouteProps) {
|
||||
const params = await searchParams;
|
||||
const taskSeComCd = params.taskSeComCd
|
||||
? params.taskSeComCd.split(",").filter(Boolean)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<InstlRegistPage
|
||||
defaults={{
|
||||
taskSeComCd,
|
||||
admdstCd: params.admdstCd,
|
||||
sido: params.sido,
|
||||
gugun: params.gugun,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { SewerBscPlanEditPage } from "@/pages/sewer-bsc-plan";
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
searchParams: Promise<{ chgCycl?: string }>;
|
||||
}
|
||||
|
||||
export default async function Page({ params, searchParams }: PageProps) {
|
||||
const { id } = await params;
|
||||
const { chgCycl } = await searchParams;
|
||||
const propChgCycl = chgCycl ? Number(chgCycl) : undefined;
|
||||
return <SewerBscPlanEditPage id={id} propChgCycl={propChgCycl} />;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import Link from "next/link";
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default async function Page({ params }: PageProps) {
|
||||
const { id } = await params;
|
||||
return (
|
||||
<div className="mt-6 px-4 text-[1.4rem]">
|
||||
하수도기본계획 상세({id})는 후속 구현 예정입니다.{" "}
|
||||
<Link className="underline" href={`/sewer-bsc-plan/${id}/edit`}>
|
||||
수정 화면으로 이동
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { SewerBscPlanListPage } from "@/pages/sewer-bsc-plan";
|
||||
|
||||
export default SewerBscPlanListPage;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { SewerBscPlanRegistPage } from "@/pages/sewer-bsc-plan";
|
||||
|
||||
interface PageProps {
|
||||
searchParams: Promise<{
|
||||
taskSeComCd?: string;
|
||||
taskSeCdNm?: string;
|
||||
sewerBscPlanSeComCd?: string;
|
||||
sewerBscPlanSeCdNm?: string;
|
||||
admdstCd?: string;
|
||||
sido?: string;
|
||||
gugun?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export default async function Page({ searchParams }: PageProps) {
|
||||
const params = await searchParams;
|
||||
|
||||
return <SewerBscPlanRegistPage defaults={params} />;
|
||||
}
|
||||
@@ -6,8 +6,8 @@ import { Providers } from "@/app/providers";
|
||||
import "@/app/styles/globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "행정지원시스템",
|
||||
description: "행정지원시스템",
|
||||
title: "디지털 행정지원 시스템",
|
||||
description: "디지털 행정지원 시스템",
|
||||
};
|
||||
|
||||
interface RootLayoutProps {
|
||||
@@ -1,12 +0,0 @@
|
||||
import { DesignRegistPage } from "@/pages/design-cnstn";
|
||||
import { parseTaskSeComCdFromApi } from "@/pages/design-cnstn/lib/design-cnstn-regist-schema";
|
||||
|
||||
interface PageProps {
|
||||
searchParams: Promise<{ taskSeComCd?: string }>;
|
||||
}
|
||||
|
||||
export default async function Page({ searchParams }: PageProps) {
|
||||
const params = await searchParams;
|
||||
const taskSeComCd = parseTaskSeComCdFromApi(params.taskSeComCd);
|
||||
return <DesignRegistPage taskSeComCd={taskSeComCd.length ? taskSeComCd : undefined} />;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { FnrsRegistPage } from "@/pages/fnrs-cnsltn";
|
||||
import { parseTaskSeComCdFromApi } from "@/pages/fnrs-cnsltn/lib/fnrs-cnsltn-regist-schema";
|
||||
|
||||
interface PageProps {
|
||||
searchParams: Promise<{ taskSeComCd?: string }>;
|
||||
}
|
||||
|
||||
export default async function Page({ searchParams }: PageProps) {
|
||||
const params = await searchParams;
|
||||
const taskSeComCd = parseTaskSeComCdFromApi(params.taskSeComCd);
|
||||
return <FnrsRegistPage taskSeComCd={taskSeComCd.length ? taskSeComCd : undefined} />;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { InstlRegistPage } from "@/pages/instl-sncn";
|
||||
|
||||
interface InstlSncnRegistRouteProps {
|
||||
searchParams: Promise<{ taskSeComCd?: string }>;
|
||||
}
|
||||
|
||||
export default async function Page({ searchParams }: InstlSncnRegistRouteProps) {
|
||||
const params = await searchParams;
|
||||
const taskSeComCd = params.taskSeComCd
|
||||
? params.taskSeComCd.split(",").filter(Boolean)
|
||||
: undefined;
|
||||
|
||||
return <InstlRegistPage taskSeComCd={taskSeComCd} />;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { cookies } from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { COOKIE_DOMAIN, REFRESH_TOKEN } from "@/shared/config";
|
||||
|
||||
export async function POST() {
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.delete({ name: REFRESH_TOKEN, domain: COOKIE_DOMAIN });
|
||||
return NextResponse.json({ message: "Logged out" }, { status: 200 });
|
||||
}
|
||||
@@ -1,367 +0,0 @@
|
||||
# 재원협의(fnrs-cnsltn) vs 설치인가(instl-sncn) 차이 분석
|
||||
|
||||
| 항목 | 내용 |
|
||||
| --------- | ----------------------------------------------------------------------------- |
|
||||
| 작성일 | 2026-07-09 |
|
||||
| 기준 구현 | `fnrs-cnsltn` (추가 요구사항 반영 완료) |
|
||||
| 비교 대상 | `instl-sncn` |
|
||||
| 목적 | 두 페이지의 동작·구조 차이를 정리하고, 설치인가 정렬 시 이식 대상을 명확히 함 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 요약
|
||||
|
||||
두 모듈은 **동일한 PBADMS(공공행정 지원) 업무 패턴**을 따르는 형제 페이지다.
|
||||
|
||||
공통 흐름:
|
||||
|
||||
```
|
||||
목록 → TaskSe 선택 후 신청 → 저장(SV) → 신청완료(ST)
|
||||
→ 환경청제출(LE) → 기술검토 → 검토결과 회신(EP)
|
||||
→ 인가승인(TA) → 고시(TB)
|
||||
```
|
||||
|
||||
| 구분 | fnrs-cnsltn | instl-sncn |
|
||||
| ------------- | ----------------- | ---------------- |
|
||||
| 업무명 | 재원협의 | 설치인가 |
|
||||
| 문서번호 | `fnrsCnsltnDocNo` | `instlSncnDocNo` |
|
||||
| docSeComCd | `FNRS` | `INSTL` |
|
||||
| 라우트 | `/fnrs-cnsltn/*` | `/instl-sncn/*` |
|
||||
| pages 파일 수 | 39 | 32 |
|
||||
|
||||
**결론:** `fnrs-cnsltn`이 추가 요구사항이 반영된 기준(reference) 구현이고, `instl-sncn`은 동일 골격이지만 보완요청·결재선 편집·등록/수정 폼 완성도 등에서 미반영·TODO·단순화 상태다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 파일/폴더 구조
|
||||
|
||||
### 2-1. fnrs-cnsltn에만 존재
|
||||
|
||||
| 경로 | 역할 |
|
||||
| -------------------------------------------- | ------------------------- |
|
||||
| `ui/sections/fnrs-shar-section.tsx` | 재원분담 조회 섹션 |
|
||||
| `ui/sections/supplement-demand-section.tsx` | 보완요청 이력 섹션 |
|
||||
| `ui/components/sortable-prgrs-stts-row.tsx` | 결재선 DnD 편집 행 |
|
||||
| `ui/components/sortable-apln-picker-row.tsx` | 결재선 템플릿 피커 DnD 행 |
|
||||
| `ui/components/aprvr-picker-dialog.tsx` | 결재자 선택 다이얼로그 |
|
||||
| `ui/components/elapr-apln-picker-grid.tsx` | 결재선 편집 그리드 |
|
||||
| `ui/components/elapr-tmplt-select-table.tsx` | 결재서식 선택 테이블 |
|
||||
| `model/reviewer-option.ts` | 결재자 옵션 타입 |
|
||||
|
||||
### 2-2. instl-sncn에만 존재
|
||||
|
||||
| 경로 | 역할 | 비고 |
|
||||
| -------------------------------------- | ------------------- | ------------------------------------------ |
|
||||
| `ui/sections/history-info-section.tsx` | 등록/수정 이력 표시 | **어디에서도 import되지 않음 (dead code)** |
|
||||
|
||||
### 2-3. 공통 구조
|
||||
|
||||
```
|
||||
index.ts
|
||||
ui/
|
||||
{list,view,form,regist,edit}-page.tsx
|
||||
sections/ (basic-info, contact, scale, cost, checklist, attachments, tech-review)
|
||||
components/ (floating-action-buttons, elapr-approval-dialog, elapr-tmplt-picker-dialog, checklist-task-panel)
|
||||
lib/ button-config, save-payload-utils, form-data-mapper, regist-schema, checklist/review utils
|
||||
model/ columns.tsx, view-types.ts
|
||||
```
|
||||
|
||||
App Router도 동일: `page.tsx`, `regist/page.tsx`, `[id]/page.tsx`, `[id]/edit/page.tsx`
|
||||
|
||||
---
|
||||
|
||||
## 3. 목록 페이지
|
||||
|
||||
### 3-1. 검색 필터
|
||||
|
||||
| 항목 | fnrs-cnsltn | instl-sncn |
|
||||
| ------------------ | ----------------------------------------- | --------------------------------- |
|
||||
| 시도/시군구 API | `@/entities/adstdstr` | 동일 |
|
||||
| 지역 검색 파라미터 | **`admdstCd`** (시군구 우선, 없으면 시도) | **`sidoComCd` + `sggComCd`** 분리 |
|
||||
| "전체" 옵션 | `ALL_SELECT_VALUE = "__ALL__"` SelectItem | 없음 (placeholder만) |
|
||||
| 구분 CodeSelect | `upCd="INSTL"` ※ | `upCd="INSTL"` |
|
||||
| 사업명 | 동일 | 동일 |
|
||||
|
||||
> ※ fnrs 목록의 구분 필터가 `INSTL` 코드를 사용 중이며, TaskSePickerDialog는 `codeUpCd="FNRS"`를 쓴다. **불일치 가능성**이 있어 확인이 필요하다.
|
||||
|
||||
### 3-2. 정렬·페이지네이션
|
||||
|
||||
| 항목 | fnrs-cnsltn | instl-sncn |
|
||||
| ------------------------ | ---------------------- | ------------------------------------- |
|
||||
| 기본 sort (list-page) | `fnrsCnsltnDocNo,desc` | `instlSncnDocNo,desc` |
|
||||
| fallback sort (list.tsx) | `fnrsCnsltnDocNo,desc` | **`regDt,desc`** (list-page와 불일치) |
|
||||
| 페이지 크기 | 10 | 10 |
|
||||
| URL 동기화 | 없음 (`useState` 로컬) | 없음 |
|
||||
|
||||
### 3-3. 테이블 컬럼
|
||||
|
||||
`model/columns.tsx` 로직은 사실상 동일하다. DTO 타입명만 다르다.
|
||||
|
||||
컬럼: 시도, 시군구, 사업명, 업무구분, 담당자, 해당기관, 진행상황, 신청일
|
||||
|
||||
### 3-4. 행 클릭·신청
|
||||
|
||||
| 항목 | fnrs-cnsltn | instl-sncn |
|
||||
| ------------ | ------------------------------------------------------------------ | ------------------ |
|
||||
| 행 클릭 | **조건부 라우팅**: `SV`/`SL` + 등록자 본인 → `/edit`, 그 외 → view | **항상 view** |
|
||||
| useUserStore | 사용 | 미사용 |
|
||||
| 신청 버튼 | TaskSePickerDialog `codeUpCd="FNRS"` | `codeUpCd="INSTL"` |
|
||||
|
||||
```46:54:apps/as/src/pages/fnrs-cnsltn/ui/fnrs-list.tsx
|
||||
const handleRowClick = (row: FnrsCnsltnMngResponseDto) => {
|
||||
const canEdit =
|
||||
(row.prgrsSttsComCd === "SV" || row.prgrsSttsComCd === "SL") &&
|
||||
row.rgtr === userInfo?.userUnqId;
|
||||
|
||||
router.push(
|
||||
canEdit ? `/fnrs-cnsltn/${row.fnrsCnsltnDocNo}/edit` : `/fnrs-cnsltn/${row.fnrsCnsltnDocNo}`,
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 상세(View) 페이지
|
||||
|
||||
### 4-1. 섹션 구성
|
||||
|
||||
| 섹션 | fnrs-cnsltn | instl-sncn |
|
||||
| ------------------------------- | ------------------------------------------ | -------------------------- |
|
||||
| BasicInfo | 환경청구분, 확정예산사업번호, 예산신청연도 | **사업구분(`sncnSeCdNm`)** |
|
||||
| ContactInfo | 동일 패턴 | 동일 |
|
||||
| BusinessScale / Cost | 동일 | 동일 |
|
||||
| **FnrsShar (재원분담)** | **있음** | **없음** (도메인 고유) |
|
||||
| Checklist | 동일 | 동일 |
|
||||
| Attachments | 동일 | 동일 |
|
||||
| **SupplementDemand (보완요청)** | **있음** | **없음** |
|
||||
| TechReviewOpinion | 동일 | 동일 |
|
||||
| HistoryInfo | 없음 | 파일만 존재, view 미연결 |
|
||||
| FloatingActionButtons | `docSeComCd="FNRS"` | `docSeComCd="INSTL"` |
|
||||
|
||||
### 4-2. 데이터 훅
|
||||
|
||||
| 항목 | fnrs-cnsltn | instl-sncn |
|
||||
| --------- | ---------------------------- | -------------------- |
|
||||
| 상세 훅 | `useFnrsCnsltnMng` | `useInstlSncnDetail` |
|
||||
| 에러 판정 | `isError \|\| !data?.detDto` | `isError \|\| !data` |
|
||||
| 로딩 | Skeleton | Skeleton |
|
||||
|
||||
---
|
||||
|
||||
## 5. 등록/수정(Form) 페이지
|
||||
|
||||
### 5-1. 폼 완성도
|
||||
|
||||
| 항목 | fnrs-cnsltn | instl-sncn |
|
||||
| ------------- | --------------------------------- | --------------- |
|
||||
| 저장(SV) | O | O (SV 고정) |
|
||||
| 신청완료(ST) | O (edit 모드) | **없음** |
|
||||
| 삭제 | O (edit 모드, `useConfirm`) | **없음** |
|
||||
| 재원분담 섹션 | `FnrsSharFormSection` + 잔액 검증 | 없음 |
|
||||
| 저장 후 이동 | **`/edit` 유지** | **view로 이동** |
|
||||
| 목록 버튼 | `/fnrs-cnsltn` 고정 | `router.back()` |
|
||||
|
||||
### 5-2. 기본정보 Form 필드
|
||||
|
||||
| 필드 | fnrs | instl |
|
||||
| ----------------- | ---------------- | --------------------------------------- |
|
||||
| taskSe 코드 | `FNRS` | `INSTL` |
|
||||
| 사업구분 | 없음 | **`sncnSeComCd` (SNCNSE 라디오, 필수)** |
|
||||
| BnsPicker 연동 | bizNm ~ bizCn | + **`bgtBizCd`, `bgtAplyYr`** |
|
||||
| 재이용(RE) 조건부 | rtlztnFclt / Usg | 동일 |
|
||||
|
||||
### 5-3. Schema
|
||||
|
||||
| 항목 | fnrs | instl |
|
||||
| ------------------- | ------------------- | -------------- |
|
||||
| 관리 객체 | `fnrsCnsltnMng` | `instlSncnMng` |
|
||||
| 예산 필드 | `cfmtnBgtBizNo` | `bgtBizCd` |
|
||||
| 재원분담 | `fnrsSharList` 배열 | 없음 |
|
||||
| sharAtchFileGroupId | 있음 | 없음 |
|
||||
| privacyConsent | 있음 | 있음 |
|
||||
|
||||
### 5-4. Edit 페이지 래퍼
|
||||
|
||||
| 항목 | fnrs | instl |
|
||||
| ------------------ | ------------------------------- | ------------------------------- |
|
||||
| 로딩 Skeleton | 있음 | **없음** |
|
||||
| 데이터 없음 메시지 | "상세 정보를 찾을 수 없습니다." | "수정 대상을 찾을 수 없습니다." |
|
||||
|
||||
---
|
||||
|
||||
## 6. FloatingActionButtons / 결재 / 보완
|
||||
|
||||
### 6-1. FAB 동작 차이
|
||||
|
||||
공통: 역할×상태 기반 버튼(`button-config.ts`), checklist 진행률 검증, elapr pending approval 체크
|
||||
|
||||
| 기능 | fnrs-cnsltn | instl-sncn |
|
||||
| ------------------------------- | ------------------------------------------------------- | ------------------------------------------------------- |
|
||||
| 보완완료 (`splmntCmptn`) | **`submitSupplement` API 연동** | **`// TODO` stub** |
|
||||
| 보완요청 (`dmndSplmnt`) | **`buildSupplementReqPayload` + `submitReqSupplement`** | **`// TODO` stub** (문자열만 전달) |
|
||||
| 검토완료 (`rvwCmptn`) | `techReViewSaveCd: "SV"` 후 **결재 다이얼로그 오픈** | `techReViewSaveCd: "CP"`, **다이얼로그 없이 즉시 완료** |
|
||||
| 검토자지정 | `buildElaprPrgrsSttsListFromAplnRows(aplnRows)` | `buildElaprPrgrsSttsListFromApln(aplnPage.content)` |
|
||||
| atrzAprv / atrzRply / grndsChck | TODO handler | TODO handler |
|
||||
|
||||
```576:617:apps/as/src/pages/instl-sncn/ui/components/floating-action-buttons.tsx
|
||||
async function handleSplmntCmptn(context: ActionHandlerContext): Promise<void> {
|
||||
// TODO: 보완완료 API 호출
|
||||
context.onSuccess?.();
|
||||
}
|
||||
// ...
|
||||
async function handleDmndSplmnt(context: ActionHandlerContext): Promise<void> {
|
||||
// TODO: 보완요청 API 호출
|
||||
context.onSuccess?.();
|
||||
}
|
||||
```
|
||||
|
||||
### 6-2. ElaprApprovalDialog (가장 큰 UX 격차)
|
||||
|
||||
| 항목 | fnrs (419줄) | instl (231줄) |
|
||||
| ---------------- | ------------------------------------------------------ | -------------------------------- |
|
||||
| 결재선 표시 | DnD sortable 편집 테이블 | **읽기 전용** 테이블 |
|
||||
| 결재선 추가/저장 | **지원** (`usePrgrsSttsRows`, `useSaveElaprPrgrsStts`) | 없음 |
|
||||
| 결재자 변경 | **AprvrPickerDialog** | 없음 |
|
||||
| 전결 체크박스 | **있음** (`finalApprovalYn`) | 없음 |
|
||||
| 반려 사유 | API에 `rjctRsn` 전달 | UI는 있으나 **payload에 미전달** |
|
||||
| 저장 전 검증 | `hasUnsavedPrgrsSttsChanges` | 없음 |
|
||||
|
||||
### 6-3. ElaprTmpltPickerDialog
|
||||
|
||||
| 항목 | fnrs | instl |
|
||||
| --------------- | -------------------------------------------------------------------- | ----------------------- |
|
||||
| UI | 2-pane: 서식 테이블 + **결재선 편집 그리드** | 단일 테이블 + 선택 버튼 |
|
||||
| docSeComCd 필터 | `FNRS` | `INSTL` |
|
||||
| onConfirm | `{ tmplt, aplnRows }` 편집 결과 | `onSelect(tmplt)`만 |
|
||||
| 검증 | `validateAllRowsForSave`, `validateAtrzOrder`, `validateUniqueSeqNo` | 없음 |
|
||||
|
||||
---
|
||||
|
||||
## 7. Entity / API / Hook
|
||||
|
||||
### 7-1. Service 메서드
|
||||
|
||||
| 메서드 | fnrs | instl |
|
||||
| ----------------------- | --------------------------- | --------- |
|
||||
| 목록 | `findPage` | `getPage` |
|
||||
| 상세 | `findById` | `getById` |
|
||||
| saveApply | O | O |
|
||||
| techReview | O | O |
|
||||
| changePrgrsSttsComCd | O | O |
|
||||
| remove | O | O |
|
||||
| **submitSupplement** | **O** (`/submitsupplement`) | **X** |
|
||||
| **submitReqSupplement** | **O** (`/reqsupplement`) | **X** |
|
||||
|
||||
### 7-2. 상세 DTO 확장
|
||||
|
||||
**fnrs 추가 필드**
|
||||
|
||||
- `fnrsSharList`, `sharAtchFileList`
|
||||
- `splmntDmndList` (typed)
|
||||
|
||||
**instl 추가 필드**
|
||||
|
||||
- `sncnSeComCd` / `sncnSeCdNm` (인가구분)
|
||||
- `bgtBizCd` (예산사업코드)
|
||||
- `rtlztnFcltSeComNm`, `rtlztnUsgSeComNm`
|
||||
- `splmntDmndList` 타입은 base에 존재하나 view/API 미연결
|
||||
- `instlSncnRegionService` (시도/시군구 — 목록은 adstdstr 사용, 미활용)
|
||||
|
||||
### 7-3. save-payload-utils
|
||||
|
||||
| 함수 | fnrs | instl |
|
||||
| -------------------------------- | ---------------- | --------- |
|
||||
| buildSavePayload | + `fnrsSharList` | shar 없음 |
|
||||
| buildTechReviewReplyPayload | O | O |
|
||||
| **buildSupplementReqPayload** | **O** | **X** |
|
||||
| **buildSupplementSubmitPayload** | **O** | **X** |
|
||||
|
||||
---
|
||||
|
||||
## 8. 비즈니스 도메인 차이
|
||||
|
||||
| 영역 | fnrs-cnsltn | instl-sncn |
|
||||
| -------------- | ----------------------------------------- | ------------------------ |
|
||||
| 업무 코드 | `FNRS` | `INSTL` |
|
||||
| TaskSe | FNRS 하위 | INSTL 하위 |
|
||||
| 고시 버튼 라벨 | "재원협의 고시" | "설치인가 고시" |
|
||||
| 재원분담 | **핵심 도메인** (`fnrs-shar-se`) | 해당 없음 |
|
||||
| 인가구분 | 없음 | **`SNCNSE` 코드 (필수)** |
|
||||
| 예산 식별 | `cfmtnBgtBizNo` | `bgtBizCd` |
|
||||
| 진행상태 코드 | SV, ST, LE, ER, TE, EP, TA, TB, SL, TP 등 | **동일 세트 공유** |
|
||||
| 역할 매핑 | ROLE_USER05/04/03/01 | 동일 |
|
||||
|
||||
---
|
||||
|
||||
## 9. fnrs-cnsltn에만 있는 기능 (추가 요구사항 후보)
|
||||
|
||||
instl-sncn 정렬 시 **우선 이식 대상**:
|
||||
|
||||
1. 목록 행 클릭 조건부 edit 라우팅 (`SV`/`SL` + 등록자)
|
||||
2. 보완요청 전체 플로우 — view 섹션, payload builder, API, FAB handler
|
||||
3. 보완완료 API 연동 (`submitSupplement`)
|
||||
4. 등록/수정 폼 완성 — 신청완료, 삭제, 저장 후 edit 유지
|
||||
5. 결재선 DnD 편집형 `ElaprApprovalDialog` (+ aprvr-picker, sortable rows)
|
||||
6. 결재선 편집형 `ElaprTmpltPickerDialog` (+ apln grid, validation)
|
||||
7. 검토완료 → 결재 다이얼로그 연계 흐름
|
||||
8. 검색 필터 "전체" 옵션 및 `admdstCd` 통합 방식 (백엔드 API에 맞게)
|
||||
9. Edit 페이지 로딩 Skeleton
|
||||
10. entity에 supplement API/hooks 추가
|
||||
|
||||
---
|
||||
|
||||
## 10. instl-sncn에만 있는 기능 (유지 필요)
|
||||
|
||||
1. **`sncnSeComCd` (사업구분/인가구분)** — 도메인 고유
|
||||
2. **`bgtBizCd` / `bgtAplyYr`** BnsPicker 연동
|
||||
3. **`history-info-section.tsx`** — 구현만 있고 view 미연결 (활성화 또는 삭제 결정 필요)
|
||||
4. entity **`instlSncnRegionService`** — 목록은 adstdstr 사용 중이라 중복/레거시 가능
|
||||
5. **`INSTL_SNCN_REGISTER_SELECTION_KEY`** sessionStorage 키
|
||||
|
||||
---
|
||||
|
||||
## 11. 동일·거의 동일 패턴
|
||||
|
||||
| 영역 | 동일도 |
|
||||
| ----------------------------------------------------- | --------------------- |
|
||||
| App Router 4-route 구조 | 100% |
|
||||
| `button-config.ts` 역할×상태 매트릭스 | ~99% (액션명만 다름) |
|
||||
| `columns.tsx` 진행상황 Badge | ~100% |
|
||||
| checklist / contact / scale / cost / attachments 섹션 | ~95% (import 경로만) |
|
||||
| review-step-utils 로직 | ~100% (상수명만 다름) |
|
||||
| TaskSePickerDialog 신청 플로우 | 100% |
|
||||
| FAB 전체 구조·대부분 handler | ~85% |
|
||||
| checklist-form-state, business-cost-utils | 거의 동일 |
|
||||
| shared widgets (`@/widgets/pbadms-sprt`) | 동일 |
|
||||
|
||||
---
|
||||
|
||||
## 12. instl-sncn 정렬 권장 우선순위
|
||||
|
||||
| 우선순위 | 항목 | 근거 |
|
||||
| -------- | ------------------------------------------------------- | ---------------------------- |
|
||||
| P0 | 보완요청/보완완료 API + UI | fnrs 완전 구현 vs instl TODO |
|
||||
| P0 | ElaprApprovalDialog 고도화 | 결재선 편집·전결·반려사유 |
|
||||
| P0 | ElaprTmpltPickerDialog 2-pane | 검토자지정 품질 |
|
||||
| P1 | Form 신청완료/삭제 + 저장 후 edit 유지 | 등록 UX parity |
|
||||
| P1 | 목록 행 클릭 edit 라우팅 | fnrs 추가 요구사항 |
|
||||
| P1 | 검토완료 → 결재 다이얼로그 연계 | 흐름 통일 |
|
||||
| P2 | sort fallback 불일치 수정 (`regDt` vs `instlSncnDocNo`) | instl 내부 버그성 |
|
||||
| P2 | history-info-section 연결 또는 삭제 | dead code 정리 |
|
||||
| P3 | URL search params 동기화 | 양쪽 모두 미구현 (공통 개선) |
|
||||
|
||||
---
|
||||
|
||||
## 13. 참고 코드 위치
|
||||
|
||||
| 주제 | 경로 |
|
||||
| ---------------------------- | ------------------------------------------------------------ |
|
||||
| 목록 edit 라우팅 (fnrs) | `pages/fnrs-cnsltn/ui/fnrs-list.tsx` |
|
||||
| 목록 항상 view (instl) | `pages/instl-sncn/ui/instl-list.tsx` |
|
||||
| view 추가 섹션 (fnrs) | `pages/fnrs-cnsltn/ui/fnrs-view-page.tsx` |
|
||||
| FAB 보완 TODO (instl) | `pages/instl-sncn/ui/components/floating-action-buttons.tsx` |
|
||||
| 결재 다이얼로그 (fnrs) | `pages/fnrs-cnsltn/ui/components/elapr-approval-dialog.tsx` |
|
||||
| 결재 다이얼로그 (instl) | `pages/instl-sncn/ui/components/elapr-approval-dialog.tsx` |
|
||||
| 폼 완성도 (fnrs) | `pages/fnrs-cnsltn/ui/fnrs-form-page.tsx` |
|
||||
| 폼 단순화 (instl) | `pages/instl-sncn/ui/instl-form-page.tsx` |
|
||||
| supplement API (fnrs entity) | `entities/fnrs-cnsltn/api/fnrs-cnsltn-service.ts` |
|
||||
@@ -0,0 +1,674 @@
|
||||
# dmbpRegist.jsp → React 마이그레이션 분석·구현 가이드
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ----------- | ------------------------------------------------------ |
|
||||
| 원본 JSP | `adssNewOld/.../fnrsc/dmbp/dmbpRegist.jsp` |
|
||||
| 대상 앱 | `frontend/apps/as` |
|
||||
| 도메인 | 하수도기본계획 (구: 기본계획검토 / dmbp) |
|
||||
| API 엔티티 | `@/entities/sewer-bsc-plan` (`SewerBscPlanController`) |
|
||||
| 참조 패턴 | `@/pages/fnrs-cnsltn` + `@/widgets/pbadms-sprt` |
|
||||
| 문서 목적 | contents 영역 **섹션별** UI·상태·API 매핑 및 구현 방향 |
|
||||
| 작성 기준일 | 2026-07-27 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 마이그레이션 범위
|
||||
|
||||
### 1-1. 포함 (contents만)
|
||||
|
||||
JSP `568~1274`행: `<div class="contents">` ~ `<!-- //contents -->`
|
||||
|
||||
| 영역 | 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 (등록/수정) / 읽기 섹션 (상세)
|
||||
└─ <form|div className="mt-6 space-y-4">
|
||||
├─ BasicInfoFormSection // JSP slide_10
|
||||
├─ ContactInfoFormSection // JSP slide_15 (pbadms-sprt)
|
||||
├─ PrivacyConsentSection // JSP slide_19 (pbadms-sprt, 등록·수정)
|
||||
├─ OutlineFormSection // JSP slide_11 (사업개요, 신규)
|
||||
├─ BizCstMatrixFormSection // JSP slide_12 (총사업비 전용, 신규)
|
||||
├─ FnrsCstFormSection // JSP slide_13 (재원분담, 신규 ※ FnrsShar 아님)
|
||||
├─ TmplntFormSection // JSP slide_14 (처리시설, Tabs로 시설별 패널)
|
||||
├─ ChecklistFormSection // JSP slide_16 (pbadms-sprt)
|
||||
├─ AttachmentsFormSection // JSP 필수·기타첨부 (pbadms-sprt)
|
||||
├─ SplmntDmndFormSection // edit + SL 상태만 (pbadms-sprt)
|
||||
└─ form footer action bar // fnrs-form-page 279~323 동일
|
||||
// [목록] / [저장] / edit시 [신청완료][삭제] / SL시 [보완완료]
|
||||
```
|
||||
|
||||
**폼 하단 액션바 버튼 구성**
|
||||
기준: [`fnrs-form-page.tsx` 279~323](../../src/pages/fnrs-cnsltn/ui/fnrs-form-page.tsx). UI·분기만 동일하고 훅/라우트는 sewer.
|
||||
|
||||
| 순서 | 버튼 | 조건 | sewer 비고 |
|
||||
| ---- | -------- | ----------------------------------------- | ---------------------------------------------------- |
|
||||
| 1 | 목록 | 항상 | `router.push("/sewer-bsc-plan")` |
|
||||
| 2 | 저장 | `!isSplmntPrgrsStts` | `type="submit"` → `saveApply` / 임시저장(SV) |
|
||||
| 3 | 신청완료 | `!isSplmntPrgrsStts && isEditMode` | 체크리스트+필수첨부 검증 → `saveApply` / LE |
|
||||
| 4 | 삭제 | `!isSplmntPrgrsStts && isEditMode` | confirm → `delete` → 목록 |
|
||||
| 5 | 보완완료 | `isSplmntPrgrsStts` (`prgrsSttsComCd=SL`) | `submitSupplement` (fnrs `onSplmntCmptn` 대응) |
|
||||
|
||||
**fnrs와의 차이 (섹션 치환만, 골격 동일)**
|
||||
|
||||
| fnrs-form-page | sewer-bsc-plan |
|
||||
| ---------------------------------------------- | ------------------------------------------------------ |
|
||||
| `BasicInfoFormSection` | 동일 역할 (마스터·연도·개요 필드는 sewer DTO) |
|
||||
| `ContactInfoFormSection` | 동일 (`fieldPrefix="sewerBscPlanDtl"`) |
|
||||
| `PrivacyConsentSection` | 동일 |
|
||||
| `BusinessScaleFormSection` | → `OutlineFormSection` (단계별 인구·보급률) |
|
||||
| `BusinessCostFormSection` | → `BizCstMatrixFormSection` (단계×원가 계층 전용) |
|
||||
| `FnrsSharFormSection` | → `FnrsCstFormSection` (국고·지방비·원인자·기타) |
|
||||
| _(없음)_ | `TmplntFormSection` (체크박스 + Tabs 시설별 매트릭스) |
|
||||
| `ChecklistFormSection` `docSeComCd="FNRS"` | `docSeComCd="SEWER"` (TBD) |
|
||||
| `AttachmentsFormSection` | 동일 패턴 |
|
||||
| `SplmntDmndFormSection` | 동일 (`readOnly={!isSplmntPrgrsStts}`) |
|
||||
| 폼 하단 액션바 (목록·저장·신청완료·삭제·보완완료) | **동일 구성, API·라우트만 sewer** |
|
||||
|
||||
### 3-2. 본문 ASCII (폼 페이지)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ PageLayout: 하수도기본계획 신청/수정 breadcrumb │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ FormProvider │
|
||||
│ <form className="mt-6 space-y-4"> │
|
||||
│ │
|
||||
│ ┌─ SectionCollapsible: 기본계획 신청 ─────────────────────┐ │
|
||||
│ │ BasicInfoFormSection │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ ┌─ 신청자 정보 ────────────────────────────────────────────┐ │
|
||||
│ │ ContactInfoFormSection │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ ┌─ 개인정보 동의 ──────────────────────────────────────────┐ │
|
||||
│ │ PrivacyConsentSection │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ ┌─ 사업개요 ────────────── [이전기본계획조회] ─────────────┐ │
|
||||
│ │ OutlineFormSection │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ ┌─ 총사업비 (백만원) ──────────────────────────────────────┐ │
|
||||
│ │ BizCstMatrixFormSection │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ ┌─ 재원분담 (백만원) ──────────────────────────────────────┐ │
|
||||
│ │ FnrsCstFormSection │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ ┌─ 처리시설별 하수도정비기본계획 ──────────────────────────┐ │
|
||||
│ │ TmplntFormSection │ │
|
||||
│ │ ├─ BscFctList Checkbox │ │
|
||||
│ │ └─ Tabs (선택 시설별 TabsTrigger / TabsContent) │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ ┌─ 체크리스트 ─────────────────────────────────────────────┐ │
|
||||
│ │ ChecklistFormSection │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ ┌─ 첨부파일 ───────────────────────────────────────────────┐ │
|
||||
│ │ AttachmentsFormSection │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ ┌─ 보완요구 (edit + SL) ───────────────────────────────────┐ │
|
||||
│ │ SplmntDmndFormSection │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ form footer (fnrs-form-page 279~323 동일) ──────────────┐ │
|
||||
│ │ [목록] [저장] / edit: [신청완료][삭제] / SL: [보완완료] │ │
|
||||
│ │ ※ API·라우트만 sewer — UI 분기는 fnrs와 동일 │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ </form> │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 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="<SEWER|DMBP TBD>"`
|
||||
`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
|
||||
<div className="mx-4 flex flex-wrap justify-center gap-3 sm:gap-4">
|
||||
<Button color="white" type="button" onClick={() => router.push("/sewer-bsc-plan")} disabled={isSubmitting}>
|
||||
목록
|
||||
</Button>
|
||||
{!isSplmntPrgrsStts ? (
|
||||
<>
|
||||
<Button type="submit" disabled={isSubmitting}>저장</Button>
|
||||
{isEditMode && (
|
||||
<>
|
||||
<Button color="green" type="button" onClick={handleApplyClick} disabled={isSubmitting}>
|
||||
신청완료
|
||||
</Button>
|
||||
<Button color="red" type="button" onClick={handleDelClick} disabled={isSubmitting}>
|
||||
삭제
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Button type="button" onClick={form.handleSubmit(onSplmntCmptn)} disabled={isSubmitting}>
|
||||
보완완료
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
```
|
||||
|
||||
| 버튼 | 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 표)
|
||||
@@ -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 — 응답은 캐시·검증용일 수 있음 |
|
||||
+18
-3
@@ -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")) {
|
||||
|
||||
@@ -39,7 +39,6 @@
|
||||
"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": {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,11 @@ async function findSidoCombo(): Promise<CmCdAdstDstrComboResponseDto[]> {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function findUserSidoCombo(): Promise<CmCdAdstDstrComboResponseDto[]> {
|
||||
const response = await client.get<CmCdAdstDstrComboResponseDto[]>(`${BASE}/user/sido`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function findGugunCombo(sido: string): Promise<CmCdAdstDstrComboResponseDto[]> {
|
||||
const response = await client.get<CmCdAdstDstrComboResponseDto[]>(`${BASE}/gugun`, {
|
||||
params: { sido },
|
||||
@@ -20,7 +25,16 @@ async function findGugunCombo(sido: string): Promise<CmCdAdstDstrComboResponseDt
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function findUserGugunCombo(sido: string): Promise<CmCdAdstDstrComboResponseDto[]> {
|
||||
const response = await client.get<CmCdAdstDstrComboResponseDto[]>(`${BASE}/user/gugun`, {
|
||||
params: { sido },
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const cmCdAdstDstrService = {
|
||||
findSidoCombo,
|
||||
findUserSidoCombo,
|
||||
findGugunCombo,
|
||||
findUserGugunCombo,
|
||||
} satisfies CmCdAdstDstrService;
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -7,5 +7,7 @@ export interface CmCdAdstDstrComboResponseDto {
|
||||
/** Controller public 메서드명·시그니처와 동일 */
|
||||
export interface CmCdAdstDstrService {
|
||||
findSidoCombo(): Promise<CmCdAdstDstrComboResponseDto[]>;
|
||||
findUserSidoCombo(): Promise<CmCdAdstDstrComboResponseDto[]>;
|
||||
findGugunCombo(sido: string): Promise<CmCdAdstDstrComboResponseDto[]>;
|
||||
findUserGugunCombo(sido: string): Promise<CmCdAdstDstrComboResponseDto[]>;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { ACCESS_TOKEN, AUTH_USER_ID, CLAIM_NAME, ENDPOINT } from "@/shared/config";
|
||||
import { type AuthPayload, type AuthService } from "@common/auth";
|
||||
import {
|
||||
type AuthPayload,
|
||||
type AuthService,
|
||||
clearDeviceId,
|
||||
getOrCreateDeviceId,
|
||||
} from "@common/auth";
|
||||
|
||||
import { clearDeviceId, getOrCreateDeviceId } from "../lib/device-id";
|
||||
import { clearRefreshTimer, setToken, tokenManager } from "../lib/token-manager";
|
||||
|
||||
async function login(payload: AuthPayload): Promise<void> {
|
||||
@@ -72,6 +76,7 @@ async function silentRefresh(): Promise<void> {
|
||||
}
|
||||
|
||||
async function logout(): Promise<void> {
|
||||
await fetch("/api/auth/logout", { method: "POST" });
|
||||
tokenManager.clearAuthToken();
|
||||
clearDeviceId();
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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<FacilityListResponseWrapperDto> {
|
||||
const response = await client.get<FacilityListResponseWrapperDto>(`${BASE}/facilityList`, {
|
||||
params,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const bnsIfService = {
|
||||
getBnsList,
|
||||
getBnsDetail,
|
||||
getEmCodeList,
|
||||
getFacilityList,
|
||||
} satisfies BnsIfService;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<BnsListResponseWrapperDto>;
|
||||
getBnsDetail(gbn: string, yr: string, seqNo: string): Promise<BnsDetailResponseWrapperDto>;
|
||||
getEmCodeList(dpmCd?: string, lclgvrnmntCd?: string): Promise<EmCodeListResponseDto>;
|
||||
getFacilityList(params: FacilitySearchDto): Promise<FacilityListResponseWrapperDto>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type {
|
||||
ClsfSystmParamsDto,
|
||||
ClsfSystmResponseDto,
|
||||
DcService,
|
||||
FileDownloadParamsDto,
|
||||
FileParamsDto,
|
||||
LvNowDmgMtncGnsvyResponseDto,
|
||||
LvNowDmgMtncGnsvySearchDto,
|
||||
NowDmgMtncGnsvyResponseDto,
|
||||
NowDmgMtncGnsvySearchDto,
|
||||
} from "../model/dc-interface";
|
||||
|
||||
async function getNowDmgMtncGnsvy(
|
||||
params: NowDmgMtncGnsvySearchDto,
|
||||
): Promise<NowDmgMtncGnsvyResponseDto[]> {
|
||||
const response = await client.get<NowDmgMtncGnsvyResponseDto[]>(
|
||||
`${ENDPOINT.DC_SERVICE}/nowDmgMtncGnsvy`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function getLvNowDmgMtncGnsvy(
|
||||
params: LvNowDmgMtncGnsvySearchDto,
|
||||
): Promise<LvNowDmgMtncGnsvyResponseDto[]> {
|
||||
const response = await client.get<LvNowDmgMtncGnsvyResponseDto[]>(
|
||||
`${ENDPOINT.DC_SERVICE}/lvNowDmgMtncGnsvy`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function getClsfSystm(params: ClsfSystmParamsDto): Promise<ClsfSystmResponseDto[]> {
|
||||
const response = await client.get<ClsfSystmResponseDto[]>(`${ENDPOINT.DC_SERVICE}/clsf`, {
|
||||
params,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function getFile(params: FileParamsDto): Promise<unknown> {
|
||||
const response = await client.get<unknown>(`${ENDPOINT.DC_SERVICE}/file`, {
|
||||
params,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function getFilePrcs(fileSn: string): Promise<unknown> {
|
||||
const response = await client.get<unknown>(`${ENDPOINT.DC_SERVICE}/fileprcs`, {
|
||||
params: { fileSn },
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function fileDownload(params: FileDownloadParamsDto): Promise<Blob> {
|
||||
const response = await client.get<Blob>(`${ENDPOINT.DC_SERVICE}/fileDownload`, {
|
||||
params,
|
||||
responseType: "blob",
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const dcService = {
|
||||
getNowDmgMtncGnsvy,
|
||||
getLvNowDmgMtncGnsvy,
|
||||
getClsfSystm,
|
||||
getFile,
|
||||
getFilePrcs,
|
||||
fileDownload,
|
||||
} satisfies DcService;
|
||||
@@ -0,0 +1,65 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type {
|
||||
ClsfSystmParamsDto,
|
||||
FileParamsDto,
|
||||
LvNowDmgMtncGnsvySearchDto,
|
||||
NowDmgMtncGnsvySearchDto,
|
||||
} from "../model/dc-interface";
|
||||
|
||||
import { dcService } from "./dc-service";
|
||||
|
||||
export const baseKey = [ENDPOINT.DC_SERVICE] as const;
|
||||
|
||||
export function useNowDmgMtncGnsvy(params: NowDmgMtncGnsvySearchDto, enabled?: boolean) {
|
||||
return useQuery({
|
||||
queryKey: [...baseKey, "nowDmgMtncGnsvy", params],
|
||||
queryFn: () => dcService.getNowDmgMtncGnsvy(params),
|
||||
enabled: enabled ?? Boolean(params.swtrmtFcltCd && params.ctpvNm),
|
||||
});
|
||||
}
|
||||
|
||||
function isLvNowDmgMtncGnsvyEnabled(params: LvNowDmgMtncGnsvySearchDto): boolean {
|
||||
if (!params.level || !params.ctpvNm) return false;
|
||||
if (params.level === "1") return true;
|
||||
if (params.level === "2") return Boolean(params.sggNm);
|
||||
if (params.level === "3") return Boolean(params.sggNm && params.gnsvyYr);
|
||||
return false;
|
||||
}
|
||||
|
||||
export function useLvNowDmgMtncGnsvy(params: LvNowDmgMtncGnsvySearchDto) {
|
||||
return useQuery({
|
||||
queryKey: [...baseKey, "lvNowDmgMtncGnsvy", params],
|
||||
queryFn: () => dcService.getLvNowDmgMtncGnsvy(params),
|
||||
enabled: isLvNowDmgMtncGnsvyEnabled(params),
|
||||
});
|
||||
}
|
||||
|
||||
export function useClsfSystm(params: ClsfSystmParamsDto) {
|
||||
return useQuery({
|
||||
queryKey: [...baseKey, "clsfSystm", params],
|
||||
queryFn: () => dcService.getClsfSystm(params),
|
||||
// TODO: T4 — upClsfSystmSn 점진 로드 여부 확정 후 enabled/파라미터 조정
|
||||
enabled: Boolean(params.clsfSystmNm),
|
||||
});
|
||||
}
|
||||
|
||||
export function useFile(params: FileParamsDto) {
|
||||
return useQuery({
|
||||
queryKey: [...baseKey, "file", params],
|
||||
queryFn: () => dcService.getFile(params),
|
||||
// TODO: T2 — getFile Response DTO 확정 후 타입 교체
|
||||
enabled: Boolean(params.clsfSystmSn),
|
||||
});
|
||||
}
|
||||
|
||||
export function useFilePrcs(fileSn: string) {
|
||||
return useQuery({
|
||||
queryKey: [...baseKey, "filePrcs", fileSn],
|
||||
queryFn: () => dcService.getFilePrcs(fileSn),
|
||||
// TODO: T2 — getFilePrcs Response DTO 확정 후 타입 교체
|
||||
enabled: Boolean(fileSn),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export { dcService } from "./api/dc-service";
|
||||
export {
|
||||
baseKey as nowDmgMtncGnsvyBaseKey,
|
||||
useClsfSystm,
|
||||
useFile,
|
||||
useFilePrcs,
|
||||
useLvNowDmgMtncGnsvy,
|
||||
useNowDmgMtncGnsvy,
|
||||
} from "./api/queries";
|
||||
export type {
|
||||
ClsfSystmParamsDto,
|
||||
ClsfSystmResponseDto,
|
||||
FileDownloadParamsDto,
|
||||
FileParamsDto,
|
||||
LvNowDmgMtncGnsvyResponseDto,
|
||||
LvNowDmgMtncGnsvySearchDto,
|
||||
NowDmgMtncGnsvyResponseDto,
|
||||
NowDmgMtncGnsvySearchDto,
|
||||
} from "./model/dc-interface";
|
||||
@@ -0,0 +1,115 @@
|
||||
export interface NowDmgMtncGnsvySearchDto {
|
||||
swtrmtFcltCd?: string;
|
||||
ctpvNm?: string;
|
||||
sggNm?: string;
|
||||
targetYear?: string;
|
||||
}
|
||||
|
||||
export interface LvNowDmgMtncGnsvySearchDto {
|
||||
level?: string;
|
||||
ctpvNm?: string;
|
||||
sggNm?: string;
|
||||
gnsvyYr?: string;
|
||||
}
|
||||
|
||||
export interface ClsfSystmParamsDto {
|
||||
clsfSystmNm?: string;
|
||||
upClsfSystmSn?: string;
|
||||
}
|
||||
|
||||
export interface FileParamsDto {
|
||||
sort?: string;
|
||||
clsfSystmSn?: string;
|
||||
vetPrcsYn?: string;
|
||||
}
|
||||
|
||||
export interface FileDownloadParamsDto {
|
||||
filePath?: string;
|
||||
fileName?: string;
|
||||
}
|
||||
|
||||
type NullableNumber = number | null;
|
||||
type NullableString = string | null;
|
||||
|
||||
/** 현재·단계별(Dt/Dc) 공통 지표 필드 */
|
||||
interface NowDmgMtncGnsvyMetricFields {
|
||||
planYr?: NullableString;
|
||||
swtrmtPpltnCnt?: NullableNumber;
|
||||
natrSwtrmtPpltnCnt?: NullableNumber;
|
||||
scilSwtrmtPpltnCnt?: NullableNumber;
|
||||
swtrmtZoneArea?: NullableNumber;
|
||||
wtrUsqtyOrgnUnit?: NullableNumber;
|
||||
drtwSwtcRt?: NullableNumber;
|
||||
flctnLodRt?: NullableNumber;
|
||||
drtwOrgnUnit?: NullableNumber;
|
||||
planSwageQntSum?: NullableNumber;
|
||||
lvngDrtwSum?: NullableNumber;
|
||||
ugwtSum?: NullableNumber;
|
||||
fctryWwtSum?: NullableNumber;
|
||||
toursmDrtwSum?: NullableNumber;
|
||||
mtbsSum?: NullableNumber;
|
||||
linkPrcsCntSum?: NullableNumber;
|
||||
etcDrtwSum?: NullableNumber;
|
||||
ugwtUseSum?: NullableNumber;
|
||||
fcltCpctSum?: NullableNumber;
|
||||
exclsMethExpln?: NullableString;
|
||||
brcPplSum?: NullableNumber;
|
||||
icptgPplSum?: NullableNumber;
|
||||
exclnPplSum?: NullableNumber;
|
||||
stofcbrSum?: NullableNumber;
|
||||
pmstnSum?: NullableNumber;
|
||||
simplPubSwtrmtFcltSum?: NullableNumber;
|
||||
swageImpdFcltSum?: NullableNumber;
|
||||
mltvFclSum?: NullableNumber;
|
||||
}
|
||||
|
||||
type StageSuffix = "Dt1" | "Dt2" | "Dt3" | "Dt4" | "Dc1" | "Dc2" | "Dc3" | "Dc4";
|
||||
|
||||
type StagedMetricFields = {
|
||||
[K in keyof NowDmgMtncGnsvyMetricFields as `${K & string}${StageSuffix}`]?: NowDmgMtncGnsvyMetricFields[K];
|
||||
};
|
||||
|
||||
export interface NowDmgMtncGnsvyResponseDto
|
||||
extends NowDmgMtncGnsvyMetricFields, StagedMetricFields {
|
||||
swtrmtFcltCd?: NullableString;
|
||||
swtrmtFcltNm?: NullableString;
|
||||
gnsvyYr?: NullableString;
|
||||
ctpvNm?: NullableString;
|
||||
sggNm?: NullableString;
|
||||
}
|
||||
|
||||
/**
|
||||
* lvNowDmgMtncGnsvy 계층 노드.
|
||||
* - level 1: ctpvNm + children(구군)
|
||||
* - level 2: sggNm + children(연도)
|
||||
* - level 3: gnsvyYr + children(처리시설)
|
||||
* - 시설 리프: swtrmtFcltCd / swtrmtFcltNm (level 없음)
|
||||
*/
|
||||
export interface LvNowDmgMtncGnsvyResponseDto {
|
||||
level?: number;
|
||||
ctpvNm?: string;
|
||||
sggNm?: string;
|
||||
gnsvyYr?: string;
|
||||
swtrmtFcltCd?: string;
|
||||
swtrmtFcltNm?: string;
|
||||
children?: LvNowDmgMtncGnsvyResponseDto[];
|
||||
}
|
||||
|
||||
export interface ClsfSystmResponseDto {
|
||||
clsfSystmSn: string;
|
||||
upClsfSystmSn: string;
|
||||
clsfSystmNm: string;
|
||||
sortSeq: string;
|
||||
droppAble: boolean;
|
||||
path: string | null;
|
||||
children: ClsfSystmResponseDto[];
|
||||
}
|
||||
|
||||
export interface DcService {
|
||||
getNowDmgMtncGnsvy(params: NowDmgMtncGnsvySearchDto): Promise<NowDmgMtncGnsvyResponseDto[]>;
|
||||
getLvNowDmgMtncGnsvy(params: LvNowDmgMtncGnsvySearchDto): Promise<LvNowDmgMtncGnsvyResponseDto[]>;
|
||||
getClsfSystm(params: ClsfSystmParamsDto): Promise<ClsfSystmResponseDto[]>;
|
||||
getFile(params: FileParamsDto): Promise<unknown>;
|
||||
getFilePrcs(fileSn: string): Promise<unknown>;
|
||||
fileDownload(params: FileDownloadParamsDto): Promise<Blob>;
|
||||
}
|
||||
@@ -39,11 +39,48 @@ export interface AdstZoneResponseDto {
|
||||
|
||||
export const INSTL_SNCN_REGISTER_SELECTION_KEY = "instlSncnRegisterSelection";
|
||||
|
||||
/** 백엔드 InstlSncnMngSearchDto + Pageable */
|
||||
export interface InstlSncnMngSearchParams extends PageableParams {
|
||||
bizNm?: string;
|
||||
/** 설치인가문서번호 */
|
||||
instlSncnDocNo?: string;
|
||||
/** 행정구역코드 */
|
||||
admdstCd?: string;
|
||||
/** 환경청구분공통코드 */
|
||||
envoSeComCd?: string;
|
||||
/** 업무구분공통코드 */
|
||||
taskSeComCd?: string;
|
||||
sidoComCd?: string;
|
||||
sggComCd?: string;
|
||||
/** 예산사업코드 */
|
||||
bgtBizCd?: string;
|
||||
/** 예산신청연도 */
|
||||
bgtAplyYr?: string;
|
||||
/** 사업명 */
|
||||
bizNm?: string;
|
||||
/** 사업시작연도 */
|
||||
bizBgngYr?: string;
|
||||
/** 사업종료연도 */
|
||||
bizEndYr?: string;
|
||||
/** 사업내용 */
|
||||
bizCn?: string;
|
||||
/** 신청자명 */
|
||||
aplcntNm?: string;
|
||||
/** 신청자휴대전화번호 */
|
||||
aplcntMblTelno?: string;
|
||||
/** 신청자소속명 */
|
||||
aplcntOgdpNm?: string;
|
||||
/** 신청자이메일주소 */
|
||||
aplcntEmlAddr?: string;
|
||||
/** 설계자명 */
|
||||
dsgnrNm?: string;
|
||||
/** 설계자전화번호 */
|
||||
dsgnrTelno?: string;
|
||||
/** 설계자소속명 */
|
||||
dsgnrOgdpNm?: string;
|
||||
/** 설계자이메일주소 */
|
||||
dsgnrEmlAddr?: string;
|
||||
/** 첨부파일그룹아이디 */
|
||||
atchFileGroupId?: string;
|
||||
/** 진행상태공통코드 */
|
||||
prgrsSttsComCd?: string;
|
||||
}
|
||||
|
||||
/** 설치인가관리 저장 요청 DTO */
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type {
|
||||
SewerBscPlanApplySaveRequestDto,
|
||||
SewerBscPlanSaveRequestDto,
|
||||
SewerBscPlanSearchRequestDto,
|
||||
SewerBscPlanUpdateRequestDto,
|
||||
} from "../model/sewer-bsc-plan-interface";
|
||||
|
||||
import { sewerBscPlanService } from "./sewer-bsc-plan-service";
|
||||
|
||||
export const sewerBscPlanBaseKey = [ENDPOINT.AS_SERVICE.SEWER_BSC_PLAN] as const;
|
||||
|
||||
export function useSewerBscPlanPage(params: SewerBscPlanSearchRequestDto) {
|
||||
return useQuery({
|
||||
queryKey: [...sewerBscPlanBaseKey, "findPage", params],
|
||||
queryFn: () => sewerBscPlanService.findPage(params),
|
||||
});
|
||||
}
|
||||
|
||||
export function useSewerBscPlan(sewerBscPlanDocNo: string | null | undefined) {
|
||||
return useQuery({
|
||||
queryKey: [...sewerBscPlanBaseKey, "findById", sewerBscPlanDocNo],
|
||||
queryFn: () => sewerBscPlanService.findById(sewerBscPlanDocNo!),
|
||||
enabled: Boolean(sewerBscPlanDocNo),
|
||||
});
|
||||
}
|
||||
|
||||
export function useSewerBscPlanDetails(
|
||||
sewerBscPlanDocNo: string | null | undefined,
|
||||
chgCycl: number | null | undefined,
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: [...sewerBscPlanBaseKey, "findDetails", sewerBscPlanDocNo, chgCycl],
|
||||
queryFn: () => sewerBscPlanService.findDetails(sewerBscPlanDocNo!, chgCycl!),
|
||||
enabled: Boolean(sewerBscPlanDocNo) && chgCycl != null,
|
||||
});
|
||||
}
|
||||
|
||||
export function useSaveSewerBscPlan() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (body: SewerBscPlanSaveRequestDto) => sewerBscPlanService.save(body),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: sewerBscPlanBaseKey });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateSewerBscPlan() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
sewerBscPlanDocNo,
|
||||
body,
|
||||
}: {
|
||||
sewerBscPlanDocNo: string;
|
||||
body: SewerBscPlanUpdateRequestDto;
|
||||
}) => sewerBscPlanService.update(sewerBscPlanDocNo, body),
|
||||
onSuccess: (_data, { sewerBscPlanDocNo }) => {
|
||||
queryClient.invalidateQueries({ queryKey: sewerBscPlanBaseKey });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...sewerBscPlanBaseKey, "findById", sewerBscPlanDocNo],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteSewerBscPlan() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (sewerBscPlanDocNo: string) => sewerBscPlanService.delete(sewerBscPlanDocNo),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: sewerBscPlanBaseKey });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useSaveApplySewerBscPlan() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (body: SewerBscPlanApplySaveRequestDto) => sewerBscPlanService.saveApply(body),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: sewerBscPlanBaseKey });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useSubmitSupplementSewerBscPlan() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, body }: { id: string; body: SewerBscPlanApplySaveRequestDto }) =>
|
||||
sewerBscPlanService.submitSupplement(id, body),
|
||||
onSuccess: (_data, { id }) => {
|
||||
queryClient.invalidateQueries({ queryKey: sewerBscPlanBaseKey });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...sewerBscPlanBaseKey, "findById", id],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useSubmitReqSupplementSewerBscPlan() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
id,
|
||||
chgCycl,
|
||||
body,
|
||||
}: {
|
||||
id: string;
|
||||
chgCycl: number;
|
||||
body: SewerBscPlanApplySaveRequestDto;
|
||||
}) => sewerBscPlanService.submitReqSupplement(id, chgCycl, body),
|
||||
onSuccess: (_data, { id }) => {
|
||||
queryClient.invalidateQueries({ queryKey: sewerBscPlanBaseKey });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...sewerBscPlanBaseKey, "findById", id],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useTechReviewSewerBscPlan() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, body }: { id: string; body: SewerBscPlanApplySaveRequestDto }) =>
|
||||
sewerBscPlanService.techReview(id, body),
|
||||
onSuccess: (_data, { id }) => {
|
||||
queryClient.invalidateQueries({ queryKey: sewerBscPlanBaseKey });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...sewerBscPlanBaseKey, "findById", id],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { PageResponse } from "@/shared/model";
|
||||
|
||||
import type {
|
||||
SewerBscPlanApplySaveRequestDto,
|
||||
SewerBscPlanApplySaveResponseDto,
|
||||
SewerBscPlanDetResponseDto,
|
||||
SewerBscPlanResponseDto,
|
||||
SewerBscPlanSaveRequestDto,
|
||||
SewerBscPlanSearchRequestDto,
|
||||
SewerBscPlanService,
|
||||
SewerBscPlanUpdateRequestDto,
|
||||
} from "../model/sewer-bsc-plan-interface";
|
||||
|
||||
const BASE = ENDPOINT.AS_SERVICE.SEWER_BSC_PLAN;
|
||||
|
||||
async function findPage(
|
||||
params: SewerBscPlanSearchRequestDto,
|
||||
): Promise<PageResponse<SewerBscPlanResponseDto>> {
|
||||
const response = await client.get<PageResponse<SewerBscPlanResponseDto>>(BASE, {
|
||||
params,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function findById(sewerBscPlanDocNo: string): Promise<SewerBscPlanResponseDto> {
|
||||
const response = await client.get<SewerBscPlanResponseDto>(`${BASE}/${sewerBscPlanDocNo}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function save(body: SewerBscPlanSaveRequestDto): Promise<SewerBscPlanResponseDto> {
|
||||
const response = await client.post<SewerBscPlanResponseDto>(BASE, body);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function update(
|
||||
sewerBscPlanDocNo: string,
|
||||
body: SewerBscPlanUpdateRequestDto,
|
||||
): Promise<SewerBscPlanResponseDto> {
|
||||
const response = await client.put<SewerBscPlanResponseDto>(`${BASE}/${sewerBscPlanDocNo}`, body);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function deleteFn(sewerBscPlanDocNo: string): Promise<void> {
|
||||
await client.delete<void>(`${BASE}/${sewerBscPlanDocNo}`);
|
||||
}
|
||||
|
||||
async function saveApply(
|
||||
body: SewerBscPlanApplySaveRequestDto,
|
||||
): Promise<SewerBscPlanApplySaveResponseDto> {
|
||||
const response = await client.post<SewerBscPlanApplySaveResponseDto>(`${BASE}/save-apply`, body);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function findDetails(
|
||||
sewerBscPlanDocNo: string,
|
||||
chgCycl: number,
|
||||
): Promise<SewerBscPlanDetResponseDto> {
|
||||
const response = await client.get<SewerBscPlanDetResponseDto>(
|
||||
`${BASE}/${sewerBscPlanDocNo}/dtl/${chgCycl}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function submitSupplement(
|
||||
id: string,
|
||||
body: SewerBscPlanApplySaveRequestDto,
|
||||
): Promise<string> {
|
||||
const response = await client.post<string>(`${BASE}/${id}/submitsupplement`, body);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function submitReqSupplement(
|
||||
id: string,
|
||||
chgCycl: number,
|
||||
body: SewerBscPlanApplySaveRequestDto,
|
||||
): Promise<string> {
|
||||
const response = await client.post<string>(`${BASE}/${id}/${chgCycl}/reqsupplement`, body);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function techReview(id: string, body: SewerBscPlanApplySaveRequestDto): Promise<void> {
|
||||
await client.post<void>(`${BASE}/${id}/tech-review`, body);
|
||||
}
|
||||
|
||||
export const sewerBscPlanService = {
|
||||
findPage,
|
||||
findById,
|
||||
save,
|
||||
update,
|
||||
delete: deleteFn,
|
||||
saveApply,
|
||||
findDetails,
|
||||
submitSupplement,
|
||||
submitReqSupplement,
|
||||
techReview,
|
||||
} satisfies SewerBscPlanService;
|
||||
@@ -0,0 +1,47 @@
|
||||
export { sewerBscPlanBaseKey } from "./api/queries";
|
||||
export {
|
||||
useDeleteSewerBscPlan,
|
||||
useSaveApplySewerBscPlan,
|
||||
useSaveSewerBscPlan,
|
||||
useSewerBscPlan,
|
||||
useSewerBscPlanDetails,
|
||||
useSewerBscPlanPage,
|
||||
useSubmitReqSupplementSewerBscPlan,
|
||||
useSubmitSupplementSewerBscPlan,
|
||||
useTechReviewSewerBscPlan,
|
||||
useUpdateSewerBscPlan,
|
||||
} from "./api/queries";
|
||||
export { sewerBscPlanService } from "./api/sewer-bsc-plan-service";
|
||||
export type {
|
||||
AtflMngClientResponseDto,
|
||||
CklstResultItem,
|
||||
CklstRvwRsltTreeDto,
|
||||
EsntlAtchFileItem,
|
||||
EsntlAtchFileMngTreeResponseDto,
|
||||
SewerBscPlanApplySaveRequestDto,
|
||||
SewerBscPlanApplySaveResponseDto,
|
||||
SewerBscPlanBizCstItem,
|
||||
SewerBscPlanBizCstResponseDto,
|
||||
SewerBscPlanDetResponseDto,
|
||||
SewerBscPlanDtlResponseDto,
|
||||
SewerBscPlanDtlSaveRequestDto,
|
||||
SewerBscPlanFnrsCstItem,
|
||||
SewerBscPlanFnrsCstResponseDto,
|
||||
SewerBscPlanOtlnItem,
|
||||
SewerBscPlanOtlnResponseDto,
|
||||
SewerBscPlanResponseDto,
|
||||
SewerBscPlanRvwOpnnItem,
|
||||
SewerBscPlanRvwOpnnResponseDto,
|
||||
SewerBscPlanSaveRequestDto,
|
||||
SewerBscPlanSearchRequestDto,
|
||||
SewerBscPlanService,
|
||||
SewerBscPlanSplmntDmndItem,
|
||||
SewerBscPlanSplmntDmndResponseDto,
|
||||
SewerBscPlanTmplntDtlItem,
|
||||
SewerBscPlanTmplntDtlResponseDto,
|
||||
SewerBscPlanTmplntItem,
|
||||
SewerBscPlanTmplntResponseDto,
|
||||
SewerBscPlanTmplntTmprStrgItem,
|
||||
SewerBscPlanTmplntTmprStrgResponseDto,
|
||||
SewerBscPlanUpdateRequestDto,
|
||||
} from "./model/sewer-bsc-plan-interface";
|
||||
@@ -0,0 +1,351 @@
|
||||
import type {
|
||||
AtflMngClientResponseDto,
|
||||
CklstResultSaveBase,
|
||||
CklstRvwRsltTreeDto,
|
||||
EsntlAtchFileMngTreeResponseDto,
|
||||
EsntlAtchFileSaveBase,
|
||||
PageableParams,
|
||||
PageResponse,
|
||||
PbadmsApplySaveHeader,
|
||||
RvwOpnnResponseExtension,
|
||||
RvwOpnnSaveBase,
|
||||
SplmntDmndResponseExtension,
|
||||
SplmntDmndSaveBase,
|
||||
} from "@/shared/model";
|
||||
|
||||
export type {
|
||||
AtflMngClientResponseDto,
|
||||
CklstRvwRsltTreeDto,
|
||||
EsntlAtchFileMngTreeResponseDto,
|
||||
} from "@/shared/model";
|
||||
|
||||
/** 백엔드 SewerBscPlanSaveRequestDto */
|
||||
export interface SewerBscPlanSaveRequestDto {
|
||||
sewerBscPlanDocNo?: string;
|
||||
admdstCd?: string;
|
||||
envoSeComCd?: string;
|
||||
sewerBscPlanSeComCd?: string;
|
||||
sewerBscPlanNm?: string;
|
||||
taskSeComCd?: string;
|
||||
prgrsSttsComCd?: string;
|
||||
}
|
||||
|
||||
/** 백엔드 SewerBscPlanUpdateRequestDto */
|
||||
export interface SewerBscPlanUpdateRequestDto {
|
||||
admdstCd?: string;
|
||||
envoSeComCd?: string;
|
||||
sewerBscPlanSeComCd?: string;
|
||||
sewerBscPlanNm?: string;
|
||||
taskSeComCd?: string;
|
||||
prgrsSttsComCd?: string;
|
||||
}
|
||||
|
||||
/** 백엔드 SewerBscPlanSearchRequestDto + Pageable */
|
||||
export interface SewerBscPlanSearchRequestDto extends PageableParams {
|
||||
sewerBscPlanDocNo?: string;
|
||||
admdstCd?: string;
|
||||
envoSeComCd?: string;
|
||||
sewerBscPlanSeComCd?: string;
|
||||
sewerBscPlanNm?: string;
|
||||
taskSeComCd?: string;
|
||||
}
|
||||
|
||||
/** 백엔드 SewerBscPlanResponseDto */
|
||||
export interface SewerBscPlanResponseDto {
|
||||
sewerBscPlanDocNo: string;
|
||||
admdstCd?: string;
|
||||
envoSeComCd?: string;
|
||||
envoSeCdNm?: string;
|
||||
sewerBscPlanSeComCd?: string;
|
||||
sewerBscPlanSeCdNm?: string;
|
||||
sewerBscPlanNm?: string;
|
||||
taskSeComCd?: string;
|
||||
taskSeCdNm?: string;
|
||||
prgrsSttsComCd?: string;
|
||||
prgrsSttsCdNm?: string;
|
||||
sido?: string;
|
||||
gugun?: string;
|
||||
chgCycl?: number;
|
||||
regDt?: string;
|
||||
rgtr?: string;
|
||||
rgtrNm?: string;
|
||||
mdfcnDt?: string;
|
||||
}
|
||||
|
||||
/** 백엔드 SewerBscPlanDtlSaveRequestDto */
|
||||
export interface SewerBscPlanDtlSaveRequestDto {
|
||||
sewerBscPlanDocNo?: string;
|
||||
chgCycl?: number;
|
||||
aplcntNm?: string;
|
||||
aplcntMblTelno?: string;
|
||||
aplcntOgdpNm?: string;
|
||||
aplcntEmlAddr?: string;
|
||||
dsgnrNm?: string;
|
||||
dsgnrTelno?: string;
|
||||
dsgnrOgdpNm?: string;
|
||||
dsgnrEmlAddr?: string;
|
||||
planYr?: string;
|
||||
crtrYr?: string;
|
||||
bizOtlnCn?: string;
|
||||
frlvlGoalYr?: string;
|
||||
sclvlGoalYr?: string;
|
||||
thlvlGoalYr?: string;
|
||||
folvlGoalYr?: string;
|
||||
atchFileGroupId?: string;
|
||||
}
|
||||
|
||||
/** 백엔드 SewerBscPlanDtlResponseDto */
|
||||
export interface SewerBscPlanDtlResponseDto {
|
||||
sewerBscPlanDocNo?: string;
|
||||
chgCycl?: number;
|
||||
aplcntNm?: string;
|
||||
aplcntMblTelno?: string;
|
||||
aplcntOgdpNm?: string;
|
||||
aplcntEmlAddr?: string;
|
||||
dsgnrNm?: string;
|
||||
dsgnrTelno?: string;
|
||||
dsgnrOgdpNm?: string;
|
||||
dsgnrEmlAddr?: string;
|
||||
planYr?: string;
|
||||
crtrYr?: string;
|
||||
bizOtlnCn?: string;
|
||||
frlvlGoalYr?: string;
|
||||
sclvlGoalYr?: string;
|
||||
thlvlGoalYr?: string;
|
||||
folvlGoalYr?: string;
|
||||
atchFileGroupId?: string;
|
||||
}
|
||||
|
||||
/** 백엔드 SewerBscPlanOtlnResponseDto */
|
||||
export interface SewerBscPlanOtlnResponseDto {
|
||||
sewerBscPlanDocNo?: string;
|
||||
chgCycl?: number;
|
||||
planStpNo?: number;
|
||||
sewerPlanPpltnCnt?: number;
|
||||
sewerPrcsPpltnCnt?: number;
|
||||
sewerPsplyRt?: number;
|
||||
}
|
||||
|
||||
/** 백엔드 SewerBscPlanFnrsCstResponseDto */
|
||||
export interface SewerBscPlanFnrsCstResponseDto {
|
||||
sewerBscPlanDocNo?: string;
|
||||
chgCycl?: number;
|
||||
planStpNo?: number;
|
||||
gvexpAmt?: number;
|
||||
lcexpAmt?: number;
|
||||
orgprBdchgAmt?: number;
|
||||
etcAmt?: number;
|
||||
sumAmt?: number;
|
||||
}
|
||||
|
||||
/** 백엔드 SewerBscPlanBizCstResponseDto */
|
||||
export interface SewerBscPlanBizCstResponseDto {
|
||||
sewerBscPlanDocNo?: string;
|
||||
chgCycl?: number;
|
||||
planStpNo?: number;
|
||||
acntgSeCd?: string;
|
||||
cstrnAmt?: number;
|
||||
}
|
||||
|
||||
/** 백엔드 SewerBscPlanSplmntDmndResponseDto */
|
||||
export interface SewerBscPlanSplmntDmndResponseDto
|
||||
extends SplmntDmndSaveBase, SplmntDmndResponseExtension {
|
||||
sewerBscPlanDocNo?: string;
|
||||
chgCycl?: number;
|
||||
}
|
||||
|
||||
/** 백엔드 SewerBscPlanRvwOpnnResponseDto */
|
||||
export interface SewerBscPlanRvwOpnnResponseDto extends RvwOpnnSaveBase, RvwOpnnResponseExtension {
|
||||
sewerBscPlanDocNo?: string;
|
||||
chgCycl?: number;
|
||||
}
|
||||
|
||||
/** 백엔드 SewerBscPlanTmplntDtlResponseDto */
|
||||
export interface SewerBscPlanTmplntDtlResponseDto {
|
||||
sewerBscPlanDocNo?: string;
|
||||
chgCycl?: number;
|
||||
planStpNo?: number;
|
||||
swtrmtFcltCd?: string;
|
||||
prcsZoneArea?: number;
|
||||
swtrmtPpltnCnt?: number;
|
||||
planSwqty?: number;
|
||||
fcltCpct?: number;
|
||||
sprsysDrtwPplLen?: number;
|
||||
sprsysExlnPplLen?: number;
|
||||
cbsysPplLen?: number;
|
||||
icptgPplLen?: number;
|
||||
drgCnt?: number;
|
||||
drtwRlyPumpCnt?: number;
|
||||
drtwPumpCnt?: number;
|
||||
exlnPumpCnt?: number;
|
||||
}
|
||||
|
||||
/** 백엔드 SewerBscPlanTmplntResponseDto */
|
||||
export interface SewerBscPlanTmplntResponseDto {
|
||||
sewerBscPlanDocNo?: string;
|
||||
chgCycl?: number;
|
||||
swtrmtFcltCd?: string;
|
||||
swtrmtFcltNm?: string;
|
||||
atchFileGroupId?: string;
|
||||
atchFileInfo?: AtflMngClientResponseDto;
|
||||
sewerBscPlanTmplntDtlList?: SewerBscPlanTmplntDtlResponseDto[];
|
||||
}
|
||||
|
||||
/** 백엔드 SewerBscPlanTmplntTmprStrgResponseDto */
|
||||
export interface SewerBscPlanTmplntTmprStrgResponseDto {
|
||||
swtrmtFcltCd?: string;
|
||||
rgtr?: string;
|
||||
dtStrgDt?: string;
|
||||
prcsZoneArea?: number;
|
||||
swtrmtPpltnCnt?: number;
|
||||
planSwqty?: number;
|
||||
fcltCpct?: number;
|
||||
sprsysDrtwPplLen?: number;
|
||||
sprsysExlnPplLen?: number;
|
||||
cbsysPplLen?: number;
|
||||
icptgPplLen?: number;
|
||||
drgCnt?: number;
|
||||
drtwRlyPumpCnt?: number;
|
||||
drtwPumpCnt?: number;
|
||||
exlnPumpCnt?: number;
|
||||
}
|
||||
|
||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanOtlnItem */
|
||||
export interface SewerBscPlanOtlnItem {
|
||||
planStpNo?: number;
|
||||
sewerPlanPpltnCnt?: number;
|
||||
sewerPrcsPpltnCnt?: number;
|
||||
sewerPsplyRt?: number;
|
||||
}
|
||||
|
||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.CklstResultItem */
|
||||
export interface CklstResultItem extends CklstResultSaveBase {
|
||||
selected?: string;
|
||||
}
|
||||
|
||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanFnrsCstItem */
|
||||
export interface SewerBscPlanFnrsCstItem {
|
||||
planStpNo?: number;
|
||||
gvexpAmt?: number;
|
||||
lcexpAmt?: number;
|
||||
orgprBdchgAmt?: number;
|
||||
etcAmt?: number;
|
||||
sumAmt?: number;
|
||||
}
|
||||
|
||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanSplmntDmndItem */
|
||||
export type SewerBscPlanSplmntDmndItem = SplmntDmndSaveBase;
|
||||
|
||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanRvwOpnnItem */
|
||||
export type SewerBscPlanRvwOpnnItem = RvwOpnnSaveBase;
|
||||
|
||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.EsntlAtchFileItem */
|
||||
export type EsntlAtchFileItem = EsntlAtchFileSaveBase;
|
||||
|
||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanBizCstItem */
|
||||
export interface SewerBscPlanBizCstItem {
|
||||
planStpNo?: number;
|
||||
acntgSeCd?: string;
|
||||
cstrnAmt?: number;
|
||||
}
|
||||
|
||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanTmplntDtlItem */
|
||||
export interface SewerBscPlanTmplntDtlItem {
|
||||
planStpNo?: number;
|
||||
swtrmtFcltCd?: string;
|
||||
prcsZoneArea?: number;
|
||||
swtrmtPpltnCnt?: number;
|
||||
planSwqty?: number;
|
||||
fcltCpct?: number;
|
||||
sprsysDrtwPplLen?: number;
|
||||
sprsysExlnPplLen?: number;
|
||||
cbsysPplLen?: number;
|
||||
icptgPplLen?: number;
|
||||
drgCnt?: number;
|
||||
drtwRlyPumpCnt?: number;
|
||||
drtwPumpCnt?: number;
|
||||
exlnPumpCnt?: number;
|
||||
}
|
||||
|
||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanTmplntItem */
|
||||
export interface SewerBscPlanTmplntItem {
|
||||
swtrmtFcltCd?: string;
|
||||
atchFileGroupId?: string;
|
||||
sewerBscPlanTmplntDtlList?: SewerBscPlanTmplntDtlItem[];
|
||||
}
|
||||
|
||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanTmplntTmprStrgItem */
|
||||
export interface SewerBscPlanTmplntTmprStrgItem {
|
||||
dtStrgDt?: string;
|
||||
swtrmtFcltCd?: string;
|
||||
prcsZoneArea?: number;
|
||||
swtrmtPpltnCnt?: number;
|
||||
planSwqty?: number;
|
||||
fcltCpct?: number;
|
||||
sprsysDrtwPplLen?: number;
|
||||
sprsysExlnPplLen?: number;
|
||||
cbsysPplLen?: number;
|
||||
icptgPplLen?: number;
|
||||
drgCnt?: number;
|
||||
drtwRlyPumpCnt?: number;
|
||||
drtwPumpCnt?: number;
|
||||
exlnPumpCnt?: number;
|
||||
}
|
||||
|
||||
/** 백엔드 SewerBscPlanApplySaveRequestDto */
|
||||
export interface SewerBscPlanApplySaveRequestDto extends PbadmsApplySaveHeader {
|
||||
sewerBscPlan: SewerBscPlanSaveRequestDto;
|
||||
sewerBscPlanDtl: SewerBscPlanDtlSaveRequestDto;
|
||||
sewerBscPlanOtlnList?: SewerBscPlanOtlnItem[];
|
||||
cklstResultList?: CklstResultItem[];
|
||||
fnrsCstList?: SewerBscPlanFnrsCstItem[];
|
||||
splmntDmndList?: SewerBscPlanSplmntDmndItem[];
|
||||
rvwOpnnList?: SewerBscPlanRvwOpnnItem[];
|
||||
esntlAtchFileList?: EsntlAtchFileItem[];
|
||||
bizCstList?: SewerBscPlanBizCstItem[];
|
||||
sewerBscPlanTmplntList?: SewerBscPlanTmplntItem[];
|
||||
tmplntTmprStrgList?: SewerBscPlanTmplntTmprStrgItem[];
|
||||
}
|
||||
|
||||
/** 백엔드 SewerBscPlanApplySaveResponseDto */
|
||||
export interface SewerBscPlanApplySaveResponseDto {
|
||||
sewerBscPlanDocNo: string;
|
||||
chgCycl: number;
|
||||
}
|
||||
|
||||
/** 백엔드 SewerBscPlanDetResponseDto */
|
||||
export interface SewerBscPlanDetResponseDto {
|
||||
detDto?: SewerBscPlanResponseDto;
|
||||
activeDtlDto?: SewerBscPlanDtlResponseDto;
|
||||
otlnList?: SewerBscPlanOtlnResponseDto[];
|
||||
cklstResultRows?: CklstRvwRsltTreeDto[];
|
||||
fnrsCstList?: SewerBscPlanFnrsCstResponseDto[];
|
||||
splmntDmndList?: SewerBscPlanSplmntDmndResponseDto[];
|
||||
rvwOpnnList?: SewerBscPlanRvwOpnnResponseDto[];
|
||||
esntlFileList?: EsntlAtchFileMngTreeResponseDto[];
|
||||
etcFileList?: AtflMngClientResponseDto[];
|
||||
bizCstList?: SewerBscPlanBizCstResponseDto[];
|
||||
tmplntList?: SewerBscPlanTmplntResponseDto[];
|
||||
tmplntTmprStrgList?: SewerBscPlanTmplntTmprStrgResponseDto[];
|
||||
}
|
||||
|
||||
/** Controller public 메서드명·시그니처와 동일 */
|
||||
export interface SewerBscPlanService {
|
||||
findPage(params: SewerBscPlanSearchRequestDto): Promise<PageResponse<SewerBscPlanResponseDto>>;
|
||||
findById(sewerBscPlanDocNo: string): Promise<SewerBscPlanResponseDto>;
|
||||
save(body: SewerBscPlanSaveRequestDto): Promise<SewerBscPlanResponseDto>;
|
||||
update(
|
||||
sewerBscPlanDocNo: string,
|
||||
body: SewerBscPlanUpdateRequestDto,
|
||||
): Promise<SewerBscPlanResponseDto>;
|
||||
delete(sewerBscPlanDocNo: string): Promise<void>;
|
||||
saveApply(body: SewerBscPlanApplySaveRequestDto): Promise<SewerBscPlanApplySaveResponseDto>;
|
||||
findDetails(sewerBscPlanDocNo: string, chgCycl: number): Promise<SewerBscPlanDetResponseDto>;
|
||||
submitSupplement(id: string, body: SewerBscPlanApplySaveRequestDto): Promise<string>;
|
||||
submitReqSupplement(
|
||||
id: string,
|
||||
chgCycl: number,
|
||||
body: SewerBscPlanApplySaveRequestDto,
|
||||
): Promise<string>;
|
||||
techReview(id: string, body: SewerBscPlanApplySaveRequestDto): Promise<void>;
|
||||
}
|
||||
@@ -70,7 +70,12 @@ export function mapDetailToFormData(data: DesignCnstnMngDetResponseDto): DesignC
|
||||
|
||||
export function getCreateDefaultValues(
|
||||
userInfo: IUserInfo | null,
|
||||
taskSeComCd?: string[],
|
||||
options?: {
|
||||
taskSeComCd?: string[];
|
||||
admdstCd?: string;
|
||||
sido?: string;
|
||||
gugun?: string;
|
||||
},
|
||||
): DesignCnstnRegistFormData {
|
||||
return {
|
||||
designCnstnMng: {
|
||||
@@ -78,7 +83,7 @@ export function getCreateDefaultValues(
|
||||
bizBgngYr: "",
|
||||
bizEndYr: "",
|
||||
bizCn: "",
|
||||
taskSeComCd: taskSeComCd ?? [],
|
||||
taskSeComCd: options?.taskSeComCd ?? [],
|
||||
dsgnrNm: "",
|
||||
dsgnrTelno: "",
|
||||
dsgnrOgdpNm: "",
|
||||
@@ -87,6 +92,9 @@ export function getCreateDefaultValues(
|
||||
aplcntMblTelno: userInfo?.mphn ?? "",
|
||||
aplcntOgdpNm: userInfo?.ogdpSeNm ?? "",
|
||||
aplcntEmlAddr: userInfo?.email ?? "",
|
||||
admdstCd: options?.admdstCd ?? "",
|
||||
sido: options?.sido ?? "",
|
||||
gugun: options?.gugun ?? "",
|
||||
prgrsSttsComCd: "CV",
|
||||
atchFileGroupId: "",
|
||||
},
|
||||
|
||||
@@ -36,10 +36,15 @@ interface DesignFormPageProps {
|
||||
id?: string;
|
||||
mode: "create" | "edit";
|
||||
initialData?: DesignCnstnMngDetResponseDto;
|
||||
taskSeComCd?: string[];
|
||||
createDefaults?: {
|
||||
taskSeComCd?: string[];
|
||||
admdstCd?: string;
|
||||
sido?: string;
|
||||
gugun?: string;
|
||||
};
|
||||
}
|
||||
|
||||
function DesignFormPage({ id, mode, initialData, taskSeComCd }: DesignFormPageProps) {
|
||||
function DesignFormPage({ id, mode, initialData, createDefaults }: DesignFormPageProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const router = useRouter();
|
||||
const { confirm } = useConfirm();
|
||||
@@ -54,7 +59,7 @@ function DesignFormPage({ id, mode, initialData, taskSeComCd }: DesignFormPagePr
|
||||
defaultValues:
|
||||
isEditMode && initialData
|
||||
? mapDetailToFormData(initialData)
|
||||
: getCreateDefaultValues(userInfo, taskSeComCd),
|
||||
: getCreateDefaultValues(userInfo, createDefaults),
|
||||
});
|
||||
|
||||
function buildFormPayload(
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
type DesignCnstnMngSearchDto,
|
||||
useDesignCnstnMngPage,
|
||||
} from "@/entities/design-cnstn";
|
||||
import { TaskSePickerDialog } from "@/widgets/pbadms-sprt";
|
||||
import { PbadmsTaskApplyDialog, type PbadmsTaskApplySelection } from "@/widgets/pbadms-sprt";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
import { formatTaskSeComCdForApi } from "../lib/design-cnstn-regist-schema";
|
||||
@@ -71,9 +71,15 @@ function DesignList({ searchParams, pagination, onPaginationChange }: DesignList
|
||||
},
|
||||
});
|
||||
|
||||
const handleConfirmTaskSe = (selectedCodes: string[]) => {
|
||||
const handleConfirm = (selection: PbadmsTaskApplySelection) => {
|
||||
setIsTaskSeDialogOpen(false);
|
||||
router.push(`/design-cnstn/regist?taskSeComCd=${formatTaskSeComCdForApi(selectedCodes)}`);
|
||||
const qs = new URLSearchParams({
|
||||
taskSeComCd: formatTaskSeComCdForApi(selection.selectedCodes),
|
||||
admdstCd: selection.admdstCd,
|
||||
sido: selection.sido,
|
||||
gugun: selection.gugun,
|
||||
});
|
||||
router.push(`/design-cnstn/regist?${qs}`);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -87,10 +93,10 @@ function DesignList({ searchParams, pagination, onPaginationChange }: DesignList
|
||||
</div>
|
||||
{tableComponent}
|
||||
</section>
|
||||
<TaskSePickerDialog
|
||||
<PbadmsTaskApplyDialog
|
||||
open={isTaskSeDialogOpen}
|
||||
onOpenChange={setIsTaskSeDialogOpen}
|
||||
onConfirm={handleConfirmTaskSe}
|
||||
onConfirm={handleConfirm}
|
||||
title="설계자문 신청"
|
||||
codeUpCd="CNFM"
|
||||
/>
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import { DesignFormPage } from "./design-form-page";
|
||||
|
||||
interface DesignRegistPageProps {
|
||||
interface DesignRegistDefaults {
|
||||
taskSeComCd?: string[];
|
||||
admdstCd?: string;
|
||||
sido?: string;
|
||||
gugun?: string;
|
||||
}
|
||||
|
||||
function DesignRegistPage({ taskSeComCd }: DesignRegistPageProps) {
|
||||
return <DesignFormPage mode="create" taskSeComCd={taskSeComCd} />;
|
||||
interface DesignRegistPageProps {
|
||||
defaults?: DesignRegistDefaults;
|
||||
}
|
||||
|
||||
function DesignRegistPage({ defaults }: DesignRegistPageProps) {
|
||||
return <DesignFormPage mode="create" createDefaults={defaults} />;
|
||||
}
|
||||
|
||||
export { DesignRegistPage };
|
||||
export type { DesignRegistDefaults };
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { useState } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { useUserStore } from "@/shared/lib";
|
||||
import { Field, FieldError, FieldGroup } from "@/shared/ui/field";
|
||||
import { findCodeLabel, useCodeList } from "@/entities/code";
|
||||
import { type BnsApplyData, BnsPickerDialog } from "@/widgets/bns-picker";
|
||||
@@ -21,10 +20,12 @@ import {
|
||||
import type { DesignCnstnRegistFormData } from "../../lib/design-cnstn-regist-schema";
|
||||
|
||||
function BasicInfoFormSection() {
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const { control, watch, setValue } = useFormContext<DesignCnstnRegistFormData>();
|
||||
const [isPickerOpen, setIsPickerOpen] = useState(false);
|
||||
const taskSeComCd = watch("designCnstnMng.taskSeComCd");
|
||||
const localGovLabel = [watch("designCnstnMng.sido"), watch("designCnstnMng.gugun")]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
const { data: taskOptions } = useCodeList("CNFM");
|
||||
|
||||
function handleBnsSelect(data: BnsApplyData) {
|
||||
@@ -142,7 +143,7 @@ function BasicInfoFormSection() {
|
||||
<TableHead data-required>구분</TableHead>
|
||||
<TableCell>{taskSeComCdLabel}</TableCell>
|
||||
<TableHead>신청 지자체</TableHead>
|
||||
<TableCell>{userInfo?.instSeNm}</TableCell>
|
||||
<TableCell>{localGovLabel}</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
</Table>
|
||||
|
||||
@@ -118,7 +118,12 @@ export function mapDetailToFormData(data: FnrsCnsltnMngDetResponseDto): FnrsCnsl
|
||||
|
||||
export function getCreateDefaultValues(
|
||||
userInfo: IUserInfo | null,
|
||||
taskSeComCd?: string[],
|
||||
options?: {
|
||||
taskSeComCd?: string[];
|
||||
admdstCd?: string;
|
||||
sido?: string;
|
||||
gugun?: string;
|
||||
},
|
||||
): FnrsCnsltnRegistFormData {
|
||||
return {
|
||||
fnrsCnsltnMng: {
|
||||
@@ -126,7 +131,7 @@ export function getCreateDefaultValues(
|
||||
bizBgngYr: "",
|
||||
bizEndYr: "",
|
||||
bizCn: "",
|
||||
taskSeComCd: taskSeComCd ?? [],
|
||||
taskSeComCd: options?.taskSeComCd ?? [],
|
||||
cfmtnBgtBizNo: "",
|
||||
bgtAplyYr: "",
|
||||
dsgnrNm: "",
|
||||
@@ -137,6 +142,9 @@ export function getCreateDefaultValues(
|
||||
aplcntMblTelno: userInfo?.mphn ?? "",
|
||||
aplcntOgdpNm: userInfo?.ogdpSeNm ?? "",
|
||||
aplcntEmlAddr: userInfo?.email ?? "",
|
||||
admdstCd: options?.admdstCd ?? "",
|
||||
sido: options?.sido ?? "",
|
||||
gugun: options?.gugun ?? "",
|
||||
prgrsSttsComCd: "SV",
|
||||
atchFileGroupId: "",
|
||||
sharAtchFileGroupId: "",
|
||||
|
||||
@@ -47,10 +47,15 @@ interface FnrsFormPageProps {
|
||||
id?: string;
|
||||
mode: "create" | "edit";
|
||||
initialData?: FnrsCnsltnMngDetResponseDto;
|
||||
taskSeComCd?: string[];
|
||||
createDefaults?: {
|
||||
taskSeComCd?: string[];
|
||||
admdstCd?: string;
|
||||
sido?: string;
|
||||
gugun?: string;
|
||||
};
|
||||
}
|
||||
|
||||
function FnrsFormPage({ id, mode, initialData, taskSeComCd }: FnrsFormPageProps) {
|
||||
function FnrsFormPage({ id, mode, initialData, createDefaults }: FnrsFormPageProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const router = useRouter();
|
||||
const { confirm } = useConfirm();
|
||||
@@ -73,7 +78,7 @@ function FnrsFormPage({ id, mode, initialData, taskSeComCd }: FnrsFormPageProps)
|
||||
defaultValues:
|
||||
isEditMode && initialData
|
||||
? mapDetailToFormData(initialData)
|
||||
: getCreateDefaultValues(userInfo, taskSeComCd),
|
||||
: getCreateDefaultValues(userInfo, createDefaults),
|
||||
});
|
||||
|
||||
function buildFormPayload(
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
type FnrsCnsltnMngSearchDto,
|
||||
useFnrsCnsltnMngPage,
|
||||
} from "@/entities/fnrs-cnsltn";
|
||||
import { TaskSePickerDialog } from "@/widgets/pbadms-sprt";
|
||||
import { PbadmsTaskApplyDialog, type PbadmsTaskApplySelection } from "@/widgets/pbadms-sprt";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
import { formatTaskSeComCdForApi } from "../lib/fnrs-cnsltn-regist-schema";
|
||||
@@ -71,9 +71,15 @@ function FnrsList({ searchParams, pagination, onPaginationChange }: FnrsListProp
|
||||
},
|
||||
});
|
||||
|
||||
const handleConfirmTaskSe = (selectedCodes: string[]) => {
|
||||
const handleConfirm = (selection: PbadmsTaskApplySelection) => {
|
||||
setIsTaskSeDialogOpen(false);
|
||||
router.push(`/fnrs-cnsltn/regist?taskSeComCd=${formatTaskSeComCdForApi(selectedCodes)}`);
|
||||
const qs = new URLSearchParams({
|
||||
taskSeComCd: formatTaskSeComCdForApi(selection.selectedCodes),
|
||||
admdstCd: selection.admdstCd,
|
||||
sido: selection.sido,
|
||||
gugun: selection.gugun,
|
||||
});
|
||||
router.push(`/fnrs-cnsltn/regist?${qs}`);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -87,10 +93,10 @@ function FnrsList({ searchParams, pagination, onPaginationChange }: FnrsListProp
|
||||
</div>
|
||||
{tableComponent}
|
||||
</section>
|
||||
<TaskSePickerDialog
|
||||
<PbadmsTaskApplyDialog
|
||||
open={isTaskSeDialogOpen}
|
||||
onOpenChange={setIsTaskSeDialogOpen}
|
||||
onConfirm={handleConfirmTaskSe}
|
||||
onConfirm={handleConfirm}
|
||||
title="재원협의 신청"
|
||||
codeUpCd="FNRS"
|
||||
/>
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import { FnrsFormPage } from "./fnrs-form-page";
|
||||
|
||||
interface FnrsRegistPageProps {
|
||||
interface FnrsRegistDefaults {
|
||||
taskSeComCd?: string[];
|
||||
admdstCd?: string;
|
||||
sido?: string;
|
||||
gugun?: string;
|
||||
}
|
||||
|
||||
function FnrsRegistPage({ taskSeComCd }: FnrsRegistPageProps) {
|
||||
return <FnrsFormPage mode="create" taskSeComCd={taskSeComCd} />;
|
||||
interface FnrsRegistPageProps {
|
||||
defaults?: FnrsRegistDefaults;
|
||||
}
|
||||
|
||||
function FnrsRegistPage({ defaults }: FnrsRegistPageProps) {
|
||||
return <FnrsFormPage mode="create" createDefaults={defaults} />;
|
||||
}
|
||||
|
||||
export { FnrsRegistPage };
|
||||
export type { FnrsRegistDefaults };
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { useState } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { useUserStore } from "@/shared/lib";
|
||||
import { Field, FieldError, FieldGroup, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||
import { findCodeLabel, useCodeList } from "@/entities/code";
|
||||
import { type BnsApplyData, BnsPickerDialog } from "@/widgets/bns-picker";
|
||||
@@ -24,10 +23,12 @@ import {
|
||||
import type { FnrsCnsltnRegistFormData } from "../../lib/fnrs-cnsltn-regist-schema";
|
||||
|
||||
function BasicInfoFormSection() {
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const { control, watch, setValue } = useFormContext<FnrsCnsltnRegistFormData>();
|
||||
const [isPickerOpen, setIsPickerOpen] = useState(false);
|
||||
const taskSeComCd = watch("fnrsCnsltnMng.taskSeComCd");
|
||||
const localGovLabel = [watch("fnrsCnsltnMng.sido"), watch("fnrsCnsltnMng.gugun")]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
const { data: taskOptions } = useCodeList("FNRS");
|
||||
const { data: rtlztnFcltOptions } = useCodeList("RTLZTNFCLTSE");
|
||||
const { data: rtlztnUsgOptions } = useCodeList("RTLZTNUSGSE");
|
||||
@@ -48,6 +49,12 @@ function BasicInfoFormSection() {
|
||||
return (
|
||||
<SectionCollapsible title="기본 정보">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[20rem]" data-required>
|
||||
@@ -141,7 +148,7 @@ function BasicInfoFormSection() {
|
||||
<TableHead data-required>구분</TableHead>
|
||||
<TableCell>{taskSeComCdLabel}</TableCell>
|
||||
<TableHead>신청 지자체</TableHead>
|
||||
<TableCell>{userInfo?.instSeNm}</TableCell>
|
||||
<TableCell>{localGovLabel}</TableCell>
|
||||
</TableRow>
|
||||
{taskSeComCd.includes("RE") && (
|
||||
<>
|
||||
|
||||
@@ -108,7 +108,12 @@ export function mapDetailToFormData(data: InstlSncnMngDetResponse): InstlSncnReg
|
||||
|
||||
export function getCreateDefaultValues(
|
||||
userInfo: IUserInfo | null,
|
||||
taskSeComCd?: string[],
|
||||
options?: {
|
||||
taskSeComCd?: string[];
|
||||
admdstCd?: string;
|
||||
sido?: string;
|
||||
gugun?: string;
|
||||
},
|
||||
): InstlSncnRegistFormData {
|
||||
return {
|
||||
instlSncnMng: {
|
||||
@@ -116,7 +121,7 @@ export function getCreateDefaultValues(
|
||||
bizBgngYr: "",
|
||||
bizEndYr: "",
|
||||
bizCn: "",
|
||||
taskSeComCd: taskSeComCd ?? [],
|
||||
taskSeComCd: options?.taskSeComCd ?? [],
|
||||
sncnSeComCd: "",
|
||||
dsgnrNm: "",
|
||||
dsgnrTelno: "",
|
||||
@@ -126,6 +131,9 @@ export function getCreateDefaultValues(
|
||||
aplcntMblTelno: userInfo?.mphn ?? "",
|
||||
aplcntOgdpNm: userInfo?.ogdpSeNm ?? "",
|
||||
aplcntEmlAddr: userInfo?.email ?? "",
|
||||
admdstCd: options?.admdstCd ?? "",
|
||||
sido: options?.sido ?? "",
|
||||
gugun: options?.gugun ?? "",
|
||||
prgrsSttsComCd: "SV",
|
||||
atchFileGroupId: "",
|
||||
},
|
||||
|
||||
@@ -39,10 +39,15 @@ interface InstlFormPageProps {
|
||||
id?: string;
|
||||
mode: "create" | "edit";
|
||||
initialData?: InstlSncnMngDetResponse;
|
||||
taskSeComCd?: string[];
|
||||
createDefaults?: {
|
||||
taskSeComCd?: string[];
|
||||
admdstCd?: string;
|
||||
sido?: string;
|
||||
gugun?: string;
|
||||
};
|
||||
}
|
||||
|
||||
function InstlFormPage({ id, mode, initialData, taskSeComCd }: InstlFormPageProps) {
|
||||
function InstlFormPage({ id, mode, initialData, createDefaults }: InstlFormPageProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const router = useRouter();
|
||||
const { confirm } = useConfirm();
|
||||
@@ -58,7 +63,7 @@ function InstlFormPage({ id, mode, initialData, taskSeComCd }: InstlFormPageProp
|
||||
defaultValues:
|
||||
isEditMode && initialData
|
||||
? mapDetailToFormData(initialData)
|
||||
: getCreateDefaultValues(userInfo, taskSeComCd),
|
||||
: getCreateDefaultValues(userInfo, createDefaults),
|
||||
});
|
||||
|
||||
function buildFormPayload(
|
||||
|
||||
@@ -29,8 +29,7 @@ const EMPTY_ADST_LIST: CmCdAdstDstrComboResponseDto[] = [];
|
||||
const DEFAULT_SEARCH_PARAMS: InstlSncnMngSearchParams = {
|
||||
bizNm: "",
|
||||
taskSeComCd: "",
|
||||
sidoComCd: "",
|
||||
sggComCd: "",
|
||||
admdstCd: "",
|
||||
page: DEFAULT_PAGE_INDEX,
|
||||
size: DEFAULT_PAGE_SIZE,
|
||||
sort: DEFAULT_SORT,
|
||||
@@ -59,8 +58,9 @@ function InstlListPage() {
|
||||
...DEFAULT_SEARCH_PARAMS,
|
||||
bizNm,
|
||||
taskSeComCd,
|
||||
sidoComCd,
|
||||
sggComCd,
|
||||
admdstCd: sggComCd || sidoComCd || "",
|
||||
page: DEFAULT_PAGE_INDEX,
|
||||
size: pagination.pageSize,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
type InstlSncnMngSearchParams,
|
||||
useInstlSncnList,
|
||||
} from "@/entities/instl-sncn";
|
||||
import { TaskSePickerDialog } from "@/widgets/pbadms-sprt";
|
||||
import { PbadmsTaskApplyDialog, type PbadmsTaskApplySelection } from "@/widgets/pbadms-sprt";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
import { formatTaskSeComCdForApi } from "../lib/instl-sncn-regist-schema";
|
||||
@@ -32,8 +32,7 @@ function InstlList({ searchParams, pagination, onPaginationChange }: InstlListPr
|
||||
() => ({
|
||||
bizNm: searchParams.bizNm,
|
||||
taskSeComCd: searchParams.taskSeComCd,
|
||||
sidoComCd: searchParams.sidoComCd,
|
||||
sggComCd: searchParams.sggComCd,
|
||||
admdstCd: searchParams.admdstCd,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
sort: searchParams.sort ?? "instlSncnDocNo,desc",
|
||||
@@ -76,9 +75,15 @@ function InstlList({ searchParams, pagination, onPaginationChange }: InstlListPr
|
||||
setIsTaskSeDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleConfirmTaskSe = (selectedCodes: string[]) => {
|
||||
const handleConfirm = (selection: PbadmsTaskApplySelection) => {
|
||||
setIsTaskSeDialogOpen(false);
|
||||
router.push(`/instl-sncn/regist?taskSeComCd=${formatTaskSeComCdForApi(selectedCodes)}`);
|
||||
const qs = new URLSearchParams({
|
||||
taskSeComCd: formatTaskSeComCdForApi(selection.selectedCodes),
|
||||
admdstCd: selection.admdstCd,
|
||||
sido: selection.sido,
|
||||
gugun: selection.gugun,
|
||||
});
|
||||
router.push(`/instl-sncn/regist?${qs}`);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -92,10 +97,10 @@ function InstlList({ searchParams, pagination, onPaginationChange }: InstlListPr
|
||||
</div>
|
||||
{tableComponent}
|
||||
</section>
|
||||
<TaskSePickerDialog
|
||||
<PbadmsTaskApplyDialog
|
||||
open={isTaskSeDialogOpen}
|
||||
onOpenChange={setIsTaskSeDialogOpen}
|
||||
onConfirm={handleConfirmTaskSe}
|
||||
onConfirm={handleConfirm}
|
||||
title="설치인가 신청"
|
||||
codeUpCd="INSTL"
|
||||
/>
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import { InstlFormPage } from "./instl-form-page";
|
||||
|
||||
interface InstlRegistPageProps {
|
||||
interface InstlRegistDefaults {
|
||||
taskSeComCd?: string[];
|
||||
admdstCd?: string;
|
||||
sido?: string;
|
||||
gugun?: string;
|
||||
}
|
||||
|
||||
function InstlRegistPage({ taskSeComCd }: InstlRegistPageProps) {
|
||||
return <InstlFormPage mode="create" taskSeComCd={taskSeComCd} />;
|
||||
interface InstlRegistPageProps {
|
||||
defaults?: InstlRegistDefaults;
|
||||
}
|
||||
|
||||
function InstlRegistPage({ defaults }: InstlRegistPageProps) {
|
||||
return <InstlFormPage mode="create" createDefaults={defaults} />;
|
||||
}
|
||||
|
||||
export { InstlRegistPage };
|
||||
export type { InstlRegistDefaults };
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { useState } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { useUserStore } from "@/shared/lib";
|
||||
import { Field, FieldError, FieldGroup, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||
import { findCodeLabel, useCodeList } from "@/entities/code";
|
||||
import { type BnsApplyData, BnsPickerDialog } from "@/widgets/bns-picker";
|
||||
@@ -24,10 +23,12 @@ import {
|
||||
import type { InstlSncnRegistFormData } from "../../lib/instl-sncn-regist-schema";
|
||||
|
||||
function BasicInfoFormSection() {
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const { control, watch, setValue } = useFormContext<InstlSncnRegistFormData>();
|
||||
const [isPickerOpen, setIsPickerOpen] = useState(false);
|
||||
const taskSeComCd = watch("instlSncnMng.taskSeComCd");
|
||||
const localGovLabel = [watch("instlSncnMng.sido"), watch("instlSncnMng.gugun")]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
function handleBnsSelect(data: BnsApplyData) {
|
||||
setValue("instlSncnMng.bizNm", data.bizNm);
|
||||
@@ -52,6 +53,12 @@ function BasicInfoFormSection() {
|
||||
return (
|
||||
<SectionCollapsible title="기본 정보">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[20rem]" data-required>
|
||||
@@ -145,7 +152,7 @@ function BasicInfoFormSection() {
|
||||
<TableHead data-required>구분</TableHead>
|
||||
<TableCell>{taskSeComCdLabel}</TableCell>
|
||||
<TableHead>신청 지자체</TableHead>
|
||||
<TableCell>{userInfo?.instSeNm}</TableCell>
|
||||
<TableCell>{localGovLabel}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>사업구분</TableHead>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export { SewerBscPlanEditPage } from "./ui/sewer-bsc-plan-edit-page";
|
||||
export { SewerBscPlanFormPage } from "./ui/sewer-bsc-plan-form-page";
|
||||
export { SewerBscPlanListPage } from "./ui/sewer-bsc-plan-list-page";
|
||||
export { SewerBscPlanRegistPage } from "./ui/sewer-bsc-plan-regist-page";
|
||||
@@ -0,0 +1,102 @@
|
||||
import type { CostItemDef } from "@/widgets/pbadms-sprt/lib/business-cost-form-utils";
|
||||
|
||||
export interface BizCstMatrixItem {
|
||||
planStpNo: number;
|
||||
acntgSeCd: string;
|
||||
cstrnAmt: number;
|
||||
}
|
||||
|
||||
const PLAN_STEPS = [1, 2, 3, 4] as const;
|
||||
|
||||
function getAmt(list: BizCstMatrixItem[], acntgSeCd: string, planStpNo: number): number {
|
||||
return (
|
||||
list.find((item) => item.acntgSeCd === acntgSeCd && item.planStpNo === planStpNo)
|
||||
?.cstrnAmt ?? 0
|
||||
);
|
||||
}
|
||||
|
||||
function setAmt(
|
||||
list: BizCstMatrixItem[],
|
||||
acntgSeCd: string,
|
||||
planStpNo: number,
|
||||
cstrnAmt: number,
|
||||
): BizCstMatrixItem[] {
|
||||
const index = list.findIndex(
|
||||
(item) => item.acntgSeCd === acntgSeCd && item.planStpNo === planStpNo,
|
||||
);
|
||||
if (index >= 0) {
|
||||
const next = [...list];
|
||||
next[index] = { ...next[index], cstrnAmt };
|
||||
return next;
|
||||
}
|
||||
return [...list, { planStpNo, acntgSeCd, cstrnAmt }];
|
||||
}
|
||||
|
||||
/** CostItemDef 트리의 children 기반 bottom-up 부모합산 (각 단계별 cstrnAmt) */
|
||||
function recalculateBizCstMatrix(
|
||||
list: BizCstMatrixItem[],
|
||||
costItems: CostItemDef[],
|
||||
): BizCstMatrixItem[] {
|
||||
let next = [...list];
|
||||
let hasChanges = false;
|
||||
|
||||
for (let i = costItems.length - 1; i >= 0; i--) {
|
||||
const item = costItems[i];
|
||||
if (!item.readonly || !item.children) continue;
|
||||
|
||||
for (const planStpNo of PLAN_STEPS) {
|
||||
const childSum = item.children.reduce(
|
||||
(sum, childCode) => sum + getAmt(next, childCode, planStpNo),
|
||||
0,
|
||||
);
|
||||
const current = getAmt(next, item.code, planStpNo);
|
||||
if (current !== childSum) {
|
||||
next = setAmt(next, item.code, planStpNo, childSum);
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hasChanges ? next : list;
|
||||
}
|
||||
|
||||
function createEmptyBizCstList(costItems: CostItemDef[]): BizCstMatrixItem[] {
|
||||
const list: BizCstMatrixItem[] = [];
|
||||
for (const item of costItems) {
|
||||
for (const planStpNo of PLAN_STEPS) {
|
||||
list.push({ planStpNo, acntgSeCd: item.code, cstrnAmt: 0 });
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
function mergeBizCstList(
|
||||
existing: BizCstMatrixItem[],
|
||||
costItems: CostItemDef[],
|
||||
): BizCstMatrixItem[] {
|
||||
const existingMap = new Map(
|
||||
existing.map((item) => [`${item.acntgSeCd}:${item.planStpNo}`, item.cstrnAmt]),
|
||||
);
|
||||
const base = createEmptyBizCstList(costItems).map((item) => ({
|
||||
...item,
|
||||
cstrnAmt: existingMap.get(`${item.acntgSeCd}:${item.planStpNo}`) ?? 0,
|
||||
}));
|
||||
return recalculateBizCstMatrix(base, costItems);
|
||||
}
|
||||
|
||||
function toBizCstSaveList(
|
||||
list: BizCstMatrixItem[],
|
||||
costItems: CostItemDef[],
|
||||
): BizCstMatrixItem[] {
|
||||
return recalculateBizCstMatrix(list, costItems);
|
||||
}
|
||||
|
||||
export {
|
||||
createEmptyBizCstList,
|
||||
getAmt,
|
||||
mergeBizCstList,
|
||||
PLAN_STEPS,
|
||||
recalculateBizCstMatrix,
|
||||
setAmt,
|
||||
toBizCstSaveList,
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
import type { CostItemDef } from "@/widgets/pbadms-sprt/lib/business-cost-form-utils";
|
||||
|
||||
import { type BizCstMatrixItem,getAmt, PLAN_STEPS } from "./biz-cst-matrix-utils";
|
||||
|
||||
export interface FnrsCstItem {
|
||||
planStpNo: number;
|
||||
gvexpAmt?: number;
|
||||
lcexpAmt?: number;
|
||||
orgprBdchgAmt?: number;
|
||||
etcAmt?: number;
|
||||
sumAmt?: number;
|
||||
}
|
||||
|
||||
export interface FnrsCstStepRemaining {
|
||||
planStpNo: number;
|
||||
remaining: number;
|
||||
}
|
||||
|
||||
function getBizCstRootCode(costItems: CostItemDef[]): string | undefined {
|
||||
return costItems.find((item) => item.level === 1 && item.readonly)?.code;
|
||||
}
|
||||
|
||||
function getBizCstStepTotal(
|
||||
bizCstList: BizCstMatrixItem[],
|
||||
costItems: CostItemDef[],
|
||||
planStpNo: number,
|
||||
): number {
|
||||
const rootCode = getBizCstRootCode(costItems);
|
||||
if (!rootCode) return 0;
|
||||
return getAmt(bizCstList, rootCode, planStpNo);
|
||||
}
|
||||
|
||||
function sumFnrsCstLeaf(item: FnrsCstItem): number {
|
||||
return (
|
||||
(item.gvexpAmt ?? 0) +
|
||||
(item.lcexpAmt ?? 0) +
|
||||
(item.orgprBdchgAmt ?? 0) +
|
||||
(item.etcAmt ?? 0)
|
||||
);
|
||||
}
|
||||
|
||||
function recalcFnrsCstSumAmt(item: FnrsCstItem): FnrsCstItem {
|
||||
return {
|
||||
...item,
|
||||
sumAmt: sumFnrsCstLeaf(item),
|
||||
};
|
||||
}
|
||||
|
||||
function calcFnrsCstRemaining(
|
||||
bizCstList: BizCstMatrixItem[],
|
||||
fnrsCstList: FnrsCstItem[],
|
||||
costItems: CostItemDef[],
|
||||
): FnrsCstStepRemaining[] {
|
||||
return PLAN_STEPS.map((planStpNo) => {
|
||||
const bizTotal = getBizCstStepTotal(bizCstList, costItems, planStpNo);
|
||||
const fnrsItem = fnrsCstList.find((item) => item.planStpNo === planStpNo);
|
||||
const sharTotal = fnrsItem ? sumFnrsCstLeaf(fnrsItem) : 0;
|
||||
return { planStpNo, remaining: bizTotal - sharTotal };
|
||||
});
|
||||
}
|
||||
|
||||
function validateFnrsCstBalance(
|
||||
bizCstList: BizCstMatrixItem[],
|
||||
fnrsCstList: FnrsCstItem[],
|
||||
costItems: CostItemDef[],
|
||||
): string | null {
|
||||
if (costItems.length === 0) return null;
|
||||
|
||||
const remainingList = calcFnrsCstRemaining(bizCstList, fnrsCstList, costItems);
|
||||
const hasMismatch = remainingList.some((item) => item.remaining !== 0);
|
||||
if (hasMismatch) return "재원분담 총사업비를 확인해주세요.";
|
||||
return null;
|
||||
}
|
||||
|
||||
export {
|
||||
calcFnrsCstRemaining,
|
||||
getBizCstRootCode,
|
||||
getBizCstStepTotal,
|
||||
recalcFnrsCstSumAmt,
|
||||
sumFnrsCstLeaf,
|
||||
validateFnrsCstBalance,
|
||||
};
|
||||
@@ -0,0 +1,286 @@
|
||||
import type { IUserInfo } from "@/shared/store";
|
||||
import type {
|
||||
SewerBscPlanApplySaveRequestDto,
|
||||
SewerBscPlanDetResponseDto,
|
||||
} from "@/entities/sewer-bsc-plan";
|
||||
|
||||
import type { BizCstMatrixItem } from "./biz-cst-matrix-utils";
|
||||
import { recalcFnrsCstSumAmt } from "./fnrs-cst-form-utils";
|
||||
import { type SewerBscPlanRegistFormData } from "./sewer-bsc-plan-regist-schema";
|
||||
|
||||
const PLAN_STEPS = [1, 2, 3, 4] as const;
|
||||
/** 처리시설 상세: 0=현재, 1~4=변경 단계 */
|
||||
const TMPLNT_DTL_STEPS = [0, 1, 2, 3, 4] as const;
|
||||
|
||||
function createEmptyOtlnList() {
|
||||
return PLAN_STEPS.map((planStpNo) => ({ planStpNo }));
|
||||
}
|
||||
|
||||
function createEmptyFnrsCstList() {
|
||||
return PLAN_STEPS.map((planStpNo) => ({ planStpNo }));
|
||||
}
|
||||
|
||||
function mapSelectedValue(value?: string | null): "Y" | "N" | null {
|
||||
if (value === "Y" || value === "N") return value;
|
||||
return null;
|
||||
}
|
||||
|
||||
function mapEsntlAtchFileList(esntlFileList: SewerBscPlanDetResponseDto["esntlFileList"]) {
|
||||
const esntlMap = new Map<string, string>();
|
||||
|
||||
for (const item of esntlFileList ?? []) {
|
||||
if (!item.esntlAtchFileId || !item.atflCd) continue;
|
||||
esntlMap.set(item.esntlAtchFileId, item.atflCd);
|
||||
}
|
||||
|
||||
return Array.from(esntlMap.entries()).map(([esntlAtchFileId, atchFileGroupId]) => ({
|
||||
esntlAtchFileId,
|
||||
atchFileGroupId,
|
||||
}));
|
||||
}
|
||||
|
||||
function createEmptyTmplntDtlList(swtrmtFcltCd: string) {
|
||||
return TMPLNT_DTL_STEPS.map((planStpNo) => ({
|
||||
planStpNo,
|
||||
swtrmtFcltCd,
|
||||
prcsZoneArea: 0,
|
||||
swtrmtPpltnCnt: 0,
|
||||
planSwqty: 0,
|
||||
fcltCpct: 0,
|
||||
sprsysDrtwPplLen: 0,
|
||||
sprsysExlnPplLen: 0,
|
||||
cbsysPplLen: 0,
|
||||
icptgPplLen: 0,
|
||||
drgCnt: 0,
|
||||
drtwRlyPumpCnt: 0,
|
||||
drtwPumpCnt: 0,
|
||||
exlnPumpCnt: 0,
|
||||
}));
|
||||
}
|
||||
|
||||
export function getCreateDefaultValues(
|
||||
userInfo: IUserInfo | null,
|
||||
options?: {
|
||||
taskSeComCd?: string;
|
||||
taskSeCdNm?: string;
|
||||
sewerBscPlanSeComCd?: string;
|
||||
sewerBscPlanSeCdNm?: string;
|
||||
admdstCd?: string;
|
||||
sido?: string;
|
||||
gugun?: string;
|
||||
},
|
||||
): SewerBscPlanRegistFormData {
|
||||
return {
|
||||
sewerBscPlan: {
|
||||
sewerBscPlanNm: "",
|
||||
sewerBscPlanSeComCd: options?.sewerBscPlanSeComCd ?? "",
|
||||
sewerBscPlanSeCdNm: options?.sewerBscPlanSeCdNm ?? "",
|
||||
taskSeComCd: options?.taskSeComCd ?? "",
|
||||
taskSeCdNm: options?.taskSeCdNm ?? "",
|
||||
admdstCd: options?.admdstCd ?? userInfo?.instSeCd ?? "",
|
||||
prgrsSttsComCd: "SV",
|
||||
sido: options?.sido ?? "",
|
||||
gugun: options?.gugun ?? userInfo?.instSeNm ?? "",
|
||||
},
|
||||
sewerBscPlanDtl: {
|
||||
planYr: "",
|
||||
crtrYr: "",
|
||||
frlvlGoalYr: "",
|
||||
sclvlGoalYr: "",
|
||||
thlvlGoalYr: "",
|
||||
folvlGoalYr: "",
|
||||
bizOtlnCn: "",
|
||||
aplcntNm: userInfo?.userName ?? "",
|
||||
aplcntMblTelno: userInfo?.mphn ?? "",
|
||||
aplcntOgdpNm: userInfo?.ogdpSeNm ?? "",
|
||||
aplcntEmlAddr: userInfo?.email ?? "",
|
||||
dsgnrNm: "",
|
||||
dsgnrTelno: "",
|
||||
dsgnrOgdpNm: "",
|
||||
dsgnrEmlAddr: "",
|
||||
atchFileGroupId: "",
|
||||
},
|
||||
sewerBscPlanOtlnList: createEmptyOtlnList(),
|
||||
bizCstList: [] as BizCstMatrixItem[],
|
||||
fnrsCstList: createEmptyFnrsCstList(),
|
||||
sewerBscPlanTmplntList: [],
|
||||
cklstResultList: [],
|
||||
esntlAtchFileList: [],
|
||||
splmntDmndList: [],
|
||||
privacyConsent: "N",
|
||||
};
|
||||
}
|
||||
|
||||
export function mapDetailToFormData(data: SewerBscPlanDetResponseDto): SewerBscPlanRegistFormData {
|
||||
const detDto = data.detDto;
|
||||
const dtl = data.activeDtlDto;
|
||||
if (!detDto) throw new Error("상세 정보(detDto)가 없습니다.");
|
||||
|
||||
const otlnMap = new Map((data.otlnList ?? []).map((item) => [item.planStpNo, item]));
|
||||
const fnrsMap = new Map((data.fnrsCstList ?? []).map((item) => [item.planStpNo, item]));
|
||||
|
||||
return {
|
||||
sewerBscPlan: {
|
||||
sewerBscPlanDocNo: detDto.sewerBscPlanDocNo,
|
||||
sewerBscPlanNm: detDto.sewerBscPlanNm ?? "",
|
||||
sewerBscPlanSeComCd: detDto.sewerBscPlanSeComCd ?? "",
|
||||
sewerBscPlanSeCdNm: detDto.sewerBscPlanSeCdNm ?? "",
|
||||
taskSeComCd: detDto.taskSeComCd ?? "",
|
||||
admdstCd: detDto.admdstCd,
|
||||
envoSeComCd: detDto.envoSeComCd,
|
||||
prgrsSttsComCd: detDto.prgrsSttsComCd,
|
||||
sido: detDto.sido,
|
||||
gugun: detDto.gugun,
|
||||
},
|
||||
sewerBscPlanDtl: {
|
||||
chgCycl: dtl?.chgCycl ?? detDto.chgCycl,
|
||||
planYr: dtl?.planYr ?? "",
|
||||
crtrYr: dtl?.crtrYr ?? "",
|
||||
frlvlGoalYr: dtl?.frlvlGoalYr ?? "",
|
||||
sclvlGoalYr: dtl?.sclvlGoalYr ?? "",
|
||||
thlvlGoalYr: dtl?.thlvlGoalYr ?? "",
|
||||
folvlGoalYr: dtl?.folvlGoalYr ?? "",
|
||||
bizOtlnCn: dtl?.bizOtlnCn ?? "",
|
||||
aplcntNm: dtl?.aplcntNm ?? "",
|
||||
aplcntMblTelno: dtl?.aplcntMblTelno ?? "",
|
||||
aplcntOgdpNm: dtl?.aplcntOgdpNm ?? "",
|
||||
aplcntEmlAddr: dtl?.aplcntEmlAddr ?? "",
|
||||
dsgnrNm: dtl?.dsgnrNm ?? "",
|
||||
dsgnrTelno: dtl?.dsgnrTelno ?? "",
|
||||
dsgnrOgdpNm: dtl?.dsgnrOgdpNm ?? "",
|
||||
dsgnrEmlAddr: dtl?.dsgnrEmlAddr ?? "",
|
||||
atchFileGroupId: dtl?.atchFileGroupId ?? "",
|
||||
},
|
||||
sewerBscPlanOtlnList: PLAN_STEPS.map((planStpNo) => {
|
||||
const item = otlnMap.get(planStpNo);
|
||||
return {
|
||||
planStpNo,
|
||||
sewerPlanPpltnCnt: item?.sewerPlanPpltnCnt ?? 0,
|
||||
sewerPrcsPpltnCnt: item?.sewerPrcsPpltnCnt ?? 0,
|
||||
sewerPsplyRt: item?.sewerPsplyRt ?? 0,
|
||||
};
|
||||
}),
|
||||
bizCstList: (data.bizCstList ?? []).map((item) => ({
|
||||
planStpNo: item.planStpNo ?? 1,
|
||||
acntgSeCd: item.acntgSeCd ?? "",
|
||||
cstrnAmt: item.cstrnAmt ?? 0,
|
||||
})),
|
||||
fnrsCstList: PLAN_STEPS.map((planStpNo) => {
|
||||
const item = fnrsMap.get(planStpNo);
|
||||
return {
|
||||
planStpNo,
|
||||
gvexpAmt: item?.gvexpAmt ?? 0,
|
||||
lcexpAmt: item?.lcexpAmt ?? 0,
|
||||
orgprBdchgAmt: item?.orgprBdchgAmt ?? 0,
|
||||
etcAmt: item?.etcAmt ?? 0,
|
||||
sumAmt: item?.sumAmt ?? 0,
|
||||
};
|
||||
}),
|
||||
sewerBscPlanTmplntList: (data.tmplntList ?? []).map((item) => {
|
||||
const code = item.swtrmtFcltCd ?? "";
|
||||
const dtlMap = new Map(
|
||||
(item.sewerBscPlanTmplntDtlList ?? []).map((dtlItem) => [dtlItem.planStpNo, dtlItem]),
|
||||
);
|
||||
return {
|
||||
swtrmtFcltCd: code,
|
||||
swtrmtFcltNm: item.swtrmtFcltNm,
|
||||
atchFileGroupId: item.atchFileGroupId,
|
||||
atchFileNm: item.atchFileInfo?.orgnlFileNm,
|
||||
sewerBscPlanTmplntDtlList: createEmptyTmplntDtlList(code).map((empty) => {
|
||||
const dtlItem = dtlMap.get(empty.planStpNo);
|
||||
if (!dtlItem) return empty;
|
||||
return {
|
||||
planStpNo: empty.planStpNo,
|
||||
swtrmtFcltCd: code,
|
||||
prcsZoneArea: dtlItem.prcsZoneArea ?? 0,
|
||||
swtrmtPpltnCnt: dtlItem.swtrmtPpltnCnt ?? 0,
|
||||
planSwqty: dtlItem.planSwqty ?? 0,
|
||||
fcltCpct: dtlItem.fcltCpct ?? 0,
|
||||
sprsysDrtwPplLen: dtlItem.sprsysDrtwPplLen ?? 0,
|
||||
sprsysExlnPplLen: dtlItem.sprsysExlnPplLen ?? 0,
|
||||
cbsysPplLen: dtlItem.cbsysPplLen ?? 0,
|
||||
icptgPplLen: dtlItem.icptgPplLen ?? 0,
|
||||
drgCnt: dtlItem.drgCnt ?? 0,
|
||||
drtwRlyPumpCnt: dtlItem.drtwRlyPumpCnt ?? 0,
|
||||
drtwPumpCnt: dtlItem.drtwPumpCnt ?? 0,
|
||||
exlnPumpCnt: dtlItem.exlnPumpCnt ?? 0,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}),
|
||||
cklstResultList:
|
||||
data.cklstResultRows?.map((item) => ({
|
||||
cklstSn: item.cklstSn,
|
||||
cklstArtclSn: item.cklstArtclSn,
|
||||
cklstRvwSn: item.cklstRvwSn,
|
||||
rvwStpComCd: item.rvwStpComCd,
|
||||
selected: mapSelectedValue(item.rvwRsltAnmlyYn),
|
||||
picRvwCn: item.picRvwCn,
|
||||
})) ?? [],
|
||||
esntlAtchFileList: mapEsntlAtchFileList(data.esntlFileList),
|
||||
splmntDmndList:
|
||||
data.splmntDmndList?.map((item) => ({
|
||||
splmntDmndSn: item.splmntDmndSn,
|
||||
splmntArtclComCd: item.splmntArtclComCd,
|
||||
splmntArtclCdNm: item.splmntArtclCdNm,
|
||||
splmntDmndCn: item.splmntDmndCn,
|
||||
splmntDmndDt: item.splmntDmndDt,
|
||||
splmntDmndAtchFileGroupId: item.splmntDmndAtchFileGroupId ?? "",
|
||||
splmntDmndAtchFileNm: item.splmntDmndAtchFileInfo?.orgnlFileNm ?? "",
|
||||
splmntCn: item.splmntCn ?? "",
|
||||
splmntPicNm: item.splmntPicNm ?? "",
|
||||
splmntCmptnDt: item.splmntCmptnDt ?? "",
|
||||
splmntAtchFileGroupId: item.splmntAtchFileGroupId ?? "",
|
||||
splmntAtchFileNm: item.splmntAtchFileInfo?.orgnlFileNm ?? "",
|
||||
splmntAtchFileAtflId: item.splmntAtchFileInfo?.atflId ?? "",
|
||||
cmptnYn: item.cmptnYn,
|
||||
})) ?? [],
|
||||
privacyConsent: "Y",
|
||||
};
|
||||
}
|
||||
|
||||
export function buildApplyPayload(
|
||||
data: SewerBscPlanRegistFormData,
|
||||
prgrsSttsComCd: "SV" | "LE" | "TE",
|
||||
options?: { isEditMode?: boolean; docNo?: string },
|
||||
): SewerBscPlanApplySaveRequestDto {
|
||||
const { taskSeComCd, ...sewerBscPlanRest } = data.sewerBscPlan;
|
||||
|
||||
return {
|
||||
sewerBscPlan: {
|
||||
...sewerBscPlanRest,
|
||||
taskSeComCd: taskSeComCd ?? "",
|
||||
sewerBscPlanDocNo: options?.isEditMode ? options.docNo : undefined,
|
||||
prgrsSttsComCd,
|
||||
},
|
||||
sewerBscPlanDtl: {
|
||||
...data.sewerBscPlanDtl,
|
||||
sewerBscPlanDocNo: options?.isEditMode ? options.docNo : undefined,
|
||||
},
|
||||
sewerBscPlanOtlnList: data.sewerBscPlanOtlnList,
|
||||
bizCstList: data.bizCstList,
|
||||
fnrsCstList: data.fnrsCstList.map((item) => recalcFnrsCstSumAmt(item)),
|
||||
sewerBscPlanTmplntList: data.sewerBscPlanTmplntList.map(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
({ atchFileNm, swtrmtFcltNm, ...item }) => ({
|
||||
swtrmtFcltCd: item.swtrmtFcltCd,
|
||||
atchFileGroupId: item.atchFileGroupId,
|
||||
sewerBscPlanTmplntDtlList: item.sewerBscPlanTmplntDtlList,
|
||||
}),
|
||||
),
|
||||
cklstResultList: data.cklstResultList.map((item) => ({
|
||||
...item,
|
||||
selected: item.selected ?? undefined,
|
||||
})),
|
||||
esntlAtchFileList: data.esntlAtchFileList,
|
||||
splmntDmndList: data.splmntDmndList.map(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
({ splmntAtchFileNm, splmntAtchFileAtflId, splmntDmndAtchFileNm, ...item }) => ({
|
||||
...item,
|
||||
...(prgrsSttsComCd === "TE" ? { cmptnYn: "Y" } : null),
|
||||
}),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export { createEmptyTmplntDtlList };
|
||||
@@ -0,0 +1,105 @@
|
||||
import type { LvNowDmgMtncGnsvyResponseDto, NowDmgMtncGnsvyResponseDto } from "@/entities/dc";
|
||||
|
||||
type StageKey = "Dt1" | "Dt2" | "Dt3" | "Dt4" | "" | "Dc1" | "Dc2" | "Dc3" | "Dc4";
|
||||
|
||||
const STAGE_KEYS: StageKey[] = ["Dt1", "Dt2", "Dt3", "Dt4", "", "Dc1", "Dc2", "Dc3", "Dc4"];
|
||||
|
||||
function formatPlanCell(value: string | number | null | undefined, asYear = false): string {
|
||||
if (value === null || value === undefined || value === "") return "-";
|
||||
if (asYear) return `${value}년`;
|
||||
if (typeof value === "number") return value.toLocaleString();
|
||||
const num = Number(value);
|
||||
if (!Number.isNaN(num) && value.trim() !== "" && /^-?\d+(\.\d+)?$/.test(value.trim()))
|
||||
return num.toLocaleString();
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function getStagedValue(
|
||||
data: NowDmgMtncGnsvyResponseDto | null | undefined,
|
||||
fieldPrefix: string,
|
||||
stage: StageKey,
|
||||
): string | number | null | undefined {
|
||||
if (!data) return undefined;
|
||||
const key = `${fieldPrefix}${stage}` as keyof NowDmgMtncGnsvyResponseDto;
|
||||
return data[key] as string | number | null | undefined;
|
||||
}
|
||||
|
||||
function getPlanYearCells(data: NowDmgMtncGnsvyResponseDto | null | undefined): string[] {
|
||||
return STAGE_KEYS.map((stage) => formatPlanCell(getStagedValue(data, "planYr", stage), true));
|
||||
}
|
||||
|
||||
function getMetricCells(
|
||||
data: NowDmgMtncGnsvyResponseDto | null | undefined,
|
||||
fieldPrefix: string,
|
||||
): string[] {
|
||||
return STAGE_KEYS.map((stage) => formatPlanCell(getStagedValue(data, fieldPrefix, stage)));
|
||||
}
|
||||
|
||||
function collectNodesByLevel(
|
||||
nodes: LvNowDmgMtncGnsvyResponseDto[] | undefined,
|
||||
targetLevel: number,
|
||||
acc: LvNowDmgMtncGnsvyResponseDto[] = [],
|
||||
): LvNowDmgMtncGnsvyResponseDto[] {
|
||||
if (!nodes) return acc;
|
||||
for (const node of nodes) {
|
||||
if (node.level === targetLevel) acc.push(node);
|
||||
if (node.children?.length) collectNodesByLevel(node.children, targetLevel, acc);
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
function extractYearOptions(tree: LvNowDmgMtncGnsvyResponseDto[] | undefined): string[] {
|
||||
const years = collectNodesByLevel(tree, 3)
|
||||
.map((node) => node.gnsvyYr)
|
||||
.filter((year): year is string => Boolean(year));
|
||||
return [...new Set(years)];
|
||||
}
|
||||
|
||||
interface FacilityOption {
|
||||
code: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
function extractFacilityOptions(
|
||||
tree: LvNowDmgMtncGnsvyResponseDto[] | undefined,
|
||||
gnsvyYr: string,
|
||||
): FacilityOption[] {
|
||||
const yearNodes = collectNodesByLevel(tree, 3).filter((node) => node.gnsvyYr === gnsvyYr);
|
||||
const options: FacilityOption[] = [];
|
||||
|
||||
function pushFacility(node: LvNowDmgMtncGnsvyResponseDto) {
|
||||
if (!node.swtrmtFcltCd) return;
|
||||
options.push({
|
||||
code: node.swtrmtFcltCd,
|
||||
name: node.swtrmtFcltNm ?? node.swtrmtFcltCd,
|
||||
});
|
||||
}
|
||||
|
||||
if (yearNodes.length > 0) {
|
||||
for (const yearNode of yearNodes) {
|
||||
for (const child of yearNode.children ?? []) pushFacility(child);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
// level=3 응답이 시설 리프만 내려주는 경우
|
||||
function walk(nodes: LvNowDmgMtncGnsvyResponseDto[] | undefined) {
|
||||
if (!nodes) return;
|
||||
for (const node of nodes) {
|
||||
pushFacility(node);
|
||||
walk(node.children);
|
||||
}
|
||||
}
|
||||
walk(tree);
|
||||
return options;
|
||||
}
|
||||
|
||||
export {
|
||||
extractFacilityOptions,
|
||||
extractYearOptions,
|
||||
formatPlanCell,
|
||||
getMetricCells,
|
||||
getPlanYearCells,
|
||||
STAGE_KEYS,
|
||||
};
|
||||
export type { FacilityOption };
|
||||
@@ -0,0 +1,135 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const tmplntDtlSchema = z.object({
|
||||
planStpNo: z.number(),
|
||||
swtrmtFcltCd: z.string(),
|
||||
prcsZoneArea: z.number().optional(),
|
||||
swtrmtPpltnCnt: z.number().optional(),
|
||||
planSwqty: z.number().optional(),
|
||||
fcltCpct: z.number().optional(),
|
||||
sprsysDrtwPplLen: z.number().optional(),
|
||||
sprsysExlnPplLen: z.number().optional(),
|
||||
cbsysPplLen: z.number().optional(),
|
||||
icptgPplLen: z.number().optional(),
|
||||
drgCnt: z.number().optional(),
|
||||
drtwRlyPumpCnt: z.number().optional(),
|
||||
drtwPumpCnt: z.number().optional(),
|
||||
exlnPumpCnt: z.number().optional(),
|
||||
});
|
||||
|
||||
export const sewerBscPlanRegistSchema = z.object({
|
||||
sewerBscPlan: z.object({
|
||||
sewerBscPlanDocNo: z.string().optional(),
|
||||
sewerBscPlanNm: z.string().min(1, "기본계획명은 필수입니다"),
|
||||
sewerBscPlanSeComCd: z.string().optional(),
|
||||
sewerBscPlanSeCdNm: z.string().optional(),
|
||||
taskSeComCd: z.string().optional(),
|
||||
taskSeCdNm: z.string().optional(),
|
||||
admdstCd: z.string().optional(),
|
||||
envoSeComCd: z.string().optional(),
|
||||
prgrsSttsComCd: z.string().optional(),
|
||||
sido: z.string().optional(),
|
||||
gugun: z.string().optional(),
|
||||
}),
|
||||
sewerBscPlanDtl: z.object({
|
||||
chgCycl: z.number().optional(),
|
||||
planYr: z.string().min(1, "계획년도는 필수입니다"),
|
||||
crtrYr: z.string().min(1, "기준년도는 필수입니다"),
|
||||
frlvlGoalYr: z.string().min(1, "1단계 목표연도는 필수입니다"),
|
||||
sclvlGoalYr: z.string().min(1, "2단계 목표연도는 필수입니다"),
|
||||
thlvlGoalYr: z.string().min(1, "3단계 목표연도는 필수입니다"),
|
||||
folvlGoalYr: z.string().min(1, "4단계 목표연도는 필수입니다"),
|
||||
bizOtlnCn: z.string().min(1, "계획개요는 필수입니다"),
|
||||
aplcntNm: z.string().optional(),
|
||||
aplcntMblTelno: z.string().optional(),
|
||||
aplcntOgdpNm: z.string().optional(),
|
||||
aplcntEmlAddr: z.string().optional(),
|
||||
dsgnrNm: z.string().min(1, "설계자 이름은 필수입니다"),
|
||||
dsgnrTelno: z
|
||||
.string()
|
||||
.min(1, "설계자 전화번호는 필수입니다")
|
||||
.regex(/^[0-9-]+$/, "숫자와 하이픈(-)만 입력 가능합니다"),
|
||||
dsgnrOgdpNm: z.string().min(1, "설계자 소속은 필수입니다"),
|
||||
dsgnrEmlAddr: z
|
||||
.string()
|
||||
.min(1, "설계자 이메일은 필수입니다")
|
||||
.email("올바른 이메일 형식이 아닙니다"),
|
||||
atchFileGroupId: z.string().optional(),
|
||||
}),
|
||||
sewerBscPlanOtlnList: z.array(
|
||||
z.object({
|
||||
planStpNo: z.number(),
|
||||
sewerPlanPpltnCnt: z.number().optional(),
|
||||
sewerPrcsPpltnCnt: z.number().optional(),
|
||||
sewerPsplyRt: z.number().optional(),
|
||||
}),
|
||||
),
|
||||
bizCstList: z.array(
|
||||
z.object({
|
||||
planStpNo: z.number(),
|
||||
acntgSeCd: z.string(),
|
||||
cstrnAmt: z.number(),
|
||||
}),
|
||||
),
|
||||
fnrsCstList: z.array(
|
||||
z.object({
|
||||
planStpNo: z.number(),
|
||||
gvexpAmt: z.number().optional(),
|
||||
lcexpAmt: z.number().optional(),
|
||||
orgprBdchgAmt: z.number().optional(),
|
||||
etcAmt: z.number().optional(),
|
||||
sumAmt: z.number().optional(),
|
||||
}),
|
||||
),
|
||||
sewerBscPlanTmplntList: z.array(
|
||||
z.object({
|
||||
swtrmtFcltCd: z.string(),
|
||||
swtrmtFcltNm: z.string().nullish(),
|
||||
atchFileGroupId: z.string().optional(),
|
||||
atchFileNm: z.string().optional(),
|
||||
sewerBscPlanTmplntDtlList: z.array(tmplntDtlSchema).optional(),
|
||||
}),
|
||||
),
|
||||
cklstResultList: z.array(
|
||||
z.object({
|
||||
cklstSn: z.number(),
|
||||
cklstArtclSn: z.number(),
|
||||
cklstRvwSn: z.number(),
|
||||
rvwStpComCd: z.string(),
|
||||
selected: z.enum(["Y", "N"]).nullable(),
|
||||
picRvwCn: z.string().nullable().optional(),
|
||||
}),
|
||||
),
|
||||
esntlAtchFileList: z.array(
|
||||
z.object({
|
||||
esntlAtchFileId: z.string(),
|
||||
atchFileGroupId: z.string(),
|
||||
}),
|
||||
),
|
||||
splmntDmndList: z.array(
|
||||
z.object({
|
||||
splmntDmndSn: z.number().optional(),
|
||||
splmntArtclComCd: z.string().optional(),
|
||||
splmntArtclCdNm: z.string().nullish(),
|
||||
splmntDmndCn: z.string().optional(),
|
||||
splmntDmndDt: z.string().optional(),
|
||||
splmntDmndAtchFileGroupId: z.string().optional(),
|
||||
splmntDmndAtchFileNm: z.string().optional(),
|
||||
splmntCn: z.string().optional(),
|
||||
splmntPicNm: z.string().optional(),
|
||||
splmntCmptnDt: z.string().optional(),
|
||||
splmntAtchFileGroupId: z.string().optional(),
|
||||
splmntAtchFileNm: z.string().optional(),
|
||||
splmntAtchFileAtflId: z.string().optional(),
|
||||
cmptnYn: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
privacyConsent: z.enum(["Y", "N"], {
|
||||
required_error: "개인정보 수집/이용 동의는 필수입니다",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SewerBscPlanRegistFormData = z.infer<typeof sewerBscPlanRegistSchema>;
|
||||
|
||||
/** 체크리스트·필수첨부 docSeComCd — CMS 합의 전 임시값 */
|
||||
export const SEWER_BSC_PLAN_DOC_SE_COM_CD = "SEWER";
|
||||
@@ -0,0 +1,70 @@
|
||||
import type { NowDmgMtncGnsvyResponseDto } from "@/entities/dc";
|
||||
|
||||
/**
|
||||
* 폼 매트릭스 행 키 ↔ DC 당초(Dc) 필드 prefix.
|
||||
* API에 대응 필드가 없으면 null (당초 셀 `-`).
|
||||
*/
|
||||
const TMPLNT_DC_FIELD_PREFIX: Record<string, string | null> = {
|
||||
prcsZoneArea: "swtrmtZoneArea",
|
||||
swtrmtPpltnCnt: "swtrmtPpltnCnt",
|
||||
planSwqty: "planSwageQntSum",
|
||||
fcltCpct: "fcltCpctSum",
|
||||
sprsysDrtwPplLen: "brcPplSum",
|
||||
sprsysExlnPplLen: "exclnPplSum",
|
||||
cbsysPplLen: null,
|
||||
icptgPplLen: "icptgPplSum",
|
||||
drgCnt: "mltvFclSum",
|
||||
drtwRlyPumpCnt: null,
|
||||
drtwPumpCnt: "pmstnSum",
|
||||
exlnPumpCnt: null,
|
||||
};
|
||||
|
||||
export type TmplntFieldKey =
|
||||
| "prcsZoneArea"
|
||||
| "swtrmtPpltnCnt"
|
||||
| "planSwqty"
|
||||
| "fcltCpct"
|
||||
| "sprsysDrtwPplLen"
|
||||
| "sprsysExlnPplLen"
|
||||
| "cbsysPplLen"
|
||||
| "icptgPplLen"
|
||||
| "drgCnt"
|
||||
| "drtwRlyPumpCnt"
|
||||
| "drtwPumpCnt"
|
||||
| "exlnPumpCnt";
|
||||
|
||||
export const PLAN_STP_NOS = [1, 2, 3, 4] as const;
|
||||
|
||||
export type PlanStpNo = (typeof PLAN_STP_NOS)[number];
|
||||
|
||||
function formatOriginalValue(value: string | number | null | undefined): string {
|
||||
if (value === undefined || value === null || value === "") return "-";
|
||||
const num = Number(value);
|
||||
if (!Number.isNaN(num)) return num.toLocaleString();
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function getOriginalYear(
|
||||
data: NowDmgMtncGnsvyResponseDto | null | undefined,
|
||||
planStpNo: PlanStpNo,
|
||||
): string {
|
||||
if (!data) return "-";
|
||||
const key = `planYrDc${planStpNo}` as keyof NowDmgMtncGnsvyResponseDto;
|
||||
const value = data[key];
|
||||
if (value === undefined || value === null || value === "") return "-";
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function getOriginalValue(
|
||||
data: NowDmgMtncGnsvyResponseDto | null | undefined,
|
||||
fieldKey: TmplntFieldKey,
|
||||
planStpNo: PlanStpNo,
|
||||
): string {
|
||||
if (!data) return "-";
|
||||
const prefix = TMPLNT_DC_FIELD_PREFIX[fieldKey];
|
||||
if (!prefix) return "-";
|
||||
const key = `${prefix}Dc${planStpNo}` as keyof NowDmgMtncGnsvyResponseDto;
|
||||
return formatOriginalValue(data[key] as string | number | null | undefined);
|
||||
}
|
||||
|
||||
export { getOriginalValue, getOriginalYear };
|
||||
@@ -0,0 +1,33 @@
|
||||
function buildYearOptions(from: number, to: number): { value: string; label: string }[] {
|
||||
const years: { value: string; label: string }[] = [];
|
||||
const step = from <= to ? 1 : -1;
|
||||
for (let year = from; step > 0 ? year <= to : year >= to; year += step) {
|
||||
const value = String(year);
|
||||
years.push({ value, label: value });
|
||||
}
|
||||
return years;
|
||||
}
|
||||
|
||||
/** 계획년도·기준년도: 현재 ~ 2005 */
|
||||
function getPlanYearOptions(now = new Date().getFullYear()) {
|
||||
return buildYearOptions(now, 2005);
|
||||
}
|
||||
|
||||
/** 단계 목표연도: 현재 ~ 현재+30 */
|
||||
function getGoalYearOptions(now = new Date().getFullYear()) {
|
||||
return buildYearOptions(now, now + 30);
|
||||
}
|
||||
|
||||
function getLinkedGoalYears(frlvlGoalYr: string) {
|
||||
const base = Number(frlvlGoalYr);
|
||||
if (!Number.isFinite(base) || base <= 0) {
|
||||
return { sclvlGoalYr: "", thlvlGoalYr: "", folvlGoalYr: "" };
|
||||
}
|
||||
return {
|
||||
sclvlGoalYr: String(base + 5),
|
||||
thlvlGoalYr: String(base + 10),
|
||||
folvlGoalYr: String(base + 15),
|
||||
};
|
||||
}
|
||||
|
||||
export { buildYearOptions, getGoalYearOptions, getLinkedGoalYears, getPlanYearOptions };
|
||||
@@ -0,0 +1,202 @@
|
||||
"use client";
|
||||
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
|
||||
import { dayjs } from "@/shared/lib";
|
||||
import type { SewerBscPlanResponseDto } from "@/entities/sewer-bsc-plan";
|
||||
import { Badge } from "@common/components";
|
||||
|
||||
const columnHelper = createColumnHelper<SewerBscPlanResponseDto>();
|
||||
|
||||
const MAIN_STEP_LABELS = {
|
||||
save: "저장",
|
||||
apply: "신청완료",
|
||||
envoReview: "환경청 검토",
|
||||
techReview: "기술검토",
|
||||
reply: "검토결과 회신",
|
||||
supplement: "보완요청",
|
||||
} as const;
|
||||
|
||||
const TECH_REVIEW_STATUS_CODES = new Set(["EE", "ER", "TE", "TS", "TR", "TP"]);
|
||||
|
||||
type BadgeState = "completed" | "current" | "pending" | "action";
|
||||
|
||||
interface ProgressBadge {
|
||||
label: string;
|
||||
state: BadgeState;
|
||||
}
|
||||
|
||||
function buildProgressBadges(prgrsSttsComCd: string, prgrsSttsCdNm: string): ProgressBadge[] {
|
||||
const techReviewLabel = prgrsSttsCdNm || MAIN_STEP_LABELS.techReview;
|
||||
const replyLabel = prgrsSttsCdNm || MAIN_STEP_LABELS.reply;
|
||||
|
||||
if (prgrsSttsComCd === "SV") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.save, state: "current" },
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "ST") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.save, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "action" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "LE") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.save, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "action" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "SL") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.save, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
|
||||
{ label: MAIN_STEP_LABELS.supplement, state: "action" },
|
||||
];
|
||||
}
|
||||
|
||||
if (TECH_REVIEW_STATUS_CODES.has(prgrsSttsComCd)) {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.save, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
||||
{ label: techReviewLabel, state: "action" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "EP") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.save, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "completed" },
|
||||
{ label: replyLabel, state: "action" },
|
||||
];
|
||||
}
|
||||
|
||||
if (prgrsSttsComCd === "TA" || prgrsSttsComCd === "TB") {
|
||||
return [
|
||||
{ label: MAIN_STEP_LABELS.save, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.techReview, state: "completed" },
|
||||
{ label: MAIN_STEP_LABELS.reply, state: "completed" },
|
||||
];
|
||||
}
|
||||
|
||||
return [{ label: prgrsSttsCdNm || prgrsSttsComCd || "-", state: "current" }];
|
||||
}
|
||||
|
||||
function getBadgeColor(state: BadgeState) {
|
||||
switch (state) {
|
||||
case "completed":
|
||||
return "green";
|
||||
case "current":
|
||||
return "blue";
|
||||
case "pending":
|
||||
return "lightGray";
|
||||
case "action":
|
||||
return "orange";
|
||||
}
|
||||
}
|
||||
|
||||
function formatOptional(value: string | undefined) {
|
||||
return value?.trim() ? value : "-";
|
||||
}
|
||||
|
||||
interface ProgressStepBadgeProps {
|
||||
prgrsSttsComCd: string;
|
||||
prgrsSttsCdNm: string;
|
||||
}
|
||||
|
||||
function ProgressStepBadge({ prgrsSttsComCd, prgrsSttsCdNm }: ProgressStepBadgeProps) {
|
||||
const badges = buildProgressBadges(prgrsSttsComCd, prgrsSttsCdNm);
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center justify-start">
|
||||
{badges.map((badge, index) => (
|
||||
<Fragment key={`${badge.label}-${index}`}>
|
||||
{index > 0 && <span className="mx-2">></span>}
|
||||
<Badge color={getBadgeColor(badge.state)}>{badge.label}</Badge>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function createSewerBscPlanColumns() {
|
||||
return [
|
||||
columnHelper.accessor("sido", {
|
||||
header: "시도",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("gugun", {
|
||||
header: "시군구",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("sewerBscPlanNm", {
|
||||
header: "기본계획명",
|
||||
meta: { align: "left" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("sewerBscPlanSeCdNm", {
|
||||
header: "계획구분",
|
||||
meta: { align: "center" },
|
||||
cell: ({ row }) =>
|
||||
formatOptional(row.original.sewerBscPlanSeCdNm || row.original.sewerBscPlanSeComCd),
|
||||
}),
|
||||
columnHelper.accessor("taskSeCdNm", {
|
||||
header: "업무구분",
|
||||
meta: { align: "center" },
|
||||
cell: ({ row }) => formatOptional(row.original.taskSeCdNm || row.original.taskSeComCd),
|
||||
}),
|
||||
columnHelper.accessor("rgtrNm", {
|
||||
header: "담당자",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("envoSeCdNm", {
|
||||
header: "해당기관",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "progress",
|
||||
header: "진행상황",
|
||||
meta: { align: "center" },
|
||||
cell: ({ row }) => (
|
||||
<ProgressStepBadge
|
||||
prgrsSttsComCd={row.original.prgrsSttsComCd || ""}
|
||||
prgrsSttsCdNm={row.original.prgrsSttsCdNm || ""}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("regDt", {
|
||||
header: "등록일",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue();
|
||||
return value ? dayjs(value).format("YYYY-MM-DD") : "-";
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import {
|
||||
type CmCdAdstDstrComboResponseDto,
|
||||
useFindGugunCombo,
|
||||
useFindSidoCombo,
|
||||
} from "@/entities/adstdstr";
|
||||
import { useLvNowDmgMtncGnsvy, useNowDmgMtncGnsvy } from "@/entities/dc";
|
||||
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Skeleton,
|
||||
} from "@common/components";
|
||||
|
||||
import { extractFacilityOptions, extractYearOptions } from "../lib/previous-plan-table-utils";
|
||||
|
||||
import { PreviousPlanAreaTable } from "./previous-plan/previous-plan-area-table";
|
||||
import { PreviousPlanFileSection } from "./previous-plan/previous-plan-file-section";
|
||||
import { PreviousPlanSearchFilters } from "./previous-plan/previous-plan-search-filters";
|
||||
import { PreviousPlanSummaryTable } from "./previous-plan/previous-plan-summary-table";
|
||||
|
||||
interface PreviousPlanLookupDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
/** 부모 폼 `sewerBscPlan.sido` 표시명 */
|
||||
initialSidoNm?: string;
|
||||
/** 부모 폼 `sewerBscPlan.gugun` 표시명 */
|
||||
initialGugunNm?: string;
|
||||
}
|
||||
|
||||
const EMPTY_ADST_LIST: CmCdAdstDstrComboResponseDto[] = [];
|
||||
|
||||
function PreviousPlanLookupDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
initialSidoNm = "",
|
||||
initialGugunNm = "",
|
||||
}: PreviousPlanLookupDialogProps) {
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const [sidoComCd, setSidoComCd] = useState("");
|
||||
const [sggComCd, setSggComCd] = useState("");
|
||||
const [gnsvyYr, setGnsvyYr] = useState("");
|
||||
const [swtrmtFcltCd, setSwtrmtFcltCd] = useState("");
|
||||
const [hasPrefillApplied, setHasPrefillApplied] = useState(false);
|
||||
const [searchedClsfSystmNm, setSearchedClsfSystmNm] = useState("");
|
||||
|
||||
const { data: sidoData, isPending: isSidoPending } = useFindSidoCombo();
|
||||
const { data: gugunData, isPending: isGugunPending } = useFindGugunCombo(sidoComCd);
|
||||
|
||||
const sidoOptions = sidoData ?? EMPTY_ADST_LIST;
|
||||
const gugunOptions = gugunData ?? EMPTY_ADST_LIST;
|
||||
|
||||
const ctpvNm = sidoOptions.find((option) => option.adstDstrCd === sidoComCd)?.adstDstrNm ?? "";
|
||||
const sggNm = gugunOptions.find((option) => option.adstDstrCd === sggComCd)?.adstDstrNm ?? "";
|
||||
|
||||
const searchParams = {
|
||||
swtrmtFcltCd,
|
||||
ctpvNm,
|
||||
sggNm,
|
||||
targetYear: gnsvyYr,
|
||||
};
|
||||
|
||||
const { data, isFetching, refetch } = useNowDmgMtncGnsvy(searchParams, false);
|
||||
const planData = gnsvyYr && swtrmtFcltCd ? (data?.[0] ?? null) : null;
|
||||
|
||||
const { data: yearTree, isPending: isYearPending } = useLvNowDmgMtncGnsvy({
|
||||
level: "2",
|
||||
ctpvNm: ctpvNm || undefined,
|
||||
sggNm: sggNm || undefined,
|
||||
});
|
||||
|
||||
const { data: facilityTree, isPending: isFacilityPending } = useLvNowDmgMtncGnsvy({
|
||||
level: "3",
|
||||
ctpvNm: ctpvNm || undefined,
|
||||
sggNm: sggNm || undefined,
|
||||
gnsvyYr: gnsvyYr || undefined,
|
||||
});
|
||||
|
||||
const yearOptions = useMemo(() => extractYearOptions(yearTree), [yearTree]);
|
||||
const facilityOptions = useMemo(
|
||||
() => extractFacilityOptions(facilityTree, gnsvyYr),
|
||||
[facilityTree, gnsvyYr],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setSidoComCd("");
|
||||
setSggComCd("");
|
||||
setGnsvyYr("");
|
||||
setSwtrmtFcltCd("");
|
||||
setSearchedClsfSystmNm("");
|
||||
setHasPrefillApplied(false);
|
||||
return;
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || hasPrefillApplied || isSidoPending || !sidoOptions.length) return;
|
||||
if (!initialSidoNm) {
|
||||
setHasPrefillApplied(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const matchedSido = sidoOptions.find((option) => option.adstDstrNm === initialSidoNm);
|
||||
if (matchedSido) setSidoComCd(matchedSido.adstDstrCd);
|
||||
setHasPrefillApplied(true);
|
||||
}, [open, hasPrefillApplied, isSidoPending, sidoOptions, initialSidoNm]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !sidoComCd || isGugunPending || !initialGugunNm) return;
|
||||
if (sggComCd) return;
|
||||
const matchedGugun = gugunOptions.find((option) => option.adstDstrNm === initialGugunNm);
|
||||
if (matchedGugun) setSggComCd(matchedGugun.adstDstrCd);
|
||||
}, [open, sidoComCd, isGugunPending, gugunOptions, initialGugunNm, sggComCd]);
|
||||
|
||||
function handleSidoChange(value: string) {
|
||||
setSidoComCd(value);
|
||||
setSggComCd("");
|
||||
setGnsvyYr("");
|
||||
setSwtrmtFcltCd("");
|
||||
}
|
||||
|
||||
function handleGugunChange(value: string) {
|
||||
setSggComCd(value);
|
||||
setGnsvyYr("");
|
||||
setSwtrmtFcltCd("");
|
||||
}
|
||||
|
||||
function handleYearChange(value: string) {
|
||||
setGnsvyYr(value);
|
||||
setSwtrmtFcltCd("");
|
||||
}
|
||||
|
||||
function handleFacilityChange(value: string) {
|
||||
setSwtrmtFcltCd(value);
|
||||
}
|
||||
|
||||
async function handleSearch() {
|
||||
const { data: list, error } = await refetch();
|
||||
if (error) {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
setSearchedClsfSystmNm("");
|
||||
return;
|
||||
}
|
||||
if (!list?.length) {
|
||||
alert("조회된 데이터가 없습니다.");
|
||||
setSearchedClsfSystmNm("");
|
||||
return;
|
||||
}
|
||||
setSearchedClsfSystmNm(sggNm || ctpvNm);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
className="flex max-h-[90vh] flex-col overflow-hidden sm:max-w-[90rem]"
|
||||
aria-describedby={undefined}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>기본 계획 찾기</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="min-h-0 flex-1 space-y-4 overflow-y-auto pr-1">
|
||||
{isSidoPending ? (
|
||||
<div className="mx-4">
|
||||
<Skeleton className="h-[6rem] w-full" />
|
||||
</div>
|
||||
) : (
|
||||
<PreviousPlanSearchFilters
|
||||
sidoComCd={sidoComCd}
|
||||
sggComCd={sggComCd}
|
||||
gnsvyYr={gnsvyYr}
|
||||
swtrmtFcltCd={swtrmtFcltCd}
|
||||
sidoOptions={sidoOptions}
|
||||
gugunOptions={gugunOptions}
|
||||
yearOptions={yearOptions}
|
||||
facilityOptions={facilityOptions}
|
||||
isGugunPending={isGugunPending}
|
||||
isYearPending={Boolean(sggNm) && isYearPending}
|
||||
isFacilityPending={Boolean(gnsvyYr) && isFacilityPending}
|
||||
isSearching={isFetching}
|
||||
onSidoChange={handleSidoChange}
|
||||
onGugunChange={handleGugunChange}
|
||||
onYearChange={handleYearChange}
|
||||
onFacilityChange={handleFacilityChange}
|
||||
onSearch={handleSearch}
|
||||
/>
|
||||
)}
|
||||
|
||||
<SectionCollapsible title="기본계획파일" defaultOpen>
|
||||
<PreviousPlanFileSection
|
||||
key={searchedClsfSystmNm || "empty"}
|
||||
clsfSystmNm={searchedClsfSystmNm || undefined}
|
||||
/>
|
||||
</SectionCollapsible>
|
||||
|
||||
<SectionCollapsible title="총설" defaultOpen>
|
||||
{isFetching ? (
|
||||
<Skeleton className="h-[8rem] w-full" />
|
||||
) : (
|
||||
<PreviousPlanSummaryTable data={planData} />
|
||||
)}
|
||||
</SectionCollapsible>
|
||||
|
||||
<SectionCollapsible title="처리구역별 현황" defaultOpen>
|
||||
{isFetching ? (
|
||||
<Skeleton className="h-[20rem] w-full" />
|
||||
) : (
|
||||
<PreviousPlanAreaTable data={planData} />
|
||||
)}
|
||||
</SectionCollapsible>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" color="white" onClick={() => onOpenChange(false)}>
|
||||
닫기
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { PreviousPlanLookupDialog };
|
||||
@@ -0,0 +1,103 @@
|
||||
"use client";
|
||||
|
||||
import type { NowDmgMtncGnsvyResponseDto } from "@/entities/dc";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||
|
||||
import { getMetricCells, getPlanYearCells } from "../../lib/previous-plan-table-utils";
|
||||
|
||||
const AREA_ROWS = [
|
||||
{ label: "하수처리인구(인)", field: "swtrmtPpltnCnt" },
|
||||
{ label: "자연적하수처리인구수(인)", field: "natrSwtrmtPpltnCnt" },
|
||||
{ label: "사회적하수처리인구수(인)", field: "scilSwtrmtPpltnCnt" },
|
||||
{ label: "하수처리구역면적", field: "swtrmtZoneArea" },
|
||||
{ label: "물사용량원단위", field: "wtrUsqtyOrgnUnit" },
|
||||
{ label: "오수전환율(%)", field: "drtwSwtcRt" },
|
||||
{ label: "변동부하율(%)", field: "flctnLodRt" },
|
||||
{ label: "오수원단위", field: "drtwOrgnUnit" },
|
||||
{ label: "계획하수량합계(㎥/일)", field: "planSwageQntSum" },
|
||||
{ label: "생활오수(㎥/일)", field: "lvngDrtwSum" },
|
||||
{ label: "지하수(㎥/일)", field: "ugwtSum" },
|
||||
{ label: "공장폐수(㎥/일)", field: "fctryWwtSum" },
|
||||
{ label: "관광오수(㎥/일)", field: "toursmDrtwSum" },
|
||||
{ label: "군부대(㎥/일)", field: "mtbsSum" },
|
||||
{ label: "연계처리수(㎥/일)", field: "linkPrcsCntSum" },
|
||||
{ label: "기타오수(㎥/일)", field: "etcDrtwSum" },
|
||||
{ label: "지하수사용량(㎥/일)", field: "ugwtUseSum" },
|
||||
{ label: "시설용량(㎥/일)", field: "fcltCpctSum" },
|
||||
{ label: "배제방식", field: "exclsMethExpln" },
|
||||
] as const;
|
||||
|
||||
interface PreviousPlanAreaTableProps {
|
||||
data: NowDmgMtncGnsvyResponseDto | null;
|
||||
}
|
||||
|
||||
function PreviousPlanAreaTable({ data }: PreviousPlanAreaTableProps) {
|
||||
const yearCells = getPlanYearCells(data);
|
||||
|
||||
return (
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="16%" />
|
||||
<col width="9.3%" />
|
||||
<col width="9.3%" />
|
||||
<col width="9.3%" />
|
||||
<col width="9.3%" />
|
||||
<col width="9.3%" />
|
||||
<col width="9.3%" />
|
||||
<col width="9.3%" />
|
||||
<col width="9.3%" />
|
||||
<col width="9.3%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead rowSpan={3} className="text-center">
|
||||
구분
|
||||
</TableHead>
|
||||
<TableHead colSpan={4} className="text-center">
|
||||
당초
|
||||
</TableHead>
|
||||
<TableHead rowSpan={2} className="text-center">
|
||||
기준
|
||||
</TableHead>
|
||||
<TableHead colSpan={4} className="text-center">
|
||||
변경
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead className="text-center">1단계</TableHead>
|
||||
<TableHead className="text-center">2단계</TableHead>
|
||||
<TableHead className="text-center">3단계</TableHead>
|
||||
<TableHead className="text-center">4단계</TableHead>
|
||||
<TableHead className="text-center">1단계</TableHead>
|
||||
<TableHead className="text-center">2단계</TableHead>
|
||||
<TableHead className="text-center">3단계</TableHead>
|
||||
<TableHead className="text-center">4단계</TableHead>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
{yearCells.map((cell, index) => (
|
||||
<TableHead key={index} className="text-center font-normal">
|
||||
{cell}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{AREA_ROWS.map((row) => {
|
||||
const cells = getMetricCells(data, row.field);
|
||||
return (
|
||||
<TableRow key={row.field}>
|
||||
<TableHead>{row.label}</TableHead>
|
||||
{cells.map((cell, index) => (
|
||||
<TableCell key={index} className="text-right">
|
||||
{cell}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
export { PreviousPlanAreaTable };
|
||||
@@ -0,0 +1,241 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import { ChevronRight, File, Folder } from "lucide-react";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { type ClsfSystmResponseDto, dcService, useClsfSystm, useFile } from "@/entities/dc";
|
||||
import {
|
||||
Button,
|
||||
cn,
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
Skeleton,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
|
||||
interface LegacyFileItem {
|
||||
no?: number | string;
|
||||
fileTitle?: string;
|
||||
regDt?: string;
|
||||
fileSn?: string;
|
||||
}
|
||||
|
||||
interface PreviousPlanFileSectionProps {
|
||||
clsfSystmNm?: string;
|
||||
}
|
||||
|
||||
// TODO: T2 — getFile Response DTO 확정 후 정규화 로직 교체 (레거시: no, fileTitle, regDt, fileSn)
|
||||
function normalizeFileList(data: unknown): LegacyFileItem[] {
|
||||
if (!Array.isArray(data)) return [];
|
||||
return data.map((item) => {
|
||||
if (!item || typeof item !== "object") return {};
|
||||
const row = item as Record<string, unknown>;
|
||||
return {
|
||||
no: row.no as number | string | undefined,
|
||||
fileTitle: (row.fileTitle ?? row.fileName ?? row.fileNm) as string | undefined,
|
||||
regDt: row.regDt as string | undefined,
|
||||
fileSn: (row.fileSn ?? row.fileId) as string | undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function ClsfTreeItem({
|
||||
item,
|
||||
depth,
|
||||
selectedSn,
|
||||
onSelect,
|
||||
}: {
|
||||
item: ClsfSystmResponseDto;
|
||||
depth: number;
|
||||
selectedSn: string;
|
||||
onSelect: (sn: string) => void;
|
||||
}) {
|
||||
const children = item.children ?? [];
|
||||
const isFolder = children.length > 0;
|
||||
|
||||
if (isFolder) {
|
||||
return (
|
||||
<li>
|
||||
<Collapsible defaultOpen={depth < 1} className="group/collapsible">
|
||||
<CollapsibleTrigger
|
||||
className="flex w-full items-center gap-1.5 rounded px-3 py-1.5 text-left text-xl hover:bg-muted"
|
||||
style={{ paddingLeft: `${0.5 + depth * 0.75}rem` }}
|
||||
>
|
||||
<ChevronRight className="size-4 shrink-0 transition-transform group-data-[state=open]/collapsible:rotate-90" />
|
||||
<Folder className="size-6 shrink-0" />
|
||||
<span className="truncate">{item.clsfSystmNm}</span>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<ul className="m-0 ml-3 list-none border-l border-border p-0 pl-1">
|
||||
{children.map((child) => (
|
||||
<ClsfTreeItem
|
||||
key={child.clsfSystmSn}
|
||||
item={child}
|
||||
depth={depth + 1}
|
||||
selectedSn={selectedSn}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
const isSelected = selectedSn === item.clsfSystmSn;
|
||||
|
||||
return (
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"flex w-full items-center gap-1.5 rounded px-3 py-1.5 text-left text-xl hover:bg-muted",
|
||||
isSelected && "bg-muted font-semibold",
|
||||
)}
|
||||
style={{ paddingLeft: `${0.5 + depth * 1}rem` }}
|
||||
onClick={() => onSelect(item.clsfSystmSn)}
|
||||
>
|
||||
<File className="size-6 shrink-0" />
|
||||
<span className="truncate">{item.clsfSystmNm}</span>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function PreviousPlanFileSection({ clsfSystmNm }: PreviousPlanFileSectionProps) {
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const [selectedClsfSn, setSelectedClsfSn] = useState("");
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
|
||||
const { data: clsfTree, isPending: isClsfPending } = useClsfSystm({
|
||||
clsfSystmNm,
|
||||
});
|
||||
const { data: fileData, isPending: isFilePending } = useFile({
|
||||
clsfSystmSn: selectedClsfSn || undefined,
|
||||
});
|
||||
|
||||
const tree = clsfTree ?? [];
|
||||
const files = useMemo(() => normalizeFileList(fileData), [fileData]);
|
||||
|
||||
async function handleDownload(fileSn?: string, fallbackName?: string) {
|
||||
if (!fileSn) {
|
||||
alert("파일 식별자가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDownloading(true);
|
||||
try {
|
||||
// TODO: T2 — getFilePrcs Response DTO 확정 후 타입 가드
|
||||
const prcsRaw = await dcService.getFilePrcs(fileSn);
|
||||
const fileInfo = (Array.isArray(prcsRaw) ? prcsRaw[0] : prcsRaw) as
|
||||
| { filePath?: string; fileTitle?: string; fileName?: string }
|
||||
| undefined;
|
||||
const filePath = fileInfo?.filePath;
|
||||
const fileName = fileInfo?.fileTitle ?? fileInfo?.fileName ?? fallbackName;
|
||||
|
||||
if (!filePath || !fileName) {
|
||||
alert("파일 정보를 가져올 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = await dcService.fileDownload({ filePath, fileName });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch {
|
||||
alert("파일 다운로드 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsDownloading(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!clsfSystmNm) {
|
||||
return (
|
||||
<p className="py-8 text-center text-lg text-muted-foreground">
|
||||
시도 및 구군을 선택 후 조회하세요.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-[40rem] gap-2">
|
||||
<div className="w-[40%] overflow-y-auto rounded border p-2">
|
||||
{isClsfPending ? (
|
||||
<Skeleton className="h-full w-full" />
|
||||
) : tree.length === 0 ? (
|
||||
<p className="py-8 text-center text-lg text-muted-foreground">파일이 없습니다.</p>
|
||||
) : (
|
||||
<ul className="m-0 list-none p-0">
|
||||
{tree.map((item) => (
|
||||
<ClsfTreeItem
|
||||
key={item.clsfSystmSn}
|
||||
item={item}
|
||||
depth={0}
|
||||
selectedSn={selectedClsfSn}
|
||||
onSelect={(sn) => setSelectedClsfSn(sn)}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-[60%] overflow-y-auto rounded border p-2">
|
||||
{!selectedClsfSn ? (
|
||||
<p className="py-8 text-center text-lg text-muted-foreground">
|
||||
트리에서 항목을 선택하세요.
|
||||
</p>
|
||||
) : isFilePending ? (
|
||||
<Skeleton className="h-full w-full" />
|
||||
) : files.length === 0 ? (
|
||||
<p className="py-8 text-center text-lg text-muted-foreground">파일이 없습니다.</p>
|
||||
) : (
|
||||
<Table variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="!py-2 !text-xl">번호</TableHead>
|
||||
<TableHead className="!py-2 !text-xl">파일명</TableHead>
|
||||
<TableHead className="!py-2 !text-xl">등록일</TableHead>
|
||||
<TableHead className="!py-2 !text-xl">다운로드</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{files.map((file, index) => (
|
||||
<TableRow key={file.fileSn ?? `${file.fileTitle}-${index}`}>
|
||||
<TableCell className="!py-2 text-center">{file.no ?? index + 1}</TableCell>
|
||||
<TableCell className="!py-2 text-center">{file.fileTitle ?? "-"}</TableCell>
|
||||
<TableCell className="!py-2 text-center">{file.regDt ?? "-"}</TableCell>
|
||||
<TableCell className="!py-2 text-center">
|
||||
<Button
|
||||
type="button"
|
||||
color="white"
|
||||
size="sm"
|
||||
disabled={isDownloading}
|
||||
onClick={() => handleDownload(file.fileSn, file.fileTitle)}
|
||||
>
|
||||
다운로드
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { PreviousPlanFileSection };
|
||||
@@ -0,0 +1,149 @@
|
||||
"use client";
|
||||
|
||||
import type { CmCdAdstDstrComboResponseDto } from "@/entities/adstdstr";
|
||||
import {
|
||||
Button,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
|
||||
import type { FacilityOption } from "../../lib/previous-plan-table-utils";
|
||||
|
||||
interface PreviousPlanSearchFiltersProps {
|
||||
sidoComCd: string;
|
||||
sggComCd: string;
|
||||
gnsvyYr: string;
|
||||
swtrmtFcltCd: string;
|
||||
sidoOptions: CmCdAdstDstrComboResponseDto[];
|
||||
gugunOptions: CmCdAdstDstrComboResponseDto[];
|
||||
yearOptions: string[];
|
||||
facilityOptions: FacilityOption[];
|
||||
isGugunPending: boolean;
|
||||
isYearPending: boolean;
|
||||
isFacilityPending: boolean;
|
||||
isSearching: boolean;
|
||||
onSidoChange: (value: string) => void;
|
||||
onGugunChange: (value: string) => void;
|
||||
onYearChange: (value: string) => void;
|
||||
onFacilityChange: (value: string) => void;
|
||||
onSearch: () => void;
|
||||
}
|
||||
|
||||
function PreviousPlanSearchFilters({
|
||||
sidoComCd,
|
||||
sggComCd,
|
||||
gnsvyYr,
|
||||
swtrmtFcltCd,
|
||||
sidoOptions,
|
||||
gugunOptions,
|
||||
yearOptions,
|
||||
facilityOptions,
|
||||
isGugunPending,
|
||||
isYearPending,
|
||||
isFacilityPending,
|
||||
isSearching,
|
||||
onSidoChange,
|
||||
onGugunChange,
|
||||
onYearChange,
|
||||
onFacilityChange,
|
||||
onSearch,
|
||||
}: PreviousPlanSearchFiltersProps) {
|
||||
return (
|
||||
<section className="card card-border mx-4 bg-background">
|
||||
<div className="flex flex-wrap items-center gap-4 p-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="previous-plan-sidoComCd" className="whitespace-nowrap">
|
||||
시도
|
||||
</Label>
|
||||
<Select value={sidoComCd || undefined} onValueChange={onSidoChange}>
|
||||
<SelectTrigger id="previous-plan-sidoComCd" className="w-[12rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sidoOptions.map((option) => (
|
||||
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
|
||||
{option.adstDstrNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="previous-plan-sggComCd" className="whitespace-nowrap">
|
||||
구군
|
||||
</Label>
|
||||
<Select
|
||||
key={sidoComCd || "gugun"}
|
||||
value={sggComCd || undefined}
|
||||
onValueChange={onGugunChange}
|
||||
disabled={!sidoComCd || isGugunPending}
|
||||
>
|
||||
<SelectTrigger id="previous-plan-sggComCd" className="w-[12rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{gugunOptions.map((option) => (
|
||||
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
|
||||
{option.adstDstrNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="previous-plan-gnsvyYr" className="whitespace-nowrap">
|
||||
연도
|
||||
</Label>
|
||||
<Select
|
||||
key={sggComCd || "year"}
|
||||
value={gnsvyYr || undefined}
|
||||
onValueChange={onYearChange}
|
||||
disabled={!sggComCd || isYearPending}
|
||||
>
|
||||
<SelectTrigger id="previous-plan-gnsvyYr" className="w-[12rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{yearOptions.map((year) => (
|
||||
<SelectItem key={year} value={year}>
|
||||
{year}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="previous-plan-swtrmtFcltCd" className="whitespace-nowrap">
|
||||
처리시설
|
||||
</Label>
|
||||
<Select
|
||||
key={gnsvyYr || "facility"}
|
||||
value={swtrmtFcltCd || undefined}
|
||||
onValueChange={onFacilityChange}
|
||||
disabled={!gnsvyYr || isFacilityPending}
|
||||
>
|
||||
<SelectTrigger id="previous-plan-swtrmtFcltCd" className="w-[12rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{facilityOptions.map((option) => (
|
||||
<SelectItem key={option.code} value={option.code}>
|
||||
{option.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button type="button" onClick={onSearch} disabled={isSearching}>
|
||||
조회
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export { PreviousPlanSearchFilters };
|
||||
@@ -0,0 +1,69 @@
|
||||
"use client";
|
||||
|
||||
import type { NowDmgMtncGnsvyResponseDto } from "@/entities/dc";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||
|
||||
import { getPlanYearCells } from "../../lib/previous-plan-table-utils";
|
||||
|
||||
interface PreviousPlanSummaryTableProps {
|
||||
data: NowDmgMtncGnsvyResponseDto | null;
|
||||
}
|
||||
|
||||
function PreviousPlanSummaryTable({ data }: PreviousPlanSummaryTableProps) {
|
||||
const yearCells = getPlanYearCells(data);
|
||||
|
||||
return (
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead rowSpan={2} className="text-center">
|
||||
구분
|
||||
</TableHead>
|
||||
<TableHead colSpan={4} className="text-center">
|
||||
당초
|
||||
</TableHead>
|
||||
<TableHead rowSpan={2} className="text-center">
|
||||
기준
|
||||
</TableHead>
|
||||
<TableHead colSpan={4} className="text-center">
|
||||
변경
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead className="text-center">1단계</TableHead>
|
||||
<TableHead className="text-center">2단계</TableHead>
|
||||
<TableHead className="text-center">3단계</TableHead>
|
||||
<TableHead className="text-center">4단계</TableHead>
|
||||
<TableHead className="text-center">1단계</TableHead>
|
||||
<TableHead className="text-center">2단계</TableHead>
|
||||
<TableHead className="text-center">3단계</TableHead>
|
||||
<TableHead className="text-center">4단계</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>계획목표년도</TableHead>
|
||||
{yearCells.map((cell, index) => (
|
||||
<TableCell key={index} className="text-center">
|
||||
{cell}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
export { PreviousPlanSummaryTable };
|
||||
@@ -0,0 +1,276 @@
|
||||
"use client";
|
||||
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { Field, FieldError } from "@/shared/ui/field";
|
||||
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||
import {
|
||||
Input,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Table,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
|
||||
import type { SewerBscPlanRegistFormData } from "../../lib/sewer-bsc-plan-regist-schema";
|
||||
import { getGoalYearOptions, getLinkedGoalYears, getPlanYearOptions } from "../../lib/year-options";
|
||||
|
||||
function BasicInfoFormSection() {
|
||||
const { control, setValue, watch } = useFormContext<SewerBscPlanRegistFormData>();
|
||||
const planYearOptions = getPlanYearOptions();
|
||||
const goalYearOptions = getGoalYearOptions();
|
||||
const sewerBscPlanSeLabel = [
|
||||
watch("sewerBscPlan.sewerBscPlanSeCdNm"),
|
||||
watch("sewerBscPlan.taskSeCdNm"),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
const localGovLabel = [watch("sewerBscPlan.sido"), watch("sewerBscPlan.gugun")]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
function handleFrlvlGoalYrChange(value: string) {
|
||||
setValue("sewerBscPlanDtl.frlvlGoalYr", value, { shouldDirty: true, shouldValidate: true });
|
||||
const linked = getLinkedGoalYears(value);
|
||||
setValue("sewerBscPlanDtl.sclvlGoalYr", linked.sclvlGoalYr, {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
});
|
||||
setValue("sewerBscPlanDtl.thlvlGoalYr", linked.thlvlGoalYr, {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
});
|
||||
setValue("sewerBscPlanDtl.folvlGoalYr", linked.folvlGoalYr, {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="기본계획 신청">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[20rem]" data-required>
|
||||
기본계획명
|
||||
</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<Controller
|
||||
name="sewerBscPlan.sewerBscPlanNm"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<Input
|
||||
{...field}
|
||||
id={field.name}
|
||||
placeholder="기본계획명 입력"
|
||||
aria-invalid={fieldState.invalid}
|
||||
className="w-full max-w-[48rem]"
|
||||
/>
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>계획년도</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name="sewerBscPlanDtl.planYr"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<Select value={field.value || undefined} onValueChange={field.onChange}>
|
||||
<SelectTrigger aria-invalid={fieldState.invalid} className="!w-[16rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{planYearOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableHead data-required>기준년도</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name="sewerBscPlanDtl.crtrYr"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<Select value={field.value || undefined} onValueChange={field.onChange}>
|
||||
<SelectTrigger aria-invalid={fieldState.invalid} className="!w-[16rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{planYearOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>1단계 목표연도</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name="sewerBscPlanDtl.frlvlGoalYr"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<Select
|
||||
value={field.value || undefined}
|
||||
onValueChange={handleFrlvlGoalYrChange}
|
||||
>
|
||||
<SelectTrigger aria-invalid={fieldState.invalid} className="!w-[16rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{goalYearOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableHead data-required>2단계 목표연도</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name="sewerBscPlanDtl.sclvlGoalYr"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<Select value={field.value || undefined} onValueChange={field.onChange}>
|
||||
<SelectTrigger aria-invalid={fieldState.invalid} className="!w-[16rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{goalYearOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>3단계 목표연도</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name="sewerBscPlanDtl.thlvlGoalYr"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<Select value={field.value || undefined} onValueChange={field.onChange}>
|
||||
<SelectTrigger aria-invalid={fieldState.invalid} className="!w-[16rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{goalYearOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableHead data-required>4단계 목표연도</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name="sewerBscPlanDtl.folvlGoalYr"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<Select value={field.value || undefined} onValueChange={field.onChange}>
|
||||
<SelectTrigger aria-invalid={fieldState.invalid} className="!w-[16rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{goalYearOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>계획개요</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<Controller
|
||||
name="sewerBscPlanDtl.bizOtlnCn"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<Textarea
|
||||
{...field}
|
||||
id={field.name}
|
||||
placeholder="계획개요 입력"
|
||||
className="!text-xl"
|
||||
aria-invalid={fieldState.invalid}
|
||||
rows={4}
|
||||
/>
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>구분</TableHead>
|
||||
<TableCell>{sewerBscPlanSeLabel}</TableCell>
|
||||
<TableHead>신청 지자체</TableHead>
|
||||
<TableCell>{localGovLabel}</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
</Table>
|
||||
</SectionCollapsible>
|
||||
);
|
||||
}
|
||||
|
||||
export { BasicInfoFormSection };
|
||||
@@ -0,0 +1,133 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useFormContext, useWatch } from "react-hook-form";
|
||||
|
||||
import { Field } from "@/shared/ui/field";
|
||||
import { useAcntgSeMngTree } from "@/entities/acntg";
|
||||
import { buildAcntgCostItems, SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||
import {
|
||||
Input,
|
||||
Skeleton,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
|
||||
import {
|
||||
getAmt,
|
||||
mergeBizCstList,
|
||||
recalculateBizCstMatrix,
|
||||
setAmt,
|
||||
} from "../../lib/biz-cst-matrix-utils";
|
||||
import type { SewerBscPlanRegistFormData } from "../../lib/sewer-bsc-plan-regist-schema";
|
||||
|
||||
function BizCstMatrixFormSection() {
|
||||
const { control, setValue, getValues } = useFormContext<SewerBscPlanRegistFormData>();
|
||||
const bizCstList = useWatch({ control, name: "bizCstList" }) ?? [];
|
||||
const frlvlGoalYr = useWatch({ control, name: "sewerBscPlanDtl.frlvlGoalYr" });
|
||||
const sclvlGoalYr = useWatch({ control, name: "sewerBscPlanDtl.sclvlGoalYr" });
|
||||
const thlvlGoalYr = useWatch({ control, name: "sewerBscPlanDtl.thlvlGoalYr" });
|
||||
const folvlGoalYr = useWatch({ control, name: "sewerBscPlanDtl.folvlGoalYr" });
|
||||
|
||||
const { data: acntgTree = [] } = useAcntgSeMngTree();
|
||||
const costItems = useMemo(() => buildAcntgCostItems(acntgTree), [acntgTree]);
|
||||
|
||||
useEffect(() => {
|
||||
if (costItems.length === 0) return;
|
||||
setValue("bizCstList", mergeBizCstList(getValues("bizCstList"), costItems));
|
||||
}, [costItems, getValues, setValue]);
|
||||
|
||||
const stepHeaders = [
|
||||
{ planStpNo: 1, label: "1단계", year: frlvlGoalYr },
|
||||
{ planStpNo: 2, label: "2단계", year: sclvlGoalYr },
|
||||
{ planStpNo: 3, label: "3단계", year: thlvlGoalYr },
|
||||
{ planStpNo: 4, label: "4단계", year: folvlGoalYr },
|
||||
];
|
||||
|
||||
function handleChange(acntgSeCd: string, planStpNo: number, value: string) {
|
||||
const numValue = Number(value) || 0;
|
||||
const next = recalculateBizCstMatrix(
|
||||
setAmt(getValues("bizCstList"), acntgSeCd, planStpNo, numValue),
|
||||
costItems,
|
||||
);
|
||||
setValue("bizCstList", next, { shouldDirty: true });
|
||||
}
|
||||
|
||||
if (costItems.length === 0) {
|
||||
return (
|
||||
<SectionCollapsible title="총사업비">
|
||||
<div className="space-y-2 p-2">
|
||||
<Skeleton className="h-[8rem] rounded-xl" />
|
||||
<Skeleton className="h-[32rem] rounded-xl" />
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="총사업비">
|
||||
<div className="overflow-x-auto">
|
||||
<p className="mb-2 text-right font-normal text-muted-foreground">(단위: 백만원)</p>
|
||||
<Table variant="secondary" className="table-fixed">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
<col width="22%" />
|
||||
<col width="22%" />
|
||||
<col width="22%" />
|
||||
<col width="22%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[28rem]">구분</TableHead>
|
||||
{stepHeaders.map((step) => (
|
||||
<TableHead key={step.planStpNo} className="text-center">
|
||||
{step.label}
|
||||
<br />
|
||||
<span className="font-normal text-muted-foreground">({step.year || "-"})</span>
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{costItems.map((item) => {
|
||||
const paddingLeft = `${item.level * 1.6}rem`;
|
||||
const showSubIcon = item.level >= 2;
|
||||
|
||||
return (
|
||||
<TableRow key={item.code}>
|
||||
<TableHead className="text-left" style={{ paddingLeft }}>
|
||||
<div className="relative">
|
||||
{showSubIcon && (
|
||||
<span className="absolute -left-6 top-0 text-gray-400">└</span>
|
||||
)}
|
||||
{item.label}
|
||||
</div>
|
||||
</TableHead>
|
||||
{stepHeaders.map((step) => (
|
||||
<TableCell key={step.planStpNo}>
|
||||
<Field>
|
||||
<Input
|
||||
type="number"
|
||||
value={getAmt(bizCstList, item.code, step.planStpNo) || ""}
|
||||
onChange={(e) => handleChange(item.code, step.planStpNo, e.target.value)}
|
||||
readOnly={item.readonly}
|
||||
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
|
||||
/>
|
||||
</Field>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
);
|
||||
}
|
||||
|
||||
export { BizCstMatrixFormSection };
|
||||
@@ -0,0 +1,180 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { useFormContext, useWatch } from "react-hook-form";
|
||||
|
||||
import { Field } from "@/shared/ui/field";
|
||||
import { useAcntgSeMngTree } from "@/entities/acntg";
|
||||
import { buildAcntgCostItems, SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||
import {
|
||||
cn,
|
||||
Input,
|
||||
Skeleton,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
|
||||
import { PLAN_STEPS } from "../../lib/biz-cst-matrix-utils";
|
||||
import { calcFnrsCstRemaining, recalcFnrsCstSumAmt } from "../../lib/fnrs-cst-form-utils";
|
||||
import type { SewerBscPlanRegistFormData } from "../../lib/sewer-bsc-plan-regist-schema";
|
||||
|
||||
const FNRS_CST_ROWS = [
|
||||
{ key: "sumAmt" as const, label: "총사업비", readonly: true },
|
||||
{ key: "gvexpAmt" as const, label: "국고", readonly: false },
|
||||
{ key: "lcexpAmt" as const, label: "지방비", readonly: false },
|
||||
{ key: "orgprBdchgAmt" as const, label: "원인자부담금", readonly: false },
|
||||
{ key: "etcAmt" as const, label: "기타", readonly: false },
|
||||
];
|
||||
|
||||
type EditableFnrsKey = "gvexpAmt" | "lcexpAmt" | "orgprBdchgAmt" | "etcAmt";
|
||||
|
||||
function FnrsCstFormSection() {
|
||||
const { control, setValue, getValues } = useFormContext<SewerBscPlanRegistFormData>();
|
||||
const fnrsCstList = useWatch({ control, name: "fnrsCstList" }) ?? [];
|
||||
const bizCstList = useWatch({ control, name: "bizCstList" }) ?? [];
|
||||
const frlvlGoalYr = useWatch({ control, name: "sewerBscPlanDtl.frlvlGoalYr" });
|
||||
const sclvlGoalYr = useWatch({ control, name: "sewerBscPlanDtl.sclvlGoalYr" });
|
||||
const thlvlGoalYr = useWatch({ control, name: "sewerBscPlanDtl.thlvlGoalYr" });
|
||||
const folvlGoalYr = useWatch({ control, name: "sewerBscPlanDtl.folvlGoalYr" });
|
||||
|
||||
const { data: acntgTree = [] } = useAcntgSeMngTree();
|
||||
const costItems = useMemo(() => buildAcntgCostItems(acntgTree), [acntgTree]);
|
||||
|
||||
const stepHeaders = [
|
||||
{ planStpNo: 1, label: "1단계", year: frlvlGoalYr },
|
||||
{ planStpNo: 2, label: "2단계", year: sclvlGoalYr },
|
||||
{ planStpNo: 3, label: "3단계", year: thlvlGoalYr },
|
||||
{ planStpNo: 4, label: "4단계", year: folvlGoalYr },
|
||||
];
|
||||
|
||||
const remainingList = useMemo(
|
||||
() => calcFnrsCstRemaining(bizCstList, fnrsCstList, costItems),
|
||||
[bizCstList, fnrsCstList, costItems],
|
||||
);
|
||||
|
||||
function handleChange(planStpNo: number, fieldKey: EditableFnrsKey, value: string) {
|
||||
const numValue = Number(value.replace(/,/g, "")) || 0;
|
||||
const draft = [...getValues("fnrsCstList")];
|
||||
const index = draft.findIndex((item) => item.planStpNo === planStpNo);
|
||||
if (index < 0) return;
|
||||
draft[index] = recalcFnrsCstSumAmt({ ...draft[index], [fieldKey]: numValue });
|
||||
setValue("fnrsCstList", draft, { shouldDirty: true });
|
||||
}
|
||||
|
||||
if (costItems.length === 0) {
|
||||
return (
|
||||
<SectionCollapsible title="재원분담">
|
||||
<div className="space-y-2 p-2">
|
||||
<Skeleton className="h-[8rem] rounded-xl" />
|
||||
<Skeleton className="h-[32rem] rounded-xl" />
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="재원분담">
|
||||
<div className="overflow-x-auto">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<h3 className="text-xl font-semibold">세부별 총사업비(①) - 재원분담 총사업비(②)</h3>
|
||||
<span className="font-normal text-muted-foreground">(단위: 백만원)</span>
|
||||
</div>
|
||||
<Table variant="secondary" className="table-fixed">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
<col width="22%" />
|
||||
<col width="22%" />
|
||||
<col width="22%" />
|
||||
<col width="22%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>구분</TableHead>
|
||||
{stepHeaders.map((step) => (
|
||||
<TableHead key={step.planStpNo} className="text-center">
|
||||
{step.label}
|
||||
<br />
|
||||
<span className="font-normal text-muted-foreground">({step.year || "-"})</span>
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>총사업비</TableHead>
|
||||
{remainingList.map((item) => (
|
||||
<TableCell key={item.planStpNo}>
|
||||
<div className={cn("text-right", item.remaining !== 0 ? "text-destructive" : "")}>
|
||||
{item.remaining.toLocaleString()}
|
||||
</div>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 overflow-x-auto">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<h3 className="text-xl font-semibold">재원분담</h3>
|
||||
<span className="font-normal text-muted-foreground">(단위: 백만원)</span>
|
||||
</div>
|
||||
<Table variant="secondary" className="table-fixed">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
<col width="22%" />
|
||||
<col width="22%" />
|
||||
<col width="22%" />
|
||||
<col width="22%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[20rem]">구분</TableHead>
|
||||
{stepHeaders.map((step) => (
|
||||
<TableHead key={step.planStpNo} className="text-center">
|
||||
{step.label}
|
||||
<br />
|
||||
<span className="font-normal text-muted-foreground">({step.year || "-"})</span>
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{FNRS_CST_ROWS.map((row) => (
|
||||
<TableRow key={row.key}>
|
||||
<TableHead>{row.label}</TableHead>
|
||||
{PLAN_STEPS.map((planStpNo) => {
|
||||
const item = fnrsCstList.find((cst) => cst.planStpNo === planStpNo);
|
||||
return (
|
||||
<TableCell key={planStpNo}>
|
||||
<Field>
|
||||
<Input
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
className={cn("text-right", row.readonly && "bg-muted")}
|
||||
readOnly={row.readonly}
|
||||
disabled={row.readonly}
|
||||
value={item?.[row.key] ?? ""}
|
||||
onChange={(e) => {
|
||||
if (row.readonly) return;
|
||||
handleChange(planStpNo, row.key as EditableFnrsKey, e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
);
|
||||
}
|
||||
|
||||
export { FnrsCstFormSection };
|
||||
@@ -0,0 +1,132 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useFormContext, useWatch } from "react-hook-form";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import { Field } from "@/shared/ui/field";
|
||||
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
|
||||
import type { SewerBscPlanRegistFormData } from "../../lib/sewer-bsc-plan-regist-schema";
|
||||
|
||||
const PreviousPlanLookupDialog = dynamic(
|
||||
() =>
|
||||
import("../previous-plan-lookup-dialog").then((mod) => ({
|
||||
default: mod.PreviousPlanLookupDialog,
|
||||
})),
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
const OTLN_ROWS = [
|
||||
{ key: "sewerPlanPpltnCnt" as const, label: "계획인구" },
|
||||
{ key: "sewerPrcsPpltnCnt" as const, label: "처리인구" },
|
||||
{ key: "sewerPsplyRt" as const, label: "하수도보급율" },
|
||||
];
|
||||
|
||||
function OutlineFormSection() {
|
||||
const { control, setValue, getValues } = useFormContext<SewerBscPlanRegistFormData>();
|
||||
const [isLookupOpen, setIsLookupOpen] = useState(false);
|
||||
|
||||
const otlnList = useWatch({ control, name: "sewerBscPlanOtlnList" }) ?? [];
|
||||
const frlvlGoalYr = useWatch({ control, name: "sewerBscPlanDtl.frlvlGoalYr" });
|
||||
const sclvlGoalYr = useWatch({ control, name: "sewerBscPlanDtl.sclvlGoalYr" });
|
||||
const thlvlGoalYr = useWatch({ control, name: "sewerBscPlanDtl.thlvlGoalYr" });
|
||||
const folvlGoalYr = useWatch({ control, name: "sewerBscPlanDtl.folvlGoalYr" });
|
||||
const [sido, gugun] = useWatch({
|
||||
control,
|
||||
name: ["sewerBscPlan.sido", "sewerBscPlan.gugun"],
|
||||
});
|
||||
|
||||
const stepHeaders = [
|
||||
{ label: "1단계", year: frlvlGoalYr },
|
||||
{ label: "2단계", year: sclvlGoalYr },
|
||||
{ label: "3단계", year: thlvlGoalYr },
|
||||
{ label: "4단계", year: folvlGoalYr },
|
||||
];
|
||||
|
||||
function handleChange(
|
||||
planStpNo: number,
|
||||
fieldKey: (typeof OTLN_ROWS)[number]["key"],
|
||||
value: string,
|
||||
) {
|
||||
const numValue = Number(value.replace(/,/g, "")) || 0;
|
||||
const draft = [...getValues("sewerBscPlanOtlnList")];
|
||||
const index = draft.findIndex((item) => item.planStpNo === planStpNo);
|
||||
if (index < 0) return;
|
||||
draft[index] = { ...draft[index], [fieldKey]: numValue };
|
||||
setValue("sewerBscPlanOtlnList", draft, { shouldDirty: true });
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="사업개요">
|
||||
<div className="mb-2 flex justify-end px-2">
|
||||
<Button type="button" color="white" onClick={() => setIsLookupOpen(true)}>
|
||||
이전기본계획조회
|
||||
</Button>
|
||||
</div>
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
<col width="22%" />
|
||||
<col width="22%" />
|
||||
<col width="22%" />
|
||||
<col width="22%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[20rem]">구분</TableHead>
|
||||
{stepHeaders.map((step) => (
|
||||
<TableHead key={step.label} className="text-center">
|
||||
{step.label}
|
||||
<br />
|
||||
<span className="font-normal text-muted-foreground">({step.year || "-"})</span>
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{OTLN_ROWS.map((row) => (
|
||||
<TableRow key={row.key}>
|
||||
<TableHead>{row.label}</TableHead>
|
||||
{[1, 2, 3, 4].map((planStpNo) => {
|
||||
const item = otlnList.find((otln) => otln.planStpNo === planStpNo);
|
||||
return (
|
||||
<TableCell key={planStpNo}>
|
||||
<Field>
|
||||
<Input
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
className="text-right"
|
||||
value={item?.[row.key] ?? ""}
|
||||
onChange={(e) => handleChange(planStpNo, row.key, e.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<PreviousPlanLookupDialog
|
||||
open={isLookupOpen}
|
||||
onOpenChange={setIsLookupOpen}
|
||||
initialSidoNm={sido}
|
||||
initialGugunNm={gugun}
|
||||
/>
|
||||
</SectionCollapsible>
|
||||
);
|
||||
}
|
||||
|
||||
export { OutlineFormSection };
|
||||
@@ -0,0 +1,519 @@
|
||||
"use client";
|
||||
|
||||
import { useId, useMemo, useState } from "react";
|
||||
import { Controller, useFieldArray, useFormContext, useWatch } from "react-hook-form";
|
||||
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { Paperclip } from "lucide-react";
|
||||
|
||||
import { getDefaultFileKey, useTus } from "@/shared/lib";
|
||||
import { Field } from "@/shared/ui/field";
|
||||
import { attachService, useDownloadAttachmentByAtflCd } from "@/entities/attach";
|
||||
import { useFacilityList } from "@/entities/bns-if";
|
||||
import { useNowDmgMtncGnsvy } from "@/entities/dc";
|
||||
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Input,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@common/components";
|
||||
|
||||
import { createEmptyTmplntDtlList } from "../../lib/form-data-mapper";
|
||||
import type { SewerBscPlanRegistFormData } from "../../lib/sewer-bsc-plan-regist-schema";
|
||||
import {
|
||||
getOriginalValue,
|
||||
getOriginalYear,
|
||||
PLAN_STP_NOS,
|
||||
type TmplntFieldKey,
|
||||
} from "../../lib/tmplnt-original-mapper";
|
||||
|
||||
interface FacilityOption {
|
||||
code: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface ChangeStepHeader {
|
||||
planStpNo: (typeof PLAN_STP_NOS)[number];
|
||||
year: string | undefined;
|
||||
}
|
||||
|
||||
interface UploadedTmplntFile {
|
||||
fileName: string;
|
||||
fileKey: string;
|
||||
atflCd?: string;
|
||||
atflId?: string;
|
||||
}
|
||||
|
||||
const TMPLNT_ATCH_ACCEPT = ".pdf,.png,.jpg,.jpeg";
|
||||
const TMPLNT_ATCH_FORMAT_LABEL = ".pdf, .png, .jpg, .jpeg";
|
||||
|
||||
const TMPLNT_FIELD_ROWS: { key: TmplntFieldKey; label: string }[] = [
|
||||
{ key: "prcsZoneArea", label: "처리구역면적(km²)" },
|
||||
{ key: "swtrmtPpltnCnt", label: "하수처리인구(명)" },
|
||||
{ key: "planSwqty", label: "계획하수량(㎥/일)" },
|
||||
{ key: "fcltCpct", label: "시설용량(㎥/일)" },
|
||||
{ key: "sprsysDrtwPplLen", label: "분류식 오수(km)" },
|
||||
{ key: "sprsysExlnPplLen", label: "분류식 우수(km)" },
|
||||
{ key: "cbsysPplLen", label: "합류식(km)" },
|
||||
{ key: "icptgPplLen", label: "차집관로(km)" },
|
||||
{ key: "drgCnt", label: "배수설비(개소)" },
|
||||
{ key: "drtwRlyPumpCnt", label: "오수 중계 펌프(개소)" },
|
||||
{ key: "drtwPumpCnt", label: "오수 일반 펌프(개소)" },
|
||||
{ key: "exlnPumpCnt", label: "우수 펌프(개소)" },
|
||||
];
|
||||
|
||||
interface TmplntDtlInputProps {
|
||||
facilityIndex: number;
|
||||
planStpNo: number;
|
||||
fieldKey: TmplntFieldKey;
|
||||
}
|
||||
|
||||
function TmplntDtlInput({ facilityIndex, planStpNo, fieldKey }: TmplntDtlInputProps) {
|
||||
const { control } = useFormContext<SewerBscPlanRegistFormData>();
|
||||
const fieldName =
|
||||
`sewerBscPlanTmplntList.${facilityIndex}.sewerBscPlanTmplntDtlList.${planStpNo}.${fieldKey}` as const;
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={fieldName}
|
||||
render={({ field }) => (
|
||||
<Field>
|
||||
<Input
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
className="text-right"
|
||||
value={field.value ?? ""}
|
||||
onChange={(e) => {
|
||||
const numValue = Number(e.target.value.replace(/,/g, "")) || 0;
|
||||
field.onChange(numValue);
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface TmplntFacilityMatrixProps {
|
||||
swtrmtFcltCd: string;
|
||||
sido: string;
|
||||
gugun: string;
|
||||
facilityIndex: number;
|
||||
crtrYr: string | undefined;
|
||||
changeStepHeaders: ChangeStepHeader[];
|
||||
}
|
||||
|
||||
function TmplntFacilityMatrix({
|
||||
swtrmtFcltCd,
|
||||
sido,
|
||||
gugun,
|
||||
facilityIndex,
|
||||
crtrYr,
|
||||
changeStepHeaders,
|
||||
}: TmplntFacilityMatrixProps) {
|
||||
const searchParams = useMemo(
|
||||
() => ({
|
||||
swtrmtFcltCd,
|
||||
ctpvNm: sido,
|
||||
sggNm: gugun,
|
||||
}),
|
||||
[swtrmtFcltCd, sido, gugun],
|
||||
);
|
||||
const { data } = useNowDmgMtncGnsvy(searchParams);
|
||||
const originalData = Array.isArray(data) ? (data[0] ?? null) : null;
|
||||
|
||||
return (
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
<col width="9.75%" />
|
||||
<col width="9.75%" />
|
||||
<col width="9.75%" />
|
||||
<col width="9.75%" />
|
||||
<col width="10%" />
|
||||
<col width="9.75%" />
|
||||
<col width="9.75%" />
|
||||
<col width="9.75%" />
|
||||
<col width="9.75%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[22rem]" rowSpan={2}>
|
||||
구분
|
||||
</TableHead>
|
||||
<TableHead className="text-center" colSpan={4}>
|
||||
당초
|
||||
</TableHead>
|
||||
<TableHead className="text-center" rowSpan={2}>
|
||||
현재
|
||||
<br />
|
||||
<span className="font-normal text-muted-foreground">({crtrYr || "-"}년)</span>
|
||||
</TableHead>
|
||||
<TableHead className="text-center" colSpan={4}>
|
||||
변경
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
{PLAN_STP_NOS.map((planStpNo) => (
|
||||
<TableHead key={`original-${planStpNo}`} className="text-center">
|
||||
{planStpNo}단계
|
||||
<br />
|
||||
<span className="font-normal text-muted-foreground">
|
||||
({getOriginalYear(originalData, planStpNo)})
|
||||
</span>
|
||||
</TableHead>
|
||||
))}
|
||||
{changeStepHeaders.map((step) => (
|
||||
<TableHead key={`change-${step.planStpNo}`} className="text-center">
|
||||
{step.planStpNo}단계
|
||||
<br />
|
||||
<span className="font-normal text-muted-foreground">({step.year || "-"})</span>
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{TMPLNT_FIELD_ROWS.map((row) => (
|
||||
<TableRow key={row.key}>
|
||||
<TableHead>{row.label}</TableHead>
|
||||
{PLAN_STP_NOS.map((planStpNo) => (
|
||||
<TableCell key={`original-${row.key}-${planStpNo}`} className="text-right">
|
||||
{getOriginalValue(originalData, row.key, planStpNo)}
|
||||
</TableCell>
|
||||
))}
|
||||
<TableCell key={`current-${row.key}`}>
|
||||
<TmplntDtlInput facilityIndex={facilityIndex} planStpNo={0} fieldKey={row.key} />
|
||||
</TableCell>
|
||||
{changeStepHeaders.map((step) => (
|
||||
<TableCell key={`change-${row.key}-${step.planStpNo}`}>
|
||||
<TmplntDtlInput
|
||||
facilityIndex={facilityIndex}
|
||||
planStpNo={step.planStpNo}
|
||||
fieldKey={row.key}
|
||||
/>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
interface TmplntFacilityAttachmentsProps {
|
||||
facilityIndex: number;
|
||||
facilityName?: string;
|
||||
atchFileGroupId?: string;
|
||||
atchFileNm?: string;
|
||||
}
|
||||
|
||||
function TmplntFacilityAttachments({
|
||||
facilityIndex,
|
||||
facilityName,
|
||||
atchFileGroupId,
|
||||
atchFileNm,
|
||||
}: TmplntFacilityAttachmentsProps) {
|
||||
const formId = useId();
|
||||
const { setValue } = useFormContext<SewerBscPlanRegistFormData>();
|
||||
const { mutate: downloadAttachment } = useDownloadAttachmentByAtflCd();
|
||||
const [uploadedFile, setUploadedFile] = useState<UploadedTmplntFile | null>(() =>
|
||||
atchFileGroupId && atchFileNm
|
||||
? {
|
||||
fileName: atchFileNm,
|
||||
fileKey: `server::${atchFileGroupId}`,
|
||||
atflCd: atchFileGroupId,
|
||||
}
|
||||
: null,
|
||||
);
|
||||
|
||||
const { uploadFileAsync, isUploading, uploads } = useTus({
|
||||
accept: TMPLNT_ATCH_ACCEPT,
|
||||
onSuccess: (atflCd, atflId, _file, fileKey) => {
|
||||
if (!atflCd || !atflId) return;
|
||||
setValue(`sewerBscPlanTmplntList.${facilityIndex}.atchFileGroupId`, atflCd, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev));
|
||||
},
|
||||
onError: (_error, _file, fileKey) => {
|
||||
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
|
||||
},
|
||||
});
|
||||
|
||||
const fileInputId = `${formId}-tmplnt-atch-${facilityIndex}`;
|
||||
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
|
||||
const displayFileName = uploadedFile?.fileName ?? atchFileNm ?? "";
|
||||
const downloadAtflCd = uploadedFile?.atflCd || atchFileGroupId;
|
||||
const hasFile = Boolean(uploadedFile || atchFileNm);
|
||||
|
||||
async function handleFileUpload(files: FileList | null) {
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const file = files[0];
|
||||
const fileKey = getDefaultFileKey(file);
|
||||
const existingAtflCd = atchFileGroupId || uploadedFile?.atflCd;
|
||||
|
||||
setUploadedFile({ fileName: file.name, fileKey });
|
||||
setValue(`sewerBscPlanTmplntList.${facilityIndex}.atchFileNm`, file.name, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
|
||||
try {
|
||||
await uploadFileAsync(file, {
|
||||
fileKey,
|
||||
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
|
||||
});
|
||||
} catch {
|
||||
// onError callback handles cleanup
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFileRemove() {
|
||||
if (!uploadedFile) return;
|
||||
|
||||
uploads[uploadedFile.fileKey]?.abort();
|
||||
|
||||
if (uploadedFile.atflId) {
|
||||
try {
|
||||
await attachService.deleteAttachmentByAtflId(uploadedFile.atflId);
|
||||
} catch (error) {
|
||||
console.error("처리시설 첨부파일 삭제 실패:", error);
|
||||
}
|
||||
}
|
||||
|
||||
setUploadedFile(null);
|
||||
setValue(`sewerBscPlanTmplntList.${facilityIndex}.atchFileGroupId`, "", { shouldDirty: true });
|
||||
setValue(`sewerBscPlanTmplntList.${facilityIndex}.atchFileNm`, "", { shouldDirty: true });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-6 space-y-2">
|
||||
<h3 className="mb-4 text-xl font-semibold">처리구역 필수 첨부파일</h3>
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="40%" />
|
||||
<col width="15%" />
|
||||
<col width="45%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead align="left">파일명</TableHead>
|
||||
<TableHead>파일 확장자</TableHead>
|
||||
<TableHead>파일첨부</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>{facilityName} 계획평면도</TableCell>
|
||||
<TableCell className="text-center">{TMPLNT_ATCH_FORMAT_LABEL}</TableCell>
|
||||
<TableCell>
|
||||
{hasFile ? (
|
||||
<div className="flex items-center gap-2">
|
||||
{downloadAtflCd ? (
|
||||
<a
|
||||
className="inline-flex cursor-pointer items-center gap-1 text-blue-600 hover:underline"
|
||||
onClick={() => downloadAttachment(downloadAtflCd)}
|
||||
>
|
||||
<Paperclip className="size-5 shrink-0" />
|
||||
{displayFileName}
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-xl">{displayFileName}</span>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
className="size-8 shrink-0 !bg-destructive"
|
||||
disabled={uploading}
|
||||
onClick={() => void handleFileRemove()}
|
||||
>
|
||||
<i className="diveicon di-x text-xs" aria-label="삭제" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<label htmlFor={fileInputId}>
|
||||
<input
|
||||
id={fileInputId}
|
||||
type="file"
|
||||
className="hidden"
|
||||
accept={TMPLNT_ATCH_ACCEPT}
|
||||
onChange={(e) => {
|
||||
void handleFileUpload(e.target.files);
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<Button type="button" size="sm" variant="outline" asChild>
|
||||
<span>파일선택</span>
|
||||
</Button>
|
||||
</label>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TmplntFormSection() {
|
||||
const { confirm } = useConfirm();
|
||||
const { control, getValues } = useFormContext<SewerBscPlanRegistFormData>();
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control,
|
||||
name: "sewerBscPlanTmplntList",
|
||||
});
|
||||
const tmplntList = useWatch({ control, name: "sewerBscPlanTmplntList" }) ?? [];
|
||||
const [sido, gugun] = useWatch({
|
||||
control,
|
||||
name: ["sewerBscPlan.sido", "sewerBscPlan.gugun"],
|
||||
});
|
||||
const [frlvlGoalYr, sclvlGoalYr, thlvlGoalYr, folvlGoalYr, crtrYr] = useWatch({
|
||||
control,
|
||||
name: [
|
||||
"sewerBscPlanDtl.frlvlGoalYr",
|
||||
"sewerBscPlanDtl.sclvlGoalYr",
|
||||
"sewerBscPlanDtl.thlvlGoalYr",
|
||||
"sewerBscPlanDtl.folvlGoalYr",
|
||||
"sewerBscPlanDtl.crtrYr",
|
||||
],
|
||||
});
|
||||
const { data: facilityListData } = useFacilityList(
|
||||
{ sido: sido ?? "", gugun: gugun ?? "" },
|
||||
{ enabled: Boolean(sido) },
|
||||
);
|
||||
|
||||
const facilityOptions =
|
||||
facilityListData?.fctList
|
||||
?.filter((facility) => facility.bscFctCd)
|
||||
.map((facility) => ({
|
||||
code: facility.bscFctCd!,
|
||||
name: facility.bscFctNm ?? facility.bscFctCd!,
|
||||
})) ?? [];
|
||||
|
||||
const [activeTab, setActiveTab] = useState("");
|
||||
|
||||
const selectedCodes = new Set(tmplntList.map((item) => item.swtrmtFcltCd));
|
||||
|
||||
const changeStepHeaders: ChangeStepHeader[] = [
|
||||
{ planStpNo: 1, year: frlvlGoalYr },
|
||||
{ planStpNo: 2, year: sclvlGoalYr },
|
||||
{ planStpNo: 3, year: thlvlGoalYr },
|
||||
{ planStpNo: 4, year: folvlGoalYr },
|
||||
];
|
||||
|
||||
async function handleFacilityToggle(option: FacilityOption, checked: boolean) {
|
||||
if (checked) {
|
||||
append({
|
||||
swtrmtFcltCd: option.code,
|
||||
swtrmtFcltNm: option.name,
|
||||
atchFileGroupId: "",
|
||||
atchFileNm: "",
|
||||
sewerBscPlanTmplntDtlList: createEmptyTmplntDtlList(option.code),
|
||||
});
|
||||
setActiveTab(option.code);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await confirm(`${option.name}을(를) 제거하시겠습니까?`))) return;
|
||||
|
||||
const index = getValues("sewerBscPlanTmplntList").findIndex(
|
||||
(item) => item.swtrmtFcltCd === option.code,
|
||||
);
|
||||
if (index < 0) return;
|
||||
|
||||
remove(index);
|
||||
const remaining = getValues("sewerBscPlanTmplntList");
|
||||
setActiveTab(remaining[0]?.swtrmtFcltCd ?? "");
|
||||
}
|
||||
|
||||
const resolvedActiveTab =
|
||||
activeTab && selectedCodes.has(activeTab) ? activeTab : (tmplntList[0]?.swtrmtFcltCd ?? "");
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="처리시설별 하수도정비기본계획">
|
||||
<div className="space-y-4 p-2">
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{facilityOptions.length === 0 ? (
|
||||
<p className="text-xl text-muted-foreground">조회된 처리시설이 없습니다.</p>
|
||||
) : (
|
||||
facilityOptions.map((option) => (
|
||||
<label key={option.code} className="flex cursor-pointer items-center gap-2 text-xl">
|
||||
<Checkbox
|
||||
checked={selectedCodes.has(option.code)}
|
||||
onCheckedChange={(checked) => handleFacilityToggle(option, checked === true)}
|
||||
/>
|
||||
<span>{option.name}</span>
|
||||
</label>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{fields.length > 0 ? (
|
||||
<Tabs value={resolvedActiveTab} onValueChange={setActiveTab} variant="underline">
|
||||
<TabsList className="p-0">
|
||||
{fields.map((field) => {
|
||||
const facilityName =
|
||||
field.swtrmtFcltNm ||
|
||||
facilityOptions.find((option) => option.code === field.swtrmtFcltCd)?.name;
|
||||
return (
|
||||
<TabsTrigger
|
||||
key={field.id}
|
||||
value={field.swtrmtFcltCd}
|
||||
className="px-6 py-3 text-2xl"
|
||||
>
|
||||
{facilityName}
|
||||
</TabsTrigger>
|
||||
);
|
||||
})}
|
||||
</TabsList>
|
||||
|
||||
{fields.map((field, facilityIndex) => {
|
||||
const facilityName =
|
||||
field.swtrmtFcltNm ||
|
||||
facilityOptions.find((option) => option.code === field.swtrmtFcltCd)?.name;
|
||||
return (
|
||||
<TabsContent
|
||||
key={field.id}
|
||||
value={field.swtrmtFcltCd}
|
||||
className="mt-0 border-0 p-0 shadow-none"
|
||||
>
|
||||
<TmplntFacilityMatrix
|
||||
swtrmtFcltCd={field.swtrmtFcltCd}
|
||||
sido={sido ?? ""}
|
||||
gugun={gugun ?? ""}
|
||||
facilityIndex={facilityIndex}
|
||||
crtrYr={crtrYr}
|
||||
changeStepHeaders={changeStepHeaders}
|
||||
/>
|
||||
<TmplntFacilityAttachments
|
||||
facilityIndex={facilityIndex}
|
||||
facilityName={facilityName}
|
||||
atchFileGroupId={tmplntList[facilityIndex]?.atchFileGroupId}
|
||||
atchFileNm={tmplntList[facilityIndex]?.atchFileNm}
|
||||
/>
|
||||
</TabsContent>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
) : (
|
||||
<p className="text-xl text-muted-foreground">
|
||||
처리시설을 선택하면 시설별 입력 탭이 표시됩니다.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
);
|
||||
}
|
||||
|
||||
export { TmplntFormSection };
|
||||
@@ -0,0 +1,236 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { useUserStore } from "@/shared/lib";
|
||||
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||
import {
|
||||
type CmCdAdstDstrComboResponseDto,
|
||||
useFindUserGugunCombo,
|
||||
useFindUserSidoCombo,
|
||||
} from "@/entities/adstdstr";
|
||||
import { useCodeList } from "@/entities/code";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Skeleton,
|
||||
Table,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
|
||||
interface SewerBscPlanApplySelection {
|
||||
admdstCd: string;
|
||||
sido: string;
|
||||
gugun: string;
|
||||
sewerBscPlanSeComCd: string;
|
||||
sewerBscPlanSeCdNm: string;
|
||||
taskSeComCd: string;
|
||||
taskSeCdNm: string;
|
||||
}
|
||||
|
||||
interface SewerBscPlanApplyDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onConfirm: (selection: SewerBscPlanApplySelection) => void;
|
||||
}
|
||||
|
||||
const EMPTY_ADST_LIST: CmCdAdstDstrComboResponseDto[] = [];
|
||||
|
||||
function SewerBscPlanApplyDialog({ open, onOpenChange, onConfirm }: SewerBscPlanApplyDialogProps) {
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const [sidoComCd, setSidoComCd] = useState("");
|
||||
const [sggComCd, setSggComCd] = useState("");
|
||||
const [sewerBscPlanSeComCd, setSewerBscPlanSeComCd] = useState("");
|
||||
const [taskSeComCd, setTaskSeComCd] = useState("");
|
||||
|
||||
const sidoFilterUserInfo =
|
||||
userInfo?.dtlInstCd === "ROLE_USER04" || userInfo?.dtlInstCd === "ROLE_USER05"
|
||||
? userInfo
|
||||
: undefined;
|
||||
|
||||
const { data: sidoData, isPending: isSidoPending } = useFindUserSidoCombo(sidoFilterUserInfo);
|
||||
const { data: gugunData, isPending: isGugunPending } = useFindUserGugunCombo(sidoComCd);
|
||||
const { data: planSeOptions, isPending: isPlanSePending } = useCodeList("SEWERBSCPLANSE");
|
||||
const { data: taskSeOptions, isPending: isTaskSePending } = useCodeList("SEWER");
|
||||
|
||||
const sidoOptions = sidoData ?? EMPTY_ADST_LIST;
|
||||
const gugunOptions = gugunData ?? EMPTY_ADST_LIST;
|
||||
const isPending = isSidoPending || isPlanSePending || isTaskSePending;
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setSidoComCd("");
|
||||
setSggComCd("");
|
||||
setSewerBscPlanSeComCd("");
|
||||
setTaskSeComCd("");
|
||||
}, [open]);
|
||||
|
||||
const canConfirm = Boolean(sidoComCd && sewerBscPlanSeComCd && taskSeComCd);
|
||||
|
||||
function handleConfirm() {
|
||||
if (!canConfirm) return;
|
||||
|
||||
const sidoOption = sidoOptions.find((option) => option.adstDstrCd === sidoComCd);
|
||||
const gugunOption = gugunOptions.find((option) => option.adstDstrCd === sggComCd);
|
||||
const planSeOption = planSeOptions?.find((option) => option.lwpoCd === sewerBscPlanSeComCd);
|
||||
const taskSeOption = taskSeOptions?.find((option) => option.lwpoCd === taskSeComCd);
|
||||
|
||||
onConfirm({
|
||||
admdstCd: sggComCd || sidoComCd,
|
||||
sido: sidoOption?.adstDstrNm ?? "",
|
||||
gugun: gugunOption?.adstDstrNm ?? "",
|
||||
sewerBscPlanSeComCd,
|
||||
sewerBscPlanSeCdNm: planSeOption?.lwpoCdNm ?? "",
|
||||
taskSeComCd,
|
||||
taskSeCdNm: taskSeOption?.lwpoCdNm ?? "",
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>하수도기본계획 신청</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{isPending ? (
|
||||
<Skeleton className="h-[16rem] w-full" />
|
||||
) : (
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead data-required>시도</TableHead>
|
||||
<TableCell>
|
||||
<Select
|
||||
value={sidoComCd || undefined}
|
||||
onValueChange={(value) => {
|
||||
setSidoComCd(value);
|
||||
setSggComCd("");
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sidoOptions.map((option) => (
|
||||
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
|
||||
{option.adstDstrNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
<TableHead>시군구</TableHead>
|
||||
<TableCell>
|
||||
<Select
|
||||
value={sggComCd || undefined}
|
||||
onValueChange={setSggComCd}
|
||||
disabled={!sidoComCd || isGugunPending}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{gugunOptions.map((option) => (
|
||||
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
|
||||
{option.adstDstrNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>계획구분</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<FieldSet className="px-2">
|
||||
<RadioGroup
|
||||
value={sewerBscPlanSeComCd || undefined}
|
||||
onValueChange={setSewerBscPlanSeComCd}
|
||||
>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{planSeOptions?.map((option) => (
|
||||
<FieldLabel
|
||||
key={option.lwpoCd}
|
||||
htmlFor={`sewer-plan-se-${option.lwpoCd}`}
|
||||
className="!w-auto border-none"
|
||||
>
|
||||
<Field orientation="horizontal" className="w-auto">
|
||||
<RadioGroupItem
|
||||
value={option.lwpoCd}
|
||||
id={`sewer-plan-se-${option.lwpoCd}`}
|
||||
/>
|
||||
<span className="font-normal">{option.lwpoCdNm}</span>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
))}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>업무구분</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<FieldSet className="px-2">
|
||||
<RadioGroup value={taskSeComCd || undefined} onValueChange={setTaskSeComCd}>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{taskSeOptions?.map((option) => (
|
||||
<FieldLabel
|
||||
key={option.lwpoCd}
|
||||
htmlFor={`sewer-task-se-${option.lwpoCd}`}
|
||||
className="!w-auto border-none"
|
||||
>
|
||||
<Field orientation="horizontal" className="w-auto">
|
||||
<RadioGroupItem
|
||||
value={option.lwpoCd}
|
||||
id={`sewer-task-se-${option.lwpoCd}`}
|
||||
/>
|
||||
<span className="font-normal">{option.lwpoCdNm}</span>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
))}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
</Table>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" color="white" onClick={() => onOpenChange(false)}>
|
||||
취소
|
||||
</Button>
|
||||
<Button type="button" onClick={handleConfirm} disabled={isPending || !canConfirm}>
|
||||
확인
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { SewerBscPlanApplyDialog };
|
||||
export type { SewerBscPlanApplySelection };
|
||||
@@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import { useSewerBscPlanDetails } from "@/entities/sewer-bsc-plan";
|
||||
import { Skeleton } from "@common/components";
|
||||
|
||||
import { SewerBscPlanFormPage } from "./sewer-bsc-plan-form-page";
|
||||
|
||||
interface SewerBscPlanEditPageProps {
|
||||
id: string;
|
||||
propChgCycl?: number;
|
||||
}
|
||||
|
||||
function SewerBscPlanEditPage({ id, propChgCycl }: SewerBscPlanEditPageProps) {
|
||||
const chgCycl = propChgCycl ?? 1;
|
||||
const { data, isLoading: isDetailsLoading } = useSewerBscPlanDetails(id, chgCycl);
|
||||
|
||||
if (isDetailsLoading) {
|
||||
return (
|
||||
<div className="mt-6 space-y-6 px-4">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-[4rem] w-full" />
|
||||
<Skeleton className="h-[20rem] w-full" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-[4rem] w-full" />
|
||||
<Skeleton className="h-[20rem] w-full" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-[4rem] w-full" />
|
||||
<Skeleton className="h-[20rem] w-full" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data?.detDto) {
|
||||
return <div className="mt-6 text-[1.4rem]">상세 정보를 찾을 수 없습니다.</div>;
|
||||
}
|
||||
|
||||
return <SewerBscPlanFormPage id={id} mode="edit" initialData={data} />;
|
||||
}
|
||||
|
||||
export { SewerBscPlanEditPage };
|
||||
@@ -0,0 +1,303 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
import { useAcntgSeMngTree } from "@/entities/acntg";
|
||||
import {
|
||||
SewerBscPlanApplySaveResponseDto,
|
||||
sewerBscPlanBaseKey,
|
||||
type SewerBscPlanDetResponseDto,
|
||||
sewerBscPlanService,
|
||||
} from "@/entities/sewer-bsc-plan";
|
||||
import {
|
||||
AttachmentsFormSection,
|
||||
buildAcntgCostItems,
|
||||
ChecklistFormSection,
|
||||
ContactInfoFormSection,
|
||||
isFormChecklistComplete,
|
||||
PrivacyConsentSection,
|
||||
SplmntDmndFormSection,
|
||||
} from "@/widgets/pbadms-sprt";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
import { validateFnrsCstBalance } from "../lib/fnrs-cst-form-utils";
|
||||
import {
|
||||
buildApplyPayload,
|
||||
getCreateDefaultValues,
|
||||
mapDetailToFormData,
|
||||
} from "../lib/form-data-mapper";
|
||||
import {
|
||||
SEWER_BSC_PLAN_DOC_SE_COM_CD,
|
||||
type SewerBscPlanRegistFormData,
|
||||
sewerBscPlanRegistSchema,
|
||||
} from "../lib/sewer-bsc-plan-regist-schema";
|
||||
|
||||
import { BasicInfoFormSection } from "./sections/basic-info-form-section";
|
||||
import { BizCstMatrixFormSection } from "./sections/biz-cst-matrix-form-section";
|
||||
import { FnrsCstFormSection } from "./sections/fnrs-cst-form-section";
|
||||
import { OutlineFormSection } from "./sections/outline-form-section";
|
||||
import { TmplntFormSection } from "./sections/tmplnt-form-section";
|
||||
|
||||
interface SewerBscPlanFormPageProps {
|
||||
id?: string;
|
||||
mode: "create" | "edit";
|
||||
initialData?: SewerBscPlanDetResponseDto;
|
||||
createDefaults?: {
|
||||
taskSeComCd?: string;
|
||||
taskSeCdNm?: string;
|
||||
sewerBscPlanSeComCd?: string;
|
||||
sewerBscPlanSeCdNm?: string;
|
||||
admdstCd?: string;
|
||||
sido?: string;
|
||||
gugun?: string;
|
||||
};
|
||||
}
|
||||
|
||||
function SewerBscPlanFormPage({
|
||||
id,
|
||||
mode,
|
||||
initialData,
|
||||
createDefaults,
|
||||
}: SewerBscPlanFormPageProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const router = useRouter();
|
||||
const { confirm } = useConfirm();
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const isEditMode = mode === "edit";
|
||||
const isSplmntPrgrsStts = initialData?.detDto?.prgrsSttsComCd === "SL";
|
||||
|
||||
const { data: acntgTree = [] } = useAcntgSeMngTree();
|
||||
const costItems = useMemo(() => buildAcntgCostItems(acntgTree), [acntgTree]);
|
||||
|
||||
const form = useForm<SewerBscPlanRegistFormData>({
|
||||
resolver: zodResolver(sewerBscPlanRegistSchema),
|
||||
mode: "onChange",
|
||||
defaultValues:
|
||||
isEditMode && initialData
|
||||
? mapDetailToFormData(initialData)
|
||||
: getCreateDefaultValues(userInfo, createDefaults),
|
||||
});
|
||||
|
||||
function assertFnrsCstBalance(data: SewerBscPlanRegistFormData): boolean {
|
||||
const validationMessage = validateFnrsCstBalance(data.bizCstList, data.fnrsCstList, costItems);
|
||||
if (validationMessage) {
|
||||
alert(validationMessage);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function submitForm(
|
||||
data: SewerBscPlanRegistFormData,
|
||||
prgrsSttsComCd: "SV" | "LE",
|
||||
): Promise<SewerBscPlanApplySaveResponseDto | undefined> {
|
||||
if (!assertFnrsCstBalance(data)) return undefined;
|
||||
|
||||
const payload = buildApplyPayload(data, prgrsSttsComCd, {
|
||||
isEditMode,
|
||||
docNo: initialData?.detDto?.sewerBscPlanDocNo ?? id,
|
||||
});
|
||||
return sewerBscPlanService.saveApply(payload);
|
||||
}
|
||||
|
||||
async function onSave(data: SewerBscPlanRegistFormData) {
|
||||
if (isSubmitting) return;
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
const result = await submitForm(data, "SV");
|
||||
if (!result?.sewerBscPlanDocNo) return;
|
||||
|
||||
alert("저장되었습니다.");
|
||||
router.push(`/sewer-bsc-plan/${result.sewerBscPlanDocNo}/edit?chgCycl=${result.chgCycl}`);
|
||||
if (isEditMode && id) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...sewerBscPlanBaseKey, "findDetails", id],
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("저장 실패:", error);
|
||||
alert("저장 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function onApply(data: SewerBscPlanRegistFormData) {
|
||||
if (isSubmitting) return;
|
||||
|
||||
if (data.privacyConsent !== "Y") {
|
||||
alert("개인정보 수집/이용에 동의해야 신청이 가능합니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isFormChecklistComplete(data.cklstResultList)) {
|
||||
alert("모든 체크리스트 항목을 체크해야 신청이 가능합니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
const docNo = await submitForm(data, "LE");
|
||||
if (!docNo) return;
|
||||
|
||||
alert("신청 완료되었습니다.");
|
||||
router.push(`/sewer-bsc-plan/${docNo}`);
|
||||
queryClient.invalidateQueries({ queryKey: sewerBscPlanBaseKey });
|
||||
} catch (error) {
|
||||
console.error("신청 실패:", error);
|
||||
alert("신청 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function onSplmntCmptn(data: SewerBscPlanRegistFormData) {
|
||||
if (isSubmitting) return;
|
||||
|
||||
const hasEmptySplmntCn = data.splmntDmndList?.some(
|
||||
(item) => !item.splmntCn || !item.splmntCn.trim(),
|
||||
);
|
||||
if (hasEmptySplmntCn) {
|
||||
alert("보완요청내용이 있습니다. 보완완료내용을 입력해 주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
const docNo = initialData?.detDto?.sewerBscPlanDocNo ?? id;
|
||||
if (!docNo) {
|
||||
alert("문서번호가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!assertFnrsCstBalance(data)) return;
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
const payload = buildApplyPayload(data, "TE", { isEditMode: true, docNo });
|
||||
const result = await sewerBscPlanService.submitSupplement(docNo, payload);
|
||||
if (!result) return;
|
||||
|
||||
alert("보완완료 처리되었습니다.");
|
||||
router.push(`/sewer-bsc-plan/${docNo}`);
|
||||
queryClient.invalidateQueries({ queryKey: sewerBscPlanBaseKey });
|
||||
} catch (error) {
|
||||
console.error("보완완료 실패:", error);
|
||||
alert("보완완료 처리 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleApplyClick() {
|
||||
form.handleSubmit(onApply)();
|
||||
}
|
||||
|
||||
async function handleDelClick() {
|
||||
if (isSubmitting) return;
|
||||
|
||||
const docNo = initialData?.detDto?.sewerBscPlanDocNo ?? id;
|
||||
if (!docNo) {
|
||||
alert("문서번호가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await confirm("삭제하시겠습니까?"))) return;
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
await sewerBscPlanService.delete(docNo);
|
||||
alert("삭제되었습니다.");
|
||||
router.push("/sewer-bsc-plan");
|
||||
queryClient.invalidateQueries({ queryKey: [...sewerBscPlanBaseKey, "findPage"] });
|
||||
} catch (error) {
|
||||
console.error("삭제 실패:", error);
|
||||
alert("삭제 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSave)} className="mt-6 space-y-4">
|
||||
<BasicInfoFormSection />
|
||||
<ContactInfoFormSection fieldPrefix="sewerBscPlanDtl" />
|
||||
<PrivacyConsentSection />
|
||||
<OutlineFormSection />
|
||||
<BizCstMatrixFormSection />
|
||||
<FnrsCstFormSection />
|
||||
<TmplntFormSection />
|
||||
<ChecklistFormSection
|
||||
docSeComCd={SEWER_BSC_PLAN_DOC_SE_COM_CD}
|
||||
fieldPrefix="sewerBscPlan"
|
||||
/>
|
||||
<AttachmentsFormSection
|
||||
docSeComCd={SEWER_BSC_PLAN_DOC_SE_COM_CD}
|
||||
fieldPrefix="sewerBscPlanDtl"
|
||||
initialEsntlFiles={isEditMode ? initialData?.esntlFileList : undefined}
|
||||
initialEtcFiles={isEditMode ? initialData?.etcFileList : undefined}
|
||||
/>
|
||||
{isEditMode && <SplmntDmndFormSection readOnly={!isSplmntPrgrsStts} />}
|
||||
|
||||
<div className="sticky bottom-0 z-40 !-mb-2 border-t bg-white pb-8 pt-6 shadow-[0_-4px_12px_rgba(0,0,0,0.06)]">
|
||||
<div className="mx-4 flex flex-wrap justify-center gap-3 sm:gap-4">
|
||||
<Button
|
||||
color="white"
|
||||
type="button"
|
||||
onClick={() => router.push("/sewer-bsc-plan")}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
목록
|
||||
</Button>
|
||||
{!isSplmntPrgrsStts ? (
|
||||
<>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
저장
|
||||
</Button>
|
||||
{isEditMode && (
|
||||
<>
|
||||
<Button
|
||||
color="green"
|
||||
type="button"
|
||||
onClick={handleApplyClick}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
신청완료
|
||||
</Button>
|
||||
<Button
|
||||
color="red"
|
||||
type="button"
|
||||
onClick={handleDelClick}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
삭제
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={form.handleSubmit(onSplmntCmptn)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
보완완료
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export { SewerBscPlanFormPage };
|
||||
@@ -0,0 +1,172 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import type { PaginationState } from "@tanstack/react-table";
|
||||
|
||||
import {
|
||||
type CmCdAdstDstrComboResponseDto,
|
||||
useFindGugunCombo,
|
||||
useFindSidoCombo,
|
||||
} from "@/entities/adstdstr";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import type { SewerBscPlanSearchRequestDto } from "@/entities/sewer-bsc-plan";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
|
||||
import { SewerBscPlanList } from "./sewer-bsc-plan-list";
|
||||
|
||||
const DEFAULT_PAGE_INDEX = 0;
|
||||
const DEFAULT_PAGE_SIZE = 10;
|
||||
const DEFAULT_SORT = "sewerBscPlanDocNo,desc";
|
||||
const EMPTY_ADST_LIST: CmCdAdstDstrComboResponseDto[] = [];
|
||||
|
||||
const DEFAULT_SEARCH_PARAMS: SewerBscPlanSearchRequestDto = {
|
||||
sewerBscPlanNm: "",
|
||||
sewerBscPlanSeComCd: "",
|
||||
taskSeComCd: "",
|
||||
admdstCd: "",
|
||||
page: DEFAULT_PAGE_INDEX,
|
||||
size: DEFAULT_PAGE_SIZE,
|
||||
sort: DEFAULT_SORT,
|
||||
};
|
||||
|
||||
const ALL_SELECT_VALUE = "__ALL__";
|
||||
|
||||
function SewerBscPlanListPage() {
|
||||
const [sewerBscPlanNm, setSewerBscPlanNm] = useState("");
|
||||
const [sewerBscPlanSeComCd, setSewerBscPlanSeComCd] = useState("");
|
||||
const [taskSeComCd, setTaskSeComCd] = useState("");
|
||||
const [sidoComCd, setSidoComCd] = useState("");
|
||||
const [sggComCd, setSggComCd] = useState("");
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: DEFAULT_PAGE_INDEX,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
});
|
||||
const [searchParams, setSearchParams] =
|
||||
useState<SewerBscPlanSearchRequestDto>(DEFAULT_SEARCH_PARAMS);
|
||||
|
||||
const { data: sidoData } = useFindSidoCombo();
|
||||
const { data: gugunData } = useFindGugunCombo(sidoComCd);
|
||||
const sidoOptions = sidoData ?? EMPTY_ADST_LIST;
|
||||
const gugunOptions = gugunData ?? EMPTY_ADST_LIST;
|
||||
|
||||
const handleSearch = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setPagination((prev) => ({ ...prev, pageIndex: DEFAULT_PAGE_INDEX }));
|
||||
setSearchParams({
|
||||
...DEFAULT_SEARCH_PARAMS,
|
||||
sewerBscPlanNm,
|
||||
sewerBscPlanSeComCd,
|
||||
taskSeComCd,
|
||||
admdstCd: sggComCd || sidoComCd || "",
|
||||
page: DEFAULT_PAGE_INDEX,
|
||||
size: pagination.pageSize,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4 px-[1.8rem] pb-6">
|
||||
<section className="card card-border mx-0 bg-background">
|
||||
<form className="flex flex-wrap items-center gap-4 p-2" onSubmit={handleSearch}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="sidoComCd" className="whitespace-nowrap">
|
||||
시도
|
||||
</Label>
|
||||
<Select
|
||||
value={sidoComCd}
|
||||
onValueChange={(value) => {
|
||||
setSidoComCd(value === ALL_SELECT_VALUE ? "" : value);
|
||||
setSggComCd("");
|
||||
}}
|
||||
>
|
||||
<SelectTrigger id="sidoComCd" className="w-[12rem]">
|
||||
<SelectValue placeholder="전체" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={ALL_SELECT_VALUE}>전체</SelectItem>
|
||||
{sidoOptions.map((option) => (
|
||||
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
|
||||
{option.adstDstrNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="sggComCd" className="whitespace-nowrap">
|
||||
시군구
|
||||
</Label>
|
||||
<Select
|
||||
value={sggComCd}
|
||||
onValueChange={(value) => setSggComCd(value === ALL_SELECT_VALUE ? "" : value)}
|
||||
disabled={!sidoComCd}
|
||||
>
|
||||
<SelectTrigger id="sggComCd" className="w-[12rem]">
|
||||
<SelectValue placeholder="전체" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={ALL_SELECT_VALUE}>전체</SelectItem>
|
||||
{gugunOptions.map((option) => (
|
||||
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
|
||||
{option.adstDstrNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="sewerBscPlanSeComCd" className="whitespace-nowrap">
|
||||
계획구분
|
||||
</Label>
|
||||
<CodeSelect
|
||||
upCd="SEWERBSCPLANSE"
|
||||
value={sewerBscPlanSeComCd}
|
||||
onValueChange={setSewerBscPlanSeComCd}
|
||||
placeholder="전체"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="taskSeComCd" className="whitespace-nowrap">
|
||||
업무구분
|
||||
</Label>
|
||||
<CodeSelect
|
||||
upCd="SEWER"
|
||||
value={taskSeComCd}
|
||||
onValueChange={setTaskSeComCd}
|
||||
placeholder="전체"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-[32rem] items-center gap-2">
|
||||
<Label htmlFor="sewerBscPlanNm" className="whitespace-nowrap">
|
||||
기본계획명
|
||||
</Label>
|
||||
<Input
|
||||
id="sewerBscPlanNm"
|
||||
value={sewerBscPlanNm}
|
||||
onChange={(event) => setSewerBscPlanNm(event.target.value)}
|
||||
placeholder="기본계획명"
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit">조회</Button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<SewerBscPlanList
|
||||
searchParams={searchParams}
|
||||
pagination={pagination}
|
||||
onPaginationChange={setPagination}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { SewerBscPlanListPage };
|
||||
@@ -0,0 +1,114 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
|
||||
import { useDataTable, useUserStore } from "@/shared/lib";
|
||||
import {
|
||||
type SewerBscPlanResponseDto,
|
||||
type SewerBscPlanSearchRequestDto,
|
||||
useSewerBscPlanPage,
|
||||
} from "@/entities/sewer-bsc-plan";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
import { createSewerBscPlanColumns } from "../model/columns";
|
||||
|
||||
import {
|
||||
SewerBscPlanApplyDialog,
|
||||
type SewerBscPlanApplySelection,
|
||||
} from "./sewer-bsc-plan-apply-dialog";
|
||||
|
||||
interface SewerBscPlanListProps {
|
||||
searchParams: SewerBscPlanSearchRequestDto;
|
||||
pagination: PaginationState;
|
||||
onPaginationChange: (pagination: PaginationState) => void;
|
||||
}
|
||||
|
||||
function SewerBscPlanList({ searchParams, pagination, onPaginationChange }: SewerBscPlanListProps) {
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const router = useRouter();
|
||||
const [isApplyDialogOpen, setIsApplyDialogOpen] = useState(false);
|
||||
|
||||
const queryParams = useMemo(
|
||||
() => ({
|
||||
sewerBscPlanNm: searchParams.sewerBscPlanNm,
|
||||
sewerBscPlanSeComCd: searchParams.sewerBscPlanSeComCd,
|
||||
taskSeComCd: searchParams.taskSeComCd,
|
||||
admdstCd: searchParams.admdstCd,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
sort: searchParams.sort ?? "sewerBscPlanDocNo,desc",
|
||||
}),
|
||||
[searchParams, pagination],
|
||||
);
|
||||
|
||||
const pageQuery = useSewerBscPlanPage(queryParams);
|
||||
const columns = useMemo(() => createSewerBscPlanColumns(), []);
|
||||
|
||||
const handleRowClick = (row: SewerBscPlanResponseDto) => {
|
||||
const canEdit =
|
||||
(row.prgrsSttsComCd === "SV" || row.prgrsSttsComCd === "SL") &&
|
||||
row.rgtr === userInfo?.userUnqId;
|
||||
|
||||
router.push(
|
||||
canEdit
|
||||
? `/sewer-bsc-plan/${row.sewerBscPlanDocNo}/edit?chgCycl=${row.chgCycl}`
|
||||
: `/sewer-bsc-plan/${row.sewerBscPlanDocNo}`,
|
||||
);
|
||||
};
|
||||
|
||||
const { tableComponent } = useDataTable({
|
||||
query: pageQuery,
|
||||
tableOptions: {
|
||||
columns,
|
||||
state: { pagination },
|
||||
onPaginationChange: (updater) => {
|
||||
const next = typeof updater === "function" ? updater(pagination) : updater;
|
||||
onPaginationChange(next);
|
||||
},
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
},
|
||||
dataTableOptions: {
|
||||
viewPagination: true,
|
||||
onRowClick: handleRowClick,
|
||||
},
|
||||
});
|
||||
|
||||
function handleConfirmApply(selection: SewerBscPlanApplySelection) {
|
||||
setIsApplyDialogOpen(false);
|
||||
const qs = new URLSearchParams({
|
||||
taskSeComCd: selection.taskSeComCd,
|
||||
taskSeCdNm: selection.taskSeCdNm,
|
||||
sewerBscPlanSeComCd: selection.sewerBscPlanSeComCd,
|
||||
sewerBscPlanSeCdNm: selection.sewerBscPlanSeCdNm,
|
||||
admdstCd: selection.admdstCd,
|
||||
sido: selection.sido,
|
||||
gugun: selection.gugun,
|
||||
});
|
||||
router.push(`/sewer-bsc-plan/regist?${qs}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="card card-border mx-0 bg-background pb-8 pt-0">
|
||||
<div className="card-header flex items-center justify-between">
|
||||
<h3 className="text-2xl font-semibold">하수도기본계획 목록</h3>
|
||||
<Button type="button" color="green" onClick={() => setIsApplyDialogOpen(true)}>
|
||||
신청
|
||||
</Button>
|
||||
</div>
|
||||
{tableComponent}
|
||||
</section>
|
||||
<SewerBscPlanApplyDialog
|
||||
open={isApplyDialogOpen}
|
||||
onOpenChange={setIsApplyDialogOpen}
|
||||
onConfirm={handleConfirmApply}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { SewerBscPlanList };
|
||||
@@ -0,0 +1,22 @@
|
||||
import { SewerBscPlanFormPage } from "./sewer-bsc-plan-form-page";
|
||||
|
||||
interface SewerBscPlanRegistDefaults {
|
||||
taskSeComCd?: string;
|
||||
taskSeCdNm?: string;
|
||||
sewerBscPlanSeComCd?: string;
|
||||
sewerBscPlanSeCdNm?: string;
|
||||
admdstCd?: string;
|
||||
sido?: string;
|
||||
gugun?: string;
|
||||
}
|
||||
|
||||
interface SewerBscPlanRegistPageProps {
|
||||
defaults?: SewerBscPlanRegistDefaults;
|
||||
}
|
||||
|
||||
function SewerBscPlanRegistPage({ defaults }: SewerBscPlanRegistPageProps) {
|
||||
return <SewerBscPlanFormPage mode="create" createDefaults={defaults} />;
|
||||
}
|
||||
|
||||
export { SewerBscPlanRegistPage };
|
||||
export type { SewerBscPlanRegistDefaults };
|
||||
@@ -22,6 +22,7 @@ export const ENDPOINT = {
|
||||
DESIGN_CNSTN_BIZ_SCL: "/as-service/api/v1/design-cnstn-biz-scl",
|
||||
DESIGN_CNSTN_ESNTL_ATCH_FILE: "/as-service/api/v1/design-cnstn-esntl-atch-file",
|
||||
DESIGN_CNSTN_RVW_OPNN: "/as-service/api/v1/design-cnstn-rvw-opnn",
|
||||
SEWER_BSC_PLAN: "/as-service/api/v1/sewer-bsc-plans",
|
||||
},
|
||||
USER_SERVICE: {
|
||||
LOGIN: "/user-service/login",
|
||||
@@ -43,6 +44,7 @@ export const ENDPOINT = {
|
||||
STATVERIFY_SERVICE: {
|
||||
ST_ANLS_MST: "/statverify-service/api/v1/st-anls-mst",
|
||||
},
|
||||
DC_SERVICE: "/dc-service/api/v1",
|
||||
};
|
||||
|
||||
export const JWT_EXPIRATION_TIME = 1800000;
|
||||
|
||||
@@ -4,11 +4,16 @@ export {
|
||||
createUploadKey,
|
||||
isEtcFileKey,
|
||||
} from "./lib/attachments-form-utils";
|
||||
export { buildAcntgCostItems, recalculateBizCstTotals } from "./lib/business-cost-form-utils";
|
||||
export {
|
||||
buildAcntgCostItems,
|
||||
type CostItemDef,
|
||||
recalculateBizCstTotals,
|
||||
} from "./lib/business-cost-form-utils";
|
||||
export { buildCnstpScaleItems, recalculateBzScaleTotals } from "./lib/business-scale-form-utils";
|
||||
export {
|
||||
buildChecklistTabs,
|
||||
isFormChecklistComplete,
|
||||
normalizeTaskSeComCdArray,
|
||||
syncCklstResultListFromTree,
|
||||
} from "./lib/checklist-form-utils";
|
||||
export type { FnrsSharItemDef, FnrsSharListItem } from "./lib/fnrs-shar-form-utils";
|
||||
@@ -28,6 +33,10 @@ export { BusinessScaleFormSection } from "./ui/business-scale-form-section";
|
||||
export { ChecklistFormSection } from "./ui/checklist-form-section";
|
||||
export { ContactInfoFormSection } from "./ui/contact-info-form-section";
|
||||
export { FnrsSharFormSection } from "./ui/fnrs-shar-form-section";
|
||||
export {
|
||||
PbadmsTaskApplyDialog,
|
||||
type PbadmsTaskApplySelection,
|
||||
} from "./ui/pbadms-task-apply-dialog";
|
||||
export { PrivacyConsentSection } from "./ui/privacy-consent-section";
|
||||
export { SectionCollapsible } from "./ui/section-collapsible";
|
||||
export {
|
||||
@@ -37,4 +46,3 @@ export {
|
||||
} from "./ui/splmnt-dmnd-dialog";
|
||||
export { type SplmntDmndFormItem, SplmntDmndFormSection } from "./ui/splmnt-dmnd-form-section";
|
||||
export { SupplementDemandSection } from "./ui/supplement-demand-section";
|
||||
export { TaskSePickerDialog } from "./ui/task-se-picker-dialog";
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { getDefaultFileKey } from "@/shared/lib";
|
||||
|
||||
export type FieldPrefix = "fnrsCnsltnMng" | "instlSncnMng" | "designCnstnMng";
|
||||
export type FieldPrefix =
|
||||
| "fnrsCnsltnMng"
|
||||
| "instlSncnMng"
|
||||
| "designCnstnMng"
|
||||
| "sewerBscPlan"
|
||||
| "sewerBscPlanDtl";
|
||||
|
||||
export const ETC_FILE_KEY_PREFIX = "etc::";
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user