From b6afaba02a61ac9bd60af9b5ae0869c52e793a1e Mon Sep 17 00:00:00 2001 From: bicochan Date: Mon, 1 Jun 2026 16:07:04 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[as]=20feat:=20=EA=B3=B5=EC=A2=85=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20&=20=ED=9A=8C=EA=B3=84=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/as/docs/code-management/SPEC.md | 385 ++++++++++++++++++ .../src/entities/acntg/api/acntg-service.ts | 51 ++- apps/as/src/entities/acntg/api/queries.ts | 69 ++++ apps/as/src/entities/acntg/index.ts | 19 +- .../entities/acntg/model/acntg-interface.ts | 74 +++- .../src/entities/cnstp/api/cnstp-service.ts | 51 ++- apps/as/src/entities/cnstp/api/queries.ts | 69 ++++ apps/as/src/entities/cnstp/index.ts | 19 +- .../entities/cnstp/model/cnstp-interface.ts | 74 +++- .../pages/instl-sncn/ui/cnfm-regist-page.tsx | 250 +++++++----- 10 files changed, 902 insertions(+), 159 deletions(-) create mode 100644 apps/as/docs/code-management/SPEC.md create mode 100644 apps/as/src/entities/acntg/api/queries.ts create mode 100644 apps/as/src/entities/cnstp/api/queries.ts 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 /> Date: Mon, 1 Jun 2026 16:41:18 +0900 Subject: [PATCH 2/3] =?UTF-8?q?[as]=20feat:=20=EC=BD=94=EB=93=9C=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../code-management/page.tsx | 1 + apps/as/src/pages/code-management/index.ts | 1 + .../model/acntg-se-table-columns.tsx | 61 +++++++ .../model/cnstp-se-table-columns.tsx | 61 +++++++ .../ui/acntg-se-form-dialog.tsx | 151 ++++++++++++++++++ .../code-management/ui/acntg-se-section.tsx | 144 +++++++++++++++++ .../ui/cnstp-se-form-dialog.tsx | 151 ++++++++++++++++++ .../code-management/ui/cnstp-se-section.tsx | 147 +++++++++++++++++ .../ui/code-management-page.tsx | 30 ++++ 9 files changed, 747 insertions(+) create mode 100644 apps/as/app/(with-base-layout)/code-management/page.tsx create mode 100644 apps/as/src/pages/code-management/index.ts create mode 100644 apps/as/src/pages/code-management/model/acntg-se-table-columns.tsx create mode 100644 apps/as/src/pages/code-management/model/cnstp-se-table-columns.tsx create mode 100644 apps/as/src/pages/code-management/ui/acntg-se-form-dialog.tsx create mode 100644 apps/as/src/pages/code-management/ui/acntg-se-section.tsx create mode 100644 apps/as/src/pages/code-management/ui/cnstp-se-form-dialog.tsx create mode 100644 apps/as/src/pages/code-management/ui/cnstp-se-section.tsx create mode 100644 apps/as/src/pages/code-management/ui/code-management-page.tsx diff --git a/apps/as/app/(with-base-layout)/code-management/page.tsx b/apps/as/app/(with-base-layout)/code-management/page.tsx new file mode 100644 index 000000000..7a4daf43f --- /dev/null +++ b/apps/as/app/(with-base-layout)/code-management/page.tsx @@ -0,0 +1 @@ +export { CodeManagementPage as default } from "@/pages/code-management"; diff --git a/apps/as/src/pages/code-management/index.ts b/apps/as/src/pages/code-management/index.ts new file mode 100644 index 000000000..3b00522e0 --- /dev/null +++ b/apps/as/src/pages/code-management/index.ts @@ -0,0 +1 @@ +export { CodeManagementPage } from "./ui/code-management-page"; diff --git a/apps/as/src/pages/code-management/model/acntg-se-table-columns.tsx b/apps/as/src/pages/code-management/model/acntg-se-table-columns.tsx new file mode 100644 index 000000000..2e4cc96f0 --- /dev/null +++ b/apps/as/src/pages/code-management/model/acntg-se-table-columns.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { createColumnHelper } from "@tanstack/react-table"; + +import type { AcntgSeMngResponseDto } from "@/entities/acntg"; +import { Button } from "@common/components"; + +const columnHelper = createColumnHelper(); + +interface CreateAcntgSeColumnsOptions { + onEdit: (row: AcntgSeMngResponseDto) => void; + onDelete: (row: AcntgSeMngResponseDto) => void; +} + +function formatUseYn(value: string | undefined) { + if (value === "Y") return "사용"; + if (value === "N") return "미사용"; + return "-"; +} + +function formatOptional(value: string | undefined) { + return value?.trim() ? value : "-"; +} + +export function createAcntgSeColumns({ onEdit, onDelete }: CreateAcntgSeColumnsOptions) { + return [ + columnHelper.accessor("acntgSeCd", { + header: "코드", + meta: { align: "center" }, + }), + columnHelper.accessor("acntgSeCdNm", { + header: "코드명", + meta: { align: "left" }, + cell: ({ getValue }) => formatOptional(getValue()), + }), + columnHelper.accessor("expln", { + header: "설명", + meta: { align: "left" }, + cell: ({ getValue }) => formatOptional(getValue()), + }), + columnHelper.accessor("useYn", { + header: "사용여부", + meta: { align: "center" }, + cell: ({ getValue }) => formatUseYn(getValue()), + }), + columnHelper.display({ + id: "actions", + header: "관리", + cell: ({ row }) => ( +
+ + +
+ ), + }), + ]; +} diff --git a/apps/as/src/pages/code-management/model/cnstp-se-table-columns.tsx b/apps/as/src/pages/code-management/model/cnstp-se-table-columns.tsx new file mode 100644 index 000000000..45dcadff8 --- /dev/null +++ b/apps/as/src/pages/code-management/model/cnstp-se-table-columns.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { createColumnHelper } from "@tanstack/react-table"; + +import type { CnstpSeMngResponseDto } from "@/entities/cnstp"; +import { Button } from "@common/components"; + +const columnHelper = createColumnHelper(); + +interface CreateCnstpSeColumnsOptions { + onEdit: (row: CnstpSeMngResponseDto) => void; + onDelete: (row: CnstpSeMngResponseDto) => void; +} + +function formatUseYn(value: string | undefined) { + if (value === "Y") return "사용"; + if (value === "N") return "미사용"; + return "-"; +} + +function formatOptional(value: string | undefined) { + return value?.trim() ? value : "-"; +} + +export function createCnstpSeColumns({ onEdit, onDelete }: CreateCnstpSeColumnsOptions) { + return [ + columnHelper.accessor("cnstpSeCd", { + header: "코드", + meta: { align: "center" }, + }), + columnHelper.accessor("cnstpSeCdNm", { + header: "코드명", + meta: { align: "left" }, + cell: ({ getValue }) => formatOptional(getValue()), + }), + columnHelper.accessor("expln", { + header: "설명", + meta: { align: "left" }, + cell: ({ getValue }) => formatOptional(getValue()), + }), + columnHelper.accessor("useYn", { + header: "사용여부", + meta: { align: "center" }, + cell: ({ getValue }) => formatUseYn(getValue()), + }), + columnHelper.display({ + id: "actions", + header: "관리", + cell: ({ row }) => ( +
+ + +
+ ), + }), + ]; +} diff --git a/apps/as/src/pages/code-management/ui/acntg-se-form-dialog.tsx b/apps/as/src/pages/code-management/ui/acntg-se-form-dialog.tsx new file mode 100644 index 000000000..5d6afc414 --- /dev/null +++ b/apps/as/src/pages/code-management/ui/acntg-se-form-dialog.tsx @@ -0,0 +1,151 @@ +"use client"; + +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; + +import { useAcntgSeMng } from "@/entities/acntg"; +import { + Button, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + Input, + Label, + RadioGroup, + RadioGroupItem, + Skeleton, +} from "@common/components"; + +export interface AcntgSeFormValues { + acntgSeCd: string; + acntgSeCdNm: string; + expln: string; + useYn: "Y" | "N"; +} + +interface AcntgSeFormDialogProps { + mode: "create" | "edit"; + open: boolean; + acntgSeCd?: string | null; + onOpenChange: (open: boolean) => void; + isSubmitting?: boolean; + onSubmit: (values: AcntgSeFormValues) => void; +} + +function AcntgSeFormDialog({ + mode, + open, + acntgSeCd, + onOpenChange, + isSubmitting, + onSubmit, +}: AcntgSeFormDialogProps) { + const detailQuery = useAcntgSeMng(mode === "edit" ? acntgSeCd : null); + + const { register, handleSubmit, reset, setValue, watch } = useForm({ + defaultValues: { + acntgSeCd: "", + acntgSeCdNm: "", + expln: "", + useYn: "Y", + }, + }); + + const useYn = watch("useYn"); + + useEffect(() => { + if (!open) return; + + if (mode === "edit") { + if (!detailQuery.data) return; + reset({ + acntgSeCd: detailQuery.data.acntgSeCd, + acntgSeCdNm: detailQuery.data.acntgSeCdNm ?? "", + expln: detailQuery.data.expln ?? "", + useYn: detailQuery.data.useYn === "N" ? "N" : "Y", + }); + return; + } + + reset({ + acntgSeCd: "", + acntgSeCdNm: "", + expln: "", + useYn: "Y", + }); + }, [open, mode, detailQuery.data, reset]); + + const isLoading = mode === "edit" && detailQuery.isPending; + + return ( + + + + {mode === "create" ? "회계코드 등록" : "회계코드 수정"} + + {isLoading ? ( +
+ + + +
+ ) : ( +
+
+ + +
+
+ + +
+
+ + +
+
+ + setValue("useYn", v as "Y" | "N")} + className="flex gap-6" + > +
+ + +
+
+ + +
+
+
+ + + + + + )} +
+
+ ); +} + +export { AcntgSeFormDialog }; diff --git a/apps/as/src/pages/code-management/ui/acntg-se-section.tsx b/apps/as/src/pages/code-management/ui/acntg-se-section.tsx new file mode 100644 index 000000000..f0daf9d4f --- /dev/null +++ b/apps/as/src/pages/code-management/ui/acntg-se-section.tsx @@ -0,0 +1,144 @@ +"use client"; + +import { useCallback, useMemo, useState } from "react"; + +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { getCoreRowModel, type PaginationState } from "@tanstack/react-table"; + +import { useDataTable } from "@/shared/lib"; +import { + type AcntgSeMngResponseDto, + useAcntgSeMng, + useAcntgSeMngPage, + useDeleteAcntgSeMng, + useSaveAcntgSeMng, + useUpdateAcntgSeMng, +} from "@/entities/acntg"; +import { Button } from "@common/components"; + +import { createAcntgSeColumns } from "../model/acntg-se-table-columns"; + +import { AcntgSeFormDialog, type AcntgSeFormValues } from "./acntg-se-form-dialog"; + +interface AcntgSeSectionProps { + pagination: PaginationState; + onPaginationChange: (pagination: PaginationState) => void; +} + +function AcntgSeSection({ pagination, onPaginationChange }: AcntgSeSectionProps) { + const { confirm } = useConfirm(); + const [dialogMode, setDialogMode] = useState<"create" | "edit" | null>(null); + const [editingCd, setEditingCd] = useState(null); + + const pageQuery = useAcntgSeMngPage({ + page: pagination.pageIndex, + size: pagination.pageSize, + }); + + const detailQuery = useAcntgSeMng(dialogMode === "edit" ? editingCd : null); + const saveMutation = useSaveAcntgSeMng(); + const updateMutation = useUpdateAcntgSeMng(); + const deleteMutation = useDeleteAcntgSeMng(); + + const handleDelete = useCallback( + async (row: AcntgSeMngResponseDto) => { + if (!(await confirm("삭제하시겠습니까?"))) return; + await deleteMutation.mutateAsync(row.acntgSeCd); + }, + [confirm, deleteMutation], + ); + + const columns = useMemo( + () => + createAcntgSeColumns({ + onEdit: (row) => { + setEditingCd(row.acntgSeCd); + setDialogMode("edit"); + }, + onDelete: handleDelete, + }), + [handleDelete], + ); + + 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, + }, + }); + + const handleFormSubmit = async (values: AcntgSeFormValues) => { + if (!values.acntgSeCd.trim() || !values.acntgSeCdNm.trim()) return; + + if (dialogMode === "create") { + if (!(await confirm("저장하시겠습니까?"))) return; + await saveMutation.mutateAsync({ + acntgSeCd: values.acntgSeCd.trim(), + acntgSeCdNm: values.acntgSeCdNm.trim(), + expln: values.expln.trim() || undefined, + useYn: values.useYn, + cdLvNo: 1, + }); + } else if (dialogMode === "edit" && detailQuery.data) { + if (!(await confirm("수정하시겠습니까?"))) return; + await updateMutation.mutateAsync({ + acntgSeCd: detailQuery.data.acntgSeCd, + body: { + acntgSeCd: detailQuery.data.acntgSeCd, + acntgSeCdNm: values.acntgSeCdNm.trim(), + expln: values.expln.trim() || undefined, + useYn: values.useYn, + upAcntgSeCd: detailQuery.data.upAcntgSeCd, + cdLvNo: detailQuery.data.cdLvNo, + }, + }); + } + + setDialogMode(null); + setEditingCd(null); + }; + + const handleOpenCreate = () => { + setEditingCd(null); + setDialogMode("create"); + }; + + return ( +
+
+

회계코드 관리

+ +
+ {tableComponent} + {dialogMode ? ( + { + if (!open) { + setDialogMode(null); + setEditingCd(null); + } + }} + isSubmitting={saveMutation.isPending || updateMutation.isPending} + onSubmit={handleFormSubmit} + /> + ) : null} +
+ ); +} + +export { AcntgSeSection }; diff --git a/apps/as/src/pages/code-management/ui/cnstp-se-form-dialog.tsx b/apps/as/src/pages/code-management/ui/cnstp-se-form-dialog.tsx new file mode 100644 index 000000000..362728cb9 --- /dev/null +++ b/apps/as/src/pages/code-management/ui/cnstp-se-form-dialog.tsx @@ -0,0 +1,151 @@ +"use client"; + +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; + +import { useCnstpSeMng } from "@/entities/cnstp"; +import { + Button, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + Input, + Label, + RadioGroup, + RadioGroupItem, + Skeleton, +} from "@common/components"; + +export interface CnstpSeFormValues { + cnstpSeCd: string; + cnstpSeCdNm: string; + expln: string; + useYn: "Y" | "N"; +} + +interface CnstpSeFormDialogProps { + mode: "create" | "edit"; + open: boolean; + cnstpSeCd?: string | null; + onOpenChange: (open: boolean) => void; + isSubmitting?: boolean; + onSubmit: (values: CnstpSeFormValues) => void; +} + +function CnstpSeFormDialog({ + mode, + open, + cnstpSeCd, + onOpenChange, + isSubmitting, + onSubmit, +}: CnstpSeFormDialogProps) { + const detailQuery = useCnstpSeMng(mode === "edit" ? cnstpSeCd : null); + + const { register, handleSubmit, reset, setValue, watch } = useForm({ + defaultValues: { + cnstpSeCd: "", + cnstpSeCdNm: "", + expln: "", + useYn: "Y", + }, + }); + + const useYn = watch("useYn"); + + useEffect(() => { + if (!open) return; + + if (mode === "edit") { + if (!detailQuery.data) return; + reset({ + cnstpSeCd: detailQuery.data.cnstpSeCd, + cnstpSeCdNm: detailQuery.data.cnstpSeCdNm ?? "", + expln: detailQuery.data.expln ?? "", + useYn: detailQuery.data.useYn === "N" ? "N" : "Y", + }); + return; + } + + reset({ + cnstpSeCd: "", + cnstpSeCdNm: "", + expln: "", + useYn: "Y", + }); + }, [open, mode, detailQuery.data, reset]); + + const isLoading = mode === "edit" && detailQuery.isPending; + + return ( + + + + {mode === "create" ? "공종코드 등록" : "공종코드 수정"} + + {isLoading ? ( +
+ + + +
+ ) : ( +
+
+ + +
+
+ + +
+
+ + +
+
+ + setValue("useYn", v as "Y" | "N")} + className="flex gap-6" + > +
+ + +
+
+ + +
+
+
+ + + + + + )} +
+
+ ); +} + +export { CnstpSeFormDialog }; diff --git a/apps/as/src/pages/code-management/ui/cnstp-se-section.tsx b/apps/as/src/pages/code-management/ui/cnstp-se-section.tsx new file mode 100644 index 000000000..23b9d792f --- /dev/null +++ b/apps/as/src/pages/code-management/ui/cnstp-se-section.tsx @@ -0,0 +1,147 @@ +"use client"; + +import { useCallback, useMemo, useState } from "react"; + +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { getCoreRowModel, type PaginationState } from "@tanstack/react-table"; + +import { useDataTable } from "@/shared/lib"; +import { + type CnstpSeMngResponseDto, + useCnstpSeMng, + useCnstpSeMngPage, + useDeleteCnstpSeMng, + useSaveCnstpSeMng, + useUpdateCnstpSeMng, +} from "@/entities/cnstp"; +import { Button } from "@common/components"; + +import { createCnstpSeColumns } from "../model/cnstp-se-table-columns"; + +import { CnstpSeFormDialog, type CnstpSeFormValues } from "./cnstp-se-form-dialog"; + +const DEFAULT_PAGE_INDEX = 0; +const DEFAULT_PAGE_SIZE = 10; + +interface CnstpSeSectionProps { + pagination: PaginationState; + onPaginationChange: (pagination: PaginationState) => void; +} + +function CnstpSeSection({ pagination, onPaginationChange }: CnstpSeSectionProps) { + const { confirm } = useConfirm(); + const [dialogMode, setDialogMode] = useState<"create" | "edit" | null>(null); + const [editingCd, setEditingCd] = useState(null); + + const pageQuery = useCnstpSeMngPage({ + page: pagination.pageIndex, + size: pagination.pageSize, + }); + + const detailQuery = useCnstpSeMng(dialogMode === "edit" ? editingCd : null); + const saveMutation = useSaveCnstpSeMng(); + const updateMutation = useUpdateCnstpSeMng(); + const deleteMutation = useDeleteCnstpSeMng(); + + const handleDelete = useCallback( + async (row: CnstpSeMngResponseDto) => { + if (!(await confirm("삭제하시겠습니까?"))) return; + await deleteMutation.mutateAsync(row.cnstpSeCd); + }, + [confirm, deleteMutation], + ); + + const columns = useMemo( + () => + createCnstpSeColumns({ + onEdit: (row) => { + setEditingCd(row.cnstpSeCd); + setDialogMode("edit"); + }, + onDelete: handleDelete, + }), + [handleDelete], + ); + + 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, + }, + }); + + const handleFormSubmit = async (values: CnstpSeFormValues) => { + if (!values.cnstpSeCd.trim()) return; + + if (dialogMode === "create") { + if (!(await confirm("저장하시겠습니까?"))) return; + await saveMutation.mutateAsync({ + cnstpSeCd: values.cnstpSeCd.trim(), + cnstpSeCdNm: values.cnstpSeCdNm.trim() || undefined, + expln: values.expln.trim() || undefined, + useYn: values.useYn, + cdLvNo: 1, + }); + } else if (dialogMode === "edit" && detailQuery.data) { + if (!(await confirm("수정하시겠습니까?"))) return; + await updateMutation.mutateAsync({ + cnstpSeCd: detailQuery.data.cnstpSeCd, + body: { + cnstpSeCd: detailQuery.data.cnstpSeCd, + cnstpSeCdNm: values.cnstpSeCdNm.trim() || undefined, + expln: values.expln.trim() || undefined, + useYn: values.useYn, + upCnstpCd: detailQuery.data.upCnstpCd, + cdLvNo: detailQuery.data.cdLvNo, + }, + }); + } + + setDialogMode(null); + setEditingCd(null); + }; + + const handleOpenCreate = () => { + setEditingCd(null); + setDialogMode("create"); + }; + + return ( +
+
+

공종코드 관리

+ +
+ {tableComponent} + {dialogMode ? ( + { + if (!open) { + setDialogMode(null); + setEditingCd(null); + } + }} + isSubmitting={saveMutation.isPending || updateMutation.isPending} + onSubmit={handleFormSubmit} + /> + ) : null} +
+ ); +} + +export { CnstpSeSection, DEFAULT_PAGE_INDEX, DEFAULT_PAGE_SIZE }; diff --git a/apps/as/src/pages/code-management/ui/code-management-page.tsx b/apps/as/src/pages/code-management/ui/code-management-page.tsx new file mode 100644 index 000000000..95e7dcd9f --- /dev/null +++ b/apps/as/src/pages/code-management/ui/code-management-page.tsx @@ -0,0 +1,30 @@ +"use client"; + +import { useState } from "react"; + +import type { PaginationState } from "@tanstack/react-table"; + +import { AcntgSeSection } from "./acntg-se-section"; +import { CnstpSeSection, DEFAULT_PAGE_INDEX, DEFAULT_PAGE_SIZE } from "./cnstp-se-section"; + +function CodeManagementPage() { + const [cnstpPagination, setCnstpPagination] = useState({ + pageIndex: DEFAULT_PAGE_INDEX, + pageSize: DEFAULT_PAGE_SIZE, + }); + const [acntgPagination, setAcntgPagination] = useState({ + pageIndex: DEFAULT_PAGE_INDEX, + pageSize: DEFAULT_PAGE_SIZE, + }); + + return ( +
+
+ + +
+
+ ); +} + +export { CodeManagementPage }; From db04b1331078c2a849e2a7cdbe373cd1e2849ec2 Mon Sep 17 00:00:00 2001 From: bicochan Date: Mon, 1 Jun 2026 16:50:42 +0900 Subject: [PATCH 3/3] =?UTF-8?q?[as]=20chore:=20=EC=B2=B4=ED=81=AC=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EA=B4=80=EB=A6=AC=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../checklist-management/model/cklst-artcl-columns.tsx | 8 ++++---- .../checklist-management/model/cklst-mng-columns.tsx | 10 +++++----- .../pages/checklist-management/ui/cklst-rvw-grid.tsx | 5 +++-- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/apps/as/src/pages/checklist-management/model/cklst-artcl-columns.tsx b/apps/as/src/pages/checklist-management/model/cklst-artcl-columns.tsx index 9846527ef..c356d9e04 100644 --- a/apps/as/src/pages/checklist-management/model/cklst-artcl-columns.tsx +++ b/apps/as/src/pages/checklist-management/model/cklst-artcl-columns.tsx @@ -20,25 +20,25 @@ export function createCklstArtclColumns({ onEdit, onDelete }: CreateCklstArtclCo }), columnHelper.accessor("cklstArtclNm", { header: "점검항목명", - cell: ({ getValue }) => getValue() ?? "—", + cell: ({ getValue }) => getValue() ?? "-", }), columnHelper.accessor("useYn", { header: "사용여부", cell: ({ getValue }) => { const value = getValue(); - return value === "Y" ? "예" : value === "N" ? "아니오" : "—"; + return value === "Y" ? "예" : value === "N" ? "아니오" : "-"; }, }), columnHelper.accessor("regDt", { header: "등록일시", - cell: ({ getValue }) => getValue() ?? "—", + cell: ({ getValue }) => getValue() ?? "-", }), columnHelper.display({ id: "actions", header: "관리", cell: ({ row }) => (
- -
{row.cklstArtclNm} + {row.treeLevel > 1 ? "└ " : ""} {row.rvwCn}
{item.esntlAtchFileNm} - {item.esntlAtchYn === "Y" ? * : null} + {item.esntlAtchYn === "Y" ? ( + * + ) : null}
{fileFormat}