diff --git a/apps/as/docs/fnrs-cnsltn-vs-instl-sncn-diff.md b/apps/as/docs/fnrs-cnsltn-vs-instl-sncn-diff.md new file mode 100644 index 000000000..c90dec22f --- /dev/null +++ b/apps/as/docs/fnrs-cnsltn-vs-instl-sncn-diff.md @@ -0,0 +1,367 @@ +# 재원협의(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 { + // TODO: 보완완료 API 호출 + context.onSuccess?.(); +} +// ... +async function handleDmndSplmnt(context: ActionHandlerContext): Promise { + // 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` | diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-list-page.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-list-page.tsx index 6871dd41c..0e52eba19 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-list-page.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-list-page.tsx @@ -123,7 +123,7 @@ function FnrsListPage() { - +
diff --git a/apps/as/src/pages/instl-sncn/ui/instl-list.tsx b/apps/as/src/pages/instl-sncn/ui/instl-list.tsx index 7dfdca538..79ff4586e 100644 --- a/apps/as/src/pages/instl-sncn/ui/instl-list.tsx +++ b/apps/as/src/pages/instl-sncn/ui/instl-list.tsx @@ -5,8 +5,12 @@ import { useRouter } from "next/navigation"; import { getCoreRowModel, type PaginationState } from "@tanstack/react-table"; -import { useDataTable } from "@/shared/lib"; -import { type InstlSncnMngSearchParams, useInstlSncnList } from "@/entities/instl-sncn"; +import { useDataTable, useUserStore } from "@/shared/lib"; +import { + type InstlSncnMngResponseDto, + type InstlSncnMngSearchParams, + useInstlSncnList, +} from "@/entities/instl-sncn"; import { TaskSePickerDialog } from "@/widgets/pbadms-sprt"; import { Button } from "@common/components"; @@ -20,6 +24,7 @@ interface InstlListProps { } function InstlList({ searchParams, pagination, onPaginationChange }: InstlListProps) { + const userInfo = useUserStore((state) => state.userInfo); const router = useRouter(); const [isTaskSeDialogOpen, setIsTaskSeDialogOpen] = useState(false); @@ -31,7 +36,7 @@ function InstlList({ searchParams, pagination, onPaginationChange }: InstlListPr sggComCd: searchParams.sggComCd, page: pagination.pageIndex, size: pagination.pageSize, - sort: searchParams.sort ?? "regDt,desc", + sort: searchParams.sort ?? "instlSncnDocNo,desc", }), [searchParams, pagination], ); @@ -39,6 +44,16 @@ function InstlList({ searchParams, pagination, onPaginationChange }: InstlListPr const pageQuery = useInstlSncnList(queryParams); const columns = useMemo(() => createInstlSncnColumns(), []); + const handleRowClick = (row: InstlSncnMngResponseDto) => { + const canEdit = + (row.prgrsSttsComCd === "SV" || row.prgrsSttsComCd === "SL") && + row.rgtr === userInfo?.userUnqId; + + router.push( + canEdit ? `/instl-sncn/${row.instlSncnDocNo}/edit` : `/instl-sncn/${row.instlSncnDocNo}`, + ); + }; + const { tableComponent } = useDataTable({ query: pageQuery, tableOptions: { @@ -53,7 +68,7 @@ function InstlList({ searchParams, pagination, onPaginationChange }: InstlListPr }, dataTableOptions: { viewPagination: true, - onRowClick: (row) => router.push(`/instl-sncn/${row.instlSncnDocNo}`), + onRowClick: handleRowClick, }, }); diff --git a/apps/as/src/pages/instl-sncn/ui/sections/history-info-section.tsx b/apps/as/src/pages/instl-sncn/ui/sections/history-info-section.tsx deleted file mode 100644 index 6f8e321ff..000000000 --- a/apps/as/src/pages/instl-sncn/ui/sections/history-info-section.tsx +++ /dev/null @@ -1,39 +0,0 @@ -"use client"; - -import { dayjs } from "@/shared/lib"; -import type { InstlSncnMngResponseDto } from "@/entities/instl-sncn"; -import { SectionCollapsible } from "@/widgets/pbadms-sprt"; -import { Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components"; - -interface HistoryInfoSectionProps { - det: InstlSncnMngResponseDto; -} - -function HistoryInfoSection({ det }: HistoryInfoSectionProps) { - return ( - - - - - 등록일시 - - {det.regDt ? dayjs(det.regDt).format("YYYY-MM-DD HH:mm:ss") : "-"} - - 수정일시 - - {det.mdfcnDt ? dayjs(det.mdfcnDt).format("YYYY-MM-DD HH:mm:ss") : "-"} - - - - 등록자명 - {det.rgtrNm || "-"} - 첨부파일그룹ID - {det.atchFileGroupId || "-"} - - -
-
- ); -} - -export { HistoryInfoSection };