[as] feat: 체크리스트 관리 페이지 생성
This commit is contained in:
@@ -0,0 +1 @@
|
||||
export { ChecklistManagementPage as default } from "@/pages/checklist-management";
|
||||
@@ -24,24 +24,23 @@
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 페이지 타이틀: 체크리스트관리 브레드크럼: 디지털행정지원 > … > │
|
||||
│ 페이지 타이틀: 체크리스트관리 브레드크럼: 디지털행정지원 > … > │
|
||||
├──────────────────────────────────────────────┬────────────────────────────────────────────────┤
|
||||
│ [영역 A] 점검표 관리 │ [영역 B] 점검항목 관리 │
|
||||
│ [점검표 추가] │ [점검항목 추가] │
|
||||
│ [점검표 추가] │ [점검항목 추가] │
|
||||
│ ┌────────────────────────────────────────┐ │ ┌──────────────────────────────────────────┐ │
|
||||
│ │ 점검표일련번호 | 점검표명 | 문서구분 │ │ │ 점검항목코드 | 점검항목명 | 사용여부 │ │
|
||||
│ │ 업무구분 | 사용여부 | 등록일시 | 관리 │ │ │ 등록일시 | 관리 │ │
|
||||
│ │ 점검표일련번호 | 점검표명 | 문서구분 │ │ │ 점검항목코드 | 점검항목명 | 사용여부 │ │
|
||||
│ │ 업무구분 | 사용여부 | 등록일시 | 관리 │ │ │ 등록일시 | 관리 │ │
|
||||
│ ├────────────────────────────────────────┤ │ ├──────────────────────────────────────────┤ │
|
||||
│ │ 1 (선택 행 강조) 설치인가 기본 … [관리]│ │ │ ITEM_001 요건검토 … [관리][삭제]│ │
|
||||
│ │ [삭제] │ │ │ ITEM_002 법령검토 … [관리][삭제]│ │
|
||||
│ │ 1 (선택 행 강조) [관리] [삭제] │ │ │ ITEM_001 요건검토 … [관리][삭제] │ │
|
||||
│ │ │ │ │ ITEM_002 법령검토 … [관리][삭제] │ │
|
||||
│ └────────────────────────────────────────┘ │ └──────────────────────────────────────────┘ │
|
||||
├──────────────────────────────────────────────┴────────────────────────────────────────────────┤
|
||||
│ [영역 C] 체크리스트 항목 (선택된 점검표: {cklstNm}) │
|
||||
│ [미리보기] [추가] [저장] │
|
||||
│ [영역 C] 체크리스트 항목 [미리보기] [추가] [저장] │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 점검항목 ▼ | 주요 검토내용 | 사용 여부 ○예 ○아니오 | 구분 | 관리 │ │
|
||||
│ │ L1 [요건검토 ▼] | (입력) | ○예 ○아니오 | 1단계| [하위추가]│ │
|
||||
│ │ L2 [요건검토 ▼] | (입력) | ○예 ○아니오 | 2단계| [삭제] │ │
|
||||
│ │ 점검항목 ▼ | 주요 검토내용 | 사용 여부 | 구분 | 관리 │ │
|
||||
│ │ L1 [요건검토 ▼] | (입력) | ○예 ○아니오 | 1단계 | [하위추가]│ │
|
||||
│ │ L2 [요건검토 ▼] | (입력) | ○예 ○아니오 | 2단계 | [삭제] │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────────────────────────┘ │
|
||||
└───────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
@@ -61,7 +60,6 @@ BEGIN
|
||||
// 영역 A: 점검표 행 선택
|
||||
ON 영역 A 행 CLICK DO
|
||||
SET 선택된 점검표 = 행.cklstSn
|
||||
DISPLAY "선택된 점검표: {cklstNm}" (영역 C 헤더)
|
||||
CALL GET /as-checklist-service/api/v1/cklst-rvw?cklstSn={cklstSn}
|
||||
// TBD: 점검표 단위 전체 검토 트리 조회 API — 현재는 항목별 by-artcl 조합 또는 클라이언트 병합
|
||||
DISPLAY 체크리스트 항목 그리드 (영역 C)
|
||||
@@ -174,42 +172,42 @@ END
|
||||
|
||||
### 4-1. 헤더 / 툴바
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ----------- | ----------------------------------------- |
|
||||
| 섹션 타이틀 | 점검표 관리 |
|
||||
| 항목 | 내용 |
|
||||
| ----------- | ------------------------------------------ |
|
||||
| 섹션 타이틀 | 점검표 관리 |
|
||||
| 우측 버튼 | **[점검표 추가]** — 점검표 등록 다이얼로그 |
|
||||
|
||||
### 4-2. 그리드 컬럼
|
||||
|
||||
| 컬럼명 | Field | 타입 | 비고 |
|
||||
| ---------------- | ------------- | ------ | ------------------------------------------------- |
|
||||
| 점검표일련번호 | `cklstSn` | number | PK |
|
||||
| 점검표명 | `cklstNm` | string | |
|
||||
| 컬럼명 | Field | 타입 | 비고 |
|
||||
| ---------------- | ------------- | ------ | ---------------------------------------------------- |
|
||||
| 점검표일련번호 | `cklstSn` | number | PK |
|
||||
| 점검표명 | `cklstNm` | string | |
|
||||
| 문서구분공통코드 | `docSeComCd` | string | 코드명 표시 — `ASDOCSECD` 등 공통코드 (TBD 상위코드) |
|
||||
| 업무구분공통코드 | `taskSeComCd` | string | RE/SF/SP 등 — 공통코드 (TBD 상위코드) |
|
||||
| 사용여부 | `useYn` | string | `Y`→예, `N`→아니오 |
|
||||
| 등록일시 | `regDt` | string | `yyyy-MM-dd HH:mm:ss` |
|
||||
| 관리 | — | — | 행별 **[관리] · [삭제]** |
|
||||
| 업무구분공통코드 | `taskSeComCd` | string | RE/SF/SP 등 — 공통코드 (TBD 상위코드) |
|
||||
| 사용여부 | `useYn` | string | `Y`→예, `N`→아니오 |
|
||||
| 등록일시 | `regDt` | string | `yyyy-MM-dd HH:mm:ss` |
|
||||
| 관리 | — | — | 행별 **[관리] · [삭제]** |
|
||||
|
||||
- 행 클릭 시: `cklstSn` 선택 → 영역 C 헤더·그리드 갱신.
|
||||
- 페이지네이션: Spring `Pageable` (`page`, `size`, `sort`).
|
||||
|
||||
### 4-3. 버튼
|
||||
|
||||
| 버튼명 | 위치 | 동작 |
|
||||
| ---------- | -------------- | ------------------------------------------------- |
|
||||
| 버튼명 | 위치 | 동작 |
|
||||
| ----------- | -------------- | ------------------------------------------------- |
|
||||
| 점검표 추가 | 섹션 우측 상단 | 점검표 추가 다이얼로그 오픈 |
|
||||
| 관리 | 행별 | 점검표 수정 다이얼로그 (기존 값 초기화) |
|
||||
| 삭제 | 행별 | 확인 후 삭제. 선택 중인 점검표이면 영역 C 초기화. |
|
||||
| 관리 | 행별 | 점검표 수정 다이얼로그 (기존 값 초기화) |
|
||||
| 삭제 | 행별 | 확인 후 삭제. 선택 중인 점검표이면 영역 C 초기화. |
|
||||
|
||||
### 4-4. 다이얼로그: 점검표 추가 / 수정
|
||||
|
||||
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
|
||||
| -------- | ---------------- | ---- | ------ | ----------------- | ----------------------------------------- |
|
||||
| 점검표명 | Text | Y | — | `cklstNm` | placeholder: "예: 설치인가 체크리스트" |
|
||||
| 문서구분 | Select(공통코드) | N | — | `docSeComCd` | 예: FNRS, INSTL, PWRK, DESIGN |
|
||||
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
|
||||
| -------- | ---------------- | ---- | ------ | ----------------- | ------------------------------------------------ |
|
||||
| 점검표명 | Text | Y | — | `cklstNm` | placeholder: "예: 설치인가 체크리스트" |
|
||||
| 문서구분 | Select(공통코드) | N | — | `docSeComCd` | 예: FNRS, INSTL, PWRK, DESIGN |
|
||||
| 업무구분 | Select(공통코드) | Y | — | `taskSeComCd` | 예: RE(재이용수), SF(하수처리시설), SP(하수관로) |
|
||||
| 사용여부 | Select | N | Y | `useYn` | Y/N, UI 표시: 예/아니오 |
|
||||
| 사용여부 | Select | N | Y | `useYn` | Y/N, UI 표시: 예/아니오 |
|
||||
|
||||
| 버튼명 | 동작 |
|
||||
| ------ | -------------------------------------- |
|
||||
@@ -225,29 +223,29 @@ END
|
||||
|
||||
### 5-1. 헤더 / 툴바
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ----------- | ------------------------------------------- |
|
||||
| 섹션 타이틀 | 점검항목 관리 |
|
||||
| 항목 | 내용 |
|
||||
| ----------- | ---------------------------------------------- |
|
||||
| 섹션 타이틀 | 점검항목 관리 |
|
||||
| 우측 버튼 | **[점검항목 추가]** — 점검항목 등록 다이얼로그 |
|
||||
|
||||
### 5-2. 그리드 컬럼
|
||||
|
||||
| 컬럼명 | Field | 타입 | 비고 |
|
||||
| ------------ | --------------- | ------ | ---------------------------------------------- |
|
||||
| 점검항목코드 | `cklstArtclSn` | number | UI mock `ITEM_001` 형식 표시 여부 TBD |
|
||||
| 점검항목명 | `cklstArtclNm` | string | |
|
||||
| 사용여부 | `useYn` | string | `Y`→예, `N`→아니오 |
|
||||
| 등록일시 | `regDt` | string | `yyyy-MM-dd HH:mm:ss` |
|
||||
| 관리 | — | — | 행별 **[관리] · [삭제]** |
|
||||
| 컬럼명 | Field | 타입 | 비고 |
|
||||
| ------------ | -------------- | ------ | ------------------------------------- |
|
||||
| 점검항목코드 | `cklstArtclSn` | number | UI mock `ITEM_001` 형식 표시 여부 TBD |
|
||||
| 점검항목명 | `cklstArtclNm` | string | |
|
||||
| 사용여부 | `useYn` | string | `Y`→예, `N`→아니오 |
|
||||
| 등록일시 | `regDt` | string | `yyyy-MM-dd HH:mm:ss` |
|
||||
| 관리 | — | — | 행별 **[관리] · [삭제]** |
|
||||
|
||||
> **TBD**: 백엔드 `CklstArtcl`은 복합 PK(`cklstArtclSn`, `cklstSn`)이며 등록 시 `cklstSn` 필수. UI 다이얼로그에는 `cklstSn` 입력이 없음 — 소속 점검표 지정 방식·전역 마스터 여부 백엔드 정책 확인.
|
||||
|
||||
### 5-3. 다이얼로그: 점검항목 추가 / 수정
|
||||
|
||||
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
|
||||
| ---------- | --------- | ---- | ------ | ----------------- | ----------------------- |
|
||||
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
|
||||
| ---------- | --------- | ---- | ------ | ----------------- | --------------------------- |
|
||||
| 점검항목명 | Text | Y | — | `cklstArtclNm` | placeholder: "예: 법령검토" |
|
||||
| 사용여부 | Select | N | Y | `useYn` | Y/N |
|
||||
| 사용여부 | Select | N | Y | `useYn` | Y/N |
|
||||
|
||||
| 버튼명 | 동작 |
|
||||
| ------ | -------------------------------------- |
|
||||
@@ -270,32 +268,32 @@ END
|
||||
|
||||
### 6-2. 그리드 컬럼
|
||||
|
||||
| 컬럼명 | Field | 입력 UI | 비고 |
|
||||
| -------------- | --------------- | ------------------------------- | ------------------------------------------------- |
|
||||
| 점검항목 | `cklstArtclSn` | Select (영역 B 항목 목록) | L1/L2 접두 표시, `cklstArtclNm` 라벨 |
|
||||
| 주요 검토내용 | `rvwCn` | Text / Textarea | 최대 4000자 |
|
||||
| 사용 여부 | `useYn` | Radio (예/아니오) | `Y` / `N` |
|
||||
| 구분 | `rvwCnLvNo` | 읽기 전용 | 1→1단계, 2→2단계 |
|
||||
| 관리 | — | **[하위추가]** (1단계만) · **[삭제]** | 2단계 행은 [하위추가] 미표시 |
|
||||
| 컬럼명 | Field | 입력 UI | 비고 |
|
||||
| ------------- | -------------- | ------------------------------------- | ------------------------------------ |
|
||||
| 점검항목 | `cklstArtclSn` | Select (영역 B 항목 목록) | L1/L2 접두 표시, `cklstArtclNm` 라벨 |
|
||||
| 주요 검토내용 | `rvwCn` | Text / Textarea | 최대 4000자 |
|
||||
| 사용 여부 | `useYn` | Radio (예/아니오) | `Y` / `N` |
|
||||
| 구분 | `rvwCnLvNo` | 읽기 전용 | 1→1단계, 2→2단계, 3→3단계 |
|
||||
| 관리 | — | **[하위추가]** (1·2단계, 저장 후) · **[삭제]** | 3단계 행은 [하위추가] 미표시 |
|
||||
|
||||
#### 계층 정책
|
||||
|
||||
| 항목 | 내용 |
|
||||
| -------------- | -------------------------------------------------------------------- |
|
||||
| 최대 깊이 | 2단계 (`rvwCnLvNo`: 1, 2) |
|
||||
| 최대 깊이 | 3단계 (`rvwCnLvNo`: 1, 2, 3) |
|
||||
| 상위-하위 연결 | `upCklstRvwSn` — 2단계 행은 부모 1단계 `cklstRvwSn` 참조 |
|
||||
| 정렬 | `sortSeq` — 동일 레벨 내 순서 (TBD: UI 드래그 정렬 여부) |
|
||||
| 체크 사용 | `bchckUseYn` — 미리보기·실사용 화면 체크박스 노출 여부 (TBD UI 반영) |
|
||||
|
||||
### 6-3. 버튼
|
||||
|
||||
| 버튼명 | 동작 |
|
||||
| -------- | ------------------------------------------------------------ |
|
||||
| 추가 | 1단계 검토 행 1건 추가 (편집 가능) |
|
||||
| 저장 | 변경·신규 검토 행 일괄 POST/PUT (일괄 저장 API 없음) |
|
||||
| 미리보기 | 선택 점검표 체크리스트 읽기 전용 미리보기 (TBD) |
|
||||
| 하위추가 | 선택 1단계 행 아래 2단계 자식 행 추가 |
|
||||
| 삭제 | 확인 후 DELETE 또는 미저장 행 로컬 제거 |
|
||||
| 버튼명 | 동작 |
|
||||
| -------- | ---------------------------------------------------- |
|
||||
| 추가 | 1단계 검토 행 1건 추가 (편집 가능) |
|
||||
| 저장 | 변경·신규 검토 행 일괄 POST/PUT (일괄 저장 API 없음) |
|
||||
| 미리보기 | 선택 점검표 체크리스트 읽기 전용 미리보기 (TBD) |
|
||||
| 하위추가 | 선택 1·2단계 행 아래 하위 단계(2·3단계) 자식 행 추가 |
|
||||
| 삭제 | 확인 후 DELETE 또는 미저장 행 로컬 제거 |
|
||||
|
||||
---
|
||||
|
||||
@@ -303,39 +301,38 @@ END
|
||||
|
||||
### 7-1. 공통
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ----------- | -------------------------------------------------------------------- |
|
||||
| 서비스 | `as-checklist-service` |
|
||||
| Base prefix | `/as-checklist-service/api/v1` (게이트웨이 프록시 경유) |
|
||||
| 인증 | JWT (`Authorization` 헤더), `X-Site-Id`, `token-id` |
|
||||
| 페이징 | Spring `Pageable`: `page`, `size`, `sort` → `PageResponse<T>` |
|
||||
| 엔티티 | `@/entities/cklst` (`cklstMngService`, `cklstArtclService`) |
|
||||
| 미구현 엔티티 | `@/entities/cklst-rvw` — **TBD** (영역 C용 `cklst-rvw` API) |
|
||||
| 항목 | 내용 |
|
||||
| ------------- | ------------------------------------------------------------- |
|
||||
| 서비스 | `as-checklist-service` |
|
||||
| Base prefix | `/as-checklist-service/api/v1` (게이트웨이 프록시 경유) |
|
||||
| 인증 | JWT (`Authorization` 헤더), `X-Site-Id`, `token-id` |
|
||||
| 페이징 | Spring `Pageable`: `page`, `size`, `sort` → `PageResponse<T>` |
|
||||
| 엔티티 | `@/entities/cklst` (`cklstMngService`, `cklstArtclService`, `cklstRvwService`) |
|
||||
|
||||
### 7-2. 점검표 (영역 A) — `cklst`
|
||||
|
||||
| 기능 | Method | URL | Request / Query | Response | 클라이언트 |
|
||||
| --------- | ------ | ---------------- | ---------------------------- | ------------------- | -------------------------- |
|
||||
| 목록 조회 | GET | `/cklst` | `CklstMngSearchParams` + Pageable | `Page<CklstMng>` | `cklstMngService.findPage` |
|
||||
| 단건 조회 | GET | `/cklst/{cklstSn}` | — | `CklstMng` | `cklstMngService.findById` |
|
||||
| 등록 | POST | `/cklst` | `CklstMngSaveRequest` | `number` (cklstSn) | `cklstMngService.create` |
|
||||
| 수정 | PUT | `/cklst/{cklstSn}` | `CklstMngSaveRequest` | `number` (cklstSn) | `cklstMngService.update` |
|
||||
| 삭제 | DELETE | `/cklst/{cklstSn}` | — | `number` (cklstSn) | `cklstMngService.remove` |
|
||||
| 기능 | Method | URL | Request / Query | Response | 클라이언트 |
|
||||
| --------- | ------ | ------------------ | --------------------------------- | ------------------ | -------------------------- |
|
||||
| 목록 조회 | GET | `/cklst` | `CklstMngSearchParams` + Pageable | `Page<CklstMng>` | `cklstMngService.findPage` |
|
||||
| 단건 조회 | GET | `/cklst/{cklstSn}` | — | `CklstMng` | `cklstMngService.findById` |
|
||||
| 등록 | POST | `/cklst` | `CklstMngSaveRequest` | `number` (cklstSn) | `cklstMngService.create` |
|
||||
| 수정 | PUT | `/cklst/{cklstSn}` | `CklstMngSaveRequest` | `number` (cklstSn) | `cklstMngService.update` |
|
||||
| 삭제 | DELETE | `/cklst/{cklstSn}` | — | `number` (cklstSn) | `cklstMngService.remove` |
|
||||
|
||||
**CklstMng (응답)**
|
||||
|
||||
| Field | 타입 | 설명 |
|
||||
| ------------- | ------- | ------------------ |
|
||||
| `cklstSn` | number | 점검표일련번호 |
|
||||
| `cklstNm` | string | 점검표명 |
|
||||
| `taskSeComCd` | string | 업무구분공통코드 |
|
||||
| `useYn` | string? | 사용여부 |
|
||||
| `docSeComCd` | string? | 문서구분공통코드 |
|
||||
| `rgtr` | string? | 등록자 UUID |
|
||||
| `rgtrNm` | string? | 등록자명 |
|
||||
| `mdfr` | string? | 수정자 UUID |
|
||||
| `regDt` | string? | 등록일시 |
|
||||
| `mdfcnDt` | string? | 수정일시 |
|
||||
| Field | 타입 | 설명 |
|
||||
| ------------- | ------- | ---------------- |
|
||||
| `cklstSn` | number | 점검표일련번호 |
|
||||
| `cklstNm` | string | 점검표명 |
|
||||
| `taskSeComCd` | string | 업무구분공통코드 |
|
||||
| `useYn` | string? | 사용여부 |
|
||||
| `docSeComCd` | string? | 문서구분공통코드 |
|
||||
| `rgtr` | string? | 등록자 UUID |
|
||||
| `rgtrNm` | string? | 등록자명 |
|
||||
| `mdfr` | string? | 수정자 UUID |
|
||||
| `regDt` | string? | 등록일시 |
|
||||
| `mdfcnDt` | string? | 수정일시 |
|
||||
|
||||
**CklstMngSaveRequest**: `cklstNm`\*, `taskSeComCd`\*, `useYn?`, `docSeComCd?`
|
||||
|
||||
@@ -345,25 +342,25 @@ END
|
||||
|
||||
### 7-3. 점검표항목 (영역 B) — `cklst-artcl`
|
||||
|
||||
| 기능 | Method | URL | Request / Query | Response | 클라이언트 |
|
||||
| ----------------------- | ------ | ---------------------------------------- | --------------------------------- | --------------------- | ------------------------------- |
|
||||
| 목록 조회 (페이징) | GET | `/cklst-artcl` | `CklstArtclSearchParams` + Pageable | `Page<CklstArtcl>` | `cklstArtclService.findPage` |
|
||||
| 점검표별 전체 목록 | GET | `/cklst-artcl/by-cklst/{cklstSn}` | — | `CklstArtcl[]` | `cklstArtclService.findByCklstSn` |
|
||||
| 단건 조회 | GET | `/cklst-artcl/{cklstArtclSn}/{cklstSn}` | — | `CklstArtcl` | `cklstArtclService.findById` |
|
||||
| 등록 | POST | `/cklst-artcl` | `CklstArtclSaveRequest` | `CklstArtclId` | `cklstArtclService.create` |
|
||||
| 수정 | PUT | `/cklst-artcl/{cklstArtclSn}/{cklstSn}` | `CklstArtclUpdateRequest` | `CklstArtclId` | `cklstArtclService.update` |
|
||||
| 삭제 | DELETE | `/cklst-artcl/{cklstArtclSn}/{cklstSn}` | — | `CklstArtclId` | `cklstArtclService.remove` |
|
||||
| 기능 | Method | URL | Request / Query | Response | 클라이언트 |
|
||||
| ------------------ | ------ | --------------------------------------- | ----------------------------------- | ------------------ | --------------------------------- |
|
||||
| 목록 조회 (페이징) | GET | `/cklst-artcl` | `CklstArtclSearchParams` + Pageable | `Page<CklstArtcl>` | `cklstArtclService.findPage` |
|
||||
| 점검표별 전체 목록 | GET | `/cklst-artcl/by-cklst/{cklstSn}` | — | `CklstArtcl[]` | `cklstArtclService.findByCklstSn` |
|
||||
| 단건 조회 | GET | `/cklst-artcl/{cklstArtclSn}/{cklstSn}` | — | `CklstArtcl` | `cklstArtclService.findById` |
|
||||
| 등록 | POST | `/cklst-artcl` | `CklstArtclSaveRequest` | `CklstArtclId` | `cklstArtclService.create` |
|
||||
| 수정 | PUT | `/cklst-artcl/{cklstArtclSn}/{cklstSn}` | `CklstArtclUpdateRequest` | `CklstArtclId` | `cklstArtclService.update` |
|
||||
| 삭제 | DELETE | `/cklst-artcl/{cklstArtclSn}/{cklstSn}` | — | `CklstArtclId` | `cklstArtclService.remove` |
|
||||
|
||||
**CklstArtcl (응답)**
|
||||
|
||||
| Field | 타입 | 설명 |
|
||||
| ---------------- | ------- | -------------------- |
|
||||
| `cklstArtclSn` | number | 점검표항목일련번호 |
|
||||
| `cklstSn` | number | 점검표일련번호 |
|
||||
| `cklstArtclNm` | string? | 점검표항목명 |
|
||||
| `useYn` | string? | 사용여부 |
|
||||
| `rgtr` / `rgtrNm`| string? | 등록자 |
|
||||
| `regDt` | string? | 등록일시 |
|
||||
| Field | 타입 | 설명 |
|
||||
| ----------------- | ------- | ------------------ |
|
||||
| `cklstArtclSn` | number | 점검표항목일련번호 |
|
||||
| `cklstSn` | number | 점검표일련번호 |
|
||||
| `cklstArtclNm` | string? | 점검표항목명 |
|
||||
| `useYn` | string? | 사용여부 |
|
||||
| `rgtr` / `rgtrNm` | string? | 등록자 |
|
||||
| `regDt` | string? | 등록일시 |
|
||||
|
||||
**CklstArtclSaveRequest**: `cklstSn`\*, `cklstArtclNm?`, `useYn?`
|
||||
|
||||
@@ -375,37 +372,39 @@ END
|
||||
|
||||
### 7-4. 점검표검토 (영역 C) — `cklst-rvw`
|
||||
|
||||
> **entities 미구현** — `@/entities/cklst-rvw` 추가 예정. 아래는 백엔드 `CklstRvwController` 기준.
|
||||
|
||||
| 기능 | Method | URL | Request / Query | Response | 클라이언트 (예정) |
|
||||
| ---------------------------- | ------ | -------------------------------------------------- | -------------------------- | ------------------ | ------------------------ |
|
||||
| 목록 조회 (페이징) | GET | `/cklst-rvw` | `CklstRvwSearchDto` + Pageable | `Page<CklstRvw>` | TBD `cklstRvwService.findPage` |
|
||||
| 항목별 검토 전체 목록 | GET | `/cklst-rvw/by-artcl/{cklstSn}/{cklstArtclSn}` | — | `CklstRvw[]` | TBD `findByArtcl` |
|
||||
| 단건 조회 | GET | `/cklst-rvw/{cklstRvwSn}/{cklstArtclSn}/{cklstSn}` | — | `CklstRvw` | TBD `findById` |
|
||||
| 등록 | POST | `/cklst-rvw` | `CklstRvwSaveRequestDto` | `CklstRvwId` | TBD `create` |
|
||||
| 수정 | PUT | `/cklst-rvw/{cklstRvwSn}/{cklstArtclSn}/{cklstSn}` | `CklstRvwUpdateRequestDto` | `CklstRvwId` | TBD `update` |
|
||||
| 삭제 | DELETE | `/cklst-rvw/{cklstRvwSn}/{cklstArtclSn}/{cklstSn}` | — | `CklstRvwId` | TBD `remove` |
|
||||
| 기능 | Method | URL | Request / Query | Response | 클라이언트 |
|
||||
| --------------------- | ------ | -------------------------------------------------- | ------------------------------ | ---------------- | ------------------------------- |
|
||||
| 목록 조회 (페이징) | GET | `/cklst-rvw` | `CklstRvwSearchParams` + Pageable | `Page<CklstRvw>` | `cklstRvwService.findPage` |
|
||||
| 항목별 검토 전체 목록 | GET | `/cklst-rvw/by-artcl/{cklstSn}/{cklstArtclSn}` | — | `CklstRvw[]` | `cklstRvwService.findByArtcl` |
|
||||
| 단건 조회 | GET | `/cklst-rvw/{cklstRvwSn}/{cklstArtclSn}/{cklstSn}` | — | `CklstRvw` | `cklstRvwService.findById` |
|
||||
| 등록 | POST | `/cklst-rvw` | `CklstRvwSaveRequest` | `CklstRvwId` | `cklstRvwService.create` |
|
||||
| 수정 | PUT | `/cklst-rvw/{cklstRvwSn}/{cklstArtclSn}/{cklstSn}` | `CklstRvwUpdateRequest` | `CklstRvwId` | `cklstRvwService.update` |
|
||||
| 삭제 | DELETE | `/cklst-rvw/{cklstRvwSn}/{cklstArtclSn}/{cklstSn}` | — | `CklstRvwId` | `cklstRvwService.remove` |
|
||||
|
||||
**CklstRvw (응답)**
|
||||
|
||||
| Field | 타입 | 설명 |
|
||||
| --------------- | ------- | ---------------------- |
|
||||
| `cklstRvwSn` | number | 점검표검토일련번호 |
|
||||
| `cklstArtclSn` | number | 점검표항목일련번호 |
|
||||
| `cklstSn` | number | 점검표일련번호 |
|
||||
| `upCklstRvwSn` | number? | 상위점검표검토일련번호 |
|
||||
| `rvwCnLvNo` | number? | 검토내용레벨번호 |
|
||||
| `rvwCn` | string? | 검토내용 |
|
||||
| `bchckUseYn` | string? | 체크사용여부 |
|
||||
| `sortSeq` | number? | 정렬순서 |
|
||||
| `useYn` | string? | 사용여부 |
|
||||
| `regDt` | string? | 등록일시 |
|
||||
| Field | 타입 | 설명 |
|
||||
| -------------- | ------- | ---------------------- |
|
||||
| `cklstRvwSn` | number | 점검표검토일련번호 |
|
||||
| `cklstArtclSn` | number | 점검표항목일련번호 |
|
||||
| `cklstSn` | number | 점검표일련번호 |
|
||||
| `upCklstRvwSn` | number? | 상위점검표검토일련번호 |
|
||||
| `rvwCnLvNo` | number? | 검토내용레벨번호 |
|
||||
| `rvwCn` | string? | 검토내용 |
|
||||
| `bchckUseYn` | string? | 체크사용여부 |
|
||||
| `sortSeq` | number? | 정렬순서 |
|
||||
| `useYn` | string? | 사용여부 |
|
||||
| `regDt` | string? | 등록일시 |
|
||||
|
||||
**CklstRvwSaveRequestDto**: `cklstArtclSn`\*, `cklstSn`\*, `upCklstRvwSn?`, `rvwCnLvNo?`, `rvwCn?`, `bchckUseYn?`, `sortSeq?`, `useYn?`
|
||||
**CklstRvwSaveRequest**: `cklstArtclSn`\*, `cklstSn`\*, `upCklstRvwSn?`, `rvwCnLvNo?`, `rvwCn?`, `bchckUseYn?`, `sortSeq?`, `useYn?`
|
||||
|
||||
**CklstRvwUpdateRequestDto**: `upCklstRvwSn?`, `rvwCnLvNo?`, `rvwCn?`, `bchckUseYn?`, `sortSeq?`, `useYn?`
|
||||
**CklstRvwUpdateRequest**: `upCklstRvwSn?`, `rvwCnLvNo?`, `rvwCn?`, `bchckUseYn?`, `sortSeq?`, `useYn?`
|
||||
|
||||
**CklstRvwSearchDto**: `cklstSn?`, `cklstArtclSn?`, `useYn?` + Pageable
|
||||
**CklstRvwSearchParams**: `cklstSn?`, `cklstArtclSn?`, `useYn?` + `page`, `size`, `sort`
|
||||
|
||||
**CklstRvwId**: `{ cklstRvwSn: number; cklstArtclSn: number; cklstSn: number }`
|
||||
|
||||
**React Query hooks**: `useCklstRvwPage`, `useCklstRvwByArtcl`, `useCklstRvw`, `useCreateCklstRvw`, `useUpdateCklstRvw`, `useDeleteCklstRvw`
|
||||
|
||||
### 7-5. 공통코드 (보조)
|
||||
|
||||
@@ -425,49 +424,50 @@ END
|
||||
|
||||
- **공통 컴포넌트**: `@common/components` (`Dialog`, `Button`, `Input`, `Select`, `RadioGroup` 등)
|
||||
- **테이블**: `@tanstack/react-table` + `useDataTable`
|
||||
- **데이터**: React Query — `@/entities/cklst` hooks (검토 API는 entities 추가 후 연동)
|
||||
- **데이터**: React Query — `@/entities/cklst` hooks (`useCklstRvwPage` 등 영역 C 포함)
|
||||
- **공통코드**: `@/entities/code` — `CodeSelect`, `findCodeLabel`
|
||||
- **상태**: 선택된 `cklstSn`, 영역 C 검토 행 배열(신규·dirty·계층 트리)
|
||||
- **모의 데이터**: API 연동 전 `@faker-js/faker`
|
||||
|
||||
### 8-2. 모듈·파일 배치(제안)
|
||||
|
||||
| 구분 | 경로 |
|
||||
| ------ | ----------------------------------------------------------------------------------------- |
|
||||
| 라우트 | `apps/as/app/(with-base-layout)/checklist-management/page.tsx` |
|
||||
| 페이지 | `apps/as/src/pages/checklist-management/` (Public API: page만 export) |
|
||||
| 구분 | 경로 |
|
||||
| ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 라우트 | `apps/as/app/(with-base-layout)/checklist-management/page.tsx` |
|
||||
| 페이지 | `apps/as/src/pages/checklist-management/` (Public API: page만 export) |
|
||||
| UI | `.../ui/cklst-mng-list.tsx`, `cklst-mng-form-dialog.tsx`, `cklst-artcl-list.tsx`, `cklst-artcl-form-dialog.tsx`, `cklst-rvw-grid.tsx`, `cklst-preview-dialog.tsx` |
|
||||
| model | `.../model/cklst-mng-columns.tsx`, `cklst-artcl-columns.tsx`, `cklst-rvw-row-types.ts` |
|
||||
| lib | `.../lib/use-cklst-rvw-rows.ts`, `validators.ts`, `build-rvw-tree.ts` |
|
||||
| 엔티티 | `apps/as/src/entities/cklst/` (구현됨), `entities/cklst-rvw/` (TBD) |
|
||||
| 문서 | `apps/as/docs/checklist-management/SPEC.md` |
|
||||
| model | `.../model/cklst-mng-columns.tsx`, `cklst-artcl-columns.tsx`, `cklst-rvw-row-types.ts` |
|
||||
| lib | `.../lib/use-cklst-rvw-rows.ts`, `validators.ts`, `build-rvw-tree.ts` |
|
||||
| 엔티티 | `apps/as/src/entities/cklst/` (`cklstMng`, `cklstArtcl`, `cklstRvw` 서비스·hooks) |
|
||||
| 페이지 | `apps/as/src/pages/checklist-management/` — `/checklist-management` |
|
||||
| 문서 | `apps/as/docs/checklist-management/SPEC.md` |
|
||||
|
||||
### 8-3. 영역별 UI 매핑
|
||||
|
||||
| 영역 | 주요 UI |
|
||||
| ---- | ----------------------------------------------------------------------- |
|
||||
| A | DataTable + 행 선택 + 점검표 추가/수정 Dialog |
|
||||
| B | DataTable + 점검항목 추가/수정 Dialog |
|
||||
| 영역 | 주요 UI |
|
||||
| ---- | ---------------------------------------------------------------------------- |
|
||||
| A | DataTable + 행 선택 + 점검표 추가/수정 Dialog |
|
||||
| B | DataTable + 점검항목 추가/수정 Dialog |
|
||||
| C | 계층형 편집 그리드(1·2단계) + [추가][저장][미리보기] + 행별 [하위추가][삭제] |
|
||||
| 공통 | `CodeSelect` (문서구분·업무구분), 사용여부 Y/N Select/Radio |
|
||||
| 공통 | `CodeSelect` (문서구분·업무구분), 사용여부 Y/N Select/Radio |
|
||||
|
||||
### 8-4. 다이얼로그·라우팅
|
||||
|
||||
| 다이얼로그명 | 트리거 | API 연동 |
|
||||
| ------------------ | ----------------- | ------------------------------------- |
|
||||
| 점검표 추가 | [점검표 추가] | `useCreateCklstMng` |
|
||||
| 점검표 수정 | 행 [관리] | `useUpdateCklstMng` |
|
||||
| 점검표 삭제 확인 | 행 [삭제] | `useDeleteCklstMng` |
|
||||
| 점검항목 추가 | [점검항목 추가] | `useCreateCklstArtcl` |
|
||||
| 점검항목 수정 | 행 [관리] | `useUpdateCklstArtcl` |
|
||||
| 체크리스트 미리보기 | [미리보기] | TBD — 읽기 전용 트리 렌더 |
|
||||
| 다이얼로그명 | 트리거 | API 연동 |
|
||||
| ------------------- | --------------- | ------------------------- |
|
||||
| 점검표 추가 | [점검표 추가] | `useCreateCklstMng` |
|
||||
| 점검표 수정 | 행 [관리] | `useUpdateCklstMng` |
|
||||
| 점검표 삭제 확인 | 행 [삭제] | `useDeleteCklstMng` |
|
||||
| 점검항목 추가 | [점검항목 추가] | `useCreateCklstArtcl` |
|
||||
| 점검항목 수정 | 행 [관리] | `useUpdateCklstArtcl` |
|
||||
| 체크리스트 미리보기 | [미리보기] | TBD — 읽기 전용 트리 렌더 |
|
||||
|
||||
라우트: `/checklist-management` (메뉴 URL은 CMS 등록값과 일치시킴)
|
||||
|
||||
### 8-5. 데이터 흐름(요약)
|
||||
|
||||
- **진입**: `useCklstMngPage` → 영역 A, `useCklstArtclPage` → 영역 B. C 비활성.
|
||||
- **점검표 선택**: `cklstSn` 설정 → 영역 C 활성 → `cklst-rvw` 목록 조회(TBD) → 트리 그리드 표시.
|
||||
- **점검표 선택**: `cklstSn` 설정 → 영역 C 활성 → `useCklstRvwPage({ cklstSn })` → 트리 그리드 표시.
|
||||
- **점검표 CRUD**: Dialog → `useCreateCklstMng` / `useUpdateCklstMng` / `useDeleteCklstMng`.
|
||||
- **점검항목 CRUD**: Dialog → `useCreateCklstArtcl` / `useUpdateCklstArtcl` / `useDeleteCklstArtcl`.
|
||||
- **검토 저장**: [저장] → 신규 POST / 기존 PUT 순차 호출 (일괄 API 없음).
|
||||
@@ -478,7 +478,8 @@ END
|
||||
- [ ] 영역 B 점검항목 등록 시 `cklstSn` 필수 vs UI 무소속 마스터 — 백엔드·기획 확인
|
||||
- [ ] `점검항목코드`(ITEM_001) 표시 형식 vs `cklstArtclSn` 숫자 PK
|
||||
- [ ] 영역 C 전체 검토 트리 조회: `cklstSn` 단위 일괄 API 부재 — `by-artcl` 다건 호출 또는 백엔드 확장
|
||||
- [ ] `@/entities/cklst-rvw` entities·hooks 구현
|
||||
- [x] `@/entities/cklst` — `cklstRvwService`·hooks 구현
|
||||
- [x] `pages/checklist-management` — 영역 A·B·C UI 구현 (`/checklist-management`)
|
||||
- [ ] 문서구분·업무구분 공통코드 상위코드 ID (`ASDOCSECD` 등)
|
||||
- [ ] [미리보기] 다이얼로그 UI·데이터 소스
|
||||
- [ ] `bchckUseYn` UI 반영 여부
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { PageResponse } from "@/shared/model";
|
||||
|
||||
import type {
|
||||
CklstRvw,
|
||||
CklstRvwId,
|
||||
CklstRvwSaveRequest,
|
||||
CklstRvwSearchParams,
|
||||
CklstRvwService,
|
||||
CklstRvwUpdateRequest,
|
||||
} from "../model/cklst-rvw-interface";
|
||||
|
||||
async function findPage(params: CklstRvwSearchParams): Promise<PageResponse<CklstRvw>> {
|
||||
const response = await client.get<PageResponse<CklstRvw>>(
|
||||
ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_RVW,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function findByArtcl(cklstSn: number, cklstArtclSn: number): Promise<CklstRvw[]> {
|
||||
const response = await client.get<CklstRvw[]>(
|
||||
`${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_RVW}/by-artcl/${cklstSn}/${cklstArtclSn}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function findById(
|
||||
cklstRvwSn: number,
|
||||
cklstArtclSn: number,
|
||||
cklstSn: number,
|
||||
): Promise<CklstRvw> {
|
||||
const response = await client.get<CklstRvw>(
|
||||
`${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_RVW}/${cklstRvwSn}/${cklstArtclSn}/${cklstSn}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function create(body: CklstRvwSaveRequest): Promise<CklstRvwId> {
|
||||
const response = await client.post<CklstRvwId>(
|
||||
ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_RVW,
|
||||
body,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function update(
|
||||
cklstRvwSn: number,
|
||||
cklstArtclSn: number,
|
||||
cklstSn: number,
|
||||
body: CklstRvwUpdateRequest,
|
||||
): Promise<CklstRvwId> {
|
||||
const response = await client.put<CklstRvwId>(
|
||||
`${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_RVW}/${cklstRvwSn}/${cklstArtclSn}/${cklstSn}`,
|
||||
body,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function remove(
|
||||
cklstRvwSn: number,
|
||||
cklstArtclSn: number,
|
||||
cklstSn: number,
|
||||
): Promise<CklstRvwId> {
|
||||
const response = await client.delete<CklstRvwId>(
|
||||
`${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_RVW}/${cklstRvwSn}/${cklstArtclSn}/${cklstSn}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const cklstRvwService = {
|
||||
findPage,
|
||||
findByArtcl,
|
||||
findById,
|
||||
create,
|
||||
update,
|
||||
remove,
|
||||
} satisfies CklstRvwService;
|
||||
@@ -8,12 +8,19 @@ import type {
|
||||
CklstArtclUpdateRequest,
|
||||
} from "../model/cklst-artcl-interface";
|
||||
import type { CklstMngSaveRequest, CklstMngSearchParams } from "../model/cklst-mng-interface";
|
||||
import type {
|
||||
CklstRvwSaveRequest,
|
||||
CklstRvwSearchParams,
|
||||
CklstRvwUpdateRequest,
|
||||
} from "../model/cklst-rvw-interface";
|
||||
|
||||
import { cklstArtclService } from "./cklst-artcl-service";
|
||||
import { cklstMngService } from "./cklst-mng-service";
|
||||
import { cklstRvwService } from "./cklst-rvw-service";
|
||||
|
||||
const cklstMngBaseKey = [ENDPOINT.AS_CHECKLIST_SERVICE.CKLST] as const;
|
||||
const cklstArtclBaseKey = [ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_ARTCL] as const;
|
||||
const cklstRvwBaseKey = [ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_RVW] as const;
|
||||
|
||||
export function useCklstMngPage(params: CklstMngSearchParams) {
|
||||
return useQuery({
|
||||
@@ -165,3 +172,110 @@ export function useDeleteCklstArtcl() {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useCklstRvwPage(params: CklstRvwSearchParams) {
|
||||
const { cklstSn } = params;
|
||||
|
||||
return useQuery({
|
||||
queryKey: [...cklstRvwBaseKey, "page", params],
|
||||
queryFn: () => cklstRvwService.findPage(params),
|
||||
enabled: cklstSn != null,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCklstRvwByArtcl(
|
||||
cklstSn: number | null | undefined,
|
||||
cklstArtclSn: number | null | undefined,
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: [...cklstRvwBaseKey, "by-artcl", cklstSn, cklstArtclSn],
|
||||
queryFn: () => cklstRvwService.findByArtcl(cklstSn!, cklstArtclSn!),
|
||||
enabled: cklstSn != null && cklstArtclSn != null,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCklstRvw(params: {
|
||||
cklstRvwSn: number | null | undefined;
|
||||
cklstArtclSn: number | null | undefined;
|
||||
cklstSn: number | null | undefined;
|
||||
}) {
|
||||
const { cklstRvwSn, cklstArtclSn, cklstSn } = params;
|
||||
|
||||
return useQuery({
|
||||
queryKey: [...cklstRvwBaseKey, cklstRvwSn, cklstArtclSn, cklstSn],
|
||||
queryFn: () => cklstRvwService.findById(cklstRvwSn!, cklstArtclSn!, cklstSn!),
|
||||
enabled: cklstRvwSn != null && cklstArtclSn != null && cklstSn != null,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateCklstRvw() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (body: CklstRvwSaveRequest) => cklstRvwService.create(body),
|
||||
onSuccess: (_data, { cklstSn, cklstArtclSn }) => {
|
||||
queryClient.invalidateQueries({ queryKey: cklstRvwBaseKey });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...cklstRvwBaseKey, "page", { cklstSn }],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...cklstRvwBaseKey, "by-artcl", cklstSn, cklstArtclSn],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateCklstRvw() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
cklstRvwSn,
|
||||
cklstArtclSn,
|
||||
cklstSn,
|
||||
body,
|
||||
}: {
|
||||
cklstRvwSn: number;
|
||||
cklstArtclSn: number;
|
||||
cklstSn: number;
|
||||
body: CklstRvwUpdateRequest;
|
||||
}) => cklstRvwService.update(cklstRvwSn, cklstArtclSn, cklstSn, body),
|
||||
onSuccess: (_data, { cklstRvwSn, cklstArtclSn, cklstSn }) => {
|
||||
queryClient.invalidateQueries({ queryKey: cklstRvwBaseKey });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...cklstRvwBaseKey, cklstRvwSn, cklstArtclSn, cklstSn],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...cklstRvwBaseKey, "page", { cklstSn }],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...cklstRvwBaseKey, "by-artcl", cklstSn, cklstArtclSn],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteCklstRvw() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
cklstRvwSn,
|
||||
cklstArtclSn,
|
||||
cklstSn,
|
||||
}: {
|
||||
cklstRvwSn: number;
|
||||
cklstArtclSn: number;
|
||||
cklstSn: number;
|
||||
}) => cklstRvwService.remove(cklstRvwSn, cklstArtclSn, cklstSn),
|
||||
onSuccess: (_data, { cklstSn, cklstArtclSn }) => {
|
||||
queryClient.invalidateQueries({ queryKey: cklstRvwBaseKey });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...cklstRvwBaseKey, "page", { cklstSn }],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...cklstRvwBaseKey, "by-artcl", cklstSn, cklstArtclSn],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
export { cklstArtclService } from "./api/cklst-artcl-service";
|
||||
export { cklstMngService } from "./api/cklst-mng-service";
|
||||
export { cklstRvwService } from "./api/cklst-rvw-service";
|
||||
export {
|
||||
useCklstArtcl,
|
||||
useCklstArtclList,
|
||||
useCklstArtclPage,
|
||||
useCklstMng,
|
||||
useCklstMngPage,
|
||||
useCklstRvw,
|
||||
useCklstRvwByArtcl,
|
||||
useCklstRvwPage,
|
||||
useCreateCklstArtcl,
|
||||
useCreateCklstMng,
|
||||
useCreateCklstRvw,
|
||||
useDeleteCklstArtcl,
|
||||
useDeleteCklstMng,
|
||||
useDeleteCklstRvw,
|
||||
useUpdateCklstArtcl,
|
||||
useUpdateCklstMng,
|
||||
useUpdateCklstRvw,
|
||||
} from "./api/queries";
|
||||
export type {
|
||||
CklstArtcl,
|
||||
@@ -27,3 +34,11 @@ export type {
|
||||
CklstMngSearchParams,
|
||||
CklstMngService,
|
||||
} from "./model/cklst-mng-interface";
|
||||
export type {
|
||||
CklstRvw,
|
||||
CklstRvwId,
|
||||
CklstRvwSaveRequest,
|
||||
CklstRvwSearchParams,
|
||||
CklstRvwService,
|
||||
CklstRvwUpdateRequest,
|
||||
} from "./model/cklst-rvw-interface";
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { PageableParams, PageResponse } from "@/shared/model";
|
||||
|
||||
export interface CklstRvwId {
|
||||
cklstRvwSn: number;
|
||||
cklstArtclSn: number;
|
||||
cklstSn: number;
|
||||
}
|
||||
|
||||
export interface CklstRvw {
|
||||
cklstRvwSn: number;
|
||||
cklstArtclSn: number;
|
||||
cklstSn: number;
|
||||
upCklstRvwSn?: number;
|
||||
rvwCnLvNo?: number;
|
||||
rvwCn?: string;
|
||||
bchckUseYn?: "Y" | "N";
|
||||
sortSeq?: number;
|
||||
useYn?: "Y" | "N";
|
||||
rgtr?: string;
|
||||
rgtrNm?: string;
|
||||
mdfr?: string;
|
||||
regDt?: string;
|
||||
mdfcnDt?: string;
|
||||
}
|
||||
|
||||
export interface CklstRvwSearchParams extends PageableParams {
|
||||
cklstSn?: number;
|
||||
cklstArtclSn?: number;
|
||||
useYn?: "Y" | "N";
|
||||
}
|
||||
|
||||
export interface CklstRvwSaveRequest {
|
||||
cklstArtclSn: number;
|
||||
cklstSn: number;
|
||||
upCklstRvwSn?: number;
|
||||
rvwCnLvNo?: number;
|
||||
rvwCn?: string;
|
||||
bchckUseYn?: "Y" | "N";
|
||||
sortSeq?: number;
|
||||
useYn?: "Y" | "N";
|
||||
}
|
||||
|
||||
export interface CklstRvwUpdateRequest {
|
||||
upCklstRvwSn?: number;
|
||||
rvwCnLvNo?: number;
|
||||
rvwCn?: string;
|
||||
bchckUseYn?: "Y" | "N";
|
||||
sortSeq?: number;
|
||||
useYn?: "Y" | "N";
|
||||
}
|
||||
|
||||
export interface CklstRvwService {
|
||||
findPage(params: CklstRvwSearchParams): Promise<PageResponse<CklstRvw>>;
|
||||
findByArtcl(cklstSn: number, cklstArtclSn: number): Promise<CklstRvw[]>;
|
||||
findById(cklstRvwSn: number, cklstArtclSn: number, cklstSn: number): Promise<CklstRvw>;
|
||||
create(body: CklstRvwSaveRequest): Promise<CklstRvwId>;
|
||||
update(
|
||||
cklstRvwSn: number,
|
||||
cklstArtclSn: number,
|
||||
cklstSn: number,
|
||||
body: CklstRvwUpdateRequest,
|
||||
): Promise<CklstRvwId>;
|
||||
remove(cklstRvwSn: number, cklstArtclSn: number, cklstSn: number): Promise<CklstRvwId>;
|
||||
}
|
||||
@@ -1,48 +1,48 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { PageResponse } from "@/shared/model";
|
||||
|
||||
import type {
|
||||
ElaprApln,
|
||||
ElaprAplnListParams,
|
||||
ElaprAplnSaveRequest,
|
||||
ElaprAplnService,
|
||||
ElaprAplnUpdateRequest,
|
||||
} from "../model/elapr-apln-interface";
|
||||
|
||||
async function findPage(params: ElaprAplnListParams): Promise<PageResponse<ElaprApln>> {
|
||||
const response = await client.get<PageResponse<ElaprApln>>(ENDPOINT.AS_ELAPR_SERVICE.ELAPR_APLN, {
|
||||
params,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function findById(aplnId: string): Promise<ElaprApln> {
|
||||
const response = await client.get<ElaprApln>(`${ENDPOINT.AS_ELAPR_SERVICE.ELAPR_APLN}/${aplnId}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function create(body: ElaprAplnSaveRequest): Promise<ElaprApln> {
|
||||
const response = await client.post<ElaprApln>(ENDPOINT.AS_ELAPR_SERVICE.ELAPR_APLN, body);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function update(aplnId: string, body: ElaprAplnUpdateRequest): Promise<ElaprApln> {
|
||||
const response = await client.put<ElaprApln>(
|
||||
`${ENDPOINT.AS_ELAPR_SERVICE.ELAPR_APLN}/${aplnId}`,
|
||||
body,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function remove(aplnId: string): Promise<void> {
|
||||
await client.delete(`${ENDPOINT.AS_ELAPR_SERVICE.ELAPR_APLN}/${aplnId}`);
|
||||
}
|
||||
|
||||
export const elaprAplnService = {
|
||||
findPage,
|
||||
findById,
|
||||
create,
|
||||
update,
|
||||
remove,
|
||||
} satisfies ElaprAplnService;
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { PageResponse } from "@/shared/model";
|
||||
|
||||
import type {
|
||||
ElaprApln,
|
||||
ElaprAplnListParams,
|
||||
ElaprAplnSaveRequest,
|
||||
ElaprAplnService,
|
||||
ElaprAplnUpdateRequest,
|
||||
} from "../model/elapr-apln-interface";
|
||||
|
||||
async function findPage(params: ElaprAplnListParams): Promise<PageResponse<ElaprApln>> {
|
||||
const response = await client.get<PageResponse<ElaprApln>>(ENDPOINT.AS_ELAPR_SERVICE.ELAPR_APLN, {
|
||||
params,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function findById(aplnId: string): Promise<ElaprApln> {
|
||||
const response = await client.get<ElaprApln>(`${ENDPOINT.AS_ELAPR_SERVICE.ELAPR_APLN}/${aplnId}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function create(body: ElaprAplnSaveRequest): Promise<ElaprApln> {
|
||||
const response = await client.post<ElaprApln>(ENDPOINT.AS_ELAPR_SERVICE.ELAPR_APLN, body);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function update(aplnId: string, body: ElaprAplnUpdateRequest): Promise<ElaprApln> {
|
||||
const response = await client.put<ElaprApln>(
|
||||
`${ENDPOINT.AS_ELAPR_SERVICE.ELAPR_APLN}/${aplnId}`,
|
||||
body,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function remove(aplnId: string): Promise<void> {
|
||||
await client.delete(`${ENDPOINT.AS_ELAPR_SERVICE.ELAPR_APLN}/${aplnId}`);
|
||||
}
|
||||
|
||||
export const elaprAplnService = {
|
||||
findPage,
|
||||
findById,
|
||||
create,
|
||||
update,
|
||||
remove,
|
||||
} satisfies ElaprAplnService;
|
||||
|
||||
@@ -1,51 +1,51 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { PageResponse } from "@/shared/model";
|
||||
|
||||
import type {
|
||||
ElaprTmplt,
|
||||
ElaprTmpltSaveRequest,
|
||||
ElaprTmpltSearchParams,
|
||||
ElaprTmpltService,
|
||||
ElaprTmpltUpdateRequest,
|
||||
} from "../model/elapr-tmplt-interface";
|
||||
|
||||
async function findPage(params: ElaprTmpltSearchParams): Promise<PageResponse<ElaprTmplt>> {
|
||||
const response = await client.get<PageResponse<ElaprTmplt>>(
|
||||
ENDPOINT.AS_ELAPR_SERVICE.ELAPR_TMPLT,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function findById(elaprTmpltId: string): Promise<ElaprTmplt> {
|
||||
const response = await client.get<ElaprTmplt>(
|
||||
`${ENDPOINT.AS_ELAPR_SERVICE.ELAPR_TMPLT}/${elaprTmpltId}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function create(body: ElaprTmpltSaveRequest): Promise<ElaprTmplt> {
|
||||
const response = await client.post<ElaprTmplt>(ENDPOINT.AS_ELAPR_SERVICE.ELAPR_TMPLT, body);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function update(elaprTmpltId: string, body: ElaprTmpltUpdateRequest): Promise<ElaprTmplt> {
|
||||
const response = await client.put<ElaprTmplt>(
|
||||
`${ENDPOINT.AS_ELAPR_SERVICE.ELAPR_TMPLT}/${elaprTmpltId}`,
|
||||
body,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function remove(elaprTmpltId: string): Promise<void> {
|
||||
await client.delete(`${ENDPOINT.AS_ELAPR_SERVICE.ELAPR_TMPLT}/${elaprTmpltId}`);
|
||||
}
|
||||
|
||||
export const elaprTmpltService = {
|
||||
findPage,
|
||||
findById,
|
||||
create,
|
||||
update,
|
||||
remove,
|
||||
} satisfies ElaprTmpltService;
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { PageResponse } from "@/shared/model";
|
||||
|
||||
import type {
|
||||
ElaprTmplt,
|
||||
ElaprTmpltSaveRequest,
|
||||
ElaprTmpltSearchParams,
|
||||
ElaprTmpltService,
|
||||
ElaprTmpltUpdateRequest,
|
||||
} from "../model/elapr-tmplt-interface";
|
||||
|
||||
async function findPage(params: ElaprTmpltSearchParams): Promise<PageResponse<ElaprTmplt>> {
|
||||
const response = await client.get<PageResponse<ElaprTmplt>>(
|
||||
ENDPOINT.AS_ELAPR_SERVICE.ELAPR_TMPLT,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function findById(elaprTmpltId: string): Promise<ElaprTmplt> {
|
||||
const response = await client.get<ElaprTmplt>(
|
||||
`${ENDPOINT.AS_ELAPR_SERVICE.ELAPR_TMPLT}/${elaprTmpltId}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function create(body: ElaprTmpltSaveRequest): Promise<ElaprTmplt> {
|
||||
const response = await client.post<ElaprTmplt>(ENDPOINT.AS_ELAPR_SERVICE.ELAPR_TMPLT, body);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function update(elaprTmpltId: string, body: ElaprTmpltUpdateRequest): Promise<ElaprTmplt> {
|
||||
const response = await client.put<ElaprTmplt>(
|
||||
`${ENDPOINT.AS_ELAPR_SERVICE.ELAPR_TMPLT}/${elaprTmpltId}`,
|
||||
body,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function remove(elaprTmpltId: string): Promise<void> {
|
||||
await client.delete(`${ENDPOINT.AS_ELAPR_SERVICE.ELAPR_TMPLT}/${elaprTmpltId}`);
|
||||
}
|
||||
|
||||
export const elaprTmpltService = {
|
||||
findPage,
|
||||
findById,
|
||||
create,
|
||||
update,
|
||||
remove,
|
||||
} satisfies ElaprTmpltService;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { ChecklistManagementPage } from "./ui/checklist-management-page";
|
||||
@@ -0,0 +1,90 @@
|
||||
import type { CklstRvwRow } from "./cklst-rvw-row-types";
|
||||
|
||||
export const MAX_RVW_CN_LV_NO = 3;
|
||||
|
||||
export interface CklstRvwDisplayRow {
|
||||
row: CklstRvwRow;
|
||||
depth: number;
|
||||
}
|
||||
|
||||
export function formatRvwCnLvNo(lvNo?: number): string {
|
||||
if (lvNo === 3) return "3단계";
|
||||
if (lvNo === 2) return "2단계";
|
||||
return "1단계";
|
||||
}
|
||||
|
||||
export function canAddChildRow(lvNo?: number): boolean {
|
||||
return (lvNo ?? 1) < MAX_RVW_CN_LV_NO;
|
||||
}
|
||||
|
||||
const TREE_BRANCH_ENTITY = {
|
||||
horizontal: "─",
|
||||
vertical: "│",
|
||||
branch: "├",
|
||||
} as const;
|
||||
|
||||
const TREE_BRANCH_ENTITY_CHARS: Record<string, string> = {
|
||||
[TREE_BRANCH_ENTITY.horizontal]: "\u2500",
|
||||
[TREE_BRANCH_ENTITY.vertical]: "\u2502",
|
||||
[TREE_BRANCH_ENTITY.branch]: "\u251C",
|
||||
};
|
||||
|
||||
function decodeTreeBranchEntityCodes(value: string): string {
|
||||
return Object.entries(TREE_BRANCH_ENTITY_CHARS).reduce(
|
||||
(text, [entity, char]) => text.replaceAll(entity, char),
|
||||
value,
|
||||
);
|
||||
}
|
||||
|
||||
export function formatTreeBranchPrefix(depth: number): string {
|
||||
if (depth === 1)
|
||||
return decodeTreeBranchEntityCodes(
|
||||
`${TREE_BRANCH_ENTITY.branch}${TREE_BRANCH_ENTITY.horizontal}`,
|
||||
);
|
||||
if (depth === 2) {
|
||||
return decodeTreeBranchEntityCodes(
|
||||
`${TREE_BRANCH_ENTITY.vertical} ${TREE_BRANCH_ENTITY.branch}${TREE_BRANCH_ENTITY.horizontal}`,
|
||||
);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function compareSortSeq(a: CklstRvwRow, b: CklstRvwRow): number {
|
||||
return (a.sortSeq ?? 0) - (b.sortSeq ?? 0);
|
||||
}
|
||||
|
||||
export function buildRvwDisplayRows(rows: CklstRvwRow[]): CklstRvwDisplayRow[] {
|
||||
const result: CklstRvwDisplayRow[] = [];
|
||||
|
||||
const roots = rows
|
||||
.filter((row) => (row.rvwCnLvNo ?? 1) === 1 && row.upCklstRvwSn == null)
|
||||
.sort(compareSortSeq);
|
||||
|
||||
function appendSubtree(parent: CklstRvwRow, depth: number) {
|
||||
const children = rows
|
||||
.filter((row) => row.upCklstRvwSn != null && row.upCklstRvwSn === parent.cklstRvwSn)
|
||||
.sort(compareSortSeq);
|
||||
|
||||
for (const child of children) {
|
||||
result.push({ row: child, depth });
|
||||
if (depth < MAX_RVW_CN_LV_NO - 1) {
|
||||
appendSubtree(child, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const root of roots) {
|
||||
result.push({ row: root, depth: 0 });
|
||||
appendSubtree(root, 1);
|
||||
}
|
||||
|
||||
const orphans = rows.filter(
|
||||
(row) => !result.some((display) => display.row.rowKey === row.rowKey),
|
||||
);
|
||||
|
||||
for (const orphan of orphans.sort(compareSortSeq)) {
|
||||
result.push({ row: orphan, depth: Math.max(0, (orphan.rvwCnLvNo ?? 1) - 1) });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
|
||||
import type { CklstArtcl } from "@/entities/cklst";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
const columnHelper = createColumnHelper<CklstArtcl>();
|
||||
|
||||
interface CreateCklstArtclColumnsOptions {
|
||||
onEdit: (row: CklstArtcl) => void;
|
||||
onDelete: (row: CklstArtcl) => void;
|
||||
}
|
||||
|
||||
export function createCklstArtclColumns({ onEdit, onDelete }: CreateCklstArtclColumnsOptions) {
|
||||
return [
|
||||
columnHelper.accessor("cklstArtclSn", {
|
||||
header: "점검항목코드",
|
||||
meta: { align: "center" },
|
||||
}),
|
||||
columnHelper.accessor("cklstArtclNm", {
|
||||
header: "점검항목명",
|
||||
cell: ({ getValue }) => getValue() ?? "—",
|
||||
}),
|
||||
columnHelper.accessor("useYn", {
|
||||
header: "사용여부",
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue();
|
||||
return value === "Y" ? "예" : value === "N" ? "아니오" : "—";
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("regDt", {
|
||||
header: "등록일시",
|
||||
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>
|
||||
<Button type="button" size="sm" color="red" onClick={() => onDelete(row.original)}>
|
||||
삭제
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
}),
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
"use client";
|
||||
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
|
||||
import type { CklstMng } from "@/entities/cklst";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
type CklstMngRow = CklstMng & {
|
||||
docSeComNm?: string;
|
||||
taskSeCdNm?: string;
|
||||
};
|
||||
|
||||
const columnHelper = createColumnHelper<CklstMngRow>();
|
||||
|
||||
interface CreateCklstMngColumnsOptions {
|
||||
onEdit: (row: CklstMngRow) => void;
|
||||
onDelete: (row: CklstMngRow) => void;
|
||||
}
|
||||
|
||||
export function createCklstMngColumns({ onEdit, onDelete }: CreateCklstMngColumnsOptions) {
|
||||
return [
|
||||
columnHelper.accessor("cklstSn", {
|
||||
header: "점검표일련번호",
|
||||
meta: { align: "center" },
|
||||
}),
|
||||
columnHelper.accessor("cklstNm", {
|
||||
header: "점검표명",
|
||||
meta: { align: "left" },
|
||||
}),
|
||||
columnHelper.accessor("docSeComNm", {
|
||||
header: "문서구분",
|
||||
cell: ({ getValue, row }) => getValue() ?? row.original.docSeComCd ?? "—",
|
||||
}),
|
||||
columnHelper.accessor("taskSeCdNm", {
|
||||
header: "업무구분",
|
||||
cell: ({ getValue, row }) => getValue() ?? row.original.taskSeComCd ?? "—",
|
||||
}),
|
||||
columnHelper.accessor("useYn", {
|
||||
header: "사용여부",
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue();
|
||||
return value === "Y" ? "예" : value === "N" ? "아니오" : "—";
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("regDt", {
|
||||
header: "등록일시",
|
||||
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>
|
||||
<Button type="button" size="sm" color="red" onClick={() => onDelete(row.original)}>
|
||||
삭제
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
}),
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { CklstRvw } from "@/entities/cklst";
|
||||
|
||||
export interface CklstRvwRow extends Omit<CklstRvw, "cklstRvwSn"> {
|
||||
cklstRvwSn?: number;
|
||||
rowKey: string;
|
||||
isNew: boolean;
|
||||
isDirty: boolean;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import type { CklstMng } from "@/entities/cklst";
|
||||
|
||||
import { CklstArtclList } from "./cklst-artcl-list";
|
||||
import { CklstMngList } from "./cklst-mng-list";
|
||||
import { CklstRvwGrid } from "./cklst-rvw-grid";
|
||||
|
||||
function ChecklistManagementPage() {
|
||||
const [selectedCklst, setSelectedCklst] = useState<CklstMng | null>(null);
|
||||
|
||||
return (
|
||||
<div className="space-y-6 px-[1.8rem] pb-6">
|
||||
<div className="grid grid-cols-1 gap-6 xl:grid-cols-2">
|
||||
<CklstMngList
|
||||
selectedCklstSn={selectedCklst?.cklstSn ?? null}
|
||||
onSelectCklst={setSelectedCklst}
|
||||
/>
|
||||
<CklstArtclList cklstSn={selectedCklst?.cklstSn} />
|
||||
</div>
|
||||
<CklstRvwGrid selectedCklst={selectedCklst} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ChecklistManagementPage };
|
||||
@@ -0,0 +1,112 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import type { CklstArtcl } from "@/entities/cklst";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
|
||||
export interface CklstArtclFormValues {
|
||||
cklstArtclNm: string;
|
||||
useYn: "Y" | "N";
|
||||
}
|
||||
|
||||
interface CklstArtclFormDialogProps {
|
||||
mode: "create" | "edit";
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
initial?: CklstArtcl | null;
|
||||
isSubmitting?: boolean;
|
||||
onSubmit: (values: CklstArtclFormValues) => void;
|
||||
}
|
||||
|
||||
function CklstArtclFormDialog({
|
||||
mode,
|
||||
open,
|
||||
onOpenChange,
|
||||
initial,
|
||||
isSubmitting,
|
||||
onSubmit,
|
||||
}: CklstArtclFormDialogProps) {
|
||||
const { register, handleSubmit, reset, setValue, watch } = useForm<CklstArtclFormValues>({
|
||||
defaultValues: {
|
||||
cklstArtclNm: "",
|
||||
useYn: "Y",
|
||||
},
|
||||
});
|
||||
|
||||
const useYn = watch("useYn");
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
if (mode === "edit" && initial) {
|
||||
reset({
|
||||
cklstArtclNm: initial.cklstArtclNm ?? "",
|
||||
useYn: initial.useYn ?? "Y",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
reset({
|
||||
cklstArtclNm: "",
|
||||
useYn: "Y",
|
||||
});
|
||||
}, [open, mode, initial, reset]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-[40rem]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{mode === "create" ? "점검항목 추가" : "점검항목 수정"}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form className="space-y-4" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="cklstArtclNm">점검항목명</Label>
|
||||
<Input
|
||||
id="cklstArtclNm"
|
||||
placeholder="예: 법령검토"
|
||||
{...register("cklstArtclNm", { required: true })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>사용여부</Label>
|
||||
<Select value={useYn} onValueChange={(v) => setValue("useYn", v as "Y" | "N")}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="사용여부" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Y">예</SelectItem>
|
||||
<SelectItem value="N">아니오</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="button" color="white" onClick={() => onOpenChange(false)}>
|
||||
취소
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
저장
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { CklstArtclFormDialog };
|
||||
@@ -0,0 +1,131 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
|
||||
import { matchQueryStatus } from "@/shared/lib";
|
||||
import { NoData } from "@/shared/ui/no-data";
|
||||
import {
|
||||
type CklstArtcl,
|
||||
useCklstArtclList,
|
||||
useCreateCklstArtcl,
|
||||
useDeleteCklstArtcl,
|
||||
useUpdateCklstArtcl,
|
||||
} from "@/entities/cklst";
|
||||
import { Button, DataTable, Skeleton } from "@common/components";
|
||||
|
||||
import { createCklstArtclColumns } from "../model/cklst-artcl-columns";
|
||||
|
||||
import { CklstArtclFormDialog, type CklstArtclFormValues } from "./cklst-artcl-form-dialog";
|
||||
|
||||
interface CklstArtclListProps {
|
||||
cklstSn?: number;
|
||||
}
|
||||
|
||||
function CklstArtclList({ cklstSn }: CklstArtclListProps) {
|
||||
const [dialogMode, setDialogMode] = useState<"create" | "edit" | null>(null);
|
||||
const [editingArtcl, setEditingArtcl] = useState<CklstArtcl | null>(null);
|
||||
|
||||
const artclQuery = useCklstArtclList(cklstSn);
|
||||
const createMutation = useCreateCklstArtcl();
|
||||
const updateMutation = useUpdateCklstArtcl();
|
||||
const deleteMutation = useDeleteCklstArtcl();
|
||||
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
createCklstArtclColumns({
|
||||
onEdit: (row) => {
|
||||
setEditingArtcl(row);
|
||||
setDialogMode("edit");
|
||||
},
|
||||
onDelete: async (row) => {
|
||||
if (!window.confirm("선택한 점검항목을 삭제하시겠습니까?")) return;
|
||||
await deleteMutation.mutateAsync({
|
||||
cklstArtclSn: row.cklstArtclSn,
|
||||
cklstSn: row.cklstSn,
|
||||
});
|
||||
},
|
||||
}),
|
||||
[deleteMutation],
|
||||
);
|
||||
|
||||
const table = useReactTable({
|
||||
data: artclQuery.data ?? [],
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getRowId: (row) => `${row.cklstArtclSn}-${row.cklstSn}`,
|
||||
});
|
||||
|
||||
const tableComponent = matchQueryStatus(artclQuery, {
|
||||
Pending: (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-[4rem]" />
|
||||
<Skeleton className="h-[12rem]" />
|
||||
</div>
|
||||
),
|
||||
Empty: <NoData />,
|
||||
Success: () => <DataTable table={table} />,
|
||||
});
|
||||
|
||||
const handleFormSubmit = async (values: CklstArtclFormValues) => {
|
||||
if (!cklstSn) return;
|
||||
|
||||
if (dialogMode === "create") {
|
||||
await createMutation.mutateAsync({
|
||||
cklstSn,
|
||||
cklstArtclNm: values.cklstArtclNm,
|
||||
useYn: values.useYn,
|
||||
});
|
||||
} else if (dialogMode === "edit" && editingArtcl) {
|
||||
await updateMutation.mutateAsync({
|
||||
cklstArtclSn: editingArtcl.cklstArtclSn,
|
||||
cklstSn: editingArtcl.cklstSn,
|
||||
body: {
|
||||
cklstArtclNm: values.cklstArtclNm,
|
||||
useYn: values.useYn,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setDialogMode(null);
|
||||
setEditingArtcl(null);
|
||||
};
|
||||
|
||||
const isDisabled = !cklstSn;
|
||||
|
||||
return (
|
||||
<section className="card card-border mx-0 bg-background pt-0">
|
||||
<div className="card-header flex items-center justify-between">
|
||||
<h3 className="text-2xl font-semibold">점검항목 관리</h3>
|
||||
<Button type="button" disabled={isDisabled} onClick={() => setDialogMode("create")}>
|
||||
점검항목 추가
|
||||
</Button>
|
||||
</div>
|
||||
{isDisabled ? (
|
||||
<p className="py-8 text-center text-lg text-muted-foreground">
|
||||
점검표를 선택하면 점검항목을 관리할 수 있습니다.
|
||||
</p>
|
||||
) : (
|
||||
tableComponent
|
||||
)}
|
||||
{dialogMode && (
|
||||
<CklstArtclFormDialog
|
||||
mode={dialogMode}
|
||||
open={Boolean(dialogMode)}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setDialogMode(null);
|
||||
setEditingArtcl(null);
|
||||
}
|
||||
}}
|
||||
initial={dialogMode === "edit" ? editingArtcl : null}
|
||||
isSubmitting={createMutation.isPending || updateMutation.isPending}
|
||||
onSubmit={handleFormSubmit}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export { CklstArtclList };
|
||||
@@ -0,0 +1,149 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import type { CklstMng } from "@/entities/cklst";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
|
||||
export interface CklstMngFormValues {
|
||||
cklstNm: string;
|
||||
docSeComCd: string;
|
||||
taskSeComCd: string;
|
||||
useYn: "Y" | "N";
|
||||
}
|
||||
|
||||
interface CklstMngFormDialogProps {
|
||||
mode: "create" | "edit";
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
initial?: CklstMng | null;
|
||||
isSubmitting?: boolean;
|
||||
onSubmit: (values: CklstMngFormValues) => void;
|
||||
}
|
||||
|
||||
function CklstMngFormDialog({
|
||||
mode,
|
||||
open,
|
||||
onOpenChange,
|
||||
initial,
|
||||
isSubmitting,
|
||||
onSubmit,
|
||||
}: CklstMngFormDialogProps) {
|
||||
const { register, handleSubmit, reset, setValue, watch } = useForm<CklstMngFormValues>({
|
||||
defaultValues: {
|
||||
cklstNm: "",
|
||||
docSeComCd: "",
|
||||
taskSeComCd: "",
|
||||
useYn: "Y",
|
||||
},
|
||||
});
|
||||
|
||||
const docSeComCd = watch("docSeComCd");
|
||||
const useYn = watch("useYn");
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
if (mode === "edit" && initial) {
|
||||
reset({
|
||||
cklstNm: initial.cklstNm,
|
||||
docSeComCd: initial.docSeComCd ?? "",
|
||||
taskSeComCd: initial.taskSeComCd,
|
||||
useYn: initial.useYn ?? "Y",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
reset({
|
||||
cklstNm: "",
|
||||
docSeComCd: "",
|
||||
taskSeComCd: "",
|
||||
useYn: "Y",
|
||||
});
|
||||
}, [open, mode, initial, reset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (mode === "create" && docSeComCd) {
|
||||
setValue("taskSeComCd", "");
|
||||
}
|
||||
}, [docSeComCd, mode, setValue]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-[52rem]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{mode === "create" ? "점검표 추가" : "점검표 수정"}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form className="space-y-4" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="cklstNm">점검표명</Label>
|
||||
<Input
|
||||
id="cklstNm"
|
||||
placeholder="예: 설치인가 체크리스트"
|
||||
{...register("cklstNm", { required: true })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>문서구분</Label>
|
||||
<CodeSelect
|
||||
upCd="ASDOCSECD"
|
||||
value={docSeComCd}
|
||||
onValueChange={(v) => setValue("docSeComCd", v)}
|
||||
placeholder="문서구분 선택"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>업무구분</Label>
|
||||
<CodeSelect
|
||||
upCd={docSeComCd || "ASDOCSECD"}
|
||||
value={watch("taskSeComCd")}
|
||||
onValueChange={(v) => setValue("taskSeComCd", v)}
|
||||
placeholder="업무구분 선택"
|
||||
className="w-full"
|
||||
disabled={!docSeComCd}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>사용여부</Label>
|
||||
<Select value={useYn} onValueChange={(v) => setValue("useYn", v as "Y" | "N")}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="사용여부" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Y">예</SelectItem>
|
||||
<SelectItem value="N">아니오</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="button" color="white" onClick={() => onOpenChange(false)}>
|
||||
취소
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
저장
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { CklstMngFormDialog };
|
||||
@@ -0,0 +1,148 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import { getCoreRowModel, type RowSelectionState } from "@tanstack/react-table";
|
||||
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import {
|
||||
type CklstMng,
|
||||
useCklstMngPage,
|
||||
useCreateCklstMng,
|
||||
useDeleteCklstMng,
|
||||
useUpdateCklstMng,
|
||||
} from "@/entities/cklst";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
import { createCklstMngColumns } from "../model/cklst-mng-columns";
|
||||
|
||||
import { CklstMngFormDialog, type CklstMngFormValues } from "./cklst-mng-form-dialog";
|
||||
|
||||
interface CklstMngListProps {
|
||||
selectedCklstSn: number | null;
|
||||
onSelectCklst: (cklst: CklstMng | null) => void;
|
||||
}
|
||||
|
||||
function CklstMngList({ selectedCklstSn, onSelectCklst }: CklstMngListProps) {
|
||||
const [dialogMode, setDialogMode] = useState<"create" | "edit" | null>(null);
|
||||
const [editingCklst, setEditingCklst] = useState<CklstMng | null>(null);
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>(() =>
|
||||
selectedCklstSn != null ? { [String(selectedCklstSn)]: true } : {},
|
||||
);
|
||||
|
||||
const mngQuery = useCklstMngPage({
|
||||
page: 0,
|
||||
size: 50,
|
||||
sort: "regDt,desc",
|
||||
});
|
||||
|
||||
const createMutation = useCreateCklstMng();
|
||||
const updateMutation = useUpdateCklstMng();
|
||||
const deleteMutation = useDeleteCklstMng();
|
||||
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
createCklstMngColumns({
|
||||
onEdit: (row) => {
|
||||
setEditingCklst(row);
|
||||
setDialogMode("edit");
|
||||
},
|
||||
onDelete: async (row) => {
|
||||
if (!window.confirm("선택한 점검표를 삭제하시겠습니까?")) return;
|
||||
await deleteMutation.mutateAsync(row.cklstSn);
|
||||
if (selectedCklstSn === row.cklstSn) {
|
||||
onSelectCklst(null);
|
||||
setRowSelection({});
|
||||
}
|
||||
},
|
||||
}),
|
||||
[deleteMutation, onSelectCklst, selectedCklstSn],
|
||||
);
|
||||
|
||||
const { tableComponent } = useDataTable({
|
||||
query: mngQuery,
|
||||
tableOptions: {
|
||||
columns,
|
||||
state: { rowSelection },
|
||||
enableRowSelection: true,
|
||||
enableMultiRowSelection: false,
|
||||
getRowId: (row) => String(row.cklstSn),
|
||||
onRowSelectionChange: (updater) => {
|
||||
const next = typeof updater === "function" ? updater(rowSelection) : updater;
|
||||
setRowSelection(next);
|
||||
const selectedId = Object.keys(next)[0];
|
||||
if (!selectedId) {
|
||||
onSelectCklst(null);
|
||||
return;
|
||||
}
|
||||
const selected = mngQuery.data?.content.find((c) => String(c.cklstSn) === selectedId);
|
||||
onSelectCklst(selected ?? null);
|
||||
},
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
},
|
||||
dataTableOptions: {
|
||||
onRowClick: (row) => {
|
||||
setRowSelection({ [String(row.cklstSn)]: true });
|
||||
onSelectCklst(row);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const handleFormSubmit = async (values: CklstMngFormValues) => {
|
||||
const body = {
|
||||
cklstNm: values.cklstNm,
|
||||
taskSeComCd: values.taskSeComCd,
|
||||
useYn: values.useYn,
|
||||
docSeComCd: values.docSeComCd || undefined,
|
||||
};
|
||||
|
||||
if (dialogMode === "create") {
|
||||
const cklstSn = await createMutation.mutateAsync(body);
|
||||
const { data } = await mngQuery.refetch();
|
||||
const created = data?.content.find((c) => c.cklstSn === cklstSn);
|
||||
if (created) {
|
||||
onSelectCklst(created);
|
||||
setRowSelection({ [String(created.cklstSn)]: true });
|
||||
}
|
||||
} else if (dialogMode === "edit" && editingCklst) {
|
||||
await updateMutation.mutateAsync({ cklstSn: editingCklst.cklstSn, body });
|
||||
const { data } = await mngQuery.refetch();
|
||||
const updated = data?.content.find((c) => c.cklstSn === editingCklst.cklstSn);
|
||||
if (updated && selectedCklstSn === editingCklst.cklstSn) {
|
||||
onSelectCklst(updated);
|
||||
}
|
||||
}
|
||||
|
||||
setDialogMode(null);
|
||||
setEditingCklst(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="card card-border mx-0 bg-background pt-0">
|
||||
<div className="card-header flex items-center justify-between">
|
||||
<h3 className="text-2xl font-semibold">점검표 관리</h3>
|
||||
<Button type="button" onClick={() => setDialogMode("create")}>
|
||||
점검표 추가
|
||||
</Button>
|
||||
</div>
|
||||
{tableComponent}
|
||||
{dialogMode && (
|
||||
<CklstMngFormDialog
|
||||
mode={dialogMode}
|
||||
open={Boolean(dialogMode)}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setDialogMode(null);
|
||||
setEditingCklst(null);
|
||||
}
|
||||
}}
|
||||
initial={dialogMode === "edit" ? editingCklst : null}
|
||||
isSubmitting={createMutation.isPending || updateMutation.isPending}
|
||||
onSubmit={handleFormSubmit}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export { CklstMngList };
|
||||
@@ -0,0 +1,92 @@
|
||||
"use client";
|
||||
|
||||
import type { CklstArtcl } from "@/entities/cklst";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
|
||||
import { buildRvwDisplayRows, formatRvwCnLvNo, formatTreeBranchPrefix } from "../model/build-rvw-display-rows";
|
||||
import type { CklstRvwRow } from "../model/cklst-rvw-row-types";
|
||||
|
||||
interface CklstPreviewDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
cklstNm: string;
|
||||
rows: CklstRvwRow[];
|
||||
artclOptions: CklstArtcl[];
|
||||
}
|
||||
|
||||
function CklstPreviewDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
cklstNm,
|
||||
rows,
|
||||
artclOptions,
|
||||
}: CklstPreviewDialogProps) {
|
||||
const displayRows = buildRvwDisplayRows(rows);
|
||||
|
||||
function getArtclLabel(cklstArtclSn: number): string {
|
||||
const artcl = artclOptions.find((item) => item.cklstArtclSn === cklstArtclSn);
|
||||
return artcl?.cklstArtclNm ?? String(cklstArtclSn);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-[64rem]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>체크리스트 미리보기 — {cklstNm}</DialogTitle>
|
||||
</DialogHeader>
|
||||
{displayRows.length === 0 ? (
|
||||
<p className="py-6 text-center text-muted-foreground">표시할 항목이 없습니다.</p>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>점검항목</TableHead>
|
||||
<TableHead>주요 검토내용</TableHead>
|
||||
<TableHead>사용 여부</TableHead>
|
||||
<TableHead>구분</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{displayRows.map(({ row, depth }) => {
|
||||
const branchPrefix = formatTreeBranchPrefix(depth);
|
||||
|
||||
return (
|
||||
<TableRow key={row.rowKey}>
|
||||
<TableCell>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{branchPrefix ? (
|
||||
<span className="font-mono text-muted-foreground">{branchPrefix}</span>
|
||||
) : null}
|
||||
<span>{getArtclLabel(row.cklstArtclSn)}</span>
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>{row.rvwCn ?? "—"}</TableCell>
|
||||
<TableCell align="center">
|
||||
{row.useYn === "Y" ? "예" : row.useYn === "N" ? "아니오" : "—"}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{formatRvwCnLvNo(row.rvwCnLvNo)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { CklstPreviewDialog };
|
||||
@@ -0,0 +1,301 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import type { CklstMng, CklstRvw } from "@/entities/cklst";
|
||||
import {
|
||||
useCklstArtclList,
|
||||
useCklstRvwPage,
|
||||
useCreateCklstRvw,
|
||||
useDeleteCklstRvw,
|
||||
useUpdateCklstRvw,
|
||||
} from "@/entities/cklst";
|
||||
import {
|
||||
Button,
|
||||
Skeleton,
|
||||
Table,
|
||||
TableBody,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
|
||||
import { buildRvwDisplayRows, MAX_RVW_CN_LV_NO } from "../model/build-rvw-display-rows";
|
||||
import type { CklstRvwRow } from "../model/cklst-rvw-row-types";
|
||||
|
||||
import { CklstPreviewDialog } from "./cklst-preview-dialog";
|
||||
import { CklstRvwRowComponent } from "./cklst-rvw-row";
|
||||
|
||||
interface CklstRvwGridProps {
|
||||
selectedCklst: CklstMng | null;
|
||||
}
|
||||
|
||||
function toRow(item: CklstRvw): CklstRvwRow {
|
||||
return {
|
||||
...item,
|
||||
rowKey: `${item.cklstRvwSn}-${item.cklstArtclSn}-${item.cklstSn}`,
|
||||
isNew: false,
|
||||
isDirty: false,
|
||||
};
|
||||
}
|
||||
|
||||
function createDraftRowKey(): string {
|
||||
return `draft-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
||||
}
|
||||
|
||||
function validateRvwRow(row: CklstRvwRow): string | null {
|
||||
if (!row.cklstArtclSn) return "점검항목을 선택하세요.";
|
||||
if (!row.rvwCn?.trim()) return "주요 검토내용을 입력하세요.";
|
||||
return null;
|
||||
}
|
||||
|
||||
function CklstRvwGrid({ selectedCklst }: CklstRvwGridProps) {
|
||||
const [rows, setRows] = useState<CklstRvwRow[]>([]);
|
||||
const [previewOpen, setPreviewOpen] = useState(false);
|
||||
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const cklstSn = selectedCklst?.cklstSn ?? null;
|
||||
|
||||
const rvwQuery = useCklstRvwPage({
|
||||
cklstSn: cklstSn ?? undefined,
|
||||
page: 0,
|
||||
size: 500,
|
||||
sort: "sortSeq,asc",
|
||||
});
|
||||
|
||||
const artclQuery = useCklstArtclList(cklstSn);
|
||||
const createMutation = useCreateCklstRvw();
|
||||
const updateMutation = useUpdateCklstRvw();
|
||||
const deleteMutation = useDeleteCklstRvw();
|
||||
|
||||
const artclOptions = useMemo(
|
||||
() => (artclQuery.data ?? []).filter((artcl) => artcl.useYn !== "N"),
|
||||
[artclQuery.data],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!cklstSn) {
|
||||
setRows([]);
|
||||
return;
|
||||
}
|
||||
setRows((rvwQuery.data?.content ?? []).map(toRow));
|
||||
}, [cklstSn, rvwQuery.data?.content]);
|
||||
|
||||
const displayRows = useMemo(() => buildRvwDisplayRows(rows), [rows]);
|
||||
|
||||
const getDirtyRows = useCallback(() => rows.filter((row) => row.isDirty || row.isNew), [rows]);
|
||||
|
||||
const updateRow = useCallback((rowKey: string, patch: Partial<CklstRvwRow>) => {
|
||||
setRows((prev) =>
|
||||
prev.map((row) => (row.rowKey === rowKey ? { ...row, ...patch, isDirty: true } : row)),
|
||||
);
|
||||
}, []);
|
||||
|
||||
const removeRowLocal = useCallback((rowKey: string) => {
|
||||
setRows((prev) => prev.filter((row) => row.rowKey !== rowKey));
|
||||
}, []);
|
||||
|
||||
const addRootRow = useCallback(() => {
|
||||
if (!cklstSn) return;
|
||||
|
||||
const maxSort = rows.reduce((max, row) => Math.max(max, row.sortSeq ?? 0), 0);
|
||||
|
||||
setRows((prev) => [
|
||||
...prev,
|
||||
{
|
||||
rowKey: createDraftRowKey(),
|
||||
cklstSn,
|
||||
cklstArtclSn: artclOptions[0]?.cklstArtclSn ?? 0,
|
||||
rvwCnLvNo: 1,
|
||||
rvwCn: "",
|
||||
useYn: "Y",
|
||||
sortSeq: maxSort + 1,
|
||||
isNew: true,
|
||||
isDirty: true,
|
||||
},
|
||||
]);
|
||||
}, [artclOptions, cklstSn, rows]);
|
||||
|
||||
const addChildRow = useCallback(
|
||||
(parent: CklstRvwRow) => {
|
||||
if (!cklstSn || parent.isNew || parent.cklstRvwSn == null) {
|
||||
alert("상위 행을 먼저 저장한 후 하위 항목을 추가할 수 있습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const parentLv = parent.rvwCnLvNo ?? 1;
|
||||
if (parentLv >= MAX_RVW_CN_LV_NO) {
|
||||
alert("최대 3단계까지 추가할 수 있습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const maxSort = rows.reduce((max, row) => Math.max(max, row.sortSeq ?? 0), 0);
|
||||
|
||||
setRows((prev) => [
|
||||
...prev,
|
||||
{
|
||||
rowKey: createDraftRowKey(),
|
||||
cklstSn,
|
||||
cklstArtclSn: parent.cklstArtclSn,
|
||||
upCklstRvwSn: parent.cklstRvwSn,
|
||||
rvwCnLvNo: parentLv + 1,
|
||||
rvwCn: "",
|
||||
useYn: "Y",
|
||||
sortSeq: maxSort + 1,
|
||||
isNew: true,
|
||||
isDirty: true,
|
||||
},
|
||||
]);
|
||||
},
|
||||
[alert, cklstSn, rows],
|
||||
);
|
||||
|
||||
const handleDelete = async (row: CklstRvwRow) => {
|
||||
if (!window.confirm("선택한 항목을 삭제하시겠습니까?")) return;
|
||||
|
||||
if (row.isNew) {
|
||||
removeRowLocal(row.rowKey);
|
||||
return;
|
||||
}
|
||||
|
||||
if (row.cklstRvwSn == null) return;
|
||||
|
||||
await deleteMutation.mutateAsync({
|
||||
cklstRvwSn: row.cklstRvwSn,
|
||||
cklstArtclSn: row.cklstArtclSn,
|
||||
cklstSn: row.cklstSn,
|
||||
});
|
||||
await rvwQuery.refetch();
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!cklstSn) return;
|
||||
|
||||
const dirty = getDirtyRows().sort((a, b) => (a.rvwCnLvNo ?? 1) - (b.rvwCnLvNo ?? 1));
|
||||
|
||||
for (const row of dirty) {
|
||||
const fieldError = validateRvwRow(row);
|
||||
if (fieldError) {
|
||||
alert(fieldError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (row.isNew) {
|
||||
await createMutation.mutateAsync({
|
||||
cklstSn: row.cklstSn,
|
||||
cklstArtclSn: row.cklstArtclSn,
|
||||
upCklstRvwSn: row.upCklstRvwSn,
|
||||
rvwCnLvNo: row.rvwCnLvNo,
|
||||
rvwCn: row.rvwCn,
|
||||
useYn: row.useYn,
|
||||
sortSeq: row.sortSeq,
|
||||
bchckUseYn: "Y",
|
||||
});
|
||||
} else if (row.cklstRvwSn != null) {
|
||||
await updateMutation.mutateAsync({
|
||||
cklstRvwSn: row.cklstRvwSn,
|
||||
cklstArtclSn: row.cklstArtclSn,
|
||||
cklstSn: row.cklstSn,
|
||||
body: {
|
||||
upCklstRvwSn: row.upCklstRvwSn,
|
||||
rvwCnLvNo: row.rvwCnLvNo,
|
||||
rvwCn: row.rvwCn,
|
||||
useYn: row.useYn,
|
||||
sortSeq: row.sortSeq,
|
||||
bchckUseYn: row.bchckUseYn,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
alert("체크리스트 항목이 저장되었습니다.");
|
||||
await rvwQuery.refetch();
|
||||
};
|
||||
|
||||
const isDisabled = !selectedCklst;
|
||||
const isSaving = createMutation.isPending || updateMutation.isPending || deleteMutation.isPending;
|
||||
|
||||
return (
|
||||
<section className="card card-border mx-0 pt-0">
|
||||
<div className="card-header flex flex-wrap items-center justify-between gap-3">
|
||||
<h3 className="text-2xl font-semibold">체크리스트 항목</h3>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Button
|
||||
type="button"
|
||||
color="white"
|
||||
disabled={isDisabled}
|
||||
onClick={() => setPreviewOpen(true)}
|
||||
>
|
||||
미리보기
|
||||
</Button>
|
||||
<Button type="button" color="white" disabled={isDisabled} onClick={addRootRow}>
|
||||
추가
|
||||
</Button>
|
||||
<Button type="button" disabled={isDisabled || isSaving} onClick={handleSave}>
|
||||
저장
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{rvwQuery.isPending && cklstSn ? (
|
||||
<div className="flex flex-col gap-2 px-4 pb-4">
|
||||
<Skeleton className="h-[4rem]" />
|
||||
<Skeleton className="h-[16rem]" />
|
||||
</div>
|
||||
) : isDisabled ? (
|
||||
<p className="py-8 text-center text-lg text-muted-foreground">
|
||||
상단에서 점검표를 선택하면 체크리스트 항목을 편집할 수 있습니다.
|
||||
</p>
|
||||
) : displayRows.length === 0 ? (
|
||||
<p className="py-8 text-center text-lg text-muted-foreground">
|
||||
등록된 항목이 없습니다. [추가]로 1단계 항목을 등록하세요.
|
||||
</p>
|
||||
) : (
|
||||
<Table className="w-full">
|
||||
<colgroup>
|
||||
<col width="20%" />
|
||||
<col width="50%" />
|
||||
<col width="15%" />
|
||||
<col width="5%" />
|
||||
<col width="10%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>점검항목</TableHead>
|
||||
<TableHead>주요 검토내용</TableHead>
|
||||
<TableHead>사용 여부</TableHead>
|
||||
<TableHead>구분</TableHead>
|
||||
<TableHead>관리</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{displayRows.map(({ row, depth }) => (
|
||||
<CklstRvwRowComponent
|
||||
key={row.rowKey}
|
||||
row={row}
|
||||
depth={depth}
|
||||
artclOptions={artclOptions}
|
||||
onChange={(patch) => updateRow(row.rowKey, patch)}
|
||||
onAddChild={() => addChildRow(row)}
|
||||
onDelete={() => handleDelete(row)}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
|
||||
{selectedCklst && (
|
||||
<CklstPreviewDialog
|
||||
open={previewOpen}
|
||||
onOpenChange={setPreviewOpen}
|
||||
cklstNm={selectedCklst.cklstNm}
|
||||
rows={rows}
|
||||
artclOptions={artclOptions}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export { CklstRvwGrid };
|
||||
@@ -0,0 +1,110 @@
|
||||
"use client";
|
||||
|
||||
import type { CklstArtcl } from "@/entities/cklst";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Label,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
TableCell,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
|
||||
import { canAddChildRow, formatRvwCnLvNo, formatTreeBranchPrefix } from "../model/build-rvw-display-rows";
|
||||
import type { CklstRvwRow } from "../model/cklst-rvw-row-types";
|
||||
|
||||
interface CklstRvwRowProps {
|
||||
row: CklstRvwRow;
|
||||
depth: number;
|
||||
artclOptions: CklstArtcl[];
|
||||
onChange: (patch: Partial<CklstRvwRow>) => void;
|
||||
onAddChild: () => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
function CklstRvwRowComponent({
|
||||
row,
|
||||
depth,
|
||||
artclOptions,
|
||||
onChange,
|
||||
onAddChild,
|
||||
onDelete,
|
||||
}: CklstRvwRowProps) {
|
||||
const levelLabel = formatRvwCnLvNo(row.rvwCnLvNo);
|
||||
const branchPrefix = formatTreeBranchPrefix(depth);
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<div className="flex min-w-[12rem] items-center gap-1">
|
||||
{branchPrefix ? (
|
||||
<span className="shrink-0 font-mono text-muted-foreground">{branchPrefix}</span>
|
||||
) : null}
|
||||
<Select
|
||||
value={row.cklstArtclSn ? String(row.cklstArtclSn) : ""}
|
||||
onValueChange={(v) => onChange({ cklstArtclSn: Number(v), isDirty: true })}
|
||||
>
|
||||
<SelectTrigger className="w-full flex-1">
|
||||
<SelectValue placeholder="점검항목 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{artclOptions.map((artcl) => (
|
||||
<SelectItem key={artcl.cklstArtclSn} value={String(artcl.cklstArtclSn)}>
|
||||
{artcl.cklstArtclNm ?? artcl.cklstArtclSn}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Input
|
||||
value={row.rvwCn ?? ""}
|
||||
onChange={(e) => onChange({ rvwCn: e.target.value, isDirty: true })}
|
||||
placeholder="주요 검토내용"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<RadioGroup
|
||||
value={row.useYn ?? "Y"}
|
||||
onValueChange={(v) => onChange({ useYn: v as "Y" | "N", isDirty: true })}
|
||||
className="flex justify-center gap-4"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="Y" id={`${row.rowKey}-use-y`} />
|
||||
<Label htmlFor={`${row.rowKey}-use-y`}>예</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="N" id={`${row.rowKey}-use-n`} />
|
||||
<Label htmlFor={`${row.rowKey}-use-n`}>아니오</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</TableCell>
|
||||
<TableCell align="center">{levelLabel}</TableCell>
|
||||
<TableCell align="center">
|
||||
<div className="flex justify-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="white"
|
||||
disabled={row.isNew || !canAddChildRow(row.rvwCnLvNo)}
|
||||
onClick={onAddChild}
|
||||
>
|
||||
하위추가
|
||||
</Button>
|
||||
<Button type="button" size="sm" color="red" onClick={onDelete}>
|
||||
삭제
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
export { CklstRvwRowComponent };
|
||||
@@ -14,6 +14,7 @@ export const ENDPOINT = {
|
||||
AS_CHECKLIST_SERVICE: {
|
||||
CKLST: "/as-checklist-service/api/v1/cklst",
|
||||
CKLST_ARTCL: "/as-checklist-service/api/v1/cklst-artcl",
|
||||
CKLST_RVW: "/as-checklist-service/api/v1/cklst-rvw",
|
||||
},
|
||||
CMS_SERVICE: {
|
||||
CODE: "/cms-service/api/v1/code/cache",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import type { UseQueryResult } from "@tanstack/react-query";
|
||||
|
||||
interface MatchQueryStatusOptions {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
function NoData({ message = "조회된 데이터가 없습니다." }: { message?: string }) {
|
||||
return (
|
||||
<div className="flex min-h-[120px] items-center justify-center text-muted-foreground">
|
||||
<p className="text-sm">{message}</p>
|
||||
<p className="text-lg">{message}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user