diff --git a/apps/as/docs/code-management/SPEC.md b/apps/as/docs/code-management/SPEC.md new file mode 100644 index 000000000..e745ab071 --- /dev/null +++ b/apps/as/docs/code-management/SPEC.md @@ -0,0 +1,385 @@ +# 화면설계서: 코드관리 + +## 1. 기본 정보 + +| 항목 | 내용 | +| ---------- | -------------------------------------------- | +| 버전 | 0.1 | +| 프로세스명 | UI설계 | +| 화면명 | 코드관리 | +| 화면 유형 | [v] 조회 [v] 입력 [v] 수정 [v] 삭제 [ ] 출력 | +| 메뉴 경로 | 디지털행정지원 > 사이트관리 > 코드관리 | + +--- + +## 2. 화면 목적 + +설치인가 등 AS 업무에서 참조하는 **공종구분코드**(공종코드)와 **회계구분코드**(회계코드)를 한 화면에서 조회·등록·수정·삭제한다. 좌측 **공종코드 관리**와 우측 **회계코드 관리**는 독립된 목록·CRUD 영역이며, 각 섹션은 자체 API(`cnstp-se`, `acntg-se`)와 연동한다. + +--- + +## 3. 화면 구성 + +### 3-1. 전체 레이아웃 + +``` +┌───────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ 페이지 타이틀: 코드관리 브레드크럼: 디지털행정지원 > 사이트관리 > 코드관리 │ +├──────────────────────────────────────────────────┬────────────────────────────────────────────────┤ +│ [영역 A] 공종코드 관리 │ [영역 B] 회계코드 관리 │ +│ [등록] │ [등록] │ +│ ┌────────────────────────────────────────────┐ │ ┌──────────────────────────────────────────┐ │ +│ │ 코드 | 코드명 | 설명 | 사용여부 | 관리 │ │ │ 코드 | 코드명 | 설명 | 사용여부 | 관리 │ │ +│ ├────────────────────────────────────────────┤ │ ├──────────────────────────────────────────┤ │ +│ │ WC001 공공하수처리시설 … 사용 [수정][삭제]│ │ │ AC101 시설비 … 사용 [수정][삭제] │ │ +│ │ WC002 하수관로정비 … 사용 [수정][삭제]│ │ │ AC102 용역비 … 사용 [수정][삭제] │ │ +│ │ WC003 재이용시설 … 사용 [수정][삭제]│ │ │ AC103 운영비 … 미사용 [수정][삭제] │ │ +│ └────────────────────────────────────────────┘ │ └──────────────────────────────────────────┘ │ +│ « 1 2 3 … » │ « 1 2 3 … » │ +└──────────────────────────────────────────────────┴────────────────────────────────────────────────┘ +``` + +### 3-2. 화면 데이터 흐름 + +``` +BEGIN + + // 최초 진입 — 좌·우 독립 조회 + CALL cnstpSeMngService.findAll({ page, size }) + DISPLAY 공종코드 그리드 (영역 A) + + CALL acntgSeMngService.findAll({ page, size }) + DISPLAY 회계코드 그리드 (영역 B) + + // 영역 A: 공종코드 등록 + ON 영역 A [등록] 버튼 CLICK DO + OPEN 공종코드 등록 다이얼로그 + ON [저장] DO + VALIDATE cnstpSeCd + CALL cnstpSeMngService.save(CnstpSeMngSaveRequestDto) + CALL cnstpSeMngService.findAll + CLOSE 다이얼로그 + END ON + ON [취소] / [닫기] DO + CLOSE 다이얼로그 + END ON + END ON + + // 영역 A: 공종코드 수정 + ON 영역 A 행 [수정] CLICK DO + CALL cnstpSeMngService.findById(cnstpSeCd) + OPEN 공종코드 수정 다이얼로그 (기존 값 초기화) + ON [저장] DO + CALL cnstpSeMngService.update(cnstpSeCd, CnstpSeMngSaveRequestDto) + CALL cnstpSeMngService.findAll + CLOSE 다이얼로그 + END ON + END ON + + // 영역 A: 공종코드 삭제 + ON 영역 A 행 [삭제] CLICK DO + CONFIRM "선택한 공종코드를 삭제하시겠습니까?" + CALL cnstpSeMngService.delete(cnstpSeCd) + CALL cnstpSeMngService.findAll + END ON + + // 영역 B: 회계코드 등록 + ON 영역 B [등록] 버튼 CLICK DO + OPEN 회계코드 등록 다이얼로그 + ON [저장] DO + VALIDATE acntgSeCd, acntgSeCdNm + CALL acntgSeMngService.save(AcntgSeMngSaveRequestDto) + CALL acntgSeMngService.findAll + CLOSE 다이얼로그 + END ON + ON [취소] / [닫기] DO + CLOSE 다이얼로그 + END ON + END ON + + // 영역 B: 회계코드 수정 + ON 영역 B 행 [수정] CLICK DO + CALL acntgSeMngService.findById(acntgSeCd) + OPEN 회계코드 수정 다이얼로그 (기존 값 초기화) + ON [저장] DO + CALL acntgSeMngService.update(acntgSeCd, AcntgSeMngSaveRequestDto) + CALL acntgSeMngService.findAll + CLOSE 다이얼로그 + END ON + END ON + + // 영역 B: 회계코드 삭제 + ON 영역 B 행 [삭제] CLICK DO + CONFIRM "선택한 회계코드를 삭제하시겠습니까?" + CALL acntgSeMngService.delete(acntgSeCd) + CALL acntgSeMngService.findAll + END ON + +END +``` + +--- + +## 4. 영역 A: 공종코드 관리 + +백엔드 **공종구분코드**(`CnstpSeMng`) CRUD. 계층 구조(`upCnstpCd`, `cdLvNo`)를 지원하나, 본 화면은 **평면 목록**으로 표시. + +### 4-1. 헤더 / 툴바 + +| 항목 | 내용 | +| ----------- | ------------------------------------- | +| 섹션 타이틀 | 공종코드 관리 | +| 우측 버튼 | **[등록]** — 공종코드 등록 다이얼로그 | + +### 4-2. 그리드 컬럼 + +| 컬럼명 | Field | 타입 | 비고 | +| -------- | ------------- | ------ | ------------------------ | +| 코드 | `cnstpSeCd` | string | PK, 최대 10자 | +| 코드명 | `cnstpSeCdNm` | string | 최대 100자 | +| 설명 | `expln` | string | 최대 1000자, 선택 | +| 사용여부 | `useYn` | string | `Y`→사용, `N`→미사용 | +| 관리 | — | — | 행별 **[수정] · [삭제]** | + +- 행 클릭 시 별도 상세 이동 없음. 수정은 **[수정]** 버튼으로 다이얼로그 진입. +- 페이지네이션: Spring `Pageable` (`page`, `size`, `sort`) — **UI 노출**. 기본 `size: 10`, `@common/components` 공통 페이지네이션 컴포넌트 사용. + +### 4-3. 버튼 + +| 버튼명 | 위치 | 동작 | +| ------ | -------------- | ---------------------------------------- | +| 등록 | 섹션 우측 상단 | 공종코드 등록 다이얼로그 오픈 | +| 수정 | 행별 | 상세 조회 후 수정 다이얼로그 (값 초기화) | +| 삭제 | 행별 | 확인 후 삭제, 목록 갱신 | + +### 4-4. 다이얼로그: 공종코드 등록 / 수정 + +다이얼로그 입력 필드는 **코드 · 코드명 · 설명 · 사용여부** 4개만 노출한다. + +| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 | +| -------- | --------- | ---- | ------ | ----------------- | ------------------------------- | +| 코드 | Input | Y | — | `cnstpSeCd` | 등록 시 입력, 수정 시 읽기 전용 | +| 코드명 | Input | N | — | `cnstpSeCdNm` | 최대 100자 | +| 설명 | Input | N | — | `expln` | 최대 1000자 | +| 사용여부 | Radio | N | Y | `useYn` | `Y`→사용, `N`→미사용 | + +> UI에 노출하지 않는 API 필드(`upCnstpCd`, `cdLvNo`): 등록 시 `cdLvNo: 1` 등 기본값 전송, 수정 시 `findById` 응답값 유지. + +| 버튼명 | 동작 | +| ------ | ------------------------------------------------ | +| 저장 | POST(등록) 또는 PUT(수정) 후 `findAll` 목록 갱신 | +| 취소 | 다이얼로그 닫기, 입력값 폐기 | +| 닫기 | 다이얼로그 닫기 | + +--- + +## 5. 영역 B: 회계코드 관리 + +백엔드 **회계구분관리**(`AcntgSeMng`) CRUD. 영역 A와 동일한 **평면 목록** UI 패턴을 따른다. + +### 5-1. 헤더 / 툴바 + +| 항목 | 내용 | +| ----------- | ------------------------------------- | +| 섹션 타이틀 | 회계코드 관리 | +| 우측 버튼 | **[등록]** — 회계코드 등록 다이얼로그 | + +### 5-2. 그리드 컬럼 + +| 컬럼명 | Field | 타입 | 비고 | +| -------- | ------------- | ------ | ------------------------ | +| 코드 | `acntgSeCd` | string | PK, 최대 10자 | +| 코드명 | `acntgSeCdNm` | string | 최대 100자 | +| 설명 | `expln` | string | 최대 1000자, 선택 | +| 사용여부 | `useYn` | string | `Y`→사용, `N`→미사용 | +| 관리 | — | — | 행별 **[수정] · [삭제]** | + +- 페이지네이션: 영역 A와 동일 — **UI 노출**, 기본 `size: 10`. + +### 5-3. 버튼 + +| 버튼명 | 위치 | 동작 | +| ------ | -------------- | ---------------------------------------- | +| 등록 | 섹션 우측 상단 | 회계코드 등록 다이얼로그 오픈 | +| 수정 | 행별 | 상세 조회 후 수정 다이얼로그 (값 초기화) | +| 삭제 | 행별 | 확인 후 삭제, 목록 갱신 | + +### 5-4. 다이얼로그: 회계코드 등록 / 수정 + +다이얼로그 입력 필드는 **코드 · 코드명 · 설명 · 사용여부** 4개만 노출한다. (영역 A와 동일 패턴) + +| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 | +| -------- | --------- | ---- | ------ | ----------------- | ------------------------------- | +| 코드 | Input | Y | — | `acntgSeCd` | 등록 시 입력, 수정 시 읽기 전용 | +| 코드명 | Input | Y | — | `acntgSeCdNm` | 최대 100자 | +| 설명 | Input | N | — | `expln` | 최대 1000자 | +| 사용여부 | Radio | N | Y | `useYn` | `Y`→사용, `N`→미사용 | + +> UI에 노출하지 않는 API 필드(`upAcntgSeCd`, `cdLvNo`): 등록 시 `cdLvNo: 1` 등 기본값 전송, 수정 시 `findById` 응답값 유지. + +| 버튼명 | 동작 | +| ------ | ------------------------------------------------ | +| 저장 | POST(등록) 또는 PUT(수정) 후 `findAll` 목록 갱신 | +| 취소 | 다이얼로그 닫기, 입력값 폐기 | +| 닫기 | 다이얼로그 닫기 | + +--- + +## 6. API/DTO 명세 + +### 6-1. 공통 + +| 항목 | 내용 | +| ----------- | ---------------------------------------------------------------------------------- | +| 서비스 | `as-service` | +| Base prefix | `/as-service/api/v1` (`ENDPOINT.AS_COMMON_SERVICE`) | +| 인증 | JWT (`Authorization` 헤더), `X-Site-Id`, `token-id` | +| 페이징 | Spring `Pageable`: `page`, `size`, `sort` → `PageResponse` | +| 엔티티 | `@/entities/cnstp` (`cnstpSeMngService`), `@/entities/acntg` (`acntgSeMngService`) | + +**부가 API (본 화면 목록 외 참조용)** + +| 리소스 | Method | URL | Response | 용도 | +| ------------ | ------ | --------------------- | --------------------- | --------------------------------- | +| 공종구분코드 | GET | `/cnstp-se/list/tree` | `CnstpSeMngTreeDto[]` | 계층 트리 (타 화면·상위코드 선택) | +| 회계구분관리 | GET | `/acntg-se/list/tree` | `AcntgSeMngTreeDto[]` | 계층 트리 (타 화면·상위코드 선택) | + +### 6-2. 공종구분코드 (영역 A) — `cnstp-se` + +| 기능 | Method | URL | Request / Query | Response | 클라이언트 | +| --------- | ------ | ----------------------- | -------------------------------- | ----------------------------- | ---------------------------- | +| 목록 조회 | GET | `/cnstp-se` | `CnstpSeMngSearchDto` + Pageable | `Page` | `cnstpSeMngService.findAll` | +| 트리 조회 | GET | `/cnstp-se/list/tree` | — | `CnstpSeMngTreeDto[]` | `cnstpSeMngService.findTree` | +| 상세 조회 | GET | `/cnstp-se/{cnstpSeCd}` | — | `CnstpSeMngResponseDto` | `cnstpSeMngService.findById` | +| 등록 | POST | `/cnstp-se` | `CnstpSeMngSaveRequestDto` | `string` (cnstpSeCd) | `cnstpSeMngService.save` | +| 수정 | PUT | `/cnstp-se/{cnstpSeCd}` | `CnstpSeMngSaveRequestDto` | `string` (cnstpSeCd) | `cnstpSeMngService.update` | +| 삭제 | DELETE | `/cnstp-se/{cnstpSeCd}` | — | `string` (cnstpSeCd) | `cnstpSeMngService.delete` | + +**CnstpSeMngResponseDto** + +| Field | 타입 | 설명 | +| ------------- | ------- | ------------------ | +| `cnstpSeCd` | string | 공종구분코드 | +| `cnstpSeCdNm` | string | 공종구분코드명 | +| `upCnstpCd` | string? | 상위공종코드 | +| `cdLvNo` | number | 코드레벨번호 | +| `useYn` | string | 사용여부 | +| `expln` | string? | 설명 (최대 1000자) | +| `rgtr` | string? | 등록자 UUID | +| `rgtrNm` | string? | 등록자명 | +| `mdfr` | string? | 수정자 UUID | +| `regDt` | string? | 등록일시 | +| `mdfcnDt` | string? | 수정일시 | + +**CnstpSeMngSaveRequestDto**: `cnstpSeCd`, `cnstpSeCdNm?`, `upCnstpCd?`, `cdLvNo?`, `useYn?`, `expln?` + +**CnstpSeMngSearchDto**: `cnstpSeCd?`, `cnstpSeCdNm?`, `useYn?` + `page`, `size`, `sort` + +**React Query hooks**: `useCnstpSeMngPage`, `useCnstpSeMngTree`, `useCnstpSeMng`, `useSaveCnstpSeMng`, `useUpdateCnstpSeMng`, `useDeleteCnstpSeMng` + +### 6-3. 회계구분관리 (영역 B) — `acntg-se` + +| 기능 | Method | URL | Request / Query | Response | 클라이언트 | +| --------- | ------ | ----------------------- | -------------------------------- | ----------------------------- | ---------------------------- | +| 목록 조회 | GET | `/acntg-se` | `AcntgSeMngSearchDto` + Pageable | `Page` | `acntgSeMngService.findAll` | +| 트리 조회 | GET | `/acntg-se/list/tree` | — | `AcntgSeMngTreeDto[]` | `acntgSeMngService.findTree` | +| 상세 조회 | GET | `/acntg-se/{acntgSeCd}` | — | `AcntgSeMngResponseDto` | `acntgSeMngService.findById` | +| 등록 | POST | `/acntg-se` | `AcntgSeMngSaveRequestDto` | `string` (acntgSeCd) | `acntgSeMngService.save` | +| 수정 | PUT | `/acntg-se/{acntgSeCd}` | `AcntgSeMngSaveRequestDto` | `string` (acntgSeCd) | `acntgSeMngService.update` | +| 삭제 | DELETE | `/acntg-se/{acntgSeCd}` | — | `string` (acntgSeCd) | `acntgSeMngService.delete` | + +**AcntgSeMngResponseDto** + +| Field | 타입 | 설명 | +| ------------- | ------- | ------------------ | +| `acntgSeCd` | string | 회계구분코드 | +| `acntgSeCdNm` | string | 회계구분코드명 | +| `upAcntgSeCd` | string? | 상위회계구분코드 | +| `cdLvNo` | number | 코드레벨번호 | +| `useYn` | string | 사용여부 | +| `expln` | string? | 설명 (최대 1000자) | +| `rgtr` | string? | 등록자 UUID | +| `rgtrNm` | string? | 등록자명 | +| `mdfr` | string? | 수정자 UUID | +| `regDt` | string? | 등록일시 | +| `mdfcnDt` | string? | 수정일시 | + +**AcntgSeMngSaveRequestDto**: `acntgSeCd`, `acntgSeCdNm`, `upAcntgSeCd?`, `cdLvNo`, `useYn?`, `expln?` + +**AcntgSeMngSearchDto**: `acntgSeCd?`, `acntgSeCdNm?`, `useYn?` + `page`, `size`, `sort` + +**React Query hooks**: `useAcntgSeMngPage`, `useAcntgSeMngTree`, `useAcntgSeMng`, `useSaveAcntgSeMng`, `useUpdateAcntgSeMng`, `useDeleteAcntgSeMng` + +--- + +## 7. UI 구현 가이드 + +### 7-1. 기술 스택·원칙 + +- Next.js App Router, React Query (`@tanstack/react-query`) +- `@common/components` — `Card`, `Button`, `Dialog`, `DataTable` / `useDataTable` +- `@tanstack/react-table` — 컬럼 정의는 `model/*-table-columns.tsx` 분리 +- 기존 entity 레이어 재사용: `@/entities/cnstp`, `@/entities/acntg` (service·queries·interface 구현 완료) +- 좌·우 2열 레이아웃: `grid grid-cols-1 lg:grid-cols-2 gap-4` (모바일 1열 스택) +- 삭제 전 `confirm` / Toast 피드백 — 프로젝트 공통 패턴 준수 +- 목록 하단 **페이지네이션 UI 노출** — 영역 A·B 각각 독립 page state +- **검색 필터 UI 미제공** — `findAll` 호출 시 `page`, `size`만 전달 +- **등록/수정 다이얼로그** — 코드·코드명·설명은 `Input`, 사용여부는 `Radio` (4필드만 노출) +- 상위 코드 삭제 시 **하위 코드 연쇄 삭제 UI·로직 미구현** — 단건 삭제만 수행 + +### 7-2. 모듈·파일 배치 + +| 구분 | 경로 (제안) | +| ----------------- | -------------------------------------------------------------------- | +| 라우트 | `apps/as/src/app/(authenticated)/site/code-management/page.tsx` | +| 페이지 컴포넌트 | `apps/as/src/pages/code-management/ui/code-management-page.tsx` | +| 공종 그리드 컬럼 | `apps/as/src/pages/code-management/model/cnstp-se-table-columns.tsx` | +| 회계 그리드 컬럼 | `apps/as/src/pages/code-management/model/acntg-se-table-columns.tsx` | +| 공종 등록/수정 폼 | `apps/as/src/pages/code-management/ui/cnstp-se-form-dialog.tsx` | +| 회계 등록/수정 폼 | `apps/as/src/pages/code-management/ui/acntg-se-form-dialog.tsx` | +| API·타입 (기존) | `@/entities/cnstp`, `@/entities/acntg` | + +### 7-3. 영역별 UI 매핑 + +| 영역 | UI 컴포넌트 | 데이터 소스 | 비고 | +| ------------ | ------------------------------------- | ------------------------------------------- | ------------------------------ | +| A | `CnstpSeSection` + Table + Pagination | `useCnstpSeMngPage({ page, size })` | 독립 page state·mutation | +| B | `AcntgSeSection` + Table + Pagination | `useAcntgSeMngPage({ page, size })` | A와 상호 의존 없음 | +| A 다이얼로그 | `CnstpSeFormDialog` | `useCnstpSeMng`, `useSave/UpdateCnstpSeMng` | Input 4필드 + Radio 사용여부 | +| B 다이얼로그 | `AcntgSeFormDialog` | `useAcntgSeMng`, `useSave/UpdateAcntgSeMng` | Input 4필드 + Radio 사용여부 | + +### 7-4. 다이얼로그·라우팅 + +| 다이얼로그 | 트리거 | 라우팅 | +| ------------- | ---------------- | -------------------- | +| 공종코드 등록 | 영역 A [등록] | URL 변경 없음 (모달) | +| 공종코드 수정 | 영역 A 행 [수정] | URL 변경 없음 (모달) | +| 회계코드 등록 | 영역 B [등록] | URL 변경 없음 (모달) | +| 회계코드 수정 | 영역 B 행 [수정] | URL 변경 없음 (모달) | + +### 7-5. 데이터 흐름 (구현 관점) + +``` +BEGIN + + // 페이지 마운트 — 검색 조건 없이 page·size만 전달 + const cnstpQuery = useCnstpSeMngPage({ page: 0, size: 20 }) + const acntgQuery = useAcntgSeMngPage({ page: 0, size: 20 }) + + DISPLAY CodeManagementPage + ├─ CnstpSeSection(data=cnstpQuery.data, pagination) + └─ AcntgSeSection(data=acntgQuery.data, pagination) + + // 페이지 변경 + ON pagination page/size CHANGE DO + refetch cnstpQuery | acntgQuery with new page, size + END ON + + // mutation 성공 시 + ON save/update/delete SUCCESS DO + invalidateQueries cnstp-se | acntg-se baseKey + TOAST "저장되었습니다." | "삭제되었습니다." + END ON + +END +``` diff --git a/apps/as/src/entities/acntg/api/acntg-service.ts b/apps/as/src/entities/acntg/api/acntg-service.ts index 32afc1730..354ad35f5 100644 --- a/apps/as/src/entities/acntg/api/acntg-service.ts +++ b/apps/as/src/entities/acntg/api/acntg-service.ts @@ -1,15 +1,52 @@ import { client } from "@/shared/api"; import { ENDPOINT } from "@/shared/config"; +import type { PageResponse } from "@/shared/model"; -import type { AcntgSeMngTreeDto, AcntgService } from "../model/acntg-interface"; +import type { + AcntgSeMngResponseDto, + AcntgSeMngSaveRequestDto, + AcntgSeMngSearchDto, + AcntgSeMngService, + AcntgSeMngTreeDto, +} from "../model/acntg-interface"; -async function findTree(): Promise { - const response = await client.get( - `${ENDPOINT.AS_COMMON_SERVICE.ACNTG_SE}/list/tree`, - ); +const BASE = ENDPOINT.AS_COMMON_SERVICE.ACNTG_SE; + +async function findAll(params: AcntgSeMngSearchDto): Promise> { + const response = await client.get>(BASE, { params }); return response.data; } -export const acntgService = { +async function findTree(): Promise { + const response = await client.get(`${BASE}/list/tree`); + return response.data; +} + +async function findById(acntgSeCd: string): Promise { + const response = await client.get(`${BASE}/${acntgSeCd}`); + return response.data; +} + +async function save(body: AcntgSeMngSaveRequestDto): Promise { + const response = await client.post(BASE, body); + return response.data; +} + +async function update(acntgSeCd: string, body: AcntgSeMngSaveRequestDto): Promise { + const response = await client.put(`${BASE}/${acntgSeCd}`, body); + return response.data; +} + +async function deleteAcntgSeCd(acntgSeCd: string): Promise { + const response = await client.delete(`${BASE}/${acntgSeCd}`); + return response.data; +} + +export const acntgSeMngService = { + findAll, findTree, -} satisfies AcntgService; + findById, + save, + update, + delete: deleteAcntgSeCd, +} satisfies AcntgSeMngService; diff --git a/apps/as/src/entities/acntg/api/queries.ts b/apps/as/src/entities/acntg/api/queries.ts new file mode 100644 index 000000000..ad1527b71 --- /dev/null +++ b/apps/as/src/entities/acntg/api/queries.ts @@ -0,0 +1,69 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; + +import { ENDPOINT } from "@/shared/config"; + +import type { AcntgSeMngSaveRequestDto, AcntgSeMngSearchDto } from "../model/acntg-interface"; + +import { acntgSeMngService } from "./acntg-service"; + +const acntgSeMngBaseKey = [ENDPOINT.AS_COMMON_SERVICE.ACNTG_SE] as const; + +export function useAcntgSeMngPage(params: AcntgSeMngSearchDto) { + return useQuery({ + queryKey: [...acntgSeMngBaseKey, "findAll", params], + queryFn: () => acntgSeMngService.findAll(params), + }); +} + +export function useAcntgSeMngTree() { + return useQuery({ + queryKey: [...acntgSeMngBaseKey, "findTree"], + queryFn: () => acntgSeMngService.findTree(), + staleTime: Infinity, + }); +} + +export function useAcntgSeMng(acntgSeCd: string | null | undefined) { + return useQuery({ + queryKey: [...acntgSeMngBaseKey, "findById", acntgSeCd], + queryFn: () => acntgSeMngService.findById(acntgSeCd!), + enabled: Boolean(acntgSeCd), + }); +} + +export function useSaveAcntgSeMng() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (body: AcntgSeMngSaveRequestDto) => acntgSeMngService.save(body), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: acntgSeMngBaseKey }); + }, + }); +} + +export function useUpdateAcntgSeMng() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ acntgSeCd, body }: { acntgSeCd: string; body: AcntgSeMngSaveRequestDto }) => + acntgSeMngService.update(acntgSeCd, body), + onSuccess: (_data, { acntgSeCd }) => { + queryClient.invalidateQueries({ queryKey: acntgSeMngBaseKey }); + queryClient.invalidateQueries({ + queryKey: [...acntgSeMngBaseKey, "findById", acntgSeCd], + }); + }, + }); +} + +export function useDeleteAcntgSeMng() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (acntgSeCd: string) => acntgSeMngService.delete(acntgSeCd), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: acntgSeMngBaseKey }); + }, + }); +} diff --git a/apps/as/src/entities/acntg/index.ts b/apps/as/src/entities/acntg/index.ts index 7a1562423..48f14aa1d 100644 --- a/apps/as/src/entities/acntg/index.ts +++ b/apps/as/src/entities/acntg/index.ts @@ -1,2 +1,17 @@ -export type { AcntgSeMngTreeDto, AcntgService } from "./model/acntg-interface"; -export { acntgService } from "./api/acntg-service"; +export { acntgSeMngService } from "./api/acntg-service"; +export { + useAcntgSeMng, + useAcntgSeMngPage, + useAcntgSeMngTree, + useDeleteAcntgSeMng, + useSaveAcntgSeMng, + useUpdateAcntgSeMng, +} from "./api/queries"; +export type { + AcntgSeMngResponseDto, + AcntgSeMngSaveRequestDto, + AcntgSeMngSearchDto, + AcntgSeMngService, + AcntgSeMngTreeDto, + BizCstTreeDto, +} from "./model/acntg-interface"; diff --git a/apps/as/src/entities/acntg/model/acntg-interface.ts b/apps/as/src/entities/acntg/model/acntg-interface.ts index 52bae8010..006472152 100644 --- a/apps/as/src/entities/acntg/model/acntg-interface.ts +++ b/apps/as/src/entities/acntg/model/acntg-interface.ts @@ -1,23 +1,67 @@ -/** 회계구분 트리 노드 DTO */ -export interface AcntgSeMngTreeDto { - /** 들여쓰기 포함 표시명 (앞 공백 수 = 레벨 인디케이터) */ - indAcntgSeCdNm: string; - /** 회계구분코드 */ +import type { PageableParams, PageResponse } from "@/shared/model"; + +/** 백엔드 AcntgSeMngResponseDto */ +export interface AcntgSeMngResponseDto { acntgSeCd: string; - /** 회계구분코드명 */ acntgSeCdNm: string; - /** 상위 회계구분코드 (null = 최상위) */ - upAcntgSeCd: string | null; - /** 사용여부 */ + upAcntgSeCd?: string; + cdLvNo: number; + useYn: string; + expln?: string; + rgtr?: string; + rgtrNm?: string; + mdfr?: string; + regDt?: string; + mdfcnDt?: string; +} + +/** 백엔드 AcntgSeMngSearchDto + Pageable */ +export interface AcntgSeMngSearchDto extends PageableParams { + acntgSeCd?: string; + acntgSeCdNm?: string; + useYn?: string; +} + +/** 백엔드 AcntgSeMngSaveRequestDto */ +export interface AcntgSeMngSaveRequestDto { + acntgSeCd: string; + acntgSeCdNm: string; + upAcntgSeCd?: string; + cdLvNo: number; + useYn?: string; + expln?: string; +} + +/** 백엔드 AcntgSeMngTreeDto */ +export interface AcntgSeMngTreeDto { + indAcntgSeCdNm: string; + acntgSeCd: string; + acntgSeCdNm: string; + upAcntgSeCd?: string; useYn: string; - /** 트리 레벨 (1 = 총사업비, 2 = 1.공사비/2.보상비/3.부대경비, …) */ treeLevel: number; - /** 최상위~현재 노드까지 "|" 구분 경로 */ treePath: string; } -/** 회계구분 서비스 인터페이스 */ -export interface AcntgService { - /** 회계구분 트리 전체 조회 */ - findTree(): Promise; +/** 백엔드 BizCstTreeDto (InstlSncnMngDetResponseDto 등에서 참조) */ +export interface BizCstTreeDto { + indAcntgSeCdNm: string; + acntgSeCd: string; + acntgSeCdNm: string; + useYn: string; + treeLevel: number; + treePath: string; + bscPlanAmt?: number; + bgtRfltAmt?: number; + tstmDesignAmt?: number; +} + +/** Controller public 메서드명·시그니처와 동일 */ +export interface AcntgSeMngService { + findAll(params: AcntgSeMngSearchDto): Promise>; + findTree(): Promise; + findById(acntgSeCd: string): Promise; + save(body: AcntgSeMngSaveRequestDto): Promise; + update(acntgSeCd: string, body: AcntgSeMngSaveRequestDto): Promise; + delete(acntgSeCd: string): Promise; } diff --git a/apps/as/src/entities/cnstp/api/cnstp-service.ts b/apps/as/src/entities/cnstp/api/cnstp-service.ts index e8f264b23..1d40eee1a 100644 --- a/apps/as/src/entities/cnstp/api/cnstp-service.ts +++ b/apps/as/src/entities/cnstp/api/cnstp-service.ts @@ -1,15 +1,52 @@ import { client } from "@/shared/api"; import { ENDPOINT } from "@/shared/config"; +import type { PageResponse } from "@/shared/model"; -import type { CnstpSeMngTreeDto, CnstpService } from "../model/cnstp-interface"; +import type { + CnstpSeMngResponseDto, + CnstpSeMngSaveRequestDto, + CnstpSeMngSearchDto, + CnstpSeMngService, + CnstpSeMngTreeDto, +} from "../model/cnstp-interface"; -async function findTree(): Promise { - const response = await client.get( - `${ENDPOINT.AS_COMMON_SERVICE.CNSTP_SE}/list/tree`, - ); +const BASE = ENDPOINT.AS_COMMON_SERVICE.CNSTP_SE; + +async function findAll(params: CnstpSeMngSearchDto): Promise> { + const response = await client.get>(BASE, { params }); return response.data; } -export const cnstpService = { +async function findTree(): Promise { + const response = await client.get(`${BASE}/list/tree`); + return response.data; +} + +async function findById(cnstpSeCd: string): Promise { + const response = await client.get(`${BASE}/${cnstpSeCd}`); + return response.data; +} + +async function save(body: CnstpSeMngSaveRequestDto): Promise { + const response = await client.post(BASE, body); + return response.data; +} + +async function update(cnstpSeCd: string, body: CnstpSeMngSaveRequestDto): Promise { + const response = await client.put(`${BASE}/${cnstpSeCd}`, body); + return response.data; +} + +async function deleteCnstpSeCd(cnstpSeCd: string): Promise { + const response = await client.delete(`${BASE}/${cnstpSeCd}`); + return response.data; +} + +export const cnstpSeMngService = { + findAll, findTree, -} satisfies CnstpService; + findById, + save, + update, + delete: deleteCnstpSeCd, +} satisfies CnstpSeMngService; diff --git a/apps/as/src/entities/cnstp/api/queries.ts b/apps/as/src/entities/cnstp/api/queries.ts new file mode 100644 index 000000000..703f82679 --- /dev/null +++ b/apps/as/src/entities/cnstp/api/queries.ts @@ -0,0 +1,69 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; + +import { ENDPOINT } from "@/shared/config"; + +import type { CnstpSeMngSaveRequestDto, CnstpSeMngSearchDto } from "../model/cnstp-interface"; + +import { cnstpSeMngService } from "./cnstp-service"; + +const cnstpSeMngBaseKey = [ENDPOINT.AS_COMMON_SERVICE.CNSTP_SE] as const; + +export function useCnstpSeMngPage(params: CnstpSeMngSearchDto) { + return useQuery({ + queryKey: [...cnstpSeMngBaseKey, "findAll", params], + queryFn: () => cnstpSeMngService.findAll(params), + }); +} + +export function useCnstpSeMngTree() { + return useQuery({ + queryKey: [...cnstpSeMngBaseKey, "findTree"], + queryFn: () => cnstpSeMngService.findTree(), + staleTime: Infinity, + }); +} + +export function useCnstpSeMng(cnstpSeCd: string | null | undefined) { + return useQuery({ + queryKey: [...cnstpSeMngBaseKey, "findById", cnstpSeCd], + queryFn: () => cnstpSeMngService.findById(cnstpSeCd!), + enabled: Boolean(cnstpSeCd), + }); +} + +export function useSaveCnstpSeMng() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (body: CnstpSeMngSaveRequestDto) => cnstpSeMngService.save(body), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: cnstpSeMngBaseKey }); + }, + }); +} + +export function useUpdateCnstpSeMng() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ cnstpSeCd, body }: { cnstpSeCd: string; body: CnstpSeMngSaveRequestDto }) => + cnstpSeMngService.update(cnstpSeCd, body), + onSuccess: (_data, { cnstpSeCd }) => { + queryClient.invalidateQueries({ queryKey: cnstpSeMngBaseKey }); + queryClient.invalidateQueries({ + queryKey: [...cnstpSeMngBaseKey, "findById", cnstpSeCd], + }); + }, + }); +} + +export function useDeleteCnstpSeMng() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (cnstpSeCd: string) => cnstpSeMngService.delete(cnstpSeCd), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: cnstpSeMngBaseKey }); + }, + }); +} diff --git a/apps/as/src/entities/cnstp/index.ts b/apps/as/src/entities/cnstp/index.ts index 1415c59bd..3f585c1d6 100644 --- a/apps/as/src/entities/cnstp/index.ts +++ b/apps/as/src/entities/cnstp/index.ts @@ -1,2 +1,17 @@ -export type { CnstpSeMngTreeDto, CnstpService } from "./model/cnstp-interface"; -export { cnstpService } from "./api/cnstp-service"; +export { cnstpSeMngService } from "./api/cnstp-service"; +export { + useCnstpSeMng, + useCnstpSeMngPage, + useCnstpSeMngTree, + useDeleteCnstpSeMng, + useSaveCnstpSeMng, + useUpdateCnstpSeMng, +} from "./api/queries"; +export type { + BizSclTreeDto, + CnstpSeMngResponseDto, + CnstpSeMngSaveRequestDto, + CnstpSeMngSearchDto, + CnstpSeMngService, + CnstpSeMngTreeDto, +} from "./model/cnstp-interface"; diff --git a/apps/as/src/entities/cnstp/model/cnstp-interface.ts b/apps/as/src/entities/cnstp/model/cnstp-interface.ts index d68f0625b..fad6d983d 100644 --- a/apps/as/src/entities/cnstp/model/cnstp-interface.ts +++ b/apps/as/src/entities/cnstp/model/cnstp-interface.ts @@ -1,23 +1,67 @@ -/** 공종구분 트리 노드 DTO */ -export interface CnstpSeMngTreeDto { - /** 들여쓰기 포함 표시명 */ - indCnstpSeCdNm: string; - /** 공종구분코드 */ +import type { PageableParams, PageResponse } from "@/shared/model"; + +/** 백엔드 CnstpSeMngResponseDto */ +export interface CnstpSeMngResponseDto { cnstpSeCd: string; - /** 공종구분코드명 */ cnstpSeCdNm: string; - /** 상위 공종구분코드 (null = 최상위) */ - upCnstpCd: string | null; - /** 사용여부 */ + upCnstpCd?: string; + cdLvNo: number; + useYn: string; + expln?: string; + rgtr?: string; + rgtrNm?: string; + mdfr?: string; + regDt?: string; + mdfcnDt?: string; +} + +/** 백엔드 CnstpSeMngSearchDto + Pageable */ +export interface CnstpSeMngSearchDto extends PageableParams { + cnstpSeCd?: string; + cnstpSeCdNm?: string; + useYn?: string; +} + +/** 백엔드 CnstpSeMngSaveRequestDto */ +export interface CnstpSeMngSaveRequestDto { + cnstpSeCd: string; + cnstpSeCdNm?: string; + upCnstpCd?: string; + cdLvNo?: number; + useYn?: string; + expln?: string; +} + +/** 백엔드 CnstpSeMngTreeDto */ +export interface CnstpSeMngTreeDto { + indCnstpSeCdNm: string; + cnstpSeCd: string; + cnstpSeCdNm: string; + upCnstpCd?: string; useYn: string; - /** 트리 레벨 */ treeLevel: number; - /** 최상위~현재 노드까지 "|" 구분 경로 */ treePath: string; } -/** 공종구분 서비스 인터페이스 */ -export interface CnstpService { - /** 공종구분 트리 전체 조회 */ - findTree(): Promise; +/** 백엔드 BizSclTreeDto */ +export interface BizSclTreeDto { + indCnstpSeCdNm: string; + cnstpSeCd: string; + cnstpSeCdNm: string; + useYn: string; + treeLevel: number; + treePath: string; + bscPlanQty?: number; + bgtRfltQty?: number; + tstmDesignQty?: number; +} + +/** Controller public 메서드명·시그니처와 동일 */ +export interface CnstpSeMngService { + findAll(params: CnstpSeMngSearchDto): Promise>; + findTree(): Promise; + findById(cnstpSeCd: string): Promise; + save(body: CnstpSeMngSaveRequestDto): Promise; + update(cnstpSeCd: string, body: CnstpSeMngSaveRequestDto): Promise; + delete(cnstpSeCd: string): Promise; } diff --git a/apps/as/src/pages/instl-sncn/ui/cnfm-regist-page.tsx b/apps/as/src/pages/instl-sncn/ui/cnfm-regist-page.tsx index 058bdd368..7f815ea84 100644 --- a/apps/as/src/pages/instl-sncn/ui/cnfm-regist-page.tsx +++ b/apps/as/src/pages/instl-sncn/ui/cnfm-regist-page.tsx @@ -10,18 +10,13 @@ import { type HttpResponse, Upload } from "tus-js-client"; import { AUTH_USER_ID, CLAIM_NAME, ENDPOINT, PROXY_HOST } from "@/shared/config"; import { getCookie, setCookie, useAlertStore, useUserStore } from "@/shared/lib"; -import { deleteAttachment } from "@/shared/lib/attach"; +import { deleteAttachment } from "@/shared/lib/attach"; import { formatFileSize } from "@/shared/lib/file-utils"; -import type { AcntgSeMngTreeDto } from "@/entities/acntg"; -import { acntgService } from "@/entities/acntg"; +import { type AcntgSeMngTreeDto, useAcntgSeMngTree } from "@/entities/acntg"; import type { CklstTreeNode } from "@/entities/cklst"; import { cklstMngService } from "@/entities/cklst"; -import type { CnstpSeMngTreeDto } from "@/entities/cnstp"; -import { cnstpService } from "@/entities/cnstp"; -import { - esntlAtchFileMngService, - type EsntlAtchFileMngTreeResponseDto, -} from "@/entities/esntl"; +import { type CnstpSeMngTreeDto, useCnstpSeMngTree } from "@/entities/cnstp"; +import { esntlAtchFileMngService, type EsntlAtchFileMngTreeResponseDto } from "@/entities/esntl"; import { INSTL_SNCN_REGISTER_SELECTION_KEY, InstlSncnApi, @@ -30,7 +25,7 @@ import { type InstlSncnBzScaleSaveDto, type InstlSncnMng, } from "@/entities/instl-sncn"; -import { EtcAttachments,NumCell, PrivacyConsent, ReadCell } from "@/widgets/cnfm"; +import { EtcAttachments, NumCell, PrivacyConsent, ReadCell } from "@/widgets/cnfm"; import { Button, Input } from "@common/components"; import "./cnfm-page.css"; @@ -125,7 +120,7 @@ const normalizeChecklistResultRows = (payload: any): any[] => { /* 메인 컴포넌트 */ /* ------------------------------------------------------------------ */ /** - * initialData 매핑 안내 + * initialData 매핑 안내 * - initialData 데이터는 cnfm-modify-page에서 서버로부터 받아온 상세 데이터를 그대로 전달받거나, cnfm-regist-page에서 수정 모드로 진입할 때 전달받을 수 있습니다. * - 이 컴포넌트는 수정 모드로 호출될 때 `initialData`를 받습니다. * - 서버 응답은 두 가지 형태 중 하나일 수 있습니다: @@ -237,7 +232,9 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { staleTime: Infinity, }); - const [requiredFiles, setRequiredFiles] = useState>({}); + const [requiredFiles, setRequiredFiles] = useState>( + {}, + ); const requiredFileInputRefs = useRef>({}); useEffect(() => { @@ -321,22 +318,22 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { upload.start(); }; - /* ── 기타 첨부파일 ── */ - type EtcFile = { - id: string; - name: string; - ext: string; - size: string; - uploader: string; - atflCd: string; - atflId: string; - }; - const [etcFiles, setEtcFiles] = useState([]); - const [etcNoticeOpen, setEtcNoticeOpen] = useState(true); - const [etcFileGroupId, setEtcFileGroupId] = useState(""); - const [etcUploading, setEtcUploading] = useState(false); - const etcFileGroupIdRef = useRef(""); - const initialMappedRef = useRef(false); + /* ── 기타 첨부파일 ── */ + type EtcFile = { + id: string; + name: string; + ext: string; + size: string; + uploader: string; + atflCd: string; + atflId: string; + }; + const [etcFiles, setEtcFiles] = useState([]); + const [etcNoticeOpen, setEtcNoticeOpen] = useState(true); + const [etcFileGroupId, setEtcFileGroupId] = useState(""); + const [etcUploading, setEtcUploading] = useState(false); + const etcFileGroupIdRef = useRef(""); + const initialMappedRef = useRef(false); // 초기 데이터가 주어지면 폼 상태로 매핑 (수정 모드에서 사용) useEffect(() => { @@ -351,7 +348,6 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { //const cklstResultList = initialData.cklstResultList ?? []; const esntlFileList = initialData.esntlFileList ?? []; const etcFileList = initialData.etcFileList ?? []; - // 기본 정보 및 설계자 정보 setBzNm(det.bizNm ?? ""); @@ -381,9 +377,12 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { .filter((f: any) => f && typeof f === "object") .map((f: any) => { const fileName = f.orgnlFileNm ?? f.physFileNm ?? ""; - const ext = fileName && typeof fileName === "string" && fileName.includes(".") ? fileName.split(".").pop() ?? "" : ""; + const ext = + fileName && typeof fileName === "string" && fileName.includes(".") + ? (fileName.split(".").pop() ?? "") + : ""; return { - id: f.atflId , + id: f.atflId, name: fileName, ext, size: f.atflFlieSz ?? 0, @@ -397,13 +396,14 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { // 수정모드시 기타첨부 그룹아이디 적용 (중복 매핑 방지) // - det.atchFileGroupId 또는 det.atflCd 우선, 없으면 etcFileList 첫 항목의 atflCd 사용 - const existingGroup = (det.atchFileGroupId ) || (Array.isArray(etcFileList) && etcFileList[0]?.atflCd) || ""; + const existingGroup = + det.atchFileGroupId || (Array.isArray(etcFileList) && etcFileList[0]?.atflCd) || ""; //console.log("etcFileGroupIdRef.current 초기화1:", existingGroup); if (existingGroup && etcFileGroupIdRef.current !== existingGroup) { etcFileGroupIdRef.current = existingGroup; setEtcFileGroupId((prev) => prev || existingGroup); } - // console.log("etcFileGroupIdRef.current 초기화:", etcFileGroupIdRef.current); + // console.log("etcFileGroupIdRef.current 초기화:", etcFileGroupIdRef.current); // 매핑 완료 플래그 설정 — 이후 동일 컴포넌트 인스턴스에서 재매핑 방지 initialMappedRef.current = true; @@ -465,14 +465,14 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { }; } setAmtMap(map); - } + } } catch (err) { console.error("initialData 매핑 실패", err); } }, [initialData]); - /** 기타첨부파일 삭제 핸들러: 백엔드에 DELETE 요청을 보내 소프트삭제 처리 후 상태 갱신 */ - const handleDeleteEtcFile = async (fileId: string, atflId: string) => { + /** 기타첨부파일 삭제 핸들러: 백엔드에 DELETE 요청을 보내 소프트삭제 처리 후 상태 갱신 */ + const handleDeleteEtcFile = async (fileId: string, atflId: string) => { try { if (atflId) { await deleteAttachment(atflId); @@ -493,18 +493,21 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { }); }; - const handleRequiredFileChange = (key: RequiredAttachKey, e: React.ChangeEvent) => { + const handleRequiredFileChange = ( + key: RequiredAttachKey, + e: React.ChangeEvent, + ) => { const file = e.target.files?.[0]; if (!file) return; uploadRequiredFile(file, key); e.target.value = ""; }; - + const uploadEtcFile = (file: File): Promise => new Promise((resolve, reject) => { let uploadedAtflCd = ""; let uploadedAtflId = ""; - console.log("기타첨부파일 업로드 시작:", file.name ,etcFileGroupIdRef.current); + console.log("기타첨부파일 업로드 시작:", file.name, etcFileGroupIdRef.current); const upload = new Upload(file, { endpoint: `${PROXY_HOST}${ENDPOINT.ATTACH_SERVICE.TUS_UPLOAD}`, chunkSize: 10 * 1024 * 1024, @@ -534,7 +537,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { } }, onSuccess() { - const ext = file.name.includes(".") ? file.name.split(".").pop() ?? "" : ""; + const ext = file.name.includes(".") ? (file.name.split(".").pop() ?? "") : ""; resolve({ id: uploadedAtflId || `${Date.now()}_${Math.random()}`, name: file.name, @@ -702,7 +705,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { // 수정 모드에서 서버의 체크리스트 결과값을 폼 상태로 반영 useEffect(() => { - if (!initialData || cklstRows.length === 0) return; + if (!initialData || cklstRows.length === 0) return; const results = normalizeChecklistResultRows(initialData); if (!Array.isArray(results) || results.length === 0) return; @@ -750,25 +753,24 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { return { total, completed, percent }; }, [cklstRows, checkResults, selectedReviewStep]); - const toCklstSaveDtoList = () => - { - const persistedStepCodeSet = new Set(); - normalizeChecklistResultRows(initialData).forEach((r: any) => { - const code = r?.rvwStpComCd as ReviewStepCode | undefined; - if (code && REVIEW_STEP_CODES.includes(code)) persistedStepCodeSet.add(code); - }); - visibleReviewStepCodes.forEach((code) => persistedStepCodeSet.add(code)); + const toCklstSaveDtoList = () => { + const persistedStepCodeSet = new Set(); + normalizeChecklistResultRows(initialData).forEach((r: any) => { + const code = r?.rvwStpComCd as ReviewStepCode | undefined; + if (code && REVIEW_STEP_CODES.includes(code)) persistedStepCodeSet.add(code); + }); + visibleReviewStepCodes.forEach((code) => persistedStepCodeSet.add(code)); - return Array.from(persistedStepCodeSet).flatMap((stepCode) => - cklstRows.map((row) => ({ - cklstSn: row.cklstSn, - cklstArtclSn: row.cklstArtclSn, - cklstRvwSn: row.cklstRvwSn, - rvwStpComCd: stepCode, - selected: checkResults[cklstKey(row, stepCode)] ?? null, - })), - ); - }; + return Array.from(persistedStepCodeSet).flatMap((stepCode) => + cklstRows.map((row) => ({ + cklstSn: row.cklstSn, + cklstArtclSn: row.cklstArtclSn, + cklstRvwSn: row.cklstRvwSn, + rvwStpComCd: stepCode, + selected: checkResults[cklstKey(row, stepCode)] ?? null, + })), + ); + }; // 맵 형식으로 저장할 경우(업서트 처리용) - 키는 상세 코드(EX: LOCAL.지자체) 포맷 const REVIEW_STEP_FULL: Record = { @@ -813,11 +815,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { const [bzScaleMap, setBzScaleMap] = useState>({}); /* ── 공종구분 트리 로드 ── */ - const { data: cnstpTree = [] } = useQuery({ - queryKey: ["cnstpTree"], - queryFn: cnstpService.findTree, - staleTime: Infinity, - }); + const { data: cnstpTree = [] } = useCnstpSeMngTree(); /* ── 공종구분 리프 노드 집합 ── */ const cnstpLeafSet = useMemo(() => { @@ -827,7 +825,9 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { const subtotalScale = (node: CnstpSeMngTreeDto, col: keyof BzScaleRow): number => cnstpTree - .filter((n) => cnstpLeafSet.has(n.cnstpSeCd) && n.treePath.split("|").includes(node.cnstpSeCd)) + .filter( + (n) => cnstpLeafSet.has(n.cnstpSeCd) && n.treePath.split("|").includes(node.cnstpSeCd), + ) .reduce((sum, n) => sum + num((bzScaleMap[n.cnstpSeCd] ?? emptyScale())[col]), 0); const scaleNodeVal = (node: CnstpSeMngTreeDto, col: keyof BzScaleRow): number => @@ -865,11 +865,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { const [amtMap, setAmtMap] = useState>({}); /* ── 회계구분 트리 로드 ── */ - const { data: acntgTree = [] } = useQuery({ - queryKey: ["acntgTree"], - queryFn: acntgService.findTree, - staleTime: Infinity, // 코드 데이터 — 앱 수명 동안 재요청 불필요 - }); + const { data: acntgTree = [] } = useAcntgSeMngTree(); /* ── 리프 노드 집합 (자식이 없는 노드 = 직접 입력 가능) ── */ const leafSet = useMemo(() => { @@ -943,7 +939,6 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { /* ── 검증 ── */ function fnValidation(mode: "SAVE" | "PRESENTN") { - if (!bzNm.trim()) { alert("사업명을 입력하세요."); return false; @@ -960,8 +955,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { alert("개인정보 제공에 동의하셔야 제출이 가능합니다."); return false; } - - + if (!editableReviewStep) { alert("현재 사용자 권한/진행상태 조합에서 입력 가능한 체크리스트 단계가 없습니다."); return false; @@ -974,7 +968,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { if (hasUncheckedChecklist) { alert("체크리스트를 모두 체크하세요."); return false; - } + } // 저장 시: 필수 첨부파일(esntlAtchYn=Y) 업로드 여부 검증 if (mode === "SAVE") { const missingRequiredFile = requiredAttachmentDefs.find((item) => { @@ -1070,7 +1064,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { function handleSubmit() { if (!fnValidation("PRESENTN")) return; const payload = { - instlSncnMng: toInstlSncnMngPayload("ST"), + instlSncnMng: toInstlSncnMngPayload("ST"), bzScaleList: toBzScaleSaveDtoList(), bizCstList: toBizCstSaveDtoList(), cklstRvwRsltList: toCklstSaveDtoList(), @@ -1134,7 +1128,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { - + * 사업기간 @@ -1156,7 +1150,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { - + * 사업내용 @@ -1169,12 +1163,12 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { - 구분 + 구분
{taskSeComCdNm}
- 신청 지자체 + 신청 지자체 {userInfo?.enoNm ?? "\u00A0"} @@ -1182,7 +1176,9 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { - *사업구분 + + *사업구분 +
{[ @@ -1214,31 +1210,47 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { - - + + - + - + - - + + - - + + - + - - + +
신청자이름 + 신청자 + 이름 - {aplcntNm || userInfo?.userName || "\u00A0"} + + {aplcntNm || userInfo?.userName || "\u00A0"} + 전화번호전화번호 - {aplcntMblTelno || userInfo?.mphn || userInfo?.telno || "\u00A0"} + + {aplcntMblTelno || userInfo?.mphn || userInfo?.telno || "\u00A0"} +
부서부서 - {aplcntOgdpNm || userInfo?.instSeNm || "\u00A0"} + + {aplcntOgdpNm || userInfo?.instSeNm || "\u00A0"} + 이메일이메일 - {aplcntEmlAddr || userInfo?.email || "\u00A0"} + + {aplcntEmlAddr || userInfo?.email || "\u00A0"} +
설계자이름 + 설계자 + 이름 setDsgnrNm(e.target.value)} /> 전화번호전화번호 setDsgnrPsitn(e.target.value)} /> 이메일이메일

[ 개인정보 수집항목 ]

- * 필수항목 : 소속기관, 소속부서, 담당자, 직급, 사무실 번호, FAX 번호
- ․ 선택항목 : 휴대폰 번호, 이메일 주소
- ․ 기타항목 : IP Address, 쿠키, 시스템 접속 및 이용 기록
- ※ 시스템 이용과정에서 기타 항목의 정보들이 자동으로 생성되어 수집될 수 있습니다. + * 필수항목 : 소속기관, 소속부서, 담당자, 직급, 사무실 번호, FAX 번호 +
+ ․ 선택항목 : 휴대폰 번호, 이메일 주소 +
+ ․ 기타항목 : IP Address, 쿠키, 시스템 접속 및 이용 기록 +
※ 시스템 이용과정에서 기타 항목의 정보들이 자동으로 생성되어 수집될 수 있습니다.

[ 개인정보 이용목적 ]

- ․ 국가하수도정보시스템 웹서비스 이용에 따른 본인 확인 절차에 이용
- ․ 국가하수도정보시스템 사용에 필요한 권한 부여
- ․ 사용자별 맞춤 컨텐츠 (처리시설의 운영 데이터 조회, 수질현황 데이터 조회, 예산 편성 / 예산편성 등) 제공에 이용 + ․ 국가하수도정보시스템 웹서비스 이용에 따른 본인 확인 절차에 이용 +
+ ․ 국가하수도정보시스템 사용에 필요한 권한 부여 +
․ 사용자별 맞춤 컨텐츠 (처리시설의 운영 데이터 조회, 수질현황 데이터 조회, 예산 편성 + / 예산편성 등) 제공에 이용
● 개인정보의 보유 및 이용기간
- ․ 수집된 고객의 개인정보는 2년 동안 보유하며 그 이후에는 문서를 파쇄합니다.
- ․ 정보제공자가 개인정보 파쇄를 요청할 경우 즉시 파쇄합니다. + ․ 수집된 고객의 개인정보는 2년 동안 보유하며 그 이후에는 문서를 파쇄합니다. +
․ 정보제공자가 개인정보 파쇄를 요청할 경우 즉시 파쇄합니다.

● 개인정보 제공 거부에 따른 제한사항
- ․ 귀하는 개인정보 제공 동의를 거부할 권리가 있으나, 동의 거부 시 국가하수도정보시스템 이용 서비스에 제한이 있을 수 있습니다. + ․ 귀하는 개인정보 제공 동의를 거부할 권리가 있으나, 동의 거부 시 국가하수도정보시스템 이용 + 서비스에 제한이 있을 수 있습니다.
@@ -1482,7 +1499,8 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {

체크리스트

@@ -1513,8 +1531,9 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { )}
- 체크리스트 진행상황 [{selectedReviewStep}] — 총합: {cklstStats.total} · 완료: {" "} - {cklstStats.completed} · 진행률: {cklstStats.percent}% + 체크리스트 진행상황 [{selectedReviewStep}] — 총합: {cklstStats.total} · + 완료: {cklstStats.completed} · 진행률:{" "} + {cklstStats.percent}%
@@ -1583,7 +1602,10 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { }} > - @@ -1650,7 +1672,9 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { @@ -1660,7 +1684,11 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { type="text" placeholder="첨부문서를 올려주세요" className="cnfm-input flex-1" - value={requiredFile?.uploading ? "업로드 중..." : requiredFile?.fileName || ""} + value={ + requiredFile?.uploading + ? "업로드 중..." + : requiredFile?.fileName || "" + } readOnly />
{row.cklstArtclNm} + {row.treeLevel > 1 ? "└ " : ""} {row.rvwCn}
{item.esntlAtchFileNm} - {item.esntlAtchYn === "Y" ? * : null} + {item.esntlAtchYn === "Y" ? ( + * + ) : null}
{fileFormat}