diff --git a/apps/as/docs/approval-line-management/SPEC.md b/apps/as/docs/approval-line-management/SPEC.md index 9af80580b..1ae5e65ae 100644 --- a/apps/as/docs/approval-line-management/SPEC.md +++ b/apps/as/docs/approval-line-management/SPEC.md @@ -231,7 +231,7 @@ END | ------------- | ------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------- | | (드래그 핸들) | — | — | `useSortable` 연동 | | 결재선아이디 | `aplnId` | 읽기 전용 | 신규 행: `—`, 저장 후 서버 `aplnId` 표시 | -| 결재순서번호 | `atrzSeqNo` | 숫자 Input | 드래그 시 자동 재부여 가능 | +| 결재순서번호 | `atrzSeqNo` | 읽기 전용 (그리드 행 순서 1부터 표시) | 저장 시 행 순서 기준 자동 채번 | | 결재구분 | `atrzSeComCd` | Select (`ATRZSECD` 공통코드) | 표시명 `atrzSeComNm` (조회 행) | | 결재자 | `aprvrUuid` | 읽기 전용 이름 + **[선택]** 버튼 | 응답에 성명 없음 — 선택 시 로컬 `aprvrNm` 유지 | | 등록일시 | `regDt` | 읽기 전용 | 신규 행은 `—` | @@ -239,26 +239,28 @@ END #### 인라인 편집 정책 -| 항목 | 내용 | -| ------------------- | -------------------------------------------------------------------------------------------------------- | -| **단일 행 편집** | **행 추가 또는 한 행 수정 중에는 다른 행을 편집할 수 없다.** `editingRowId` 로 현재 편집 행 1건만 허용. | -| 편집 잠금 시 비활성 | 다른 행: 셀·[수정]·[삭제] 비활성 · `[행 추가]` 비활성 · 행 드래그 비활성 · (선택) `[결재선 저장]` 비활성 | -| 편집 해제 | 행별 **[저장]** 성공 또는 **[취소]** 시 `editingRowId` 초기화 후 전체 조회 모드 복귀 | -| 조회 행 | 편집 중이 아닐 때 읽기 전용. **[수정]** 클릭 시 해당 행만 편집 모드 진입 | -| 신규 행 | `[행 추가]` 시 맨 아래 편집 행 1건 생성 (`editingRowId` = 임시 키) | -| 필수값 | `atrzSeComCd`, `aprvrUuid`, `atrzSeqNo` | -| 순서 중복 | 동일 서식 내 `atrzSeqNo` 중복 시 저장 전 검증·안내 | -| 결재구분 코드 | `ATRZSECD` — 예: `ATRZ01` 검토자, `ATRZ02` 승인자, `ATRZ03` 최종승인자 | -| 저장 단위 | 행별 **[저장]** (단건 POST/PUT) 또는 툴바 **[결재선 저장]** (일괄) | +| 항목 | 내용 | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| **단일 행 편집** | **행 추가 또는 한 행 수정 중에는 다른 행을 편집할 수 없다.** `editingRowId` 로 현재 편집 행 1건만 허용. | +| 편집 잠금 시 비활성 | 다른 행: 셀·[수정]·[삭제] 비활성 · `[행 추가]` 비활성 · 행 드래그 비활성 · (선택) `[결재선 저장]` 비활성 | +| 편집 해제 | 행별 **[저장]** 성공 또는 **[취소]** 시 `editingRowId` 초기화 후 전체 조회 모드 복귀 | +| 조회 행 | 편집 중이 아닐 때 읽기 전용. **[수정]** 클릭 시 해당 행만 편집 모드 진입 | +| 신규 행 | `[행 추가]` 시 맨 아래 편집 행 1건 생성 (`editingRowId` = 임시 키) | +| 필수값 | `atrzSeComCd`, `aprvrUuid` | +| 순서 중복 | 저장 시 행 순서로 자동 채번하므로 중복 없음 | +| 결재구분 코드 | `ATRZSECD` — 예: `ATRZ01` 검토자, `ATRZ02` 승인자, `ATRZ03` 최종승인자 | +| **순서 제약** | `ATRZ01`은 최상단 블록, `ATRZ03`은 최하단 블록. 드래그 종료·저장 시 `validateAtrzOrder`로 검증, 위반 시 적용/저장 불가 (결재구분 변경 시에는 미검증) | +| 저장 단위 | 행별 **[저장]** (단건 POST/PUT) 또는 툴바 **[결재선 저장]** (일괄) | #### 행 드래그 정렬 (`@dnd-kit` + `useSortable`) -| 항목 | 내용 | -| ---------- | ------------------------------------------------------------------------------------------- | -| 목적 | 결재 단계 순서를 그리드에서 직접 변경 | -| 라이브러리 | `@dnd-kit/core`, `@dnd-kit/sortable` — 테이블 행 POC 후 확정 | -| 저장 시점 | 드래그는 클라이언트 상태만 변경 → **[결재선 저장]** 또는 행별 **[저장]** 시 `atrzSeqNo` PUT | -| 편집 중 | `editingRowId != null` 이면 드래그 **불가** | +| 항목 | 내용 | +| ---------- | ------------------------------------------------------------------------------------------------------- | +| 목적 | 결재 단계 순서를 그리드에서 직접 변경 | +| 라이브러리 | `@dnd-kit/core`, `@dnd-kit/sortable` — 테이블 행 POC 후 확정 | +| 저장 시점 | 드래그는 클라이언트 상태만 변경 → **[결재선 저장]** 또는 행별 **[저장]** 시 `atrzSeqNo` PUT | +| 편집 중 | `editingRowKey != null` 이면 드래그 **불가** | +| 순서 제약 | 드래그 종료 시 `validateAtrzOrder` — 실패하면 순서 변경 미적용 + 안내. 결재구분 미선택 행은 드래그 불가 | ### 5-3. 보조 다이얼로그 (검토인력만) @@ -369,13 +371,14 @@ END ### 7-2. 모듈·파일 배치(제안) -| 구분 | 경로 | -| ------ | ------------------------------------------------------------------ | -| 라우트 | `apps/as/app/(with-base-layout)/approval-line-management/page.tsx` | -| 페이지 | `apps/as/src/pages/approval-line-management/` | -| 위젯 | `widgets/elapr-tmplt-list/`, `widgets/elapr-apln-grid/` | -| 피처 | `features/aprvr-picker/` (검토인력 [선택]만) | -| 엔티티 | `apps/as/src/entities/elapr/` | +| 구분 | 경로 | +| ------ | -------------------------------------------------------------------------------------- | +| 라우트 | `apps/as/app/(with-base-layout)/approval-line-management/page.tsx` | +| 페이지 | `apps/as/src/pages/approval-line-management/` (Public API: page만 export) | +| UI | `.../ui/elapr-tmplt-list.tsx`, `elapr-tmplt-form-dialog.tsx`, `elapr-apln-grid.tsx` 등 | +| model | `.../model/columns.tsx`, `apln-row-types.ts`, `mock-reviewers.ts` | +| lib | `.../lib/use-apln-rows.ts`, `validators.ts`, `use-dnd-sensors.ts` | +| 엔티티 | `apps/as/src/entities/elapr/` | ### 7-3. 영역별 UI 매핑 diff --git a/apps/as/docs/checklist-management/SPEC.md b/apps/as/docs/checklist-management/SPEC.md new file mode 100644 index 000000000..6b1a0abd6 --- /dev/null +++ b/apps/as/docs/checklist-management/SPEC.md @@ -0,0 +1,486 @@ +# 화면설계서: 체크리스트관리 + +## 1. 기본 정보 + +| 항목 | 내용 | +| ---------- | -------------------------------------------- | +| 버전 | 0.1 | +| 프로세스명 | UI설계 | +| 화면명 | 체크리스트관리 | +| 화면 유형 | [v] 조회 [v] 입력 [v] 수정 [v] 삭제 [ ] 출력 | +| 메뉴 경로 | 디지털행정지원 > 시스템관리 > 체크리스트관리 | + +--- + +## 2. 화면 목적 + +업무별·문서별 **점검표**(체크리스트)를 등록·관리하고, **점검항목** 마스터를 유지한다. 상단 좌측에서 점검표를 선택하면 하단 **체크리스트 항목** 영역에 해당 점검표의 검토 구성(계층형 검토내용)을 편집한다. 상단 우측 **점검항목 관리**는 점검표에 연결할 항목 풀을 등록·수정·삭제한다. + +--- + +## 3. 화면 구성 + +### 3-1. 전체 레이아웃 + +``` +┌───────────────────────────────────────────────────────────────────────────────────────────────┐ +│ 페이지 타이틀: 체크리스트관리 브레드크럼: 디지털행정지원 > … > │ +├──────────────────────────────────────────────┬────────────────────────────────────────────────┤ +│ [영역 A] 점검표 관리 │ [영역 B] 점검항목 관리 │ +│ [점검표 추가] │ [점검항목 추가] │ +│ ┌────────────────────────────────────────┐ │ ┌──────────────────────────────────────────┐ │ +│ │ 점검표일련번호 | 점검표명 | 문서구분 │ │ │ 점검항목코드 | 점검항목명 | 사용여부 │ │ +│ │ 업무구분 | 사용여부 | 등록일시 | 관리 │ │ │ 등록일시 | 관리 │ │ +│ ├────────────────────────────────────────┤ │ ├──────────────────────────────────────────┤ │ +│ │ 1 (선택 행 강조) 설치인가 기본 … [관리]│ │ │ ITEM_001 요건검토 … [관리][삭제]│ │ +│ │ [삭제] │ │ │ ITEM_002 법령검토 … [관리][삭제]│ │ +│ └────────────────────────────────────────┘ │ └──────────────────────────────────────────┘ │ +├──────────────────────────────────────────────┴────────────────────────────────────────────────┤ +│ [영역 C] 체크리스트 항목 (선택된 점검표: {cklstNm}) │ +│ [미리보기] [추가] [저장] │ +│ ┌─────────────────────────────────────────────────────────────────────────────────────────┐ │ +│ │ 점검항목 ▼ | 주요 검토내용 | 사용 여부 ○예 ○아니오 | 구분 | 관리 │ │ +│ │ L1 [요건검토 ▼] | (입력) | ○예 ○아니오 | 1단계| [하위추가]│ │ +│ │ L2 [요건검토 ▼] | (입력) | ○예 ○아니오 | 2단계| [삭제] │ │ +│ └─────────────────────────────────────────────────────────────────────────────────────────┘ │ +└───────────────────────────────────────────────────────────────────────────────────────────────┘ +``` + +### 3-2. 화면 데이터 흐름 + +``` +BEGIN + + // 최초 진입 + CALL GET /as-checklist-service/api/v1/cklst + DISPLAY 점검표 그리드 (영역 A) + CALL GET /as-checklist-service/api/v1/cklst-artcl + DISPLAY 점검항목 그리드 (영역 B) + DISPLAY 빈 상태 안내 (영역 C) // 점검표 미선택 시 C 비활성 + + // 영역 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) + END ON + + // 영역 A: 점검표 추가 + ON [점검표 추가] 버튼 CLICK DO + OPEN 점검표 추가 다이얼로그 + ON [저장] DO + VALIDATE cklstNm, taskSeComCd + CALL POST /as-checklist-service/api/v1/cklst + CALL GET /as-checklist-service/api/v1/cklst + CLOSE 다이얼로그 + END ON + ON [취소] / [닫기] DO + CLOSE 다이얼로그 + END ON + END ON + + // 영역 A: 점검표 수정·삭제 + ON 영역 A 행 [관리] CLICK DO + OPEN 점검표 수정 다이얼로그 (기존 값 초기화) + ON [저장] DO + CALL PUT /as-checklist-service/api/v1/cklst/{cklstSn} + CALL GET /as-checklist-service/api/v1/cklst + END ON + END ON + + ON 영역 A 행 [삭제] CLICK DO + CONFIRM "선택한 점검표를 삭제하시겠습니까?" + CALL DELETE /as-checklist-service/api/v1/cklst/{cklstSn} + IF 삭제 대상 == 선택된 점검표 THEN RESET 영역 C + CALL GET /as-checklist-service/api/v1/cklst + END ON + + // 영역 B: 점검항목 추가 + ON [점검항목 추가] 버튼 CLICK DO + OPEN 점검항목 추가 다이얼로그 + ON [저장] DO + VALIDATE cklstArtclNm + // TBD: cklstSn 필수 — UI에 소속 점검표 선택 없음. 백엔드 정책 확인 + CALL POST /as-checklist-service/api/v1/cklst-artcl + CALL GET /as-checklist-service/api/v1/cklst-artcl + CLOSE 다이얼로그 + END ON + END ON + + // 영역 B: 점검항목 수정·삭제 + ON 영역 B 행 [관리] CLICK DO + OPEN 점검항목 수정 다이얼로그 + ON [저장] DO + CALL PUT /as-checklist-service/api/v1/cklst-artcl/{cklstArtclSn}/{cklstSn} + END ON + END ON + + ON 영역 B 행 [삭제] CLICK DO + CONFIRM "선택한 점검항목을 삭제하시겠습니까?" + CALL DELETE /as-checklist-service/api/v1/cklst-artcl/{cklstArtclSn}/{cklstSn} + CALL GET /as-checklist-service/api/v1/cklst-artcl + END ON + + // 영역 C: 검토 행 추가 + ON [추가] 버튼 CLICK DO + VALIDATE 선택된 점검표 존재 + ADD 영역 C 그리드 맨 아래 편집 가능 행 (1단계, rvwCnLvNo=1) + END ON + + // 영역 C: 하위 검토 추가 + ON 행 [하위추가] CLICK DO + ADD 부모행 하위 편집 행 (2단계, upCklstRvwSn=부모.cklstRvwSn, rvwCnLvNo=2) + END ON + + // 영역 C: 일괄 저장 + ON [저장] 버튼 CLICK DO + VALIDATE 선택된 점검표 존재 + FOR EACH 신규·변경된 검토 행 DO + IF 신규 THEN + CALL POST /as-checklist-service/api/v1/cklst-rvw + ELSE + CALL PUT /as-checklist-service/api/v1/cklst-rvw/{cklstRvwSn}/{cklstArtclSn}/{cklstSn} + END IF + END FOR + CALL GET /as-checklist-service/api/v1/cklst-rvw?cklstSn={cklstSn} + TOAST "체크리스트 항목이 저장되었습니다." + END ON + + // 영역 C: 행 삭제 + ON 행 [삭제] CLICK DO + CONFIRM "선택한 항목을 삭제하시겠습니까?" + IF 서버 저장된 행 THEN + CALL DELETE /as-checklist-service/api/v1/cklst-rvw/{cklstRvwSn}/{cklstArtclSn}/{cklstSn} + END IF + REMOVE 그리드 행 (미저장 행은 API 호출 없이 제거) + END ON + + // 영역 C: 미리보기 + ON [미리보기] 버튼 CLICK DO + OPEN 체크리스트 미리보기 다이얼로그 (읽기 전용 트리) + // TBD: 미리보기 UI·API + END ON + +END +``` + +--- + +## 4. 영역 A: 점검표 관리 + +선택된 점검표가 영역 C의 편집 대상이 된다. 행 클릭으로 선택 상태를 표시한다. + +### 4-1. 헤더 / 툴바 + +| 항목 | 내용 | +| ----------- | ----------------------------------------- | +| 섹션 타이틀 | 점검표 관리 | +| 우측 버튼 | **[점검표 추가]** — 점검표 등록 다이얼로그 | + +### 4-2. 그리드 컬럼 + +| 컬럼명 | 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` | +| 관리 | — | — | 행별 **[관리] · [삭제]** | + +- 행 클릭 시: `cklstSn` 선택 → 영역 C 헤더·그리드 갱신. +- 페이지네이션: Spring `Pageable` (`page`, `size`, `sort`). + +### 4-3. 버튼 + +| 버튼명 | 위치 | 동작 | +| ---------- | -------------- | ------------------------------------------------- | +| 점검표 추가 | 섹션 우측 상단 | 점검표 추가 다이얼로그 오픈 | +| 관리 | 행별 | 점검표 수정 다이얼로그 (기존 값 초기화) | +| 삭제 | 행별 | 확인 후 삭제. 선택 중인 점검표이면 영역 C 초기화. | + +### 4-4. 다이얼로그: 점검표 추가 / 수정 + +| 표시명 | 입력 유형 | 필수 | 기본값 | 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 표시: 예/아니오 | + +| 버튼명 | 동작 | +| ------ | -------------------------------------- | +| 저장 | POST(등록) 또는 PUT(수정) 후 목록 갱신 | +| 취소 | 다이얼로그 닫기, 입력값 폐기 | +| 닫기 | 다이얼로그 닫기 | + +--- + +## 5. 영역 B: 점검항목 관리 + +점검표 하단(영역 C)에서 선택할 **점검항목** 마스터를 관리한다. + +### 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` | +| 관리 | — | — | 행별 **[관리] · [삭제]** | + +> **TBD**: 백엔드 `CklstArtcl`은 복합 PK(`cklstArtclSn`, `cklstSn`)이며 등록 시 `cklstSn` 필수. UI 다이얼로그에는 `cklstSn` 입력이 없음 — 소속 점검표 지정 방식·전역 마스터 여부 백엔드 정책 확인. + +### 5-3. 다이얼로그: 점검항목 추가 / 수정 + +| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 | +| ---------- | --------- | ---- | ------ | ----------------- | ----------------------- | +| 점검항목명 | Text | Y | — | `cklstArtclNm` | placeholder: "예: 법령검토" | +| 사용여부 | Select | N | Y | `useYn` | Y/N | + +| 버튼명 | 동작 | +| ------ | -------------------------------------- | +| 저장 | POST(등록) 또는 PUT(수정) 후 목록 갱신 | +| 취소 | 다이얼로그 닫기 | +| 닫기 | 다이얼로그 닫기 | + +--- + +## 6. 영역 C: 체크리스트 항목 (선택된 점검표) + +영역 A에서 선택된 `cklstSn` 기준으로 **점검표검토**(계층형 검토내용)를 구성한다. + +### 6-1. 헤더 / 툴바 + +| 항목 | 내용 | +| ----------- | -------------------------------------------------------------------- | +| 섹션 타이틀 | 체크리스트 항목 (선택된 점검표: {cklstNm}) | +| 우측 버튼 | **[미리보기]** · **[추가]** · **[저장]** — 점검표 미선택 시 C 비활성 | + +### 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단계 행은 [하위추가] 미표시 | + +#### 계층 정책 + +| 항목 | 내용 | +| -------------- | -------------------------------------------------------------------- | +| 최대 깊이 | 2단계 (`rvwCnLvNo`: 1, 2) | +| 상위-하위 연결 | `upCklstRvwSn` — 2단계 행은 부모 1단계 `cklstRvwSn` 참조 | +| 정렬 | `sortSeq` — 동일 레벨 내 순서 (TBD: UI 드래그 정렬 여부) | +| 체크 사용 | `bchckUseYn` — 미리보기·실사용 화면 체크박스 노출 여부 (TBD UI 반영) | + +### 6-3. 버튼 + +| 버튼명 | 동작 | +| -------- | ------------------------------------------------------------ | +| 추가 | 1단계 검토 행 1건 추가 (편집 가능) | +| 저장 | 변경·신규 검토 행 일괄 POST/PUT (일괄 저장 API 없음) | +| 미리보기 | 선택 점검표 체크리스트 읽기 전용 미리보기 (TBD) | +| 하위추가 | 선택 1단계 행 아래 2단계 자식 행 추가 | +| 삭제 | 확인 후 DELETE 또는 미저장 행 로컬 제거 | + +--- + +## 7. API/DTO 명세 + +### 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` | +| 엔티티 | `@/entities/cklst` (`cklstMngService`, `cklstArtclService`) | +| 미구현 엔티티 | `@/entities/cklst-rvw` — **TBD** (영역 C용 `cklst-rvw` API) | + +### 7-2. 점검표 (영역 A) — `cklst` + +| 기능 | Method | URL | Request / Query | Response | 클라이언트 | +| --------- | ------ | ---------------- | ---------------------------- | ------------------- | -------------------------- | +| 목록 조회 | GET | `/cklst` | `CklstMngSearchParams` + Pageable | `Page` | `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? | 수정일시 | + +**CklstMngSaveRequest**: `cklstNm`\*, `taskSeComCd`\*, `useYn?`, `docSeComCd?` + +**CklstMngSearchParams**: `cklstNm?`, `taskSeComCd?`, `useYn?`, `docSeComCd?` + `page`, `size`, `sort` + +**React Query hooks**: `useCklstMngPage`, `useCklstMng`, `useCreateCklstMng`, `useUpdateCklstMng`, `useDeleteCklstMng` + +### 7-3. 점검표항목 (영역 B) — `cklst-artcl` + +| 기능 | Method | URL | Request / Query | Response | 클라이언트 | +| ----------------------- | ------ | ---------------------------------------- | --------------------------------- | --------------------- | ------------------------------- | +| 목록 조회 (페이징) | GET | `/cklst-artcl` | `CklstArtclSearchParams` + Pageable | `Page` | `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? | 등록일시 | + +**CklstArtclSaveRequest**: `cklstSn`\*, `cklstArtclNm?`, `useYn?` + +**CklstArtclUpdateRequest**: `cklstArtclNm?`, `useYn?` + +**CklstArtclId**: `{ cklstArtclSn: number; cklstSn: number }` + +**React Query hooks**: `useCklstArtclPage`, `useCklstArtclList`, `useCklstArtcl`, `useCreateCklstArtcl`, `useUpdateCklstArtcl`, `useDeleteCklstArtcl` + +### 7-4. 점검표검토 (영역 C) — `cklst-rvw` + +> **entities 미구현** — `@/entities/cklst-rvw` 추가 예정. 아래는 백엔드 `CklstRvwController` 기준. + +| 기능 | Method | URL | Request / Query | Response | 클라이언트 (예정) | +| ---------------------------- | ------ | -------------------------------------------------- | -------------------------- | ------------------ | ------------------------ | +| 목록 조회 (페이징) | GET | `/cklst-rvw` | `CklstRvwSearchDto` + Pageable | `Page` | 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` | + +**CklstRvw (응답)** + +| 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?` + +**CklstRvwUpdateRequestDto**: `upCklstRvwSn?`, `rvwCnLvNo?`, `rvwCn?`, `bchckUseYn?`, `sortSeq?`, `useYn?` + +**CklstRvwSearchDto**: `cklstSn?`, `cklstArtclSn?`, `useYn?` + Pageable + +### 7-5. 공통코드 (보조) + +| UI 필드 | 코드 그룹 (TBD) | 예시 값 | +| -------- | --------------- | -------------------------------------------- | +| 문서구분 | `ASDOCSECD` 등 | FNRS, INSTL, PWRK, DESIGN | +| 업무구분 | TBD | RE(재이용수), SF(하수처리시설), SP(하수관로) | +| 사용여부 | 고정 | Y, N | + +공통코드 조회: `@/entities/code` — `codeService`, `CodeSelect` + +--- + +## 8. UI 구현 가이드 + +### 8-1. 기술 스택·원칙 + +- **공통 컴포넌트**: `@common/components` (`Dialog`, `Button`, `Input`, `Select`, `RadioGroup` 등) +- **테이블**: `@tanstack/react-table` + `useDataTable` +- **데이터**: React Query — `@/entities/cklst` hooks (검토 API는 entities 추가 후 연동) +- **공통코드**: `@/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) | +| 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` | + +### 8-3. 영역별 UI 매핑 + +| 영역 | 주요 UI | +| ---- | ----------------------------------------------------------------------- | +| A | DataTable + 행 선택 + 점검표 추가/수정 Dialog | +| B | DataTable + 점검항목 추가/수정 Dialog | +| C | 계층형 편집 그리드(1·2단계) + [추가][저장][미리보기] + 행별 [하위추가][삭제] | +| 공통 | `CodeSelect` (문서구분·업무구분), 사용여부 Y/N Select/Radio | + +### 8-4. 다이얼로그·라우팅 + +| 다이얼로그명 | 트리거 | API 연동 | +| ------------------ | ----------------- | ------------------------------------- | +| 점검표 추가 | [점검표 추가] | `useCreateCklstMng` | +| 점검표 수정 | 행 [관리] | `useUpdateCklstMng` | +| 점검표 삭제 확인 | 행 [삭제] | `useDeleteCklstMng` | +| 점검항목 추가 | [점검항목 추가] | `useCreateCklstArtcl` | +| 점검항목 수정 | 행 [관리] | `useUpdateCklstArtcl` | +| 체크리스트 미리보기 | [미리보기] | TBD — 읽기 전용 트리 렌더 | + +라우트: `/checklist-management` (메뉴 URL은 CMS 등록값과 일치시킴) + +### 8-5. 데이터 흐름(요약) + +- **진입**: `useCklstMngPage` → 영역 A, `useCklstArtclPage` → 영역 B. C 비활성. +- **점검표 선택**: `cklstSn` 설정 → 영역 C 활성 → `cklst-rvw` 목록 조회(TBD) → 트리 그리드 표시. +- **점검표 CRUD**: Dialog → `useCreateCklstMng` / `useUpdateCklstMng` / `useDeleteCklstMng`. +- **점검항목 CRUD**: Dialog → `useCreateCklstArtcl` / `useUpdateCklstArtcl` / `useDeleteCklstArtcl`. +- **검토 저장**: [저장] → 신규 POST / 기존 PUT 순차 호출 (일괄 API 없음). +- **영역 C 점검항목 Select**: `useCklstArtclList(cklstSn)` 또는 영역 B 전체 목록과 교차 — **TBD**. + +### 8-6. 미확정 / 검증 필요 + +- [ ] 영역 B 점검항목 등록 시 `cklstSn` 필수 vs UI 무소속 마스터 — 백엔드·기획 확인 +- [ ] `점검항목코드`(ITEM_001) 표시 형식 vs `cklstArtclSn` 숫자 PK +- [ ] 영역 C 전체 검토 트리 조회: `cklstSn` 단위 일괄 API 부재 — `by-artcl` 다건 호출 또는 백엔드 확장 +- [ ] `@/entities/cklst-rvw` entities·hooks 구현 +- [ ] 문서구분·업무구분 공통코드 상위코드 ID (`ASDOCSECD` 등) +- [ ] [미리보기] 다이얼로그 UI·데이터 소스 +- [ ] `bchckUseYn` UI 반영 여부 +- [ ] 영역 A·B 페이지네이션·검색 UI 노출 여부 +- [ ] 권한(역할별 CRUD) diff --git a/apps/as/src/entities/cklst/api/cklst-artcl-service.ts b/apps/as/src/entities/cklst/api/cklst-artcl-service.ts new file mode 100644 index 000000000..dda8defd5 --- /dev/null +++ b/apps/as/src/entities/cklst/api/cklst-artcl-service.ts @@ -0,0 +1,70 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { PageResponse } from "@/shared/model"; + +import type { + CklstArtcl, + CklstArtclId, + CklstArtclSaveRequest, + CklstArtclSearchParams, + CklstArtclService, + CklstArtclUpdateRequest, +} from "../model/cklst-artcl-interface"; + +async function findPage(params: CklstArtclSearchParams): Promise> { + const response = await client.get>( + ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_ARTCL, + { params }, + ); + return response.data; +} + +async function findByCklstSn(cklstSn: number): Promise { + const response = await client.get( + `${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_ARTCL}/by-cklst/${cklstSn}`, + ); + return response.data; +} + +async function findById(cklstArtclSn: number, cklstSn: number): Promise { + const response = await client.get( + `${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_ARTCL}/${cklstArtclSn}/${cklstSn}`, + ); + return response.data; +} + +async function create(body: CklstArtclSaveRequest): Promise { + const response = await client.post( + ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_ARTCL, + body, + ); + return response.data; +} + +async function update( + cklstArtclSn: number, + cklstSn: number, + body: CklstArtclUpdateRequest, +): Promise { + const response = await client.put( + `${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_ARTCL}/${cklstArtclSn}/${cklstSn}`, + body, + ); + return response.data; +} + +async function remove(cklstArtclSn: number, cklstSn: number): Promise { + const response = await client.delete( + `${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_ARTCL}/${cklstArtclSn}/${cklstSn}`, + ); + return response.data; +} + +export const cklstArtclService = { + findPage, + findByCklstSn, + findById, + create, + update, + remove, +} satisfies CklstArtclService; diff --git a/apps/as/src/entities/cklst/api/cklst-mng-service.ts b/apps/as/src/entities/cklst/api/cklst-mng-service.ts new file mode 100644 index 000000000..67f22d3b1 --- /dev/null +++ b/apps/as/src/entities/cklst/api/cklst-mng-service.ts @@ -0,0 +1,48 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { PageResponse } from "@/shared/model"; + +import type { + CklstMng, + CklstMngSaveRequest, + CklstMngSearchParams, + CklstMngService, +} from "../model/cklst-mng-interface"; + +async function findPage(params: CklstMngSearchParams): Promise> { + const response = await client.get>(ENDPOINT.AS_CHECKLIST_SERVICE.CKLST, { + params, + }); + return response.data; +} + +async function findById(cklstSn: number): Promise { + const response = await client.get(`${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST}/${cklstSn}`); + return response.data; +} + +async function create(body: CklstMngSaveRequest): Promise { + const response = await client.post(ENDPOINT.AS_CHECKLIST_SERVICE.CKLST, body); + return response.data; +} + +async function update(cklstSn: number, body: CklstMngSaveRequest): Promise { + const response = await client.put( + `${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST}/${cklstSn}`, + body, + ); + return response.data; +} + +async function remove(cklstSn: number): Promise { + const response = await client.delete(`${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST}/${cklstSn}`); + return response.data; +} + +export const cklstMngService = { + findPage, + findById, + create, + update, + remove, +} satisfies CklstMngService; diff --git a/apps/as/src/entities/cklst/api/queries.ts b/apps/as/src/entities/cklst/api/queries.ts new file mode 100644 index 000000000..d7b8fd2e5 --- /dev/null +++ b/apps/as/src/entities/cklst/api/queries.ts @@ -0,0 +1,167 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; + +import { ENDPOINT } from "@/shared/config"; + +import type { + CklstArtclSaveRequest, + CklstArtclSearchParams, + CklstArtclUpdateRequest, +} from "../model/cklst-artcl-interface"; +import type { CklstMngSaveRequest, CklstMngSearchParams } from "../model/cklst-mng-interface"; + +import { cklstArtclService } from "./cklst-artcl-service"; +import { cklstMngService } from "./cklst-mng-service"; + +const cklstMngBaseKey = [ENDPOINT.AS_CHECKLIST_SERVICE.CKLST] as const; +const cklstArtclBaseKey = [ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_ARTCL] as const; + +export function useCklstMngPage(params: CklstMngSearchParams) { + return useQuery({ + queryKey: [...cklstMngBaseKey, "page", params], + queryFn: () => cklstMngService.findPage(params), + }); +} + +export function useCklstMng(cklstSn: number | null | undefined) { + return useQuery({ + queryKey: [...cklstMngBaseKey, cklstSn], + queryFn: () => cklstMngService.findById(cklstSn!), + enabled: cklstSn != null, + }); +} + +export function useCreateCklstMng() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (body: CklstMngSaveRequest) => cklstMngService.create(body), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: cklstMngBaseKey }); + }, + }); +} + +export function useUpdateCklstMng() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ cklstSn, body }: { cklstSn: number; body: CklstMngSaveRequest }) => + cklstMngService.update(cklstSn, body), + onSuccess: (_data, { cklstSn }) => { + queryClient.invalidateQueries({ queryKey: cklstMngBaseKey }); + queryClient.invalidateQueries({ + queryKey: [...cklstMngBaseKey, cklstSn], + }); + }, + }); +} + +export function useDeleteCklstMng() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (cklstSn: number) => cklstMngService.remove(cklstSn), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: cklstMngBaseKey }); + }, + }); +} + +export function useCklstArtclPage(params: CklstArtclSearchParams) { + const { cklstSn } = params; + + return useQuery({ + queryKey: [...cklstArtclBaseKey, "page", params], + queryFn: () => cklstArtclService.findPage(params), + enabled: cklstSn != null, + }); +} + +export function useCklstArtclList(cklstSn: number | null | undefined) { + return useQuery({ + queryKey: [...cklstArtclBaseKey, "by-cklst", cklstSn], + queryFn: () => cklstArtclService.findByCklstSn(cklstSn!), + enabled: cklstSn != null, + }); +} + +export function useCklstArtcl(params: { + cklstArtclSn: number | null | undefined; + cklstSn: number | null | undefined; +}) { + const { cklstArtclSn, cklstSn } = params; + + return useQuery({ + queryKey: [...cklstArtclBaseKey, cklstArtclSn, cklstSn], + queryFn: () => cklstArtclService.findById(cklstArtclSn!, cklstSn!), + enabled: cklstArtclSn != null && cklstSn != null, + }); +} + +export function useCreateCklstArtcl() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (body: CklstArtclSaveRequest) => cklstArtclService.create(body), + onSuccess: (_data, { cklstSn }) => { + queryClient.invalidateQueries({ queryKey: cklstArtclBaseKey }); + queryClient.invalidateQueries({ + queryKey: [...cklstArtclBaseKey, "page", { cklstSn }], + }); + queryClient.invalidateQueries({ + queryKey: [...cklstArtclBaseKey, "by-cklst", cklstSn], + }); + }, + }); +} + +export function useUpdateCklstArtcl() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ + cklstArtclSn, + cklstSn, + body, + }: { + cklstArtclSn: number; + cklstSn: number; + body: CklstArtclUpdateRequest; + }) => cklstArtclService.update(cklstArtclSn, cklstSn, body), + onSuccess: (_data, { cklstArtclSn, cklstSn }) => { + queryClient.invalidateQueries({ queryKey: cklstArtclBaseKey }); + queryClient.invalidateQueries({ + queryKey: [...cklstArtclBaseKey, cklstArtclSn, cklstSn], + }); + queryClient.invalidateQueries({ + queryKey: [...cklstArtclBaseKey, "page", { cklstSn }], + }); + queryClient.invalidateQueries({ + queryKey: [...cklstArtclBaseKey, "by-cklst", cklstSn], + }); + }, + }); +} + +export function useDeleteCklstArtcl() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ + cklstArtclSn, + cklstSn, + }: { + cklstArtclSn: number; + cklstSn: number; + }) => cklstArtclService.remove(cklstArtclSn, cklstSn), + onSuccess: (_data, { cklstSn }) => { + queryClient.invalidateQueries({ queryKey: cklstArtclBaseKey }); + queryClient.invalidateQueries({ + queryKey: [...cklstArtclBaseKey, "page", { cklstSn }], + }); + queryClient.invalidateQueries({ + queryKey: [...cklstArtclBaseKey, "by-cklst", cklstSn], + }); + }, + }); +} diff --git a/apps/as/src/entities/cklst/index.ts b/apps/as/src/entities/cklst/index.ts new file mode 100644 index 000000000..499003f90 --- /dev/null +++ b/apps/as/src/entities/cklst/index.ts @@ -0,0 +1,29 @@ +export { cklstArtclService } from "./api/cklst-artcl-service"; +export { cklstMngService } from "./api/cklst-mng-service"; +export { + useCklstArtcl, + useCklstArtclList, + useCklstArtclPage, + useCklstMng, + useCklstMngPage, + useCreateCklstArtcl, + useCreateCklstMng, + useDeleteCklstArtcl, + useDeleteCklstMng, + useUpdateCklstArtcl, + useUpdateCklstMng, +} from "./api/queries"; +export type { + CklstArtcl, + CklstArtclId, + CklstArtclSaveRequest, + CklstArtclSearchParams, + CklstArtclService, + CklstArtclUpdateRequest, +} from "./model/cklst-artcl-interface"; +export type { + CklstMng, + CklstMngSaveRequest, + CklstMngSearchParams, + CklstMngService, +} from "./model/cklst-mng-interface"; diff --git a/apps/as/src/entities/cklst/model/cklst-artcl-interface.ts b/apps/as/src/entities/cklst/model/cklst-artcl-interface.ts new file mode 100644 index 000000000..9d6e0e055 --- /dev/null +++ b/apps/as/src/entities/cklst/model/cklst-artcl-interface.ts @@ -0,0 +1,48 @@ +import type { PageableParams, PageResponse } from "@/shared/model"; + +export interface CklstArtclId { + cklstArtclSn: number; + cklstSn: number; +} + +export interface CklstArtcl { + cklstArtclSn: number; + cklstSn: number; + cklstArtclNm?: string; + useYn?: "Y" | "N"; + rgtr?: string; + rgtrNm?: string; + mdfr?: string; + regDt?: string; + mdfcnDt?: string; +} + +export interface CklstArtclSearchParams extends PageableParams { + cklstSn?: number; + cklstArtclNm?: string; + useYn?: "Y" | "N"; +} + +export interface CklstArtclSaveRequest { + cklstSn: number; + cklstArtclNm?: string; + useYn?: "Y" | "N"; +} + +export interface CklstArtclUpdateRequest { + cklstArtclNm?: string; + useYn?: "Y" | "N"; +} + +export interface CklstArtclService { + findPage(params: CklstArtclSearchParams): Promise>; + findByCklstSn(cklstSn: number): Promise; + findById(cklstArtclSn: number, cklstSn: number): Promise; + create(body: CklstArtclSaveRequest): Promise; + update( + cklstArtclSn: number, + cklstSn: number, + body: CklstArtclUpdateRequest, + ): Promise; + remove(cklstArtclSn: number, cklstSn: number): Promise; +} diff --git a/apps/as/src/entities/cklst/model/cklst-mng-interface.ts b/apps/as/src/entities/cklst/model/cklst-mng-interface.ts new file mode 100644 index 000000000..ad1302b00 --- /dev/null +++ b/apps/as/src/entities/cklst/model/cklst-mng-interface.ts @@ -0,0 +1,36 @@ +import type { PageableParams, PageResponse } from "@/shared/model"; + +export interface CklstMng { + cklstSn: number; + cklstNm: string; + taskSeComCd: string; + useYn?: "Y" | "N"; + docSeComCd?: string; + rgtr?: string; + rgtrNm?: string; + mdfr?: string; + regDt?: string; + mdfcnDt?: string; +} + +export interface CklstMngSearchParams extends PageableParams { + cklstNm?: string; + taskSeComCd?: string; + useYn?: "Y" | "N"; + docSeComCd?: string; +} + +export interface CklstMngSaveRequest { + cklstNm: string; + taskSeComCd: string; + useYn?: "Y" | "N"; + docSeComCd?: string; +} + +export interface CklstMngService { + findPage(params: CklstMngSearchParams): Promise>; + findById(cklstSn: number): Promise; + create(body: CklstMngSaveRequest): Promise; + update(cklstSn: number, body: CklstMngSaveRequest): Promise; + remove(cklstSn: number): Promise; +} diff --git a/apps/as/src/entities/code/api/code-service.ts b/apps/as/src/entities/code/api/code-service.ts index d2fb84f45..2545330dd 100644 --- a/apps/as/src/entities/code/api/code-service.ts +++ b/apps/as/src/entities/code/api/code-service.ts @@ -1,4 +1,4 @@ -import { client } from "@/shared/api/client"; +import { client } from "@/shared/api"; import { ENDPOINT } from "@/shared/config"; import type { Code, CodeService } from "../model/code-interface"; @@ -9,9 +9,7 @@ async function getList(upCd: string): Promise { } async function getListByUseYn(upCd: string, useYn: string): Promise { - const response = await client.get( - `${ENDPOINT.CMS_SERVICE.CODE}/${upCd}/${useYn}`, - ); + const response = await client.get(`${ENDPOINT.CMS_SERVICE.CODE}/${upCd}/${useYn}`); return response.data; } diff --git a/apps/as/src/entities/elapr/api/elapr-apln-service.ts b/apps/as/src/entities/elapr/api/elapr-apln-service.ts index 906b95de9..795ae8f77 100644 --- a/apps/as/src/entities/elapr/api/elapr-apln-service.ts +++ b/apps/as/src/entities/elapr/api/elapr-apln-service.ts @@ -1,4 +1,4 @@ -import { client } from "@/shared/api/client"; +import { client } from "@/shared/api"; import { ENDPOINT } from "@/shared/config"; import type { PageResponse } from "@/shared/model"; @@ -11,17 +11,14 @@ import type { } from "../model/elapr-apln-interface"; async function findPage(params: ElaprAplnListParams): Promise> { - const response = await client.get>( - ENDPOINT.AS_ELAPR_SERVICE.ELAPR_APLN, - { params }, - ); + const response = await client.get>(ENDPOINT.AS_ELAPR_SERVICE.ELAPR_APLN, { + params, + }); return response.data; } async function findById(aplnId: string): Promise { - const response = await client.get( - `${ENDPOINT.AS_ELAPR_SERVICE.ELAPR_APLN}/${aplnId}`, - ); + const response = await client.get(`${ENDPOINT.AS_ELAPR_SERVICE.ELAPR_APLN}/${aplnId}`); return response.data; } diff --git a/apps/as/src/entities/elapr/api/elapr-tmplt-service.ts b/apps/as/src/entities/elapr/api/elapr-tmplt-service.ts index c721ff9b9..a41299653 100644 --- a/apps/as/src/entities/elapr/api/elapr-tmplt-service.ts +++ b/apps/as/src/entities/elapr/api/elapr-tmplt-service.ts @@ -1,4 +1,4 @@ -import { client } from "@/shared/api/client"; +import { client } from "@/shared/api"; import { ENDPOINT } from "@/shared/config"; import type { PageResponse } from "@/shared/model"; @@ -26,17 +26,11 @@ async function findById(elaprTmpltId: string): Promise { } async function create(body: ElaprTmpltSaveRequest): Promise { - const response = await client.post( - ENDPOINT.AS_ELAPR_SERVICE.ELAPR_TMPLT, - body, - ); + const response = await client.post(ENDPOINT.AS_ELAPR_SERVICE.ELAPR_TMPLT, body); return response.data; } -async function update( - elaprTmpltId: string, - body: ElaprTmpltUpdateRequest, -): Promise { +async function update(elaprTmpltId: string, body: ElaprTmpltUpdateRequest): Promise { const response = await client.put( `${ENDPOINT.AS_ELAPR_SERVICE.ELAPR_TMPLT}/${elaprTmpltId}`, body, diff --git a/apps/as/src/features/aprvr-picker/index.ts b/apps/as/src/features/aprvr-picker/index.ts deleted file mode 100644 index 0b98531cc..000000000 --- a/apps/as/src/features/aprvr-picker/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { AprvrPickerDialog } from "./ui/aprvr-picker-dialog"; -export type { ReviewerOption } from "./model/mock-reviewers"; diff --git a/apps/as/src/features/elapr-tmplt-dialog/index.ts b/apps/as/src/features/elapr-tmplt-dialog/index.ts deleted file mode 100644 index 105850949..000000000 --- a/apps/as/src/features/elapr-tmplt-dialog/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { ElaprTmpltFormDialog } from "./ui/elapr-tmplt-form-dialog"; -export type { ElaprTmpltFormValues } from "./ui/elapr-tmplt-form-dialog"; diff --git a/apps/as/src/pages/approval-line-management/lib/assign-atrz-seq.ts b/apps/as/src/pages/approval-line-management/lib/assign-atrz-seq.ts new file mode 100644 index 000000000..6b5b63bd4 --- /dev/null +++ b/apps/as/src/pages/approval-line-management/lib/assign-atrz-seq.ts @@ -0,0 +1,6 @@ +import type { ElaprAplnRow } from "../model/apln-row-types"; + +/** 그리드 행 순서(1-based)로 결재순서번호 일괄 채번 */ +export function assignAtrzSeqNumbers(rows: ElaprAplnRow[]): ElaprAplnRow[] { + return rows.map((row, index) => ({ ...row, atrzSeqNo: index + 1 })); +} diff --git a/apps/as/src/widgets/elapr-apln-grid/lib/use-apln-rows.ts b/apps/as/src/pages/approval-line-management/lib/use-apln-rows.ts similarity index 78% rename from apps/as/src/widgets/elapr-apln-grid/lib/use-apln-rows.ts rename to apps/as/src/pages/approval-line-management/lib/use-apln-rows.ts index 719d5f907..26dda19be 100644 --- a/apps/as/src/widgets/elapr-apln-grid/lib/use-apln-rows.ts +++ b/apps/as/src/pages/approval-line-management/lib/use-apln-rows.ts @@ -6,7 +6,10 @@ import { arrayMove } from "@dnd-kit/sortable"; import type { ElaprApln } from "@/entities/elapr"; -import type { ElaprAplnRow } from "../model/types"; +import type { ElaprAplnRow } from "../model/apln-row-types"; + +import { assignAtrzSeqNumbers } from "./assign-atrz-seq"; +import { validateAtrzOrder } from "./validators"; function toRow(item: ElaprApln): ElaprAplnRow { return { ...item, rowKey: item.aplnId, isNew: false, isDirty: false }; @@ -41,14 +44,12 @@ export function useAplnRows({ elaprTmpltId, serverRows }: UseAplnRowsOptions) { if (!elaprTmpltId || isEditingLocked) return; const rowKey = createDraftRowKey(); - const nextSeq = rows.length + 1; setRows((prev) => [ ...prev, { rowKey, elaprTmpltId, - atrzSeqNo: nextSeq, atrzSeComCd: "", aprvrUuid: "", aprvrNm: "", @@ -57,7 +58,7 @@ export function useAplnRows({ elaprTmpltId, serverRows }: UseAplnRowsOptions) { }, ]); setEditingRowKey(rowKey); - }, [elaprTmpltId, isEditingLocked, rows.length]); + }, [elaprTmpltId, isEditingLocked]); const startEdit = useCallback( (rowKey: string) => { @@ -110,26 +111,36 @@ export function useAplnRows({ elaprTmpltId, serverRows }: UseAplnRowsOptions) { }, []); const reorderRows = useCallback( - (activeKey: string, overKey: string): ElaprAplnRow[] => { - if (isEditingLocked) return []; + ( + activeKey: string, + overKey: string, + ): { payloadRows?: ElaprAplnRow[]; orderError?: string | null } => { + if (isEditingLocked) return {}; - let newRows: ElaprAplnRow[] = []; + let payloadRows: ElaprAplnRow[] = []; + let orderError: string | null = null; setRows((prev) => { const oldIndex = prev.findIndex((r) => r.rowKey === activeKey); const newIndex = prev.findIndex((r) => r.rowKey === overKey); if (oldIndex < 0 || newIndex < 0) return prev; - const moved = arrayMove(prev, oldIndex, newIndex).map((row, idx) => ({ + const moved = assignAtrzSeqNumbers(arrayMove(prev, oldIndex, newIndex)).map((row) => ({ ...row, - atrzSeqNo: idx + 1, isDirty: true, })); - newRows = moved; + + const error = validateAtrzOrder(moved); + if (error) { + orderError = error; + return prev; + } + + payloadRows = moved; return moved; }); - return newRows; + return { payloadRows, orderError }; }, [isEditingLocked], ); diff --git a/apps/as/src/widgets/elapr-apln-grid/lib/use-dnd-sensors.ts b/apps/as/src/pages/approval-line-management/lib/use-dnd-sensors.ts similarity index 72% rename from apps/as/src/widgets/elapr-apln-grid/lib/use-dnd-sensors.ts rename to apps/as/src/pages/approval-line-management/lib/use-dnd-sensors.ts index f099a2fbd..7c4e82ba9 100644 --- a/apps/as/src/widgets/elapr-apln-grid/lib/use-dnd-sensors.ts +++ b/apps/as/src/pages/approval-line-management/lib/use-dnd-sensors.ts @@ -1,9 +1,4 @@ -import { - KeyboardSensor, - PointerSensor, - useSensor, - useSensors, -} from "@dnd-kit/core"; +import { KeyboardSensor, PointerSensor, useSensor, useSensors } from "@dnd-kit/core"; import { sortableKeyboardCoordinates } from "@dnd-kit/sortable"; export function useDndSensors() { diff --git a/apps/as/src/pages/approval-line-management/lib/validators.ts b/apps/as/src/pages/approval-line-management/lib/validators.ts new file mode 100644 index 000000000..a61de75b8 --- /dev/null +++ b/apps/as/src/pages/approval-line-management/lib/validators.ts @@ -0,0 +1,61 @@ +import type { ElaprAplnRow } from "../model/apln-row-types"; + +export const ATRZ_REVIEWER_CD = "ATRZ01"; +export const ATRZ_FINAL_CD = "ATRZ03"; + +/** 검토자 최상단 · 최종승인자 최하단 순서 검증 */ +export function validateAtrzOrder(rows: ElaprAplnRow[]): string | null { + const reviewerIndices = rows + .map((row, index) => (row.atrzSeComCd === ATRZ_REVIEWER_CD ? index : -1)) + .filter((index) => index >= 0); + + if (reviewerIndices.length > 0) { + const lastReviewerIndex = Math.max(...reviewerIndices); + const hasNonReviewerAbove = rows + .slice(0, lastReviewerIndex) + .some((row) => row.atrzSeComCd && row.atrzSeComCd !== ATRZ_REVIEWER_CD); + + if (hasNonReviewerAbove) { + return "검토자는 결재선 최상단에만 배치할 수 있습니다."; + } + } + + const finalIndices = rows + .map((row, index) => (row.atrzSeComCd === ATRZ_FINAL_CD ? index : -1)) + .filter((index) => index >= 0); + + if (finalIndices.length > 0) { + const firstFinalIndex = Math.min(...finalIndices); + const hasNonFinalBelow = rows + .slice(firstFinalIndex + 1) + .some((row) => row.atrzSeComCd && row.atrzSeComCd !== ATRZ_FINAL_CD); + + if (hasNonFinalBelow) { + return "최종승인자는 결재선 최하단에만 배치할 수 있습니다."; + } + } + + return null; +} + +export function validateAplnRow(row: ElaprAplnRow): string | null { + if (!row.atrzSeComCd?.trim()) { + return "결재구분을 선택해 주세요."; + } + if (!row.aprvrUuid?.trim()) { + return "결재자를 선택해 주세요."; + } + return null; +} + +export function validateUniqueSeqNo(rows: ElaprAplnRow[]): string | null { + const seqSet = new Set(); + for (const row of rows) { + if (row.atrzSeqNo == null) continue; + if (seqSet.has(row.atrzSeqNo)) { + return "동일 서식 내 결재순서번호가 중복됩니다."; + } + seqSet.add(row.atrzSeqNo); + } + return null; +} diff --git a/apps/as/src/widgets/elapr-apln-grid/model/types.ts b/apps/as/src/pages/approval-line-management/model/apln-row-types.ts similarity index 100% rename from apps/as/src/widgets/elapr-apln-grid/model/types.ts rename to apps/as/src/pages/approval-line-management/model/apln-row-types.ts diff --git a/apps/as/src/widgets/elapr-tmplt-list/model/columns.tsx b/apps/as/src/pages/approval-line-management/model/columns.tsx similarity index 100% rename from apps/as/src/widgets/elapr-tmplt-list/model/columns.tsx rename to apps/as/src/pages/approval-line-management/model/columns.tsx diff --git a/apps/as/src/features/aprvr-picker/model/mock-reviewers.ts b/apps/as/src/pages/approval-line-management/model/mock-reviewers.ts similarity index 100% rename from apps/as/src/features/aprvr-picker/model/mock-reviewers.ts rename to apps/as/src/pages/approval-line-management/model/mock-reviewers.ts diff --git a/apps/as/src/pages/approval-line-management/ui/approval-line-management-page.tsx b/apps/as/src/pages/approval-line-management/ui/approval-line-management-page.tsx index 96b0b0c17..11b9373ac 100644 --- a/apps/as/src/pages/approval-line-management/ui/approval-line-management-page.tsx +++ b/apps/as/src/pages/approval-line-management/ui/approval-line-management-page.tsx @@ -3,8 +3,9 @@ import { useState } from "react"; import type { ElaprTmplt } from "@/entities/elapr"; -import { ElaprAplnGrid } from "@/widgets/elapr-apln-grid"; -import { ElaprTmpltList } from "@/widgets/elapr-tmplt-list"; + +import { ElaprAplnGrid } from "./elapr-apln-grid"; +import { ElaprTmpltList } from "./elapr-tmplt-list"; function ApprovalLineManagementPage() { const [selectedTmplt, setSelectedTmplt] = useState(null); diff --git a/apps/as/src/features/aprvr-picker/ui/aprvr-picker-dialog.tsx b/apps/as/src/pages/approval-line-management/ui/aprvr-picker-dialog.tsx similarity index 88% rename from apps/as/src/features/aprvr-picker/ui/aprvr-picker-dialog.tsx rename to apps/as/src/pages/approval-line-management/ui/aprvr-picker-dialog.tsx index bb01c68e8..6f20a1db3 100644 --- a/apps/as/src/features/aprvr-picker/ui/aprvr-picker-dialog.tsx +++ b/apps/as/src/pages/approval-line-management/ui/aprvr-picker-dialog.tsx @@ -1,6 +1,9 @@ "use client"; import { useMemo, useState } from "react"; + +import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table"; + import { Button, DataTable, @@ -11,11 +14,6 @@ import { DialogTitle, Input, } from "@common/components"; -import { - createColumnHelper, - getCoreRowModel, - useReactTable, -} from "@tanstack/react-table"; import type { ReviewerOption } from "../model/mock-reviewers"; import { createMockReviewers } from "../model/mock-reviewers"; @@ -36,8 +34,7 @@ function AprvrPickerDialog({ open, onOpenChange, onSelect }: AprvrPickerDialogPr const q = keyword.trim().toLowerCase(); if (!q) return reviewers; return reviewers.filter( - (r) => - r.name.toLowerCase().includes(q) || r.deptNm.toLowerCase().includes(q), + (r) => r.name.toLowerCase().includes(q) || r.deptNm.toLowerCase().includes(q), ); }, [keyword, reviewers]); diff --git a/apps/as/src/widgets/elapr-apln-grid/ui/elapr-apln-grid.tsx b/apps/as/src/pages/approval-line-management/ui/elapr-apln-grid.tsx similarity index 75% rename from apps/as/src/widgets/elapr-apln-grid/ui/elapr-apln-grid.tsx rename to apps/as/src/pages/approval-line-management/ui/elapr-apln-grid.tsx index f8c7d1571..e3647d441 100644 --- a/apps/as/src/widgets/elapr-apln-grid/ui/elapr-apln-grid.tsx +++ b/apps/as/src/pages/approval-line-management/ui/elapr-apln-grid.tsx @@ -12,10 +12,9 @@ import { useElaprAplnPage, useUpdateElaprApln, } from "@/entities/elapr"; -import { AprvrPickerDialog, type ReviewerOption } from "@/features/aprvr-picker"; import { Button, - LoadingWithAnimation, + Skeleton, Table, TableBody, TableHead, @@ -23,11 +22,14 @@ import { TableRow, } from "@common/components"; +import { assignAtrzSeqNumbers } from "../lib/assign-atrz-seq"; import { useAplnRows } from "../lib/use-apln-rows"; import { useDndSensors } from "../lib/use-dnd-sensors"; -import { validateAplnRow, validateUniqueSeqNo } from "../lib/validators"; -import type { ElaprAplnRow } from "../model/types"; +import { validateAplnRow, validateAtrzOrder } from "../lib/validators"; +import type { ElaprAplnRow } from "../model/apln-row-types"; +import type { ReviewerOption } from "../model/mock-reviewers"; +import { AprvrPickerDialog } from "./aprvr-picker-dialog"; import { SortableAplnRow } from "./sortable-apln-row"; interface ElaprAplnGridProps { @@ -73,7 +75,15 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) { const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; if (!over || active.id === over.id) return; - reorderRows(String(active.id), String(over.id)); + + const { orderError } = reorderRows(String(active.id), String(over.id)); + if (orderError) { + alert(orderError); + } + }; + + const handleRowChange = (row: ElaprAplnRow, patch: Partial) => { + updateRow(row.rowKey, patch); }; const handleRowSave = async (row: ElaprAplnRow) => { @@ -82,29 +92,34 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) { alert(fieldError); return; } - const dupError = validateUniqueSeqNo(rows.map((r) => (r.rowKey === row.rowKey ? row : r))); - if (dupError) { - alert(dupError); + + const rowsWithSeq = assignAtrzSeqNumbers(rows.map((r) => (r.rowKey === row.rowKey ? row : r))); + const savedRow = rowsWithSeq.find((r) => r.rowKey === row.rowKey); + if (!savedRow) return; + + const orderError = validateAtrzOrder(rowsWithSeq); + if (orderError) { + alert(orderError); return; } if (!elaprTmpltId) return; - if (row.isNew) { + if (savedRow.isNew) { await createMutation.mutateAsync({ elaprTmpltId, - aprvrUuid: row.aprvrUuid, - atrzSeComCd: row.atrzSeComCd, - atrzSeqNo: row.atrzSeqNo, + aprvrUuid: savedRow.aprvrUuid, + atrzSeComCd: savedRow.atrzSeComCd, + atrzSeqNo: savedRow.atrzSeqNo, }); - } else if (row.aplnId) { + } else if (savedRow.aplnId) { await updateMutation.mutateAsync({ - aplnId: row.aplnId, + aplnId: savedRow.aplnId, body: { elaprTmpltId, - aprvrUuid: row.aprvrUuid, - atrzSeComCd: row.atrzSeComCd, - atrzSeqNo: row.atrzSeqNo, + aprvrUuid: savedRow.aprvrUuid, + atrzSeComCd: savedRow.atrzSeComCd, + atrzSeqNo: savedRow.atrzSeqNo, }, }); } @@ -130,13 +145,14 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) { const handleBulkSave = async () => { if (!elaprTmpltId || isEditingLocked) return; - const dupError = validateUniqueSeqNo(rows); - if (dupError) { - alert(dupError); + const rowsWithSeq = assignAtrzSeqNumbers(rows); + const orderError = validateAtrzOrder(rowsWithSeq); + if (orderError) { + alert(orderError); return; } - const dirtyRows = rows.filter((r) => r.isDirty || r.isNew); + const dirtyRows = rowsWithSeq.filter((r) => r.isDirty || r.isNew); for (const row of dirtyRows) { const fieldError = validateAplnRow(row); @@ -198,10 +214,9 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) { {aplnQuery.isPending && elaprTmpltId ? ( -
-
- -
+
+ +
) : !elaprTmpltId ? (

@@ -230,18 +245,19 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) { items={rows.map((r) => r.rowKey)} strategy={verticalListSortingStrategy} > - {rows.map((row) => ( + {rows.map((row, index) => ( startEdit(row.rowKey)} onSave={() => handleRowSave(row)} onCancel={cancelEdit} onDelete={() => handleRowDelete(row)} - onChange={(patch) => updateRow(row.rowKey, patch)} + onChange={(patch) => handleRowChange(row, patch)} onPickAprvr={() => { setPickingRowKey(row.rowKey); setPickerOpen(true); diff --git a/apps/as/src/features/elapr-tmplt-dialog/ui/elapr-tmplt-form-dialog.tsx b/apps/as/src/pages/approval-line-management/ui/elapr-tmplt-form-dialog.tsx similarity index 88% rename from apps/as/src/features/elapr-tmplt-dialog/ui/elapr-tmplt-form-dialog.tsx rename to apps/as/src/pages/approval-line-management/ui/elapr-tmplt-form-dialog.tsx index 79053b026..3e7f36de1 100644 --- a/apps/as/src/features/elapr-tmplt-dialog/ui/elapr-tmplt-form-dialog.tsx +++ b/apps/as/src/pages/approval-line-management/ui/elapr-tmplt-form-dialog.tsx @@ -41,13 +41,14 @@ function ElaprTmpltFormDialog({ isSubmitting, onSubmit, }: ElaprTmpltFormDialogProps) { - const { register, handleSubmit, reset, setValue, watch } = useForm({ - defaultValues: { - elaprTmpltNm: "", - docSeComCd: "", - elaprTmpltExpln: "", - }, - }); + const { register, handleSubmit, reset, getValues, setValue, watch } = + useForm({ + defaultValues: { + elaprTmpltNm: "", + docSeComCd: "", + elaprTmpltExpln: "", + }, + }); const docSeComCd = watch("docSeComCd"); @@ -81,7 +82,7 @@ function ElaprTmpltFormDialog({ {mode === "edit" ? (

- +

{getValues("elaprTmpltId")}

) : null}
diff --git a/apps/as/src/widgets/elapr-tmplt-list/ui/elapr-tmplt-list.tsx b/apps/as/src/pages/approval-line-management/ui/elapr-tmplt-list.tsx similarity index 96% rename from apps/as/src/widgets/elapr-tmplt-list/ui/elapr-tmplt-list.tsx rename to apps/as/src/pages/approval-line-management/ui/elapr-tmplt-list.tsx index 9145bdc59..ae2b97e4c 100644 --- a/apps/as/src/widgets/elapr-tmplt-list/ui/elapr-tmplt-list.tsx +++ b/apps/as/src/pages/approval-line-management/ui/elapr-tmplt-list.tsx @@ -12,11 +12,12 @@ import { useElaprTmpltPage, useUpdateElaprTmplt, } from "@/entities/elapr"; -import { ElaprTmpltFormDialog, type ElaprTmpltFormValues } from "@/features/elapr-tmplt-dialog"; import { Button } from "@common/components"; import { createTmpltColumns } from "../model/columns"; +import { ElaprTmpltFormDialog, type ElaprTmpltFormValues } from "./elapr-tmplt-form-dialog"; + interface ElaprTmpltListProps { selectedTmpltId: string | null; onSelectTmplt: (tmplt: ElaprTmplt | null) => void; diff --git a/apps/as/src/widgets/elapr-apln-grid/ui/sortable-apln-row.tsx b/apps/as/src/pages/approval-line-management/ui/sortable-apln-row.tsx similarity index 83% rename from apps/as/src/widgets/elapr-apln-grid/ui/sortable-apln-row.tsx rename to apps/as/src/pages/approval-line-management/ui/sortable-apln-row.tsx index f05d6ee64..c9fc44af6 100644 --- a/apps/as/src/widgets/elapr-apln-grid/ui/sortable-apln-row.tsx +++ b/apps/as/src/pages/approval-line-management/ui/sortable-apln-row.tsx @@ -5,12 +5,13 @@ import { CSS } from "@dnd-kit/utilities"; import { Equal } from "lucide-react"; import { CodeSelect, findCodeLabel, useCodeList } from "@/entities/code"; -import { Button, Input, TableCell, TableRow } from "@common/components"; +import { Button, TableCell, TableRow } from "@common/components"; -import type { ElaprAplnRow } from "../model/types"; +import type { ElaprAplnRow } from "../model/apln-row-types"; interface SortableAplnRowProps { row: ElaprAplnRow; + displaySeqNo: number; isEditing: boolean; isLocked: boolean; dragDisabled: boolean; @@ -24,6 +25,7 @@ interface SortableAplnRowProps { function SortableAplnRow({ row, + displaySeqNo, isEditing, isLocked, dragDisabled, @@ -66,21 +68,7 @@ function SortableAplnRow({ {row.isNew ? "-" : (row.aplnId ?? "-")} - - {isEditing ? ( - - onChange({ atrzSeqNo: Number.parseInt(e.target.value, 10) || undefined }) - } - /> - ) : ( - (row.atrzSeqNo ?? "-") - )} - + {displaySeqNo} {isEditing ? ( { dataTableOptions?: DataTableOptions; } -export function useDataTable({ - query, - tableOptions, - dataTableOptions, -}: UseDataTableOptions) { +export function useDataTable({ query, tableOptions, dataTableOptions }: UseDataTableOptions) { const table = useReactTable({ ...tableOptions, data: query.data?.content ?? [], @@ -34,7 +31,12 @@ export function useDataTable({ return { tableComponent: matchQueryStatus(query, { - Pending:

로딩 중…

, + Pending: ( +
+ + +
+ ), Empty: , Success: () => , }), diff --git a/apps/as/src/widgets/elapr-apln-grid/index.ts b/apps/as/src/widgets/elapr-apln-grid/index.ts deleted file mode 100644 index 1d52721a5..000000000 --- a/apps/as/src/widgets/elapr-apln-grid/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ElaprAplnGrid } from "./ui/elapr-apln-grid"; diff --git a/apps/as/src/widgets/elapr-apln-grid/lib/validators.ts b/apps/as/src/widgets/elapr-apln-grid/lib/validators.ts deleted file mode 100644 index 7618c3754..000000000 --- a/apps/as/src/widgets/elapr-apln-grid/lib/validators.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { ElaprAplnRow } from "../model/types"; - -export function validateAplnRow(row: ElaprAplnRow): string | null { - if (row.atrzSeqNo == null || Number.isNaN(row.atrzSeqNo)) { - return "결재순서번호를 입력해 주세요."; - } - if (!row.atrzSeComCd?.trim()) { - return "결재구분을 선택해 주세요."; - } - if (!row.aprvrUuid?.trim()) { - return "결재자를 선택해 주세요."; - } - return null; -} - -export function validateUniqueSeqNo(rows: ElaprAplnRow[]): string | null { - const seqSet = new Set(); - for (const row of rows) { - if (row.atrzSeqNo == null) continue; - if (seqSet.has(row.atrzSeqNo)) { - return "동일 서식 내 결재순서번호가 중복됩니다."; - } - seqSet.add(row.atrzSeqNo); - } - return null; -} diff --git a/apps/as/src/widgets/elapr-tmplt-list/index.ts b/apps/as/src/widgets/elapr-tmplt-list/index.ts deleted file mode 100644 index 6cbd9634d..000000000 --- a/apps/as/src/widgets/elapr-tmplt-list/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ElaprTmpltList } from "./ui/elapr-tmplt-list";