Merge remote-tracking branch 'origin/iipbs/as/dev' into iipbs/as/dev
# Conflicts: # apps/as/src/pages/instl-sncn/ui/cnfm-regist-page.tsx
This commit is contained in:
@@ -0,0 +1 @@
|
||||
export { CodeManagementPage as default } from "@/pages/code-management";
|
||||
@@ -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<T>` |
|
||||
| 엔티티 | `@/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<CnstpSeMngResponseDto>` | `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<AcntgSeMngResponseDto>` | `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
|
||||
```
|
||||
@@ -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<AcntgSeMngTreeDto[]> {
|
||||
const response = await client.get<AcntgSeMngTreeDto[]>(
|
||||
`${ENDPOINT.AS_COMMON_SERVICE.ACNTG_SE}/list/tree`,
|
||||
);
|
||||
const BASE = ENDPOINT.AS_COMMON_SERVICE.ACNTG_SE;
|
||||
|
||||
async function findAll(params: AcntgSeMngSearchDto): Promise<PageResponse<AcntgSeMngResponseDto>> {
|
||||
const response = await client.get<PageResponse<AcntgSeMngResponseDto>>(BASE, { params });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const acntgService = {
|
||||
async function findTree(): Promise<AcntgSeMngTreeDto[]> {
|
||||
const response = await client.get<AcntgSeMngTreeDto[]>(`${BASE}/list/tree`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function findById(acntgSeCd: string): Promise<AcntgSeMngResponseDto> {
|
||||
const response = await client.get<AcntgSeMngResponseDto>(`${BASE}/${acntgSeCd}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function save(body: AcntgSeMngSaveRequestDto): Promise<string> {
|
||||
const response = await client.post<string>(BASE, body);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function update(acntgSeCd: string, body: AcntgSeMngSaveRequestDto): Promise<string> {
|
||||
const response = await client.put<string>(`${BASE}/${acntgSeCd}`, body);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function deleteAcntgSeCd(acntgSeCd: string): Promise<string> {
|
||||
const response = await client.delete<string>(`${BASE}/${acntgSeCd}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const acntgSeMngService = {
|
||||
findAll,
|
||||
findTree,
|
||||
} satisfies AcntgService;
|
||||
findById,
|
||||
save,
|
||||
update,
|
||||
delete: deleteAcntgSeCd,
|
||||
} satisfies AcntgSeMngService;
|
||||
|
||||
@@ -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 });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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<AcntgSeMngTreeDto[]>;
|
||||
/** 백엔드 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<PageResponse<AcntgSeMngResponseDto>>;
|
||||
findTree(): Promise<AcntgSeMngTreeDto[]>;
|
||||
findById(acntgSeCd: string): Promise<AcntgSeMngResponseDto>;
|
||||
save(body: AcntgSeMngSaveRequestDto): Promise<string>;
|
||||
update(acntgSeCd: string, body: AcntgSeMngSaveRequestDto): Promise<string>;
|
||||
delete(acntgSeCd: string): Promise<string>;
|
||||
}
|
||||
|
||||
@@ -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<CnstpSeMngTreeDto[]> {
|
||||
const response = await client.get<CnstpSeMngTreeDto[]>(
|
||||
`${ENDPOINT.AS_COMMON_SERVICE.CNSTP_SE}/list/tree`,
|
||||
);
|
||||
const BASE = ENDPOINT.AS_COMMON_SERVICE.CNSTP_SE;
|
||||
|
||||
async function findAll(params: CnstpSeMngSearchDto): Promise<PageResponse<CnstpSeMngResponseDto>> {
|
||||
const response = await client.get<PageResponse<CnstpSeMngResponseDto>>(BASE, { params });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const cnstpService = {
|
||||
async function findTree(): Promise<CnstpSeMngTreeDto[]> {
|
||||
const response = await client.get<CnstpSeMngTreeDto[]>(`${BASE}/list/tree`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function findById(cnstpSeCd: string): Promise<CnstpSeMngResponseDto> {
|
||||
const response = await client.get<CnstpSeMngResponseDto>(`${BASE}/${cnstpSeCd}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function save(body: CnstpSeMngSaveRequestDto): Promise<string> {
|
||||
const response = await client.post<string>(BASE, body);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function update(cnstpSeCd: string, body: CnstpSeMngSaveRequestDto): Promise<string> {
|
||||
const response = await client.put<string>(`${BASE}/${cnstpSeCd}`, body);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function deleteCnstpSeCd(cnstpSeCd: string): Promise<string> {
|
||||
const response = await client.delete<string>(`${BASE}/${cnstpSeCd}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const cnstpSeMngService = {
|
||||
findAll,
|
||||
findTree,
|
||||
} satisfies CnstpService;
|
||||
findById,
|
||||
save,
|
||||
update,
|
||||
delete: deleteCnstpSeCd,
|
||||
} satisfies CnstpSeMngService;
|
||||
|
||||
@@ -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 });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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<CnstpSeMngTreeDto[]>;
|
||||
/** 백엔드 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<PageResponse<CnstpSeMngResponseDto>>;
|
||||
findTree(): Promise<CnstpSeMngTreeDto[]>;
|
||||
findById(cnstpSeCd: string): Promise<CnstpSeMngResponseDto>;
|
||||
save(body: CnstpSeMngSaveRequestDto): Promise<string>;
|
||||
update(cnstpSeCd: string, body: CnstpSeMngSaveRequestDto): Promise<string>;
|
||||
delete(cnstpSeCd: string): Promise<string>;
|
||||
}
|
||||
|
||||
@@ -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 }) => (
|
||||
<div className="flex justify-center gap-2">
|
||||
<Button type="button" size="sm" onClick={() => onEdit(row.original)}>
|
||||
<Button type="button" size="sm" color="white" onClick={() => onEdit(row.original)}>
|
||||
관리
|
||||
</Button>
|
||||
<Button type="button" size="sm" color="red" onClick={() => onDelete(row.original)}>
|
||||
|
||||
@@ -29,29 +29,29 @@ export function createCklstMngColumns({ onEdit, onDelete }: CreateCklstMngColumn
|
||||
}),
|
||||
columnHelper.accessor("docSeComNm", {
|
||||
header: "문서구분",
|
||||
cell: ({ getValue, row }) => getValue() ?? row.original.docSeComCd ?? "—",
|
||||
cell: ({ getValue, row }) => getValue() ?? row.original.docSeComCd ?? "-",
|
||||
}),
|
||||
columnHelper.accessor("taskSeCdNm", {
|
||||
header: "업무구분",
|
||||
cell: ({ getValue, row }) => getValue() ?? row.original.taskSeComCd ?? "—",
|
||||
cell: ({ getValue, row }) => getValue() ?? row.original.taskSeComCd ?? "-",
|
||||
}),
|
||||
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 }) => (
|
||||
<div className="flex justify-center gap-2" onClick={(e) => e.stopPropagation()}>
|
||||
<Button type="button" size="sm" onClick={() => onEdit(row.original)}>
|
||||
<Button type="button" size="sm" color="white" onClick={() => onEdit(row.original)}>
|
||||
관리
|
||||
</Button>
|
||||
<Button type="button" size="sm" color="red" onClick={() => onDelete(row.original)}>
|
||||
|
||||
@@ -248,11 +248,12 @@ function CklstRvwGrid({ selectedCklst }: CklstRvwGridProps) {
|
||||
>
|
||||
미리보기
|
||||
</Button>
|
||||
<Button type="button" color="white" disabled={isDisabled} onClick={addRootRow}>
|
||||
+ 체크리스트 추가
|
||||
<Button type="button" disabled={isDisabled} onClick={addRootRow}>
|
||||
체크리스트 추가
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
color="green"
|
||||
disabled={getDirtyRows().length === 0 || isDisabled || isSaving}
|
||||
onClick={handleSave}
|
||||
>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { CodeManagementPage } from "./ui/code-management-page";
|
||||
@@ -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<AcntgSeMngResponseDto>();
|
||||
|
||||
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 }) => (
|
||||
<div className="flex justify-center gap-2">
|
||||
<Button type="button" size="sm" color="white" onClick={() => onEdit(row.original)}>
|
||||
수정
|
||||
</Button>
|
||||
<Button type="button" size="sm" color="red" onClick={() => onDelete(row.original)}>
|
||||
삭제
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
}),
|
||||
];
|
||||
}
|
||||
@@ -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<CnstpSeMngResponseDto>();
|
||||
|
||||
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 }) => (
|
||||
<div className="flex justify-center gap-2">
|
||||
<Button type="button" size="sm" color="white" onClick={() => onEdit(row.original)}>
|
||||
수정
|
||||
</Button>
|
||||
<Button type="button" size="sm" color="red" onClick={() => onDelete(row.original)}>
|
||||
삭제
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
}),
|
||||
];
|
||||
}
|
||||
@@ -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<AcntgSeFormValues>({
|
||||
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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-[52rem]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{mode === "create" ? "회계코드 등록" : "회계코드 수정"}</DialogTitle>
|
||||
</DialogHeader>
|
||||
{isLoading ? (
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-[4rem]" />
|
||||
<Skeleton className="h-[4rem]" />
|
||||
<Skeleton className="h-[4rem]" />
|
||||
</div>
|
||||
) : (
|
||||
<form className="space-y-6" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="acntgSeCd">코드</Label>
|
||||
<Input
|
||||
id="acntgSeCd"
|
||||
placeholder="코드 입력"
|
||||
maxLength={10}
|
||||
readOnly={mode === "edit"}
|
||||
{...register("acntgSeCd", { required: true })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="acntgSeCdNm">코드명</Label>
|
||||
<Input
|
||||
id="acntgSeCdNm"
|
||||
placeholder="코드명 입력"
|
||||
maxLength={100}
|
||||
{...register("acntgSeCdNm", { required: true })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="expln">설명</Label>
|
||||
<Input id="expln" placeholder="설명 입력" maxLength={1000} {...register("expln")} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>사용여부</Label>
|
||||
<RadioGroup
|
||||
value={useYn}
|
||||
onValueChange={(v) => setValue("useYn", v as "Y" | "N")}
|
||||
className="flex gap-6"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="Y" id="acntg-use-y" />
|
||||
<Label htmlFor="acntg-use-y">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="N" id="acntg-use-n" />
|
||||
<Label htmlFor="acntg-use-n">미사용</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="button" color="white" onClick={() => onOpenChange(false)}>
|
||||
취소
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
저장
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { AcntgSeFormDialog };
|
||||
@@ -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<string | null>(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 (
|
||||
<section className="card card-border mx-0 bg-background pb-6 pt-0">
|
||||
<div className="card-header flex items-center justify-between">
|
||||
<h3 className="text-2xl font-semibold">회계코드 관리</h3>
|
||||
<Button type="button" onClick={handleOpenCreate}>
|
||||
등록
|
||||
</Button>
|
||||
</div>
|
||||
{tableComponent}
|
||||
{dialogMode ? (
|
||||
<AcntgSeFormDialog
|
||||
mode={dialogMode}
|
||||
open={Boolean(dialogMode)}
|
||||
acntgSeCd={editingCd}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setDialogMode(null);
|
||||
setEditingCd(null);
|
||||
}
|
||||
}}
|
||||
isSubmitting={saveMutation.isPending || updateMutation.isPending}
|
||||
onSubmit={handleFormSubmit}
|
||||
/>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export { AcntgSeSection };
|
||||
@@ -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<CnstpSeFormValues>({
|
||||
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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-[52rem]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{mode === "create" ? "공종코드 등록" : "공종코드 수정"}</DialogTitle>
|
||||
</DialogHeader>
|
||||
{isLoading ? (
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-[4rem]" />
|
||||
<Skeleton className="h-[4rem]" />
|
||||
<Skeleton className="h-[4rem]" />
|
||||
</div>
|
||||
) : (
|
||||
<form className="space-y-6" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="cnstpSeCd">코드</Label>
|
||||
<Input
|
||||
id="cnstpSeCd"
|
||||
placeholder="코드 입력"
|
||||
maxLength={10}
|
||||
readOnly={mode === "edit"}
|
||||
{...register("cnstpSeCd", { required: true })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="cnstpSeCdNm">코드명</Label>
|
||||
<Input
|
||||
id="cnstpSeCdNm"
|
||||
placeholder="코드명 입력"
|
||||
maxLength={100}
|
||||
{...register("cnstpSeCdNm")}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="expln">설명</Label>
|
||||
<Input id="expln" placeholder="설명 입력" maxLength={1000} {...register("expln")} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>사용여부</Label>
|
||||
<RadioGroup
|
||||
value={useYn}
|
||||
onValueChange={(v) => setValue("useYn", v as "Y" | "N")}
|
||||
className="flex gap-6"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="Y" id="cnstp-use-y" />
|
||||
<Label htmlFor="cnstp-use-y">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="N" id="cnstp-use-n" />
|
||||
<Label htmlFor="cnstp-use-n">미사용</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="button" color="white" onClick={() => onOpenChange(false)}>
|
||||
취소
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
저장
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { CnstpSeFormDialog };
|
||||
@@ -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<string | null>(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 (
|
||||
<section className="card card-border mx-0 bg-background pb-6 pt-0">
|
||||
<div className="card-header flex items-center justify-between">
|
||||
<h3 className="text-2xl font-semibold">공종코드 관리</h3>
|
||||
<Button type="button" onClick={handleOpenCreate}>
|
||||
등록
|
||||
</Button>
|
||||
</div>
|
||||
{tableComponent}
|
||||
{dialogMode ? (
|
||||
<CnstpSeFormDialog
|
||||
mode={dialogMode}
|
||||
open={Boolean(dialogMode)}
|
||||
cnstpSeCd={editingCd}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setDialogMode(null);
|
||||
setEditingCd(null);
|
||||
}
|
||||
}}
|
||||
isSubmitting={saveMutation.isPending || updateMutation.isPending}
|
||||
onSubmit={handleFormSubmit}
|
||||
/>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export { CnstpSeSection, DEFAULT_PAGE_INDEX, DEFAULT_PAGE_SIZE };
|
||||
@@ -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<PaginationState>({
|
||||
pageIndex: DEFAULT_PAGE_INDEX,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
});
|
||||
const [acntgPagination, setAcntgPagination] = useState<PaginationState>({
|
||||
pageIndex: DEFAULT_PAGE_INDEX,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-6 px-[1.8rem] pb-6">
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<CnstpSeSection pagination={cnstpPagination} onPaginationChange={setCnstpPagination} />
|
||||
<AcntgSeSection pagination={acntgPagination} onPaginationChange={setAcntgPagination} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CodeManagementPage };
|
||||
Reference in New Issue
Block a user