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/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/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 }) => (
- - + +
+ ), + }), + ]; +} 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 };