diff --git a/.gitignore b/.gitignore index ee2cfd052..871b209f0 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,5 @@ settings.json .agent apps/**/tsconfig.tsbuildinfo +packages/**/tsconfig.tsbuildinfo logs/ diff --git a/apps/as/app/(with-base-layout)/instl-sncn-old/[id]/modify/page.tsx b/apps/as/app/(with-base-layout)/instl-sncn-old/[id]/modify/page.tsx new file mode 100644 index 000000000..baf4de7af --- /dev/null +++ b/apps/as/app/(with-base-layout)/instl-sncn-old/[id]/modify/page.tsx @@ -0,0 +1,6 @@ +import { CnfmModifyPage } from "@/pages/instl-sncn-old"; + +export default async function InstlSncnModifyPage({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + return ; +} diff --git a/apps/as/app/(with-base-layout)/instl-sncn-old/[id]/page.tsx b/apps/as/app/(with-base-layout)/instl-sncn-old/[id]/page.tsx new file mode 100644 index 000000000..92769d3ef --- /dev/null +++ b/apps/as/app/(with-base-layout)/instl-sncn-old/[id]/page.tsx @@ -0,0 +1,6 @@ +import { CnfmViewPage } from "@/pages/instl-sncn-old"; + +export default async function InstlSncnViewPage({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + return ; +} diff --git a/apps/as/app/(with-base-layout)/instl-sncn-old/page.tsx b/apps/as/app/(with-base-layout)/instl-sncn-old/page.tsx new file mode 100644 index 000000000..6c1e37f16 --- /dev/null +++ b/apps/as/app/(with-base-layout)/instl-sncn-old/page.tsx @@ -0,0 +1,3 @@ +import { CnfmListPage } from "@/pages/instl-sncn-old"; + +export default CnfmListPage; diff --git a/apps/as/app/(with-base-layout)/instl-sncn-old/register/page.tsx b/apps/as/app/(with-base-layout)/instl-sncn-old/register/page.tsx new file mode 100644 index 000000000..82a838f8a --- /dev/null +++ b/apps/as/app/(with-base-layout)/instl-sncn-old/register/page.tsx @@ -0,0 +1,5 @@ +import { CnfmRegistPage } from "@/pages/instl-sncn-old"; + +export default function Page(props: any) { + return ; +} diff --git a/apps/as/app/(with-base-layout)/instl-sncn/[id]/edit/page.tsx b/apps/as/app/(with-base-layout)/instl-sncn/[id]/edit/page.tsx new file mode 100644 index 000000000..35d336bd3 --- /dev/null +++ b/apps/as/app/(with-base-layout)/instl-sncn/[id]/edit/page.tsx @@ -0,0 +1,10 @@ +import { InstlEditPage } from "@/pages/instl-sncn"; + +interface InstlSncnEditRouteProps { + params: Promise<{ id: string }>; +} + +export default async function Page({ params }: InstlSncnEditRouteProps) { + const { id } = await params; + return ; +} diff --git a/apps/as/app/(with-base-layout)/instl-sncn/[id]/modify/page.tsx b/apps/as/app/(with-base-layout)/instl-sncn/[id]/modify/page.tsx deleted file mode 100644 index e4718eceb..000000000 --- a/apps/as/app/(with-base-layout)/instl-sncn/[id]/modify/page.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { CnfmModifyPage } from "@/pages/instl-sncn"; - -export default async function InstlSncnModifyPage({ - params, -}: { - params: Promise<{ id: string }>; -}) { - const { id } = await params; - return ; -} diff --git a/apps/as/app/(with-base-layout)/instl-sncn/[id]/page.tsx b/apps/as/app/(with-base-layout)/instl-sncn/[id]/page.tsx index 3f25c4a3e..89cd7a329 100644 --- a/apps/as/app/(with-base-layout)/instl-sncn/[id]/page.tsx +++ b/apps/as/app/(with-base-layout)/instl-sncn/[id]/page.tsx @@ -1,10 +1,6 @@ -import { CnfmViewPage } from "@/pages/instl-sncn"; - -export default async function InstlSncnViewPage({ - params, -}: { - params: Promise<{ id: string }>; -}) { - const { id } = await params; - return ; -} +import { InstlViewPage } from "@/pages/instl-sncn"; + +export default async function InstlSncnDetailPage({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + return ; +} diff --git a/apps/as/app/(with-base-layout)/instl-sncn/page.tsx b/apps/as/app/(with-base-layout)/instl-sncn/page.tsx index 7cd7bf82e..a3d6e05ac 100644 --- a/apps/as/app/(with-base-layout)/instl-sncn/page.tsx +++ b/apps/as/app/(with-base-layout)/instl-sncn/page.tsx @@ -1,3 +1,3 @@ -import { CnfmListPage } from "@/pages/instl-sncn"; - -export default CnfmListPage; +import { InstlListPage } from "@/pages/instl-sncn"; + +export default InstlListPage; diff --git a/apps/as/app/(with-base-layout)/instl-sncn/regist/page.tsx b/apps/as/app/(with-base-layout)/instl-sncn/regist/page.tsx new file mode 100644 index 000000000..476d0b7c2 --- /dev/null +++ b/apps/as/app/(with-base-layout)/instl-sncn/regist/page.tsx @@ -0,0 +1,14 @@ +import { InstlRegistPage } from "@/pages/instl-sncn"; + +interface InstlSncnRegistRouteProps { + searchParams: Promise<{ taskSeComCd?: string }>; +} + +export default async function Page({ searchParams }: InstlSncnRegistRouteProps) { + const params = await searchParams; + const taskSeComCd = params.taskSeComCd + ? params.taskSeComCd.split(",").filter(Boolean) + : undefined; + + return ; +} diff --git a/apps/as/app/(with-base-layout)/instl-sncn/register/page.tsx b/apps/as/app/(with-base-layout)/instl-sncn/register/page.tsx deleted file mode 100644 index ffe6a71a4..000000000 --- a/apps/as/app/(with-base-layout)/instl-sncn/register/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { CnfmRegistPage } from "@/pages/instl-sncn"; - -export default function Page(props: any) { - return ; -} diff --git a/apps/as/app/(with-base-layout)/rvw-mnpw-mng/page.tsx b/apps/as/app/(with-base-layout)/rvw-mnpw-mng/page.tsx new file mode 100644 index 000000000..01cf17fb5 --- /dev/null +++ b/apps/as/app/(with-base-layout)/rvw-mnpw-mng/page.tsx @@ -0,0 +1 @@ +export { RvwMnpwMngPage as default } from "@/pages/rvw-mnpw-mng"; diff --git a/apps/as/docs/review-manpower-management/SPEC.md b/apps/as/docs/review-manpower-management/SPEC.md new file mode 100644 index 000000000..be0211783 --- /dev/null +++ b/apps/as/docs/review-manpower-management/SPEC.md @@ -0,0 +1,370 @@ +# 화면설계서: 검토인력관리 + +## 1. 기본 정보 + +| 항목 | 내용 | +| ---------- | -------------------------------------------- | +| 버전 | 0.2 | +| 프로세스명 | UI설계 | +| 화면명 | 검토인력관리 | +| 화면 유형 | [v] 조회 [v] 입력 [ ] 수정 [v] 삭제 [ ] 출력 | +| 메뉴 경로 | 디지털행정지원 > 사이트관리 > 검토인력관리 | + +--- + +## 2. 화면 목적 + +조직도에서 **한 명의 사용자**를 선택한 뒤, 결재 단계(접수·검토·승인·최종승인)별 **인원 배치** 영역에 즉시 등록한다. 등록·목록 갱신은 별도 저장 버튼 없이 **[선택 인원 배치]** 클릭 시 `rvw-mnpw` API로 바로 반영한다. 좌측 조직도는 CMS 조직·사용자 트리 API를, 우측 배치 목록은 검토인력관리 API를 사용한다. + +--- + +## 3. 화면 구성 + +### 3-1. 전체 레이아웃 + +``` +┌──────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ 페이지 타이틀: 검토인력관리 브레드크럼: 디지털행정지원 > 사이트관리 > 검토인력관리 │ +├─────────────────────────────────┬──────────────────────────────────────────────────────────────────┤ +│ [영역 A] 조직도 │ [영역 B] 인원 배치 (결재구분별 4블록, 세로 스택) │ +│ 제목: 조직도 │ │ +│ [A-1] 검색: │ ┌─ [B-1] 접수 ───────────── count ─ [선택 인원 배치] ─┐ │ +│ [성명·부서 검색] [조회][초기화]│ │ (배치 목록 또는 빈 상태 문구) │ │ +│ 선택 안내: │ └──────────────────────────────────────────────────────┘ │ +│ "선택된 인원: {이름} │ ┌─ [B-2] 검토 ───────────── count ─ [선택 인원 배치] ──┐ │ +│ ({부서} / {직급})" │ └──────────────────────────────────────────────────────┘ │ +│ [A-2] 조직 트리 + 사용자 행 │ ┌─ [B-3] 승인 ───────────── count ─ [선택 인원 배치] ──┐ │ +│ ┌────────────────────┐ │ └──────────────────────────────────────────────────────┘ │ +│ │ 조직 트리 + 사용자 │ │ ┌─ [B-4] 최종승인 ───────── count ─ [선택 인원 배치] ──┐ │ +│ │ (단일 선택 강조) │ │ └──────────────────────────────────────────────────────┘ │ +│ └────────────────────┘ │ │ +└─────────────────────────────────┴──────────────────────────────────────────────────────────────────┘ +``` + +### 3-2. 화면 데이터 흐름 + +``` +BEGIN + + // 최초 진입 + CALL userOgnzService.findOgnzUserTree() + DISPLAY 조직도 트리 (영역 A) + + CALL rvwMnpwMngService.findPage({ size: 충분히 큰 값 }) // 또는 atrzSeComCd별 분할 조회 + GROUP BY atrzSeComCd + DISPLAY 인원 배치 4블록 (영역 B-1 ~ B-4) + 건수 뱃지 + + // 영역 A-1: 조직도 검색 (클라이언트 필터 — 서버 API 없음) + ON [조회] 버튼 CLICK / 검색 폼 SUBMIT DO + SET displayTree = filterOgnzUserTree(원본트리, keyword) + SET expandedOgnzSnIds = collectExpandedOgnzSnIds(displayTree) // 매칭 노드 자동 펼침 + DISPLAY 필터된 트리 (영역 A-2) + END ON + + ON [초기화] 버튼 CLICK DO + RESET keyword = "" + RESET displayTree = 원본트리 + RESET expandedOgnzSnIds = 빈 Set (또는 기본 접힘) + DISPLAY 전체 트리 (영역 A-2) + END ON + + // 영역 A-2: 사용자 단일 선택 + ON 조직도 사용자 행 CLICK DO + VALIDATE 동일 행 재클릭이 아닌 경우만 변경 + SET selectedPerson = { userUuid, userNm, ognzSn, ognzNm, jbgdSeCd, ... } + DISPLAY "선택된 인원: {userNm} ({ognzNm} / {직급명})" + END ON + + // 영역 B-k: 선택 인원 즉시 배치 + ON [B-k] [선택 인원 배치] 버튼 CLICK DO + VALIDATE selectedPerson 존재 + SET targetAtrzSeComCd = 블록 k 에 매핑된 결재구분 코드 (§4-2 표) + CALL rvwMnpwMngService.save({ + atrzSeComCd: targetAtrzSeComCd, + ognzSn: selectedPerson.ognzSn, + userUuid: selectedPerson.userUuid + }) + ON 성공 DO + CALL rvwMnpwMngService.findPage (또는 해당 블록 목록만 갱신) + DISPLAY [B-k] 배치 목록·건수 뱃지 갱신 + TOAST "배치되었습니다." + END ON + ON 실패(이미 존재) DO + TOAST (백엔드 메시지 연동) + END ON + END ON + + // 영역 B-k: 배치 인원 제거 (UI 상 삭제 UX — 스크린샷 미표기, API는 제공됨) + ON 배치 인원 [삭제] / [제거] CLICK DO + CONFIRM "해당 인원을 배치에서 제거하시겠습니까?" + CALL rvwMnpwMngService.delete(atrzSeComCd, ognzSn, userUuid) + CALL rvwMnpwMngService.findPage + DISPLAY [B-k] 목록·뱃지 갱신 + END ON + +END +``` + +--- + +## 4. 영역 A — 조직도 + +### 4-1. 표시·상호작용 + +| 요소 | 내용 | +| ---------------- | --------------------------------------------------------------------- | +| 섹션 제목 | 조직도 | +| 검색 폼 (A-1) | 성명·부서 키워드 입력 + **[조회]** + **[초기화]** — §4-3 | +| 선택 안내 문구 | `선택된 인원: {userNm} ({ognzNm} / {직급표시})` — 미선택 시 안내 문구 | +| 트리 (A-2) | 본부 → 팀 계층 + 팀 소속 사용자 목록 (`displayTree` 기준 표시) | +| 선택 규칙 | **사용자 1명만** 선택 가능 (라디오형 단일 선택) | +| 선택 시각 피드백 | 선택된 사용자 행 강조(배경·테두리) | +| 로딩/오류/빈값 | Skeleton / 오류 문구 / "등록된 조직/사용자가 없습니다." | +| 검색 결과 없음 | 필터 후 매칭 노드·사용자 0건. "조회된 인원이 없습니다." 문구 노출 | + +### 4-2. 조직도 데이터 매핑 + +| UI 표시 | 트리 DTO 필드 (`OgnzUserTreeResponseDto`) | 비고 | +| --------------- | ----------------------------------------- | ----------------------------------------- | +| 조직명 | `ognzNm` | 접기/펼치기 노드 | +| 사용자명 | `users[].userNm` | 클릭 시 선택 | +| 부서(안내 문구) | `ognzNm` (소속 팀 노드) | 선택 시 `selectedPerson.ognzNm` | +| 직급(안내 문구) | `users[].jbgdSeCd` 또는 `jbpsSeCd` | 공통코드 라벨 변환 | +| 저장용 조직 SN | `ognzSn` | 사용자가 속한 **리프/팀 노드**의 `ognzSn` | +| 저장용 사용자 | `users[].userUnqId` | API body `userUuid`로 전달 (§6-3) | + +### 4-3. 조직도 검색 (영역 A-1) + +결재선 관리 `AprvrPickerDialog`와 동일한 **클라이언트 사이드 필터** 패턴을 사용한다. 별도 검색 API는 없으며, `useOgnzUserTree`로 받은 **원본 트리**를 `filterOgnzUserTree`로 가공해 `displayTree`에 반영한다. + +#### 필드 정의 + +| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 | +| -------------- | --------- | ---- | ------ | ----------------- | ----------------------------------------- | +| 성명·부서 검색 | Text | N | (빈값) | — (클라이언트만) | `placeholder="성명·부서 검색"` | +| 조회 | Button | — | — | — | `type="submit"` — 폼 제출 시 필터 적용 | +| 초기화 | Button | — | — | — | `type="button"` — 키워드·트리·펼침 초기화 | + +#### 필터 규칙 (`filterOgnzUserTree`) + +| 대상 | 매칭 조건 | +| ---------------- | ----------------------------------------------------- | +| 조직명 | `ognzNm`에 키워드 포함 (대소문자 무시) | +| 사용자명 | `userNm`에 키워드 포함 (대소문자 무시) | +| 조직 매칭 시 | 해당 노드 **소속 사용자 전원** 유지 | +| 사용자만 매칭 시 | 매칭 사용자만 남기고 상위 조직 노드는 유지(경로 보존) | +| 하위 조직 | 재귀 필터 — 자식·손자 노드 매칭 시 상위도 표시 | +| 키워드 빈값 | 원본 트리 전체 반환 | + +#### 조회 후 트리 펼침 (`collectExpandedOgnzSnIds`) + +- **[조회]** 성공 후 필터 결과 트리의 **모든 노드** `ognzSn`을 `expandedOgnzSnIds`에 넣어 자동 펼침 +- 검색 결과에서 사용자·하위 조직을 바로 확인할 수 있도록 함 + +#### 초기화 동작 + +| 상태 | 초기화 후 값 | +| ------------------- | --------------------------------------- | +| `keyword` | `""` | +| `displayTree` | `useOgnzUserTree` 원본 `data` | +| `expandedOgnzSnIds` | `new Set()` (전체 접힘) | +| `selectedPerson` | **유지** — 검색 초기화와 선택 해제 분리 | + +#### 버튼 + +| 버튼명 | 동작 | +| ------ | --------------------------------------------------------- | +| 조회 | `filterOgnzUserTree(data, keyword)` → `displayTree` 갱신 | +| 초기화 | 키워드·표시 트리·펼침 상태 리셋 (§3-2 `ON [초기화]` 블록) | + +--- + +## 5. 영역 B — 인원 배치 + +### 5-1. 블록 공통 + +| 요소 | 내용 | +| ------------------ | ------------------------------------------------- | +| 블록 제목 | 접수 / 검토 / 승인 / 최종승인 | +| 건수 뱃지 | 해당 `atrzSeComCd`에 배치된 인원 수 | +| 액션 버튼 | **[선택 인원 배치]** — 클릭 시 즉시 `save` (§3-2) | +| 빈 상태 | "배치된 인원이 없습니다." | +| 배치 목록(있을 때) | 인원명·부서·직급 등 | + +### 5-2. 결재구분 ↔ 블록 매핑 + +| 블록 | UI 제목 | `atrzSeComCd` | 비고 | +| ---- | -------- | ------------- | -------------------------------------------------------------- | +| B-1 | 접수 | `ATRZ00` | 결재선 관리 `ATRZ_RECEIVER_CD`와 동일 | +| B-2 | 검토 | `ATRZ01` | 결재선 `ATRZ_REVIEWER_1_CD`. `ATRZ02` 별도 블록 여부 — **TBD** | +| B-3 | 승인 | `ATRZ03` | 결재선 `ATRZ_APPROVER_CD` | +| B-4 | 최종승인 | `ATRZ99` | 결재선 `ATRZ_FINAL_CD` | + +공통코드 마스터 `ATRZSE` / 화면 셀렉트용 `ATRZSECD`와 정합성은 배포 환경 코드표로 최종 확인. + +### 5-3. 배치 목록 항목 (표시) + +| 표시명 | Field (응답 DTO) | 비고 | +| -------- | ---------------- | ---------------------------- | +| 사용자명 | `userUuidNm` | `@CodeName` 미적용 시 TBD | +| 결재구분 | `atrzSeComCdNm` | 블록 제목과 중복 — 생략 가능 | +| 조직 SN | `ognzSn` | 삭제 API path에 사용 | +| 사용자 | `userUuid` | 삭제 API path에 사용 | + +### 5-4. 버튼 + +| 버튼명 | 위치 | 동작 | +| -------------- | ----------- | --------------------------------- | +| 선택 인원 배치 | 각 B-k 헤더 | `save` 후 목록·뱃지 갱신 (§3-2) | +| 제거 | 목록 항목 | `delete` 후 목록·뱃지 갱신 (§3-2) | + +### 5-5. 검증·제약 + +| 규칙 | 내용 | +| ------------- | ------------------------------------------------------------------- | +| 선택 필수 | `[선택 인원 배치]` 시 `selectedPerson` 없으면 버튼 비활성 또는 안내 | +| 단일 선택 | 조직도에서 사용자 1명만 `selectedPerson` 유지 | +| 중복 등록 | 동일 PK `(atrzSeComCd, ognzSn, userUuid)` 재등록 시 서버 오류 | +| 다건 배치 | 동일 블록에 서로 다른 사용자·조직은 **복수 행** 가능 (PK가 다름) | +| 결재구분 변경 | 본 화면 주 흐름은 `save`만 | + +--- + +## 6. API/DTO 명세 + +### 6-1. 공통 + +| 항목 | 내용 | +| ------------- | ------------------------------------------------------------- | +| 인증 | JWT (`Authorization` 헤더) | +| 페이징 | Spring `Pageable`: `page`, `size`, `sort` → `PageResponse` | +| 프론트 엔티티 | `@/entities/user-organization`, `@/entities/rvw-mnpw` | + +### 6-2. 조직·사용자 트리 (영역 A) — `user-ognz` + +| 항목 | 내용 | +| ----------- | ------------------------------- | +| 서비스 | `cms-service` | +| Base prefix | `/cms-service/api/v1/user-ognz` | + +| 기능 | Method | URL | Request DTO | Response DTO | API | +| ---------------- | ------ | ----------- | ----------- | --------------------------- | ---------------------------------- | +| 조직·사용자 트리 | GET | `/treeuser` | — | `OgnzUserTreeResponseDto[]` | `userOgnzService.findOgnzUserTree` | + +**`OgnzUserTreeResponseDto`**: `ognzSn`, `upOgnzSn`, `ognzNm`, `users[]`, `children[]` + +**`OgnzUserTreeUserDto`**: `userUnqId`, `userNm`, `jbpsSeCd`, `jbgdSeCd` + +**React Query**: `useOgnzUserTree` + +### 6-3. 검토인력관리 (영역 B) — `rvw-mnpw` + +| 항목 | 내용 | +| ----------- | ------------------------------------- | +| 서비스 | `as-service` | +| Base prefix | `/as-service/api/v1/rvw-mnpw` | +| ENDPOINT | `ENDPOINT.AS_COMMON_SERVICE.RVW_MNPW` | + +| 기능 | Method | URL | Request DTO / Query | Response DTO | API | +| --------------- | ------ | ------------------------------------ | -------------------------------- | ----------------------------- | ---------------------------- | +| 목록 조회 | GET | `/` | `RvwMnpwMngSearchDto` + Pageable | `Page` | `rvwMnpwMngService.findPage` | +| 상세 조회 | GET | `/{atrzSeComCd}/{ognzSn}/{userUuid}` | Path | `RvwMnpwMngResponseDto` | `rvwMnpwMngService.findById` | +| 등록(즉시 배치) | POST | `/` | `RvwMnpwMngSaveRequestDto` | `RvwMnpwMngResponseDto` (201) | `rvwMnpwMngService.save` | +| 결재구분 변경 | PUT | `/{atrzSeComCd}/{ognzSn}/{userUuid}` | `RvwMnpwMngUpdateRequestDto` | `RvwMnpwMngResponseDto` | `rvwMnpwMngService.update` | +| 삭제 | DELETE | `/{atrzSeComCd}/{ognzSn}/{userUuid}` | Path | `string` | `rvwMnpwMngService.delete` | + +**`RvwMnpwMngSearchDto`**: `atrzSeComCd?`, `ognzSn?`, `userUuid?` + `page`, `size`, `sort` + +**`RvwMnpwMngSaveRequestDto`**: `atrzSeComCd*`, `ognzSn*`, `userUuid*` + +**`RvwMnpwMngUpdateRequestDto`**: `newAtrzSeComCd*` — 본 화면 기본 흐름에서는 미사용 + +**`RvwMnpwMngResponseDto`**: `atrzSeComCd`, `atrzSeComCdNm?`, `ognzSn`, `userUuid`, `userUuidNm?`, `rgtr?`, `regDt?` + +### 6-4. 조직도 ↔ 검토인력 필드 매핑 (저장 시) + +| 조직도 (`OgnzUserTreeUserDto` + 노드) | `RvwMnpwMngSaveRequestDto` | 비고 | +| ------------------------------------- | -------------------------- | --------------------- | +| (블록 k의 `atrzSeComCd`) | `atrzSeComCd` | §5-2 표 | +| 소속 팀 `ognzSn` | `ognzSn` | | +| `userUnqId` | `userUuid` | 문자열(UUID)로 직렬화 | + +--- + +## 7. UI 구현 가이드 + +### 7-1. 기술 스택·원칙 + +- **공통 컴포넌트**: `@common/components` (`Button`, `Badge`, `Card` 또는 섹션 레이아웃) +- **데이터**: React Query — `useOgnzUserTree`, `useRvwMnpwMngPage`, `useSaveRvwMnpwMng`, `useDeleteRvwMnpwMng` +- **상태**: `selectedPerson`(단일), `keyword`, `displayTree`, `expandedOgnzSnIds`, 블록별 배치 목록(서버 데이터 파생) +- **조직도 검색**: `filterOgnzUserTree`, `collectExpandedOgnzSnIds` (`widgets/ognz-user-tree/lib` 또는 페이지 `lib`) +- **저장 UX**: 별도 화면 저장 없음 — **[선택 인원 배치]** = `save` mutation 즉시 호출 +- **조직도 UI**: 결재선 관리 `ognz-user-tree.tsx`를 **widgets**로 승격해 공통화 + +### 7-2. 모듈·파일 배치(제안) + +| 구분 | 경로 | +| -------- | ---------------------------------------------------------------------------------- | +| 라우트 | `apps/as/app/(with-base-layout)/rvw-mnpw-mng/page.tsx` | +| 페이지 | `apps/as/src/pages/rvw-mnpw-mng/` | +| UI | `.../ui/rvw-mnpw-mng-page.tsx`, `rvw-mnpw-assignment-panel.tsx` 등 | +| model | `.../model/selected-ognz-user.ts`, `rvw-mnpw-block-config.ts` (`atrzSeComCd` 상수) | +| lib | `.../lib/group-rvw-mnpw-by-atrz-se.ts` (목록 → B-1~B-4 분배) | +| **위젯** | `apps/as/src/widgets/ognz-user-tree/` — 아래 이전 대상 | +| 엔티티 | `entities/user-organization/`, `entities/rvw-mnpw/` (구현 완료) | + +#### widgets/ognz-user-tree 이전 (결재선 → 공통) + +| 현재 (결재선 관리) | 이전 후 (widgets) | +| ------------------------------------------------------ | ------------------------------------------------------------------------------------ | +| `pages/approval-line-management/ui/ognz-user-tree.tsx` | `widgets/ognz-user-tree/ui/ognz-user-tree.tsx` | +| `pages/.../ui/ognz-user-tree-node.tsx` | `widgets/ognz-user-tree/ui/ognz-user-tree-node.tsx` | +| `pages/.../model/reviewer-option.ts` | `widgets/ognz-user-tree/model/selected-ognz-user.ts` (이름·필드 확장: `ognzSn` 필수) | +| `pages/.../lib/filter-ognz-user-tree.ts` | `widgets/ognz-user-tree/lib/filter-ognz-user-tree.ts` (**필수** — 검색 공통화) | +| `widgets/ognz-user-tree/index.ts` | `OgnzUserTree`, `OgnzUserTreeNode`, 타입 export | + +결재선 `AprvrPickerDialog`는 `@/widgets/ognz-user-tree` import로 교체. 검토인력관리 페이지는 **인라인 조직도**(다이얼로그 없음)로 동일 위젯 사용. + +**`SelectedOgnzUser`** — `ReviewerOption` 확장: + +```typescript +export interface SelectedOgnzUser { + userUuid: string; // OgnzUserTreeUserDto.userUnqId + userNm: string; + ognzSn: number; + ognzNm: string; + jbgdSeCd?: string; + jbpsSeCd?: string; +} +``` + +### 7-3. 영역별 UI 매핑 + +| 영역 | 주요 UI | +| ---- | -------------------------------------------------------------------- | +| A-1 | 검색 폼 (`Input` + 조회/초기화) — `AprvrPickerDialog` 90~103행 패턴 | +| A-2 | `OgnzUserTree(nodes={displayTree})` + 선택 안내 + `useOgnzUserTree` | +| B-k | 제목·Badge(count)·`선택 인원 배치`·배치 목록(또는 empty) | +| 공통 | 페이지 헤더·브레드크럼·2열 레이아웃 (`grid` / `flex`, 모바일은 스택) | + +### 7-4. 다이얼로그·라우팅 + +| 항목 | 내용 | +| ---------- | ----------------------------------- | +| 다이얼로그 | **없음** (조직도 인라인, 즉시 저장) | +| 라우팅 | 메뉴 `검토인력관리` 단일 페이지 | + +### 7-5. 데이터 흐름(구현 요약) + +- **진입**: `useOgnzUserTree` → `displayTree = data` + `useRvwMnpwMngPage({ page: 0, size: N })` → 4패널. +- **검색**: `keyword` 로컬 상태 → **[조회]** 시 `filterOgnzUserTree` + `collectExpandedOgnzSnIds` → `OgnzUserTree`에 `displayTree` 전달. +- **초기화**: `keyword`·`displayTree`·`expandedOgnzSnIds` 리셋 (`selectedPerson` 유지). +- **선택**: 트리 `onSelect` → `selectedPerson` (반드시 `ognzSn` 포함). +- **배치**: `useSaveRvwMnpwMng().mutate({ atrzSeComCd, ognzSn, userUuid })` → `onSuccess`에서 `rvw-mnpw` query invalidate. +- **삭제**: 목록 항목에서 `useDeleteRvwMnpwMng` — UI 확정 후 연결. + +### 7-6. 미확정 / 검증 필요 + +- [ ] `ATRZ02`(검토2) 별도 블록 필요 여부 +- [ ] 권한(역할별 CRUD) diff --git a/apps/as/next-env.d.ts b/apps/as/next-env.d.ts deleted file mode 100644 index 3cd7048ed..000000000 --- a/apps/as/next-env.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -/// -/// -/// - -// NOTE: This file should not be edited -// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/as/package.json b/apps/as/package.json index 5122cbbb5..ea9e0545a 100644 --- a/apps/as/package.json +++ b/apps/as/package.json @@ -24,6 +24,7 @@ "@tanstack/react-query-devtools": "^5.75.5", "@tanstack/react-table": "^8.21.2", "axios": "^1.8.1", + "class-variance-authority": "^0.7.1", "dayjs": "^1.11.13", "js-cookie": "^3.0.5", "lucide-react": "^0.474.0", diff --git a/apps/as/src/entities/acntg/api/acntg-service.ts b/apps/as/src/entities/acntg/api/acntg-service.ts index 957113dec..fd620009e 100644 --- a/apps/as/src/entities/acntg/api/acntg-service.ts +++ b/apps/as/src/entities/acntg/api/acntg-service.ts @@ -12,7 +12,7 @@ import { type AcntgSeMngTreeDto, } from "../model/acntg-interface"; -const BASE = ENDPOINT.AS_COMMON_SERVICE.ACNTG_SE; +const BASE = ENDPOINT.AS_SERVICE.ACNTG_SE; async function findAll(params: AcntgSeMngSearchDto): Promise> { const response = await client.get>(BASE, { params }); diff --git a/apps/as/src/entities/acntg/api/queries.ts b/apps/as/src/entities/acntg/api/queries.ts index c2bcc7f9f..db00d2317 100644 --- a/apps/as/src/entities/acntg/api/queries.ts +++ b/apps/as/src/entities/acntg/api/queries.ts @@ -10,7 +10,7 @@ import type { import { acntgSeMngService } from "./acntg-service"; -const acntgSeMngBaseKey = [ENDPOINT.AS_COMMON_SERVICE.ACNTG_SE] as const; +const acntgSeMngBaseKey = [ENDPOINT.AS_SERVICE.ACNTG_SE] as const; export function useAcntgSeMngPage(params: AcntgSeMngSearchDto) { return useQuery({ diff --git a/apps/as/src/entities/adstdstr/api/adstdstr-service.ts b/apps/as/src/entities/adstdstr/api/adstdstr-service.ts new file mode 100644 index 000000000..b4bb11fae --- /dev/null +++ b/apps/as/src/entities/adstdstr/api/adstdstr-service.ts @@ -0,0 +1,26 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; + +import type { + CmCdAdstDstrComboResponseDto, + CmCdAdstDstrService, +} from "../model/adstdstr-interface"; + +const BASE = ENDPOINT.AS_SERVICE.ADSTDSTR; + +async function findSidoCombo(): Promise { + const response = await client.get(`${BASE}/sido`); + return response.data; +} + +async function findGugunCombo(sido: string): Promise { + const response = await client.get(`${BASE}/gugun`, { + params: { sido }, + }); + return response.data; +} + +export const cmCdAdstDstrService = { + findSidoCombo, + findGugunCombo, +} satisfies CmCdAdstDstrService; diff --git a/apps/as/src/entities/adstdstr/api/queries.ts b/apps/as/src/entities/adstdstr/api/queries.ts new file mode 100644 index 000000000..353e25cfe --- /dev/null +++ b/apps/as/src/entities/adstdstr/api/queries.ts @@ -0,0 +1,22 @@ +import { useQuery } from "@tanstack/react-query"; + +import { ENDPOINT } from "@/shared/config"; + +import { cmCdAdstDstrService } from "./adstdstr-service"; + +const cmCdAdstDstrBaseKey = [ENDPOINT.AS_SERVICE.ADSTDSTR] as const; + +export function useFindSidoCombo() { + return useQuery({ + queryKey: [...cmCdAdstDstrBaseKey, "findSidoCombo"], + queryFn: () => cmCdAdstDstrService.findSidoCombo(), + }); +} + +export function useFindGugunCombo(sido: string) { + return useQuery({ + queryKey: [...cmCdAdstDstrBaseKey, "findGugunCombo", sido], + queryFn: () => cmCdAdstDstrService.findGugunCombo(sido), + enabled: Boolean(sido), + }); +} diff --git a/apps/as/src/entities/adstdstr/index.ts b/apps/as/src/entities/adstdstr/index.ts new file mode 100644 index 000000000..da1dbd132 --- /dev/null +++ b/apps/as/src/entities/adstdstr/index.ts @@ -0,0 +1,6 @@ +export { cmCdAdstDstrService } from "./api/adstdstr-service"; +export { useFindGugunCombo, useFindSidoCombo } from "./api/queries"; +export type { + CmCdAdstDstrComboResponseDto, + CmCdAdstDstrService, +} from "./model/adstdstr-interface"; diff --git a/apps/as/src/entities/adstdstr/model/adstdstr-interface.ts b/apps/as/src/entities/adstdstr/model/adstdstr-interface.ts new file mode 100644 index 000000000..72cb673dc --- /dev/null +++ b/apps/as/src/entities/adstdstr/model/adstdstr-interface.ts @@ -0,0 +1,11 @@ +/** 백엔드 CmCdAdstDstrComboResponseDto */ +export interface CmCdAdstDstrComboResponseDto { + adstDstrCd: string; + adstDstrNm: string; +} + +/** Controller public 메서드명·시그니처와 동일 */ +export interface CmCdAdstDstrService { + findSidoCombo(): Promise; + findGugunCombo(sido: string): Promise; +} diff --git a/apps/as/src/entities/attach/api/attach-service.ts b/apps/as/src/entities/attach/api/attach-service.ts new file mode 100644 index 000000000..0af0a916d --- /dev/null +++ b/apps/as/src/entities/attach/api/attach-service.ts @@ -0,0 +1,22 @@ +import { AxiosResponse } from "axios"; + +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; + +import type { AttachService } from "../model/attach-interface"; + +async function deleteAttachmentByAtflId(atflId: string): Promise { + if (!atflId) return; + await client.delete(`${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}/${atflId}`); +} + +async function downloadAttachmentByAtflCd(atflCd: string): Promise { + return await client.get(`${ENDPOINT.ATTACH_SERVICE.DOWNLOAD}/${atflCd}`, { + responseType: "blob", + }); +} + +export const attachService = { + deleteAttachmentByAtflId, + downloadAttachmentByAtflCd, +} satisfies AttachService; diff --git a/apps/as/src/entities/attach/api/queries.ts b/apps/as/src/entities/attach/api/queries.ts new file mode 100644 index 000000000..ed9efefbf --- /dev/null +++ b/apps/as/src/entities/attach/api/queries.ts @@ -0,0 +1,39 @@ +import { useMutation } from "@tanstack/react-query"; + +import { ENDPOINT } from "@/shared/config"; + +import { parseFilenameFromResponse } from "../lib/parse-filename-from-response"; + +import { attachService } from "./attach-service"; + +const attachBaseKey = [ENDPOINT.ATTACH_SERVICE.ATTACHMENTS] as const; +const attachDownloadBaseKey = [ENDPOINT.ATTACH_SERVICE.DOWNLOAD] as const; + +export function useDeleteAttachment() { + return useMutation({ + mutationKey: [...attachBaseKey, "delete"], + mutationFn: (atflId: string) => attachService.deleteAttachmentByAtflId(atflId), + }); +} + +export function useDownloadAttachmentByAtflCd() { + return useMutation({ + mutationKey: [...attachDownloadBaseKey, "download"], + mutationFn: async (atflCd: string) => { + const response = await attachService.downloadAttachmentByAtflCd(atflCd); + const filename = parseFilenameFromResponse(response); + + const blob = new Blob([response.data]); + const url = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + + window.URL.revokeObjectURL(url); + document.body.removeChild(link); + }, + }); +} diff --git a/apps/as/src/entities/attach/index.ts b/apps/as/src/entities/attach/index.ts new file mode 100644 index 000000000..37258e6e5 --- /dev/null +++ b/apps/as/src/entities/attach/index.ts @@ -0,0 +1,3 @@ +export { attachService } from "./api/attach-service"; +export { useDeleteAttachment, useDownloadAttachmentByAtflCd } from "./api/queries"; +export type { Attach, AttachService } from "./model/attach-interface"; diff --git a/apps/as/src/entities/attach/lib/parse-filename-from-response.ts b/apps/as/src/entities/attach/lib/parse-filename-from-response.ts new file mode 100644 index 000000000..cd8a5aa3d --- /dev/null +++ b/apps/as/src/entities/attach/lib/parse-filename-from-response.ts @@ -0,0 +1,11 @@ +import type { AxiosResponse } from "axios"; + +export function parseFilenameFromResponse(response: AxiosResponse): string { + const contentDisposition = response.headers["content-disposition"]; + if (!contentDisposition) return "download"; + + const filenameMatch = contentDisposition.match(/filename\*=UTF-8''([^;]+)/); + if (!filenameMatch?.[1]) return "download"; + + return decodeURIComponent(filenameMatch[1]); +} diff --git a/apps/as/src/entities/attach/model/attach-interface.ts b/apps/as/src/entities/attach/model/attach-interface.ts new file mode 100644 index 000000000..d6b0a08d2 --- /dev/null +++ b/apps/as/src/entities/attach/model/attach-interface.ts @@ -0,0 +1,22 @@ +import { AxiosResponse } from "axios"; + +/** 백엔드 AtflMngResponseDto */ +export interface Attach { + atflCd: string; + atflSn: number; + atflId: string; + physFileNm: string; + orgnlFileNm: string; + atflFlieSz: number; + atflType: string; + dwnldNocs: number; + delYn: string; + lnkgDmnId: string; + lnkgDmnNm: string; + regDt: string; +} + +export interface AttachService { + deleteAttachmentByAtflId: (atflId: string) => Promise; + downloadAttachmentByAtflCd: (atflCd: string) => Promise; +} diff --git a/apps/as/src/entities/bns-if/api/bns-if-service.ts b/apps/as/src/entities/bns-if/api/bns-if-service.ts new file mode 100644 index 000000000..320a01efc --- /dev/null +++ b/apps/as/src/entities/bns-if/api/bns-if-service.ts @@ -0,0 +1,47 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; + +import { buildBnsSearchDto } from "../lib/build-bns-search-dto"; +import type { + BnsDetailResponseWrapperDto, + BnsIfService, + BnsListResponseWrapperDto, + BnsSearchDto, + EmCodeListResponseDto, +} from "../model/bns-if-interface"; + +const BASE = ENDPOINT.AS_SERVICE.BNS_IF; + +async function getBnsList(params: BnsSearchDto): Promise { + const response = await client.get(`${BASE}/bns`, { + params: buildBnsSearchDto(params), + }); + return response.data; +} + +async function getBnsDetail( + gbn: string, + yr: string, + seqNo: string, +): Promise { + const response = await client.get(`${BASE}/bns/detail`, { + params: { gbn, yr, seqNo }, + }); + return response.data; +} + +async function getEmCodeList( + dpmCd?: string, + lclgvrnmntCd?: string, +): Promise { + const response = await client.get(`${BASE}/emCodeList`, { + params: { dpmCd, lclgvrnmntCd }, + }); + return response.data; +} + +export const bnsIfService = { + getBnsList, + getBnsDetail, + getEmCodeList, +} satisfies BnsIfService; diff --git a/apps/as/src/entities/bns-if/api/queries.ts b/apps/as/src/entities/bns-if/api/queries.ts new file mode 100644 index 000000000..7ab0b46a3 --- /dev/null +++ b/apps/as/src/entities/bns-if/api/queries.ts @@ -0,0 +1,33 @@ +import { keepPreviousData, useQuery } from "@tanstack/react-query"; + +import { ENDPOINT } from "@/shared/config"; + +import type { BnsSearchDto } from "../model/bns-if-interface"; + +import { bnsIfService } from "./bns-if-service"; + +const bnsIfBaseKey = [ENDPOINT.AS_SERVICE.BNS_IF] as const; + +export function useBnsList(params: BnsSearchDto, options?: { enabled?: boolean }) { + return useQuery({ + queryKey: [...bnsIfBaseKey, "getBnsList", params], + queryFn: () => bnsIfService.getBnsList(params), + enabled: options?.enabled, + placeholderData: keepPreviousData, + }); +} + +export function useBnsDetail(gbn: string, yr: string, seqNo: string) { + return useQuery({ + queryKey: [...bnsIfBaseKey, "getBnsDetail", gbn, yr, seqNo], + queryFn: () => bnsIfService.getBnsDetail(gbn, yr, seqNo), + enabled: Boolean(gbn && yr && seqNo), + }); +} + +export function useEmCodeList(dpmCd?: string, lclgvrnmntCd?: string) { + return useQuery({ + queryKey: [...bnsIfBaseKey, "getEmCodeList", dpmCd, lclgvrnmntCd], + queryFn: () => bnsIfService.getEmCodeList(dpmCd, lclgvrnmntCd), + }); +} diff --git a/apps/as/src/entities/bns-if/index.ts b/apps/as/src/entities/bns-if/index.ts new file mode 100644 index 000000000..430f4a611 --- /dev/null +++ b/apps/as/src/entities/bns-if/index.ts @@ -0,0 +1,13 @@ +export { bnsIfService } from "./api/bns-if-service"; +export { useBnsDetail, useBnsList, useEmCodeList } from "./api/queries"; +export { buildBnsSearchDto } from "./lib/build-bns-search-dto"; +export type { + BnsDetailResponseDto, + BnsDetailResponseWrapperDto, + BnsIfService, + BnsListResponseDto, + BnsListResponseWrapperDto, + BnsSearchDto, + EmCodeItemDto, + EmCodeListResponseDto, +} from "./model/bns-if-interface"; diff --git a/apps/as/src/entities/bns-if/lib/build-bns-search-dto.ts b/apps/as/src/entities/bns-if/lib/build-bns-search-dto.ts new file mode 100644 index 000000000..8e95b27dd --- /dev/null +++ b/apps/as/src/entities/bns-if/lib/build-bns-search-dto.ts @@ -0,0 +1,20 @@ +import type { BnsSearchDto } from "../model/bns-if-interface"; + +export function buildBnsSearchDto(input: { + cPage: number; + searchStartYear?: string; + searchSido?: string; + searchGugun?: string; +}): BnsSearchDto { + const dto: BnsSearchDto = { cPage: input.cPage }; + + const year = input.searchStartYear?.trim(); + const sido = input.searchSido?.trim(); + const gugun = input.searchGugun?.trim(); + + if (year) dto.searchStartYear = year; + if (sido) dto.searchSido = sido; + if (gugun) dto.searchGugun = gugun; + + return dto; +} diff --git a/apps/as/src/entities/bns-if/model/bns-if-interface.ts b/apps/as/src/entities/bns-if/model/bns-if-interface.ts new file mode 100644 index 000000000..e13ab8500 --- /dev/null +++ b/apps/as/src/entities/bns-if/model/bns-if-interface.ts @@ -0,0 +1,125 @@ +/** 백엔드 BnsSearchDto */ +export interface BnsSearchDto { + searchStartYear?: string; + cPage: number; + searchSido?: string; + searchGugun?: string; +} + +/** 백엔드 BnsListResponseWrapperDto */ +export interface BnsListResponseWrapperDto { + list: BnsListResponseDto[]; + cnt: number; +} + +/** 백엔드 BnsListResponseDto */ +export interface BnsListResponseDto { + cPage: number; + gbn: number; + chk: number; + seqNo: string; + rgnNm: string; + sido: string; + gugun: string; + bzCd: string; + bzNm: string; + yr: string; + strtYr: string; + edYr: string; + totBzamt: string; +} + +/** 백엔드 BnsDetailResponseWrapperDto */ +export interface BnsDetailResponseWrapperDto { + saup: unknown; + detail: BnsDetailResponseDto; +} + +/** 백엔드 BnsDetailResponseDto */ +export interface BnsDetailResponseDto { + cPage: number; + seqNo: string; + confBzCd: string; + chngDgr: string; + yr: string; + sido: string; + gugun: string; + rvbsnNm: string; + lgbsnNm: string; + mdmbsnNm: string; + lclgvrnmntNm: string; + accKdNm: string; + bzKdNm: string; + untBzNm: string; + dtlBzNm: string; + mevBzNm: string; + ddtlBzNm: string; + strtYr: string; + edYr: string; + aidRt: string; + bzStatus: string; + bzCd: string; + bzNm: string; + bzLoc: string; + efflWqltStndrdNm: string; + goalYear: string; + bssProm: string; + uppPlan: string; + techInsp: string; + bssCont: string; + rmk: string; + inflBscFctNm: string; + inflBscFctCd: string; + trtMtd: string; + swrExtns: string; + swrChng: string; + swrRnvt: string; + totFamNum: string; + pumpCnt: string; + rvWbsn: string; + rvText: string; + imprvExpctFlw: string; + imprvExpctWqlt: string; + trtplntPlEfflWqlt: string; + wqltAvg: string; + wqltHgh: string; + wqltLow: string; + stdCost: string; + bzEtc: string; + bscFctCd: string; + bscFctNm: string; + fctCpc: string; + fctCpcAfr: string; + trtDstrArea: string; + trtDstrAreaAfr: string; + swrTrtPopl: string; + swrTrtPoplAfr: string; + adstPopl: string; +} + +/** 백엔드 EmCodeItemDto */ +export interface EmCodeItemDto { + code: string; + data: string; + upCode: string; +} + +/** 백엔드 EmCodeListResponseDto */ +export interface EmCodeListResponseDto { + rvbsnList: EmCodeItemDto[]; + lgbsnList: EmCodeItemDto[]; + mdmbsnList: EmCodeItemDto[]; + accList: EmCodeItemDto[]; + bzList: EmCodeItemDto[]; + untList: EmCodeItemDto[]; + dtlList: EmCodeItemDto[]; + mevList: EmCodeItemDto[]; + ddtlList: EmCodeItemDto[]; +} + +/** Controller public 메서드명·시그니처와 동일 */ +export interface BnsIfService { + getBnsList(params: BnsSearchDto): Promise; + getBnsDetail(gbn: string, yr: string, seqNo: string): Promise; + getEmCodeList(dpmCd?: string, lclgvrnmntCd?: string): Promise; +} diff --git a/apps/as/src/entities/cklst/api/cklst-artcl-service.ts b/apps/as/src/entities/cklst/api/cklst-artcl-service.ts index dda8defd5..9561a659c 100644 --- a/apps/as/src/entities/cklst/api/cklst-artcl-service.ts +++ b/apps/as/src/entities/cklst/api/cklst-artcl-service.ts @@ -11,33 +11,25 @@ import type { CklstArtclUpdateRequest, } from "../model/cklst-artcl-interface"; +const BASE = ENDPOINT.AS_SERVICE.CKLST_ARTCL; + async function findPage(params: CklstArtclSearchParams): Promise> { - const response = await client.get>( - ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_ARTCL, - { params }, - ); + const response = await client.get>(BASE, { params }); return response.data; } async function findByCklstSn(cklstSn: number): Promise { - const response = await client.get( - `${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_ARTCL}/by-cklst/${cklstSn}`, - ); + const response = await client.get(`${BASE}/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}`, - ); + const response = await client.get(`${BASE}/${cklstArtclSn}/${cklstSn}`); return response.data; } async function create(body: CklstArtclSaveRequest): Promise { - const response = await client.post( - ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_ARTCL, - body, - ); + const response = await client.post(BASE, body); return response.data; } @@ -46,17 +38,12 @@ async function update( cklstSn: number, body: CklstArtclUpdateRequest, ): Promise { - const response = await client.put( - `${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_ARTCL}/${cklstArtclSn}/${cklstSn}`, - body, - ); + const response = await client.put(`${BASE}/${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}`, - ); + const response = await client.delete(`${BASE}/${cklstArtclSn}/${cklstSn}`); return response.data; } diff --git a/apps/as/src/entities/cklst/api/cklst-mng-service.ts b/apps/as/src/entities/cklst/api/cklst-mng-service.ts index fdae4e8ff..8ad2e0556 100644 --- a/apps/as/src/entities/cklst/api/cklst-mng-service.ts +++ b/apps/as/src/entities/cklst/api/cklst-mng-service.ts @@ -11,40 +11,39 @@ import type { CklstTreeSearchParams, } from "../model/cklst-mng-interface"; +const BASE = ENDPOINT.AS_SERVICE.CKLST; + async function findPage(params: CklstMngSearchParams): Promise> { - const response = await client.get>(ENDPOINT.AS_CHECKLIST_SERVICE.CKLST, { + const response = await client.get>(BASE, { params, }); return response.data; } async function findById(cklstSn: number): Promise { - const response = await client.get(`${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST}/${cklstSn}`); + const response = await client.get(`${BASE}/${cklstSn}`); return response.data; } async function findTree(params: CklstTreeSearchParams): Promise { - const response = await client.get(`${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST}/list/tree`, { + const response = await client.get(`${BASE}/list/tree`, { params, }); return response.data; } async function create(body: CklstMngSaveRequest): Promise { - const response = await client.post(ENDPOINT.AS_CHECKLIST_SERVICE.CKLST, body); + const response = await client.post(BASE, body); return response.data; } async function update(cklstSn: number, body: CklstMngSaveRequest): Promise { - const response = await client.put( - `${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST}/${cklstSn}`, - body, - ); + const response = await client.put(`${BASE}/${cklstSn}`, body); return response.data; } async function remove(cklstSn: number): Promise { - const response = await client.delete(`${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST}/${cklstSn}`); + const response = await client.delete(`${BASE}/${cklstSn}`); return response.data; } diff --git a/apps/as/src/entities/cklst/api/cklst-rvw-service.ts b/apps/as/src/entities/cklst/api/cklst-rvw-service.ts index 0039bfbc1..e184d45aa 100644 --- a/apps/as/src/entities/cklst/api/cklst-rvw-service.ts +++ b/apps/as/src/entities/cklst/api/cklst-rvw-service.ts @@ -11,18 +11,15 @@ import type { CklstRvwUpdateRequest, } from "../model/cklst-rvw-interface"; +const BASE = ENDPOINT.AS_SERVICE.CKLST_RVW; + async function findPage(params: CklstRvwSearchParams): Promise> { - const response = await client.get>( - ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_RVW, - { params }, - ); + const response = await client.get>(BASE, { params }); return response.data; } async function findByArtcl(cklstSn: number, cklstArtclSn: number): Promise { - const response = await client.get( - `${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_RVW}/by-artcl/${cklstSn}/${cklstArtclSn}`, - ); + const response = await client.get(`${BASE}/by-artcl/${cklstSn}/${cklstArtclSn}`); return response.data; } @@ -31,17 +28,12 @@ async function findById( cklstArtclSn: number, cklstSn: number, ): Promise { - const response = await client.get( - `${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_RVW}/${cklstRvwSn}/${cklstArtclSn}/${cklstSn}`, - ); + const response = await client.get(`${BASE}/${cklstRvwSn}/${cklstArtclSn}/${cklstSn}`); return response.data; } async function create(body: CklstRvwSaveRequest): Promise { - const response = await client.post( - ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_RVW, - body, - ); + const response = await client.post(BASE, body); return response.data; } @@ -52,7 +44,7 @@ async function update( body: CklstRvwUpdateRequest, ): Promise { const response = await client.put( - `${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_RVW}/${cklstRvwSn}/${cklstArtclSn}/${cklstSn}`, + `${BASE}/${cklstRvwSn}/${cklstArtclSn}/${cklstSn}`, body, ); return response.data; @@ -64,7 +56,7 @@ async function remove( cklstSn: number, ): Promise { const response = await client.delete( - `${ENDPOINT.AS_CHECKLIST_SERVICE.CKLST_RVW}/${cklstRvwSn}/${cklstArtclSn}/${cklstSn}`, + `${BASE}/${cklstRvwSn}/${cklstArtclSn}/${cklstSn}`, ); return response.data; } diff --git a/apps/as/src/entities/cklst/api/queries.ts b/apps/as/src/entities/cklst/api/queries.ts index 33280c41a..afa3e02e3 100644 --- a/apps/as/src/entities/cklst/api/queries.ts +++ b/apps/as/src/entities/cklst/api/queries.ts @@ -18,9 +18,9 @@ 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; +const cklstMngBaseKey = [ENDPOINT.AS_SERVICE.CKLST] as const; +const cklstArtclBaseKey = [ENDPOINT.AS_SERVICE.CKLST_ARTCL] as const; +const cklstRvwBaseKey = [ENDPOINT.AS_SERVICE.CKLST_RVW] as const; export function useCklstMngPage(params: CklstMngSearchParams) { return useQuery({ diff --git a/apps/as/src/entities/cnstp/api/cnstp-service.ts b/apps/as/src/entities/cnstp/api/cnstp-service.ts index f39be006a..292ac7069 100644 --- a/apps/as/src/entities/cnstp/api/cnstp-service.ts +++ b/apps/as/src/entities/cnstp/api/cnstp-service.ts @@ -12,7 +12,7 @@ import { type CnstpSeMngTreeDto, } from "../model/cnstp-interface"; -const BASE = ENDPOINT.AS_COMMON_SERVICE.CNSTP_SE; +const BASE = ENDPOINT.AS_SERVICE.CNSTP_SE; async function findAll(params: CnstpSeMngSearchDto): Promise> { const response = await client.get>(BASE, { params }); diff --git a/apps/as/src/entities/cnstp/api/queries.ts b/apps/as/src/entities/cnstp/api/queries.ts index 2ebb44dea..a419af10c 100644 --- a/apps/as/src/entities/cnstp/api/queries.ts +++ b/apps/as/src/entities/cnstp/api/queries.ts @@ -10,7 +10,7 @@ import type { import { cnstpSeMngService } from "./cnstp-service"; -const cnstpSeMngBaseKey = [ENDPOINT.AS_COMMON_SERVICE.CNSTP_SE] as const; +const cnstpSeMngBaseKey = [ENDPOINT.AS_SERVICE.CNSTP_SE] as const; export function useCnstpSeMngPage(params: CnstpSeMngSearchDto) { return useQuery({ diff --git a/apps/as/src/entities/elapr/api/elapr-dmnd-service.ts b/apps/as/src/entities/elapr/api/elapr-dmnd-service.ts new file mode 100644 index 000000000..368600110 --- /dev/null +++ b/apps/as/src/entities/elapr/api/elapr-dmnd-service.ts @@ -0,0 +1,67 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { PageResponse } from "@/shared/model"; + +import type { + ElaprDmndResponseDto, + ElaprDmndSaveRequestDto, + ElaprDmndSearchDto, + ElaprDmndService, + ElaprDocPrgrsResponseDto, +} from "../model/elapr-dmnd-interface"; + +const BASE = ENDPOINT.AS_ELAPR_SERVICE.ELAPR_DMND; + +async function findPage(params: ElaprDmndSearchDto): Promise> { + const response = await client.get>(BASE, { params }); + return response.data; +} + +async function findById(elaprDmndId: string): Promise { + const response = await client.get(`${BASE}/${elaprDmndId}`); + return response.data; +} + +async function save(body: ElaprDmndSaveRequestDto): Promise { + const response = await client.post(BASE, body); + return response.data; +} + +async function update( + elaprDmndId: string, + body: ElaprDmndSaveRequestDto, +): Promise { + const response = await client.put(`${BASE}/${elaprDmndId}`, body); + return response.data; +} + +async function updateStatus( + elaprDmndId: string, + atrzSttsComCd: string, +): Promise { + const response = await client.put(`${BASE}/${elaprDmndId}/status`, null, { + params: { atrzSttsComCd }, + }); + return response.data; +} + +async function remove(elaprDmndId: string): Promise { + await client.delete(`${BASE}/${elaprDmndId}`); +} + +async function findDocPrgrsByDocNo(elaprDocNo: string): Promise { + const response = await client.get( + `${BASE}/sanc/docprgrs/${elaprDocNo}`, + ); + return response.data; +} + +export const elaprDmndService = { + findPage, + findById, + save, + update, + updateStatus, + remove, + findDocPrgrsByDocNo, +} satisfies ElaprDmndService; diff --git a/apps/as/src/entities/elapr/api/elapr-prgrs-service.ts b/apps/as/src/entities/elapr/api/elapr-prgrs-service.ts new file mode 100644 index 000000000..97f925113 --- /dev/null +++ b/apps/as/src/entities/elapr/api/elapr-prgrs-service.ts @@ -0,0 +1,7 @@ +import { client } from "@/shared/api"; + +export async function processElaprPrgrs(elaprPrgrsId: string, atrzPrgrsSttsComCd = "ATRZ_STTS_003"): Promise { + await client.put(`/as-elapr-service/api/v1/elapr-prgrs-stts/${elaprPrgrsId}/process`, { + atrzPrgrsSttsComCd, + }); +} diff --git a/apps/as/src/entities/elapr/api/queries.ts b/apps/as/src/entities/elapr/api/queries.ts index 59a6bb91b..fae6a0d03 100644 --- a/apps/as/src/entities/elapr/api/queries.ts +++ b/apps/as/src/entities/elapr/api/queries.ts @@ -8,6 +8,7 @@ import type { ElaprAplnSaveRequest, ElaprAplnUpdateRequest, } from "../model/elapr-apln-interface"; +import type { ElaprDmndSaveRequestDto, ElaprDmndSearchDto } from "../model/elapr-dmnd-interface"; import type { ElaprTmpltSaveRequest, ElaprTmpltSearchParams, @@ -15,10 +16,12 @@ import type { } from "../model/elapr-tmplt-interface"; import { elaprAplnService } from "./elapr-apln-service"; +import { elaprDmndService } from "./elapr-dmnd-service"; import { elaprTmpltService } from "./elapr-tmplt-service"; const elaprTmpltBaseKey = [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_TMPLT] as const; const elaprAplnBaseKey = [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_APLN] as const; +const elaprDmndBaseKey = [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_DMND] as const; export function useElaprTmpltPage(params: ElaprTmpltSearchParams) { return useQuery({ @@ -50,13 +53,8 @@ export function useUpdateElaprTmplt() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: ({ - elaprTmpltId, - body, - }: { - elaprTmpltId: string; - body: ElaprTmpltUpdateRequest; - }) => elaprTmpltService.update(elaprTmpltId, body), + mutationFn: ({ elaprTmpltId, body }: { elaprTmpltId: string; body: ElaprTmpltUpdateRequest }) => + elaprTmpltService.update(elaprTmpltId, body), onSuccess: (_data, { elaprTmpltId }) => { queryClient.invalidateQueries({ queryKey: elaprTmpltBaseKey }); queryClient.invalidateQueries({ @@ -99,12 +97,8 @@ export function useSaveMultiElaprApln() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: ({ - body, - }: { - body: ElaprAplnSaveRequest[]; - elaprTmpltId: string; - }) => elaprAplnService.saveMulti(body), + mutationFn: ({ body }: { body: ElaprAplnSaveRequest[]; elaprTmpltId: string }) => + elaprAplnService.saveMulti(body), onSuccess: (_data, { elaprTmpltId }) => { queryClient.invalidateQueries({ queryKey: elaprAplnBaseKey }); queryClient.invalidateQueries({ @@ -118,13 +112,8 @@ export function useUpdateElaprApln() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: ({ - aplnId, - body, - }: { - aplnId: string; - body: ElaprAplnUpdateRequest; - }) => elaprAplnService.update(aplnId, body), + mutationFn: ({ aplnId, body }: { aplnId: string; body: ElaprAplnUpdateRequest }) => + elaprAplnService.update(aplnId, body), onSuccess: (_data, { body, aplnId }) => { queryClient.invalidateQueries({ queryKey: elaprAplnBaseKey }); queryClient.invalidateQueries({ @@ -141,12 +130,8 @@ export function useDeleteElaprApln() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: ({ - aplnId, - }: { - aplnId: string; - elaprTmpltId: string; - }) => elaprAplnService.remove(aplnId), + mutationFn: ({ aplnId }: { aplnId: string; elaprTmpltId: string }) => + elaprAplnService.remove(aplnId), onSuccess: (_data, { elaprTmpltId }) => { queryClient.invalidateQueries({ queryKey: elaprAplnBaseKey }); queryClient.invalidateQueries({ @@ -160,12 +145,8 @@ export function useReorderElaprApln() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: ({ - body, - }: { - body: ElaprAplnReorderRequest; - elaprTmpltId: string; - }) => elaprAplnService.reorder(body), + mutationFn: ({ body }: { body: ElaprAplnReorderRequest; elaprTmpltId: string }) => + elaprAplnService.reorder(body), onSuccess: (_data, { elaprTmpltId }) => { queryClient.invalidateQueries({ queryKey: elaprAplnBaseKey }); queryClient.invalidateQueries({ @@ -174,3 +155,78 @@ export function useReorderElaprApln() { }, }); } + +export function useElaprDmndPage(params: ElaprDmndSearchDto) { + return useQuery({ + queryKey: [...elaprDmndBaseKey, "findPage", params], + queryFn: () => elaprDmndService.findPage(params), + }); +} + +export function useElaprDmnd(elaprDmndId: string | null | undefined) { + return useQuery({ + queryKey: [...elaprDmndBaseKey, "findById", elaprDmndId], + queryFn: () => elaprDmndService.findById(elaprDmndId!), + enabled: Boolean(elaprDmndId), + }); +} + +export function useElaprDocPrgrsByDocNo(elaprDocNo: string | null | undefined) { + return useQuery({ + queryKey: [...elaprDmndBaseKey, "findDocPrgrsByDocNo", elaprDocNo], + queryFn: () => elaprDmndService.findDocPrgrsByDocNo(elaprDocNo!), + enabled: Boolean(elaprDocNo), + }); +} + +export function useSaveElaprDmnd() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (body: ElaprDmndSaveRequestDto) => elaprDmndService.save(body), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: elaprDmndBaseKey }); + }, + }); +} + +export function useUpdateElaprDmnd() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ elaprDmndId, body }: { elaprDmndId: string; body: ElaprDmndSaveRequestDto }) => + elaprDmndService.update(elaprDmndId, body), + onSuccess: (_data, { elaprDmndId }) => { + queryClient.invalidateQueries({ queryKey: elaprDmndBaseKey }); + queryClient.invalidateQueries({ + queryKey: [...elaprDmndBaseKey, "findById", elaprDmndId], + }); + }, + }); +} + +export function useUpdateElaprDmndStatus() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ elaprDmndId, atrzSttsComCd }: { elaprDmndId: string; atrzSttsComCd: string }) => + elaprDmndService.updateStatus(elaprDmndId, atrzSttsComCd), + onSuccess: (_data, { elaprDmndId }) => { + queryClient.invalidateQueries({ queryKey: elaprDmndBaseKey }); + queryClient.invalidateQueries({ + queryKey: [...elaprDmndBaseKey, "findById", elaprDmndId], + }); + }, + }); +} + +export function useRemoveElaprDmnd() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (elaprDmndId: string) => elaprDmndService.remove(elaprDmndId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: elaprDmndBaseKey }); + }, + }); +} diff --git a/apps/as/src/entities/elapr/index.ts b/apps/as/src/entities/elapr/index.ts index caa639e51..4b14ea0d9 100644 --- a/apps/as/src/entities/elapr/index.ts +++ b/apps/as/src/entities/elapr/index.ts @@ -1,4 +1,5 @@ export { elaprAplnService } from "./api/elapr-apln-service"; +export { elaprDmndService } from "./api/elapr-dmnd-service"; export { elaprTmpltService } from "./api/elapr-tmplt-service"; export { useCreateElaprTmplt, @@ -6,11 +7,18 @@ export { useDeleteElaprTmplt, useElaprApln, useElaprAplnPage, + useElaprDmnd, + useElaprDmndPage, + useElaprDocPrgrsByDocNo, useElaprTmplt, useElaprTmpltPage, + useRemoveElaprDmnd, useReorderElaprApln, + useSaveElaprDmnd, useSaveMultiElaprApln, useUpdateElaprApln, + useUpdateElaprDmnd, + useUpdateElaprDmndStatus, useUpdateElaprTmplt, } from "./api/queries"; export { @@ -24,6 +32,13 @@ export { type ElaprAplnService, type ElaprAplnUpdateRequest, } from "./model/elapr-apln-interface"; +export type { + ElaprDmndResponseDto, + ElaprDmndSaveRequestDto, + ElaprDmndSearchDto, + ElaprDmndService, + ElaprDocPrgrsResponseDto, +} from "./model/elapr-dmnd-interface"; export type { ElaprTmplt, ElaprTmpltSaveRequest, diff --git a/apps/as/src/entities/elapr/model/elapr-dmnd-interface.ts b/apps/as/src/entities/elapr/model/elapr-dmnd-interface.ts new file mode 100644 index 000000000..f653249f2 --- /dev/null +++ b/apps/as/src/entities/elapr/model/elapr-dmnd-interface.ts @@ -0,0 +1,62 @@ +import type { PageableParams, PageResponse } from "@/shared/model"; + +/** 백엔드 ElaprDmndSaveRequestDto */ +export interface ElaprDmndSaveRequestDto { + elaprDocNo: string; + elaprTmpltId: string; + dmndTtl: string; + dmndCn: string; +} + +/** 백엔드 ElaprDmndResponseDto */ +export interface ElaprDmndResponseDto { + elaprDmndId: string; + rqstrUuid?: string; + elaprDocNo: string; + elaprTmpltId: string; + dmndTtl: string; + dmndCn: string; + atrzSttsComCd: string; + atrzSttsComNm?: string; + dmndDt?: string; + updtDt?: string; +} + +/** 백엔드 ElaprDmndSearchDto + Pageable */ +export interface ElaprDmndSearchDto extends PageableParams { + rqstrUuid?: string; + atrzSttsComCd?: string; + dmndTtl?: string; + dmndDtStart?: string; + dmndDtEnd?: string; +} + +/** 백엔드 ElaprDocPrgrsResponseDto */ +export interface ElaprDocPrgrsResponseDto { + elaprDocNo: string; + elaprDmndId: string; + elaprPrgrsId: string; + atrzSeqNo?: number; + atrzSeComCd: string; + atrzSeComNm?: string; + aprvrUuid?: string; + aprvrUuidNm?: string; + atrzPrgrsSttsComCd: string; + atrzPrgrsSttsComNm?: string; + rjctRsn?: string; + atrzDt?: string; + jbpsSeNm?: string; + jbgdSeNm?: string; + ognzNm?: string; +} + +/** Controller public 메서드명·시그니처와 동일 */ +export interface ElaprDmndService { + findPage(params: ElaprDmndSearchDto): Promise>; + findById(elaprDmndId: string): Promise; + save(body: ElaprDmndSaveRequestDto): Promise; + update(elaprDmndId: string, body: ElaprDmndSaveRequestDto): Promise; + updateStatus(elaprDmndId: string, atrzSttsComCd: string): Promise; + remove(elaprDmndId: string): Promise; + findDocPrgrsByDocNo(elaprDocNo: string): Promise; +} diff --git a/apps/as/src/entities/esntl/api/esntl-service.ts b/apps/as/src/entities/esntl/api/esntl-service.ts index 458940399..e8de77618 100644 --- a/apps/as/src/entities/esntl/api/esntl-service.ts +++ b/apps/as/src/entities/esntl/api/esntl-service.ts @@ -11,50 +11,41 @@ import type { EsntlAtchFileMngTreeSearchParams, } from "../model/esntl-interface"; +const BASE = ENDPOINT.AS_SERVICE.ESNTL_ATCH_FILE; + async function findTree( params: EsntlAtchFileMngTreeSearchParams, ): Promise { - const response = await client.get( - `${ENDPOINT.AS_COMMON_SERVICE.ESNTL_ATCH_FILE}/list/tree`, - { params }, - ); + const response = await client.get(`${BASE}/list/tree`, { + params, + }); return response.data; } async function findPage( params: EsntlAtchFileMngSearchParams, ): Promise> { - const response = await client.get>( - ENDPOINT.AS_COMMON_SERVICE.ESNTL_ATCH_FILE, - { params }, - ); + const response = await client.get>(BASE, { params }); return response.data; } async function findById(esntlAtchFileId: string): Promise { - const response = await client.get( - `${ENDPOINT.AS_COMMON_SERVICE.ESNTL_ATCH_FILE}/${esntlAtchFileId}`, - ); + const response = await client.get(`${BASE}/${esntlAtchFileId}`); return response.data; } async function create(body: EsntlAtchFileMngSaveRequest): Promise { - const response = await client.post(ENDPOINT.AS_COMMON_SERVICE.ESNTL_ATCH_FILE, body); + const response = await client.post(BASE, body); return response.data; } async function update(esntlAtchFileId: string, body: EsntlAtchFileMngSaveRequest): Promise { - const response = await client.put( - `${ENDPOINT.AS_COMMON_SERVICE.ESNTL_ATCH_FILE}/${esntlAtchFileId}`, - body, - ); + const response = await client.put(`${BASE}/${esntlAtchFileId}`, body); return response.data; } async function remove(esntlAtchFileId: string): Promise { - const response = await client.delete( - `${ENDPOINT.AS_COMMON_SERVICE.ESNTL_ATCH_FILE}/${esntlAtchFileId}`, - ); + const response = await client.delete(`${BASE}/${esntlAtchFileId}`); return response.data; } diff --git a/apps/as/src/entities/esntl/api/queries.ts b/apps/as/src/entities/esntl/api/queries.ts index 48e30fc68..dc7afb789 100644 --- a/apps/as/src/entities/esntl/api/queries.ts +++ b/apps/as/src/entities/esntl/api/queries.ts @@ -10,7 +10,7 @@ import type { import { esntlAtchFileMngService } from "./esntl-service"; -const esntlAtchFileMngBaseKey = [ENDPOINT.AS_COMMON_SERVICE.ESNTL_ATCH_FILE] as const; +const esntlAtchFileMngBaseKey = [ENDPOINT.AS_SERVICE.ESNTL_ATCH_FILE] as const; export function useEsntlAtchFileMngTree(params: EsntlAtchFileMngTreeSearchParams) { return useQuery({ diff --git a/apps/as/src/entities/instl-sncn/api/instl-sncn-service.ts b/apps/as/src/entities/instl-sncn/api/instl-sncn-service.ts index 208e0172d..38ce8e69a 100644 --- a/apps/as/src/entities/instl-sncn/api/instl-sncn-service.ts +++ b/apps/as/src/entities/instl-sncn/api/instl-sncn-service.ts @@ -1,41 +1,91 @@ import { client } from "@/shared/api"; import { ENDPOINT } from "@/shared/config"; +import type { PageResponse } from "@/shared/model"; + import type { + AdstZoneResponseDto, + DocPrgrsUpdateRequestDto, InstlSncnApplySaveRequestDto, - InstlSncnMng, + InstlSncnMngDetResponse, + InstlSncnMngResponseDto, + InstlSncnMngSaveRequestDto, InstlSncnMngSearchParams, InstlSncnMngService, - SpringPage, - InstlSncnMngDetResponse, + InstlSncnMngUpdateRequestDto, } from "../model/instl-sncn-interface"; -export class InstlSncnApiService implements InstlSncnMngService { - public async getPage(params: InstlSncnMngSearchParams): Promise> { - const response = await client.get>(ENDPOINT.AS_INSTL_SERVICE.INSTL_SNCN, { - params, - }); - return response.data; - } +async function getSidoCombo(): Promise { + const response = await client.get( + `${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST}/sido`, + ); - public async getById(id: string): Promise { - const response = await client.get(`${ENDPOINT.AS_INSTL_SERVICE.INSTL_SNCN}/${id}`); - return response.data; - } - - public async saveApply(payload: InstlSncnApplySaveRequestDto): Promise { - const response = await client.post( - `${ENDPOINT.AS_INSTL_SERVICE.INSTL_SNCN}/save-apply`, - payload, - ); - return response.data; - } - public async techReview(payload: InstlSncnApplySaveRequestDto): Promise { - const response = await client.post( - `${ENDPOINT.AS_INSTL_SERVICE.INSTL_SNCN}/tech-review`, - payload, - ); - return response.data; - } + return response.data; } -export const InstlSncnApi = new InstlSncnApiService(); +async function getSigunguCombo(sidoCd: string): Promise { + const response = await client.get( + `${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST}/sigungu`, + { params: { sidoCd } }, + ); + + return response.data; +} + +const BASE = ENDPOINT.AS_SERVICE.INSTL_SNCN; + +async function getPage( + params: InstlSncnMngSearchParams, +): Promise> { + const response = await client.get>(BASE, { + params, + }); + return response.data; +} + +async function getById(id: string): Promise { + const response = await client.get(`${BASE}/${id}`); + return response.data; +} + +async function save(payload: InstlSncnMngSaveRequestDto): Promise { + const response = await client.post(BASE, payload); + return response.data; +} + +async function saveApply(payload: InstlSncnApplySaveRequestDto): Promise { + const response = await client.post(`${BASE}/save-apply`, payload); + return response.data; +} + +async function update(id: string, payload: InstlSncnMngUpdateRequestDto): Promise { + await client.put(`${BASE}/${id}`, payload); +} + +async function techReview(id: string, payload: InstlSncnApplySaveRequestDto): Promise { + await client.post(`${BASE}/${id}/tech-review`, payload); +} + +async function remove(id: string): Promise { + await client.delete(`${BASE}/${id}`); +} + +async function changePrgrsSttsComCd(payload: DocPrgrsUpdateRequestDto): Promise { + await client.post(`${BASE}/doc/updatePrgrsStts`, payload); +} + +export const instlSncnService = { + getPage, + getById, + save, + saveApply, + update, + techReview, + remove, + changePrgrsSttsComCd, +} satisfies InstlSncnMngService; + +// TODO: st-anls-mst service 로 분리 +export const instlSncnRegionService = { + getSidoCombo, + getSigunguCombo, +}; diff --git a/apps/as/src/entities/instl-sncn/api/queries.ts b/apps/as/src/entities/instl-sncn/api/queries.ts new file mode 100644 index 000000000..ac651be47 --- /dev/null +++ b/apps/as/src/entities/instl-sncn/api/queries.ts @@ -0,0 +1,126 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; + +import { ENDPOINT } from "@/shared/config"; + +import type { + DocPrgrsUpdateRequestDto, + InstlSncnApplySaveRequestDto, + InstlSncnMngSaveRequestDto, + InstlSncnMngSearchParams, + InstlSncnMngUpdateRequestDto, +} from "../model/instl-sncn-interface"; + +import { instlSncnRegionService, instlSncnService } from "./instl-sncn-service"; + +export const instlSncnKeys = { + all: ["instl-sncn"] as const, + list: () => [...instlSncnKeys.all, "list"] as const, + detail: (id: string) => [...instlSncnKeys.all, "detail", id] as const, + sidoCombo: () => [ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST, "sido"] as const, + sigunguCombo: (sidoCd: string) => + [ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST, "sigungu", sidoCd] as const, +}; + +export function useInstlSncnList(params: InstlSncnMngSearchParams) { + return useQuery({ + queryKey: [...instlSncnKeys.list(), params], + queryFn: () => instlSncnService.getPage(params), + }); +} + +export function useInstlSncnDetail(id: string) { + return useQuery({ + queryKey: instlSncnKeys.detail(id), + queryFn: () => instlSncnService.getById(id), + enabled: Boolean(id), + }); +} + +export function useInstlSncnSidoCombo() { + return useQuery({ + queryKey: instlSncnKeys.sidoCombo(), + queryFn: () => instlSncnRegionService.getSidoCombo(), + }); +} + +export function useInstlSncnSigunguCombo(sidoCd: string) { + return useQuery({ + queryKey: instlSncnKeys.sigunguCombo(sidoCd), + queryFn: () => instlSncnRegionService.getSigunguCombo(sidoCd), + enabled: Boolean(sidoCd), + }); +} + +export function useInstlSncnSave() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (payload: InstlSncnMngSaveRequestDto) => instlSncnService.save(payload), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: instlSncnKeys.all }); + }, + }); +} + +export function useInstlSncnSaveApply() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (payload: InstlSncnApplySaveRequestDto) => instlSncnService.saveApply(payload), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: instlSncnKeys.all }); + }, + }); +} + +export function useInstlSncnUpdate() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ id, payload }: { id: string; payload: InstlSncnMngUpdateRequestDto }) => + instlSncnService.update(id, payload), + onSuccess: (_, variables) => { + queryClient.invalidateQueries({ queryKey: instlSncnKeys.all }); + queryClient.invalidateQueries({ queryKey: instlSncnKeys.detail(variables.id) }); + }, + }); +} + +export function useInstlSncnTechReview() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ id, payload }: { id: string; payload: InstlSncnApplySaveRequestDto }) => + instlSncnService.techReview(id, payload), + onSuccess: (_, variables) => { + queryClient.invalidateQueries({ queryKey: instlSncnKeys.all }); + queryClient.invalidateQueries({ queryKey: instlSncnKeys.detail(variables.id) }); + }, + }); +} + +export function useInstlSncnRemove() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (id: string) => instlSncnService.remove(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: instlSncnKeys.all }); + }, + }); +} + +export function useInstlSncnChangePrgrsStts() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (payload: DocPrgrsUpdateRequestDto) => + instlSncnService.changePrgrsSttsComCd(payload), + onSuccess: (_, variables) => { + queryClient.invalidateQueries({ queryKey: instlSncnKeys.all }); + queryClient.invalidateQueries({ + queryKey: instlSncnKeys.detail(variables.docNo), + }); + }, + }); +} diff --git a/apps/as/src/entities/instl-sncn/index.ts b/apps/as/src/entities/instl-sncn/index.ts index 5da39b459..9547dd6db 100644 --- a/apps/as/src/entities/instl-sncn/index.ts +++ b/apps/as/src/entities/instl-sncn/index.ts @@ -1,11 +1,33 @@ -export { InstlSncnApi } from "./api/instl-sncn-service"; +export { instlSncnRegionService, instlSncnService } from "./api/instl-sncn-service"; export { + instlSncnKeys, + useInstlSncnDetail, + useInstlSncnList, + useInstlSncnSidoCombo, + useInstlSncnSigunguCombo, + useInstlSncnTechReview, +} from "./api/queries"; +export { + type AdstZoneResponseDto, + type AtflMngClientResponseDto, + type BizCstTreeDto, + type BizSclTreeDto, + type CklstRvwRsltTreeDto, + type DocPrgrsUpdateRequestDto, + type ElaprDocPrgrsClientResponseDto, + type EsntlAtchFileMngTreeResponseDto, INSTL_SNCN_REGISTER_SELECTION_KEY, type InstlSncnApplySaveRequestDto, type InstlSncnBizCstSaveRequestDto, type InstlSncnBzScaleSaveDto, type InstlSncnCklstResultSaveDto, - type InstlSncnMng, + type InstlSncnEsntlAtchFileSaveDto, + type InstlSncnMngDetResponse, + type InstlSncnMngResponseDto, + type InstlSncnMngSaveRequestDto, type InstlSncnMngSearchParams, - type SpringPage, + type InstlSncnRvwOpnnResponseDto, + type InstlSncnRvwOpnnSaveRequestDto, + type InstlSncnSplmntDmndSaveDto, + type ReviewStepCode, } from "./model/instl-sncn-interface"; diff --git a/apps/as/src/entities/instl-sncn/model/instl-sncn-interface.ts b/apps/as/src/entities/instl-sncn/model/instl-sncn-interface.ts index e9a4060ab..378d1e427 100644 --- a/apps/as/src/entities/instl-sncn/model/instl-sncn-interface.ts +++ b/apps/as/src/entities/instl-sncn/model/instl-sncn-interface.ts @@ -1,72 +1,127 @@ -export const INSTL_SNCN_REGISTER_SELECTION_KEY = "instlSncnRegisterSelection"; +import type { PageableParams, PageResponse } from "@/shared/model"; -export interface SpringPage { - content: T[]; - totalElements: number; - totalPages: number; - number: number; - size: number; - first: boolean; - last: boolean; - numberOfElements: number; +/** 행정구역(시도·시군구) 콤보 응답 DTO */ +export interface AdstZoneResponseDto { + /** 행정구역 코드 */ + adstCd: string; + /** 행정구역 명 */ + adstNm: string; } -export interface InstlSncnMngSearchParams { +export const INSTL_SNCN_REGISTER_SELECTION_KEY = "instlSncnRegisterSelection"; + +export interface InstlSncnMngSearchParams extends PageableParams { bizNm?: string; taskSeComCd?: string; sidoComCd?: string; sggComCd?: string; - page?: number; - size?: number; - sort?: string; } -export interface InstlSncnMng { - instlSncnDocNo: string; - admdstCd: string; - sidoNm?: string; - sggNm?: string; - chrgrNm?: string; - instNm?: string; - envoSeComCd: string; +/** 설치인가관리 저장 요청 DTO */ +export interface InstlSncnMngSaveRequestDto { + /** 설치인가문서번호 */ + instlSncnDocNo?: string; + /** 행정구역코드 */ + admdstCd?: string; + /** 환경청구분공통코드 */ + envoSeComCd?: string; + /** 인가구분공통코드 */ sncnSeComCd: string; - envoSeCdNm: string; + /** 업무구분공통코드 */ taskSeComCd: string; - taskSeCdNm: string; - bgtBizCd: string; - bgtAplyYr: string; + /** 예산사업코드 */ + bgtBizCd?: string; + /** 예산신청연도 */ + bgtAplyYr?: string; + /** 사업명 */ bizNm: string; + /** 사업시작연도 */ bizBgngYr: string; + /** 사업종료연도 */ bizEndYr: string; + /** 사업내용 */ bizCn: string; - aplcntNm: string; - aplcntMblTelno: string; - aplcntOgdpNm: string; - aplcntEmlAddr: string; + /** 신청자명 */ + aplcntNm?: string; + /** 신청자휴대전화번호 */ + aplcntMblTelno?: string; + /** 신청자소속명 */ + aplcntOgdpNm?: string; + /** 신청자이메일주소 */ + aplcntEmlAddr?: string; + /** 설계자명 */ dsgnrNm: string; + /** 설계자전화번호 */ dsgnrTelno: string; + /** 설계자소속명 */ dsgnrOgdpNm: string; + /** 설계자이메일주소 */ dsgnrEmlAddr: string; - atchFileGroupId: string; + /** 첨부파일그룹아이디 */ + atchFileGroupId?: string; + /** 진행상태공통코드 */ + prgrsSttsComCd?: string; + /** 재이용시설구분공통코드 */ + rtlztnFcltSeComCd?: string; + /** 재이용용도구분공통코드 */ + rtlztnUsgSeComCd?: string; +} + +/** 설치인가관리 수정 요청 DTO */ +export type InstlSncnMngUpdateRequestDto = InstlSncnMngSaveRequestDto; + +/** 문서 진행상태 수정 요청 DTO */ +export interface DocPrgrsUpdateRequestDto { + /** 행정지원문서번호 */ + docNo: string; + /** 문서구분코드 */ + docSeComCd: string; + /** 진행상태공통코드 */ prgrsSttsComCd: string; +} + +/** 설치인가관리 응답 DTO */ +export interface InstlSncnMngResponseDto extends InstlSncnMngSaveRequestDto { + /** 설치인가문서번호 (필수로 재정의) */ + instlSncnDocNo: string; + /** 환경청구분공통코드명 */ + envoSeCdNm: string; + /** 인가구분공통코드명 */ + sncnSeCdNm: string; + /** 업무구분공통코드명 */ + taskSeCdNm: string; + /** 진행상태공통코드명 */ prgrsSttsCdNm: string; + /** 재이용시설구분공통코드명 */ + rtlztnFcltSeComNm: string; + /** 재이용용도구분공통코드명 */ + rtlztnUsgSeComNm: string; + /** 등록일시 */ regDt: string; - mdfcnDt: string; + /** 등록자 UUID */ + rgtr: string; + /** 등록자명 */ rgtrNm: string; + /** 수정 일시 */ + mdfcnDt: string; + /** 시도 */ + sido: string; + /** 구군 */ + gugun: string; } /** 설치인가 신청 - 사업규모 저장 DTO(배열 원소) */ export interface InstlSncnBzScaleSaveDto { /** 공종구분코드 */ cnstpSeCd: string; - /** 기본계획(A) */ + /** 기본계획수량 */ bscPl: number; - /** 예산반영(B) */ + /** 예산반영수량 */ budgetReflct: number; - /** 금회설계시(C) */ + /** 금회설계수량 */ thtiDsgn: number; - /** 증감(C-B) */ - diff: number; + /** 증감(C-B) — 클라이언트 계산용, API 미전송 */ + diff?: number; } /** 설치인가 신청 - 총사업비 저장 DTO(배열 원소) */ @@ -83,42 +138,284 @@ export interface InstlSncnBizCstSaveRequestDto { /** 설치인가 신청 - 체크리스트 결과 저장 DTO(배열 원소) */ export interface InstlSncnCklstResultSaveDto { + /** 점검표일련번호 */ cklstSn: number; + /** 점검표항목일련번호 */ cklstArtclSn: number; + /** 점검표검토일련번호 */ cklstRvwSn: number; + /** 검토단계공통코드 */ rvwStpComCd: string; + /** 선택값(Y/N/null) */ selected: "Y" | "N" | null; + /** 담당자검토내용(기술검토의견) */ picRvwCn?: string | null; } /** 설치인가 신청 - 필수첨부파일 저장 DTO(배열 원소) */ export interface InstlSncnEsntlAtchFileSaveDto { + /** 필수첨부파일아이디 */ esntlAtchFileId: string; + /** 첨부파일그룹아이디 */ atchFileGroupId: string; } +/** 설치인가검토의견 저장 요청 DTO */ +export interface InstlSncnRvwOpnnSaveRequestDto { + /** 설치인가문서번호 */ + instlSncnDocNo: string; + /** 검토의견일련번호 */ + rvwOpnnSn?: number; + /** 의견자구분공통코드 */ + opnrSeComCd?: string; + /** 의견자명 */ + opnrNm?: string; + /** 검토결과내용 */ + rvwRsltCn?: string; + /** 검토결과이상여부 */ + rvwRsltAnmlyYn?: string; + /** 첨부파일그룹아이디 */ + atchFileGroupId?: string; +} + +/** 설치인가 보완요구 저장 DTO(배열 원소) */ +export interface InstlSncnSplmntDmndSaveDto { + /** 보완요구 일련번호 */ + splmntDmndSn?: number; + /** 보완항목 공통코드 */ + splmntArtclComCd?: string; + /** 보완요구 내용 */ + splmntDmndCn?: string; + /** 보완요구 일시 */ + splmntDmndDt?: string; + /** 보완요구 첨부파일 그룹 ID */ + splmntDmndAtchFileGroupId?: string; + /** 보완 내용 */ + splmntCn?: string; + /** 보완 담당자 명 */ + splmntPicNm?: string; + /** 보완완료 일시 */ + splmntCmptnDt?: string; + /** 보완 첨부파일 그룹 ID */ + splmntAtchFileGroupId?: string; + /** 완료 여부 */ + cmptnYn?: string; +} + /** 설치인가 신청 - 통합 저장 요청 DTO */ export interface InstlSncnApplySaveRequestDto { - instlSncnMng: InstlSncnMng; + /** 기술검토저장구분(SV.저장, CP.검토완료) */ + techReViewSaveCd?: string; + /** 검토단계공통코드(ENV.환경청.EP, KECO.환경공단.TE) */ + rvwStpCd?: string; + /** 설치인가관리 정보 */ + instlSncnMng: InstlSncnMngSaveRequestDto; + /** 사업규모 목록 */ bzScaleList: InstlSncnBzScaleSaveDto[]; + /** 총사업비 목록 */ bizCstList: InstlSncnBizCstSaveRequestDto[]; + /** 체크리스트 검토결과 목록 */ cklstResultList: InstlSncnCklstResultSaveDto[]; + /** 체크리스트 검토결과 맵 (rvwStpComCd -> 목록) */ + cklstResultMap?: Record; + /** 필수첨부파일 목록 */ esntlAtchFileList: InstlSncnEsntlAtchFileSaveDto[]; + /** 검토의견 목록 */ + rvwOpnnList?: InstlSncnRvwOpnnSaveRequestDto[]; + /** 보완요구 목록 */ + splmntDmndList?: InstlSncnSplmntDmndSaveDto[]; } export type ReviewStepCode = "LOCAL" | "ENV" | "KECO"; +/** 공종 트리와 사업규모(수량) 조합 DTO */ +export interface BizSclTreeDto { + /** 들여쓰기 적용된 공종명(시각적) */ + indCnstpSeCdNm: string; + /** 공종 코드 */ + cnstpSeCd: string; + /** 공종 코드명 */ + cnstpSeCdNm: string; + /** 사용 여부(Y/N) */ + useYn: string; + /** 트리 레벨(깊이) */ + treeLevel: number; + /** 트리 경로 표시용 문자열 */ + treePath: string; + /** 기본계획수량 */ + bscPlanQty: number; + /** 예산반영수량 */ + bgtRfltQty: number; + /** 금회설계수량 */ + tstmDesignQty: number; +} + +/** 회계구분 트리와 사업비용 조합 DTO */ +export interface BizCstTreeDto { + /** 들여쓰기 적용된 회계구분명(시각적) */ + indAcntgSeCdNm: string; + /** 회계구분 코드 */ + acntgSeCd: string; + /** 회계구분 코드명 */ + acntgSeCdNm: string; + /** 사용 여부(Y/N) */ + useYn: string; + /** 트리 레벨(깊이) */ + treeLevel: number; + /** 트리 경로 표시용 문자열 */ + treePath: string; + /** 기본계획금액 */ + bscPlanAmt: number; + /** 예산반영금액 */ + bgtRfltAmt: number; + /** 금회설계금액 */ + tstmDesignAmt: number; +} + +/** 점검표 검토결과 트리 DTO */ +export interface CklstRvwRsltTreeDto { + /** 점검표일련번호 */ + cklstSn: number; + /** 점검표명 */ + cklstNm: string; + /** 점검표항목일련번호 */ + cklstArtclSn: number; + /** 점검표항목명 */ + cklstArtclNm: string; + /** 검토일련번호 */ + cklstRvwSn: number; + /** 상위검토일련번호 */ + upCklstRvwSn: number | null; + /** 트리 레벨 */ + treeLevel: number; + /** 검토내용 */ + rvwCn: string; + /** 점검 사용 여부 */ + bchckUseYn: string; + /** 정렬순서 */ + sortSeq: number; + /** 업무구분코드 */ + taskSeComCd: string; + /** 검토단계공통코드 */ + rvwStpComCd: string; + /** 검토결과이상여부 */ + rvwRsltAnmlyYn: string; + /** 검토담당자 UUID */ + rvwPicUuid: string | null; + /** 담당자검토내용 */ + picRvwCn: string | null; + /** 검토일시 */ + rvwDt: string | null; +} + +/** 필수첨부파일관리 트리 조회 응답 DTO */ +export interface EsntlAtchFileMngTreeResponseDto { + /** 필수첨부파일아이디 */ + esntlAtchFileId: string; + /** 문서구분공통코드 */ + docSeComCd: string; + /** 필수첨부파일명 */ + esntlAtchFileNm: string; + /** 필수첨부여부 */ + esntlAtchYn: string; + /** 필수첨부파일비고 */ + esntlAtchFileRmrk: string | null; + /** 첨부파일 코드 */ + atflCd?: string; + /** 첨부파일 일련번호 */ + atflSn?: number; + /** 첨부파일 ID */ + atflId?: string; + /** 원본 파일명 */ + orgnlFileNm?: string; + /** 첨부파일 크기 */ + atflFlieSz?: number; + /** 첨부파일 타입 */ + atflType?: string; + /** 다운로드 횟수 */ + dwnldNocs?: number; + /** 등록일시 */ + regDt?: string; +} + +/** 첨부파일 응답 DTO (attach-service client) */ +export interface AtflMngClientResponseDto { + atflCd: string; + atflSn: number; + atflId: string; + physFileNm: string; + orgnlFileNm: string; + atflFlieSz: number; + atflType: string; + dwnldNocs: number; + delYn: string; + lnkgDmnId: string | null; + lnkgDmnNm: string | null; + regDt: string; +} + +/** 설치인가검토의견 응답 DTO */ +export interface InstlSncnRvwOpnnResponseDto { + /** 설치인가문서번호 */ + instlSncnDocNo: string; + /** 검토의견일련번호 */ + rvwOpnnSn: number; + /** 의견자구분공통코드 */ + opnrSeComCd: string; + /** 의견자구분공통코드명 */ + opnrSeCdNm: string | null; + /** 의견자명 */ + opnrNm: string; + /** 검토결과내용 */ + rvwRsltCn: string; + /** 검토결과이상여부 */ + rvwRsltAnmlyYn: string; + /** 첨부파일그룹아이디 */ + atchFileGroupId: string | null; + /** 첨부파일 상세 정보 */ + atchFileInfo?: AtflMngClientResponseDto; + /** 등록일시 */ + regDt: string; +} + +/** 전자결재 진행상태 (클라이언트 응답용) */ +export interface ElaprDocPrgrsClientResponseDto { + elaprDocNo?: string; + elaprDmndId?: string; + elaprPrgrsId?: string; + atrzSeqNo?: number; + atrzSeComCd?: string; + atrzSeComNm?: string; + aprvrUuid?: string; + aprvrUuidNm?: string; + atrzPrgrsSttsComCd?: string; + atrzPrgrsSttsComNm?: string; + rjctRsn?: string | null; + atrzDt?: string | null; + jbpsSeNm?: string; + jbgdSeNm?: string; + ognzNm?: string; +} + +/** 설치인가관리 상세 응답 DTO */ export interface InstlSncnMngDetResponse { - detDto: InstlSncnMng; - bizSclList: any[]; - bizCstList: any[]; - cklstResultList?: any[]; - esntlFileList: any[]; - etcFileList: any[]; + detDto: InstlSncnMngResponseDto; + bizSclList: BizSclTreeDto[]; + bizCstList: BizCstTreeDto[]; + cklstResultList?: CklstRvwRsltTreeDto[]; + esntlFileList: EsntlAtchFileMngTreeResponseDto[]; + etcFileList: AtflMngClientResponseDto[]; + rvwList?: InstlSncnRvwOpnnResponseDto[]; + elaprDocPrgrsList?: ElaprDocPrgrsClientResponseDto[] | null; } export interface InstlSncnMngService { - getPage: (params: InstlSncnMngSearchParams) => Promise>; + getPage: (params: InstlSncnMngSearchParams) => Promise>; getById: (id: string) => Promise; + save: (payload: InstlSncnMngSaveRequestDto) => Promise; saveApply: (payload: InstlSncnApplySaveRequestDto) => Promise; + update: (id: string, payload: InstlSncnMngUpdateRequestDto) => Promise; + techReview: (id: string, payload: InstlSncnApplySaveRequestDto) => Promise; + remove: (id: string) => Promise; + changePrgrsSttsComCd: (payload: DocPrgrsUpdateRequestDto) => Promise; } diff --git a/apps/as/src/entities/rvw-mnpw/api/queries.ts b/apps/as/src/entities/rvw-mnpw/api/queries.ts new file mode 100644 index 000000000..7b38c6acf --- /dev/null +++ b/apps/as/src/entities/rvw-mnpw/api/queries.ts @@ -0,0 +1,102 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; + +import { ENDPOINT } from "@/shared/config"; + +import type { + RvwMnpwMngPageSearchDto, + RvwMnpwMngSaveRequestDto, + RvwMnpwMngSearchDto, + RvwMnpwMngUpdateRequestDto, +} from "../model/rvw-mnpw-interface"; + +import { rvwMnpwMngService } from "./rvw-mnpw-service"; + +const rvwMnpwMngBaseKey = [ENDPOINT.AS_SERVICE.RVW_MNPW] as const; + +export function useRvwMnpwMngPage( + params: RvwMnpwMngPageSearchDto, + options?: { enabled?: boolean }, +) { + return useQuery({ + queryKey: [...rvwMnpwMngBaseKey, "findPage", params], + queryFn: () => rvwMnpwMngService.findPage(params), + enabled: options?.enabled, + }); +} + +export function useRvwMnpwMngLoginUserList( + searchDto: RvwMnpwMngSearchDto, + options?: { enabled?: boolean }, +) { + return useQuery({ + queryKey: [...rvwMnpwMngBaseKey, "findRvwLoginUserList", searchDto], + queryFn: () => rvwMnpwMngService.findRvwLoginUserList(searchDto), + enabled: options?.enabled, + }); +} + +export function useRvwMnpwMng( + atrzSeComCd: string | null | undefined, + ognzSn: number | null | undefined, + userUuid: string | null | undefined, +) { + return useQuery({ + queryKey: [...rvwMnpwMngBaseKey, "findById", atrzSeComCd, ognzSn, userUuid], + queryFn: () => rvwMnpwMngService.findById(atrzSeComCd!, ognzSn!, userUuid!), + enabled: Boolean(atrzSeComCd && ognzSn != null && userUuid), + }); +} + +export function useSaveRvwMnpwMng() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (body: RvwMnpwMngSaveRequestDto) => rvwMnpwMngService.save(body), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: rvwMnpwMngBaseKey }); + }, + }); +} + +export function useUpdateRvwMnpwMng() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ + atrzSeComCd, + ognzSn, + userUuid, + body, + }: { + atrzSeComCd: string; + ognzSn: number; + userUuid: string; + body: RvwMnpwMngUpdateRequestDto; + }) => rvwMnpwMngService.update(atrzSeComCd, ognzSn, userUuid, body), + onSuccess: (_data, { atrzSeComCd, ognzSn, userUuid }) => { + queryClient.invalidateQueries({ queryKey: rvwMnpwMngBaseKey }); + queryClient.invalidateQueries({ + queryKey: [...rvwMnpwMngBaseKey, "findById", atrzSeComCd, ognzSn, userUuid], + }); + }, + }); +} + +export function useDeleteRvwMnpwMng() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ + atrzSeComCd, + ognzSn, + userUuid, + }: { + atrzSeComCd: string; + ognzSn: number; + userUuid: string; + }) => rvwMnpwMngService.delete(atrzSeComCd, ognzSn, userUuid), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: rvwMnpwMngBaseKey }); + }, + }); +} diff --git a/apps/as/src/entities/rvw-mnpw/api/rvw-mnpw-service.ts b/apps/as/src/entities/rvw-mnpw/api/rvw-mnpw-service.ts new file mode 100644 index 000000000..2e99f3fba --- /dev/null +++ b/apps/as/src/entities/rvw-mnpw/api/rvw-mnpw-service.ts @@ -0,0 +1,81 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { PageResponse } from "@/shared/model"; + +import type { + RvwMnpwMngPageSearchDto, + RvwMnpwMngResponseDto, + RvwMnpwMngSaveRequestDto, + RvwMnpwMngSearchDto, + RvwMnpwMngService, + RvwMnpwMngUpdateRequestDto, +} from "../model/rvw-mnpw-interface"; + +const BASE = ENDPOINT.AS_SERVICE.RVW_MNPW; + +async function findPage( + params: RvwMnpwMngPageSearchDto, +): Promise> { + const response = await client.get>(BASE, { params }); + return response.data; +} + +async function findRvwLoginUserList( + searchDto: RvwMnpwMngSearchDto, +): Promise> { + const response = await client.get>(`${BASE}/loginuser/me`, { + params: searchDto, + }); + return response.data; +} + +async function findById( + atrzSeComCd: string, + ognzSn: number, + userUuid: string, +): Promise { + const response = await client.get( + `${BASE}/${atrzSeComCd}/${ognzSn}/${userUuid}`, + ); + return response.data; +} + +async function saveRvwMnpwMng(body: RvwMnpwMngSaveRequestDto): Promise { + const { atrzSeComCd, ognzSn, userUuid } = body; + const response = await client.post( + `${BASE}/${atrzSeComCd}/${ognzSn}/${userUuid}`, + body, + ); + return response.data; +} + +async function updateRvwMnpwMng( + atrzSeComCd: string, + ognzSn: number, + userUuid: string, + body: RvwMnpwMngUpdateRequestDto, +): Promise { + const response = await client.put( + `${BASE}/${atrzSeComCd}/${ognzSn}/${userUuid}`, + body, + ); + return response.data; +} + +async function deleteRvwMnpwMng( + atrzSeComCd: string, + ognzSn: number, + userUuid: string, +): Promise { + const response = await client.delete(`${BASE}/${atrzSeComCd}/${ognzSn}/${userUuid}`); + return response.data; +} + +export const rvwMnpwMngService = { + findPage, + findRvwLoginUserList, + findById, + save: saveRvwMnpwMng, + update: updateRvwMnpwMng, + delete: deleteRvwMnpwMng, +} satisfies RvwMnpwMngService; diff --git a/apps/as/src/entities/rvw-mnpw/index.ts b/apps/as/src/entities/rvw-mnpw/index.ts new file mode 100644 index 000000000..380c50eb7 --- /dev/null +++ b/apps/as/src/entities/rvw-mnpw/index.ts @@ -0,0 +1,17 @@ +export { + useDeleteRvwMnpwMng, + useRvwMnpwMng, + useRvwMnpwMngLoginUserList, + useRvwMnpwMngPage, + useSaveRvwMnpwMng, + useUpdateRvwMnpwMng, +} from "./api/queries"; +export { rvwMnpwMngService } from "./api/rvw-mnpw-service"; +export type { + RvwMnpwMngPageSearchDto, + RvwMnpwMngResponseDto, + RvwMnpwMngSaveRequestDto, + RvwMnpwMngSearchDto, + RvwMnpwMngService, + RvwMnpwMngUpdateRequestDto, +} from "./model/rvw-mnpw-interface"; diff --git a/apps/as/src/entities/rvw-mnpw/model/rvw-mnpw-interface.ts b/apps/as/src/entities/rvw-mnpw/model/rvw-mnpw-interface.ts new file mode 100644 index 000000000..4fee8df2e --- /dev/null +++ b/apps/as/src/entities/rvw-mnpw/model/rvw-mnpw-interface.ts @@ -0,0 +1,54 @@ +import type { PageableParams, PageResponse } from "@/shared/model"; + +/** 백엔드 RvwMnpwMngResponseDto */ +export interface RvwMnpwMngResponseDto { + atrzSeComCd: string; + atrzSeComCdNm?: string; + ognzSn: number; + userUuid: string; + userUuidNm?: string; + ognzNm?: string; + jbpsSeNm?: string; + jbgdSeNm?: string; + rgtr?: string; + regDt?: string; +} + +/** 백엔드 RvwMnpwMngSearchDto */ +export interface RvwMnpwMngSearchDto { + atrzSeComCd?: string; + ognzSn?: number; + userUuid?: string; +} + +/** findPage — RvwMnpwMngSearchDto + Pageable */ +export type RvwMnpwMngPageSearchDto = RvwMnpwMngSearchDto & PageableParams; + +/** 백엔드 RvwMnpwMngSaveRequestDto */ +export interface RvwMnpwMngSaveRequestDto { + atrzSeComCd: string; + ognzSn: number; + userUuid: string; +} + +/** 백엔드 RvwMnpwMngUpdateRequestDto */ +export interface RvwMnpwMngUpdateRequestDto { + newAtrzSeComCd: string; +} + +/** Controller public 메서드명·시그니처와 동일 */ +export interface RvwMnpwMngService { + findPage(params: RvwMnpwMngPageSearchDto): Promise>; + findRvwLoginUserList( + searchDto: RvwMnpwMngSearchDto, + ): Promise>; + findById(atrzSeComCd: string, ognzSn: number, userUuid: string): Promise; + save(body: RvwMnpwMngSaveRequestDto): Promise; + update( + atrzSeComCd: string, + ognzSn: number, + userUuid: string, + body: RvwMnpwMngUpdateRequestDto, + ): Promise; + delete(atrzSeComCd: string, ognzSn: number, userUuid: string): Promise; +} diff --git a/apps/as/src/entities/tus/index.ts b/apps/as/src/entities/tus/index.ts new file mode 100644 index 000000000..5cf2e3fbf --- /dev/null +++ b/apps/as/src/entities/tus/index.ts @@ -0,0 +1 @@ +export { handleTusUpload, type TusUploadOptions } from "./lib/utils"; diff --git a/apps/as/src/entities/tus/lib/utils.ts b/apps/as/src/entities/tus/lib/utils.ts new file mode 100644 index 000000000..6bb40bfd7 --- /dev/null +++ b/apps/as/src/entities/tus/lib/utils.ts @@ -0,0 +1,61 @@ +import { Upload } from "tus-js-client"; + +import { ENDPOINT, PROXY_HOST } from "@/shared/config"; +import { AUTH_USER_ID, CLAIM_NAME } from "@/shared/config/env"; +import { getCookie } from "@/shared/lib"; + +interface TusUploadOptions { + metadata?: Record; + handleUploadStart?: (upload: Upload) => void; + handleProgress?: (percentage: number) => void; + handleSuccess?: (upload: Upload, atflCd: string, atflId: string) => void; + handleError?: (error: Error) => void; +} + +function handleTusUpload(file: File, options?: TusUploadOptions) { + let atflCd = ""; + let atflId = ""; + + const metadata = { + filename: file.name, + filetype: file.type, + ...options?.metadata, + }; + + const upload = new Upload(file, { + endpoint: `${PROXY_HOST}${ENDPOINT.ATTACH_SERVICE.TUS_UPLOAD}`, + chunkSize: 10 * 1024 * 1024, + headers: { + [CLAIM_NAME]: getCookie(CLAIM_NAME) ?? "", + [AUTH_USER_ID]: getCookie(AUTH_USER_ID) ?? "", + }, + metadata, + onError(error) { + console.error("TUS upload failed:", error); + options?.handleError?.(error); + }, + onProgress(bytesUploaded, bytesTotal) { + const percentage = (bytesUploaded / bytesTotal) * 100; + options?.handleProgress?.(percentage); + }, + onAfterResponse(_req, res) { + const fileInfo = res.getHeader("X-File-Info"); + if (!fileInfo) return; + + const parsed = JSON.parse(fileInfo) as { atflCd?: string; atflId?: string }; + atflCd = parsed.atflCd ?? ""; + atflId = parsed.atflId ?? ""; + }, + onSuccess() { + options?.handleSuccess?.(upload, atflCd, atflId); + }, + }); + + upload.start(); + options?.handleUploadStart?.(upload); + + return upload; +} + +export { handleTusUpload }; +export type { TusUploadOptions }; diff --git a/apps/as/src/entities/user-organization/model/ognz-user-tree-interface.ts b/apps/as/src/entities/user-organization/model/ognz-user-tree-interface.ts index 71dcc5833..47e75d944 100644 --- a/apps/as/src/entities/user-organization/model/ognz-user-tree-interface.ts +++ b/apps/as/src/entities/user-organization/model/ognz-user-tree-interface.ts @@ -4,6 +4,8 @@ export interface OgnzUserTreeUserDto { userNm: string; jbpsSeCd: string; jbgdSeCd: string; + jbpsSeNm: string; + jbgdSeNm: string; } /** OgnzUserTreeResponseDto */ diff --git a/apps/as/src/pages/acntg-code-management/lib/acntg-se-grid.ts b/apps/as/src/pages/acntg-code-management/lib/acntg-se-grid.ts index 1b7eaa6bf..c0ca7abc7 100644 --- a/apps/as/src/pages/acntg-code-management/lib/acntg-se-grid.ts +++ b/apps/as/src/pages/acntg-code-management/lib/acntg-se-grid.ts @@ -145,6 +145,15 @@ export function collectDescendantRowKeys( return result; } +export function insertRootSlot( + structure: AcntgDisplaySlot[], + slot: AcntgDisplaySlot, +): AcntgDisplaySlot[] { + const firstRootIndex = structure.findIndex((item) => item.parentRowKey === null); + if (firstRootIndex === -1) return [slot, ...structure]; + return [...structure.slice(0, firstRootIndex), slot, ...structure.slice(firstRootIndex)]; +} + export function insertChildSlot( structure: AcntgDisplaySlot[], parentRowKey: string, @@ -153,12 +162,7 @@ export function insertChildSlot( const parentIndex = structure.findIndex((item) => item.rowKey === parentRowKey); if (parentIndex === -1) return [...structure, slot]; - const parentDepth = structure[parentIndex].depth; - let insertAt = parentIndex + 1; - while (insertAt < structure.length && structure[insertAt].depth > parentDepth) { - insertAt += 1; - } - + const insertAt = parentIndex + 1; return [...structure.slice(0, insertAt), slot, ...structure.slice(insertAt)]; } @@ -214,6 +218,21 @@ export function markAcntgRowsDeleted( return { ...state, deletedKeys }; } +export function validateDraftParentAcntgSeCd( + state: AcntgSeGridState, + rowKey: string, +): string | null { + const draft = state.drafts[rowKey]; + if (!draft?.parentRowKey) return null; + + const parent = mergeAcntgRow(state, draft.parentRowKey); + if (!parent?.acntgSeCd?.trim()) { + return "하위 항목을 추가하려면 상위 코드를 먼저 입력하세요."; + } + + return null; +} + export function removeDraftFromGrid(state: AcntgSeGridState, rowKey: string): AcntgSeGridState { const { [rowKey]: _removed, ...restDrafts } = state.drafts; const keysToRemove = collectDescendantRowKeys(state.structure, rowKey); diff --git a/apps/as/src/pages/acntg-code-management/lib/use-acntg-se-rows.ts b/apps/as/src/pages/acntg-code-management/lib/use-acntg-se-rows.ts index a8c9ebc72..5fd9f85c5 100644 --- a/apps/as/src/pages/acntg-code-management/lib/use-acntg-se-rows.ts +++ b/apps/as/src/pages/acntg-code-management/lib/use-acntg-se-rows.ts @@ -13,11 +13,13 @@ import { hasAcntgGridPendingChanges, hydrateGridFromServer, insertChildSlot, + insertRootSlot, markAcntgRowsDeleted, mergeAcntgRow, mergeAllAcntgRows, patchAcntgGridRow, removeDraftFromGrid, + validateDraftParentAcntgSeCd, } from "./acntg-se-grid"; import { buildSaveMultiPayload } from "./build-save-multi-payload"; @@ -81,51 +83,43 @@ export function useAcntgSeRows({ serverContent, onAlert, onSave }: UseAcntgSeRow useYn: "Y", }, }, - structure: [...prev.structure, { rowKey, depth: 0, parentRowKey: null }], + structure: insertRootSlot(prev.structure, { rowKey, depth: 0, parentRowKey: null }), })); }, []); - const addChildRow = useCallback( - (parent: AcntgSeRow) => { - if (parent.isNew) { - onAlert("상위 행을 먼저 저장한 후 하위 항목을 추가할 수 있습니다."); - return; - } + const addChildRow = useCallback((parent: AcntgSeRow) => { + const rowKey = createDraftRowKey(); + const slot = { + rowKey, + depth: 0, + parentRowKey: parent.rowKey, + }; - const rowKey = createDraftRowKey(); - const slot = { - rowKey, - depth: 0, - parentRowKey: parent.rowKey, - }; + setGrid((prev) => { + const parentSlot = prev.structure.find((item) => item.rowKey === parent.rowKey); + const depth = (parentSlot?.depth ?? 0) + 1; - setGrid((prev) => { - const parentSlot = prev.structure.find((item) => item.rowKey === parent.rowKey); - const depth = (parentSlot?.depth ?? 0) + 1; - - return { - ...prev, - drafts: { - ...prev.drafts, - [rowKey]: { - rowKey, - parentRowKey: parent.rowKey, - cdLvNo: (parent.cdLvNo ?? 1) + 1, - acntgSeCd: "", - acntgSeCdNm: "", - expln: "", - useYn: "Y", - }, + return { + ...prev, + drafts: { + ...prev.drafts, + [rowKey]: { + rowKey, + parentRowKey: parent.rowKey, + cdLvNo: (parent.cdLvNo ?? 1) + 1, + acntgSeCd: "", + acntgSeCdNm: "", + expln: "", + useYn: "Y", }, - structure: insertChildSlot(prev.structure, parent.rowKey, { - ...slot, - depth, - }), - }; - }); - }, - [onAlert], - ); + }, + structure: insertChildSlot(prev.structure, parent.rowKey, { + ...slot, + depth, + }), + }; + }); + }, []); const removeDraft = useCallback((rowKey: string) => { setGrid((prev) => removeDraftFromGrid(prev, rowKey)); @@ -146,6 +140,12 @@ export function useAcntgSeRows({ serverContent, onAlert, onSave }: UseAcntgSeRow for (const row of mergedRows) { if (!row.isDeleted && (row.isNew || row.isDirty)) { + const parentCodeError = validateDraftParentAcntgSeCd(grid, row.rowKey); + if (parentCodeError) { + onAlert(parentCodeError); + return false; + } + const fieldError = validateAcntgSeRow(row); if (fieldError) { onAlert(fieldError); diff --git a/apps/as/src/pages/acntg-code-management/ui/acntg-code-management-page.tsx b/apps/as/src/pages/acntg-code-management/ui/acntg-code-management-page.tsx index 5cf1163b4..d480ced59 100644 --- a/apps/as/src/pages/acntg-code-management/ui/acntg-code-management-page.tsx +++ b/apps/as/src/pages/acntg-code-management/ui/acntg-code-management-page.tsx @@ -86,15 +86,10 @@ function AcntgCodeManagementPage() {

회계코드 관리

- -
@@ -110,38 +105,40 @@ function AcntgCodeManagementPage() { 등록된 코드가 없습니다. [코드 추가]로 1단계 코드를 등록하세요.

) : ( - - - - - - - - - - - - 코드 - 코드명 - 설명 - 사용 여부 - 구분 - 관리 - - - - {displayRows.map(({ row, depth }) => ( - updateRow(row.rowKey, patch)} - onAddChild={() => addChildRow(row)} - onDelete={() => handleDelete(row)} - /> - ))} - -
+
+ + + + + + + + + + + + 코드 + 코드명 + 설명 + 사용 여부 + 구분 + 관리 + + + + {displayRows.map(({ row, depth }) => ( + updateRow(row.rowKey, patch)} + onAddChild={() => addChildRow(row)} + onDelete={() => handleDelete(row)} + /> + ))} + +
+
)}
diff --git a/apps/as/src/pages/acntg-code-management/ui/acntg-se-row.tsx b/apps/as/src/pages/acntg-code-management/ui/acntg-se-row.tsx index a660f86ad..69b53ec60 100644 --- a/apps/as/src/pages/acntg-code-management/ui/acntg-se-row.tsx +++ b/apps/as/src/pages/acntg-code-management/ui/acntg-se-row.tsx @@ -41,7 +41,7 @@ function AcntgSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Acn onChange={(e) => onChange({ acntgSeCd: e.target.value })} placeholder="코드" maxLength={10} - readOnly={!row.isNew} + disabled={!row.isNew} className="flex-1" /> @@ -85,12 +85,12 @@ function AcntgSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Acn type="button" size="sm" color="white" - disabled={row.isNew || !canAddChildRow()} + disabled={!canAddChildRow()} onClick={onAddChild} > 하위추가 - diff --git a/apps/as/src/pages/approval-line-management/lib/validators.ts b/apps/as/src/pages/approval-line-management/lib/validators.ts index c5fbe0b71..4e301afa9 100644 --- a/apps/as/src/pages/approval-line-management/lib/validators.ts +++ b/apps/as/src/pages/approval-line-management/lib/validators.ts @@ -1,10 +1,7 @@ import type { ElaprAplnRow } from "../model/apln-row-types"; -export const ATRZ_RECEIVER_CD = "ATRZ00"; -export const ATRZ_REVIEWER_1_CD = "ATRZ01"; -export const ATRZ_REVIEWER_2_CD = "ATRZ02"; -export const ATRZ_APPROVER_CD = "ATRZ03"; -export const ATRZ_FINAL_CD = "ATRZ99"; +const ATRZ_RECEIVER_CD = "ATRZ01"; +const ATRZ_FINAL_CD = "ATRZ99"; export function getVisibleRows(rows: ElaprAplnRow[]): ElaprAplnRow[] { return rows.filter((row) => !row.isDeleted); diff --git a/apps/as/src/pages/approval-line-management/model/columns.tsx b/apps/as/src/pages/approval-line-management/model/columns.tsx index aa00e9afc..a7beb668b 100644 --- a/apps/as/src/pages/approval-line-management/model/columns.tsx +++ b/apps/as/src/pages/approval-line-management/model/columns.tsx @@ -35,10 +35,10 @@ export function createTmpltColumns({ onEdit, onDelete }: CreateTmpltColumnsOptio header: "관리", cell: ({ row }) => (
e.stopPropagation()}> - -
diff --git a/apps/as/src/pages/approval-line-management/model/reviewer-option.ts b/apps/as/src/pages/approval-line-management/model/reviewer-option.ts index 569646744..cb79bbf13 100644 --- a/apps/as/src/pages/approval-line-management/model/reviewer-option.ts +++ b/apps/as/src/pages/approval-line-management/model/reviewer-option.ts @@ -1,4 +1,4 @@ -import type { OgnzUserTreeUserDto } from "@/entities/user-organization"; +import type { RvwMnpwMngResponseDto } from "@/entities/rvw-mnpw"; export interface ReviewerOption { uuid: string; @@ -6,10 +6,10 @@ export interface ReviewerOption { deptNm: string; } -export function toReviewerOption(user: OgnzUserTreeUserDto, ognzNm: string): ReviewerOption { +export function fromRvwMnpwMng(item: RvwMnpwMngResponseDto): ReviewerOption { return { - uuid: user.userUnqId, - name: user.userNm, - deptNm: ognzNm, + uuid: item.userUuid, + name: item.userUuidNm ?? "", + deptNm: item.ognzNm ?? "", }; } diff --git a/apps/as/src/pages/approval-line-management/ui/aprvr-picker-dialog.tsx b/apps/as/src/pages/approval-line-management/ui/aprvr-picker-dialog.tsx index 8b6b7aa1d..38f7c810d 100644 --- a/apps/as/src/pages/approval-line-management/ui/aprvr-picker-dialog.tsx +++ b/apps/as/src/pages/approval-line-management/ui/aprvr-picker-dialog.tsx @@ -1,8 +1,6 @@ "use client"; -import { useCallback, useEffect, useState } from "react"; - -import { useOgnzUserTree } from "@/entities/user-organization"; +import { useRvwMnpwMngPage } from "@/entities/rvw-mnpw"; import { Button, Dialog, @@ -10,110 +8,109 @@ import { DialogFooter, DialogHeader, DialogTitle, - Input, + Skeleton, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, } from "@common/components"; -import { collectExpandedOgnzSnIds, filterOgnzUserTree } from "../lib/filter-ognz-user-tree"; -import type { ReviewerOption } from "../model/reviewer-option"; +import { fromRvwMnpwMng, type ReviewerOption } from "../model/reviewer-option"; -import { OgnzUserTree } from "./ognz-user-tree"; +const PAGE_SIZE = 500; interface AprvrPickerDialogProps { open: boolean; onOpenChange: (open: boolean) => void; + atrzSeComCd: string; onSelect: (reviewer: ReviewerOption) => void; } -function AprvrPickerDialog({ open, onOpenChange, onSelect }: AprvrPickerDialogProps) { - const [keyword, setKeyword] = useState(""); - const [displayTree, setDisplayTree] = useState>([]); - const [expandedOgnzSnIds, setExpandedOgnzSnIds] = useState>(new Set()); - const [selectedUserId, setSelectedUserId] = useState(); +function AprvrPickerDialog({ + open, + onOpenChange, + atrzSeComCd, + onSelect, +}: AprvrPickerDialogProps) { + const { data, isPending, isError } = useRvwMnpwMngPage( + { + atrzSeComCd, + page: 0, + size: PAGE_SIZE, + sort: "userUuidNm,asc", + }, + { enabled: open }, + ); - const { data, isPending, isError } = useOgnzUserTree({ enabled: open }); + const items = data?.content ?? []; - const resetState = useCallback(() => { - setKeyword(""); - setSelectedUserId(undefined); - setExpandedOgnzSnIds(new Set()); - setDisplayTree(data ?? []); - }, [data]); - - useEffect(() => { - if (!open) { - setKeyword(""); - setSelectedUserId(undefined); - setExpandedOgnzSnIds(new Set()); - return; - } - - setDisplayTree(data ?? []); - }, [open, data]); - - const handleToggle = (ognzSn: number, isExpanded: boolean) => { - setExpandedOgnzSnIds((prev) => { - const next = new Set(prev); - if (isExpanded) next.add(ognzSn); - else next.delete(ognzSn); - return next; - }); - }; - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - e.stopPropagation(); - - const filtered = filterOgnzUserTree(data ?? [], keyword); - setDisplayTree(filtered); - setExpandedOgnzSnIds(collectExpandedOgnzSnIds(filtered)); - }; - - const handleReset = () => { - resetState(); - }; - - const handleSelect = (reviewer: ReviewerOption) => { - setSelectedUserId(reviewer.uuid); - onSelect(reviewer); + const handleSelect = (item: (typeof items)[number]) => { + onSelect(fromRvwMnpwMng(item)); onOpenChange(false); }; return ( - + 검토인력 선택 -
-
-
- setKeyword(e.target.value)} - /> - - -
-
- -
- -
+
+ {isPending ? ( +
+ + +
+ ) : isError ? ( +

+ 검토인력 목록을 불러오지 못했습니다. +

+ ) : items.length === 0 ? ( +

+ 배치된 검토인력이 없습니다. +

+ ) : ( + + + + + + + + + + + 성명 + 부서 + 직위 + 직급 + 선택 + + + + {items.map((item) => ( + + + {item.userUuidNm || item.userUuid} + + {item.ognzNm || "-"} + {item.jbpsSeNm || "-"} + {item.jbgdSeNm || "-"} + + + + + ))} + +
+ )}
diff --git a/apps/as/src/pages/approval-line-management/ui/elapr-apln-grid.tsx b/apps/as/src/pages/approval-line-management/ui/elapr-apln-grid.tsx index 1dd757ff9..c2393715b 100644 --- a/apps/as/src/pages/approval-line-management/ui/elapr-apln-grid.tsx +++ b/apps/as/src/pages/approval-line-management/ui/elapr-apln-grid.tsx @@ -75,6 +75,7 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) { const isDisabled = !elaprTmpltId; const serverRows = aplnQuery.data?.content ?? []; const canSave = hasUnsavedChanges(rows, serverRows); + const pickingRow = pickingRowKey ? rows.find((r) => r.rowKey === pickingRowKey) : undefined; const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; @@ -151,11 +152,11 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) {
diff --git a/apps/as/src/pages/approval-line-management/ui/sortable-apln-row.tsx b/apps/as/src/pages/approval-line-management/ui/sortable-apln-row.tsx index 5539d2347..fcb315bdb 100644 --- a/apps/as/src/pages/approval-line-management/ui/sortable-apln-row.tsx +++ b/apps/as/src/pages/approval-line-management/ui/sortable-apln-row.tsx @@ -36,7 +36,7 @@ function SortableAplnRow({ onChange, onPickAprvr, }: SortableAplnRowProps) { - const { data: atrzSeCodes } = useCodeList("ATRZSECD"); + const { data: atrzSeCodes } = useCodeList("ATRZSE"); const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: row.rowKey, @@ -50,7 +50,7 @@ function SortableAplnRow({ }; const rowDisabled = isLocked && !isEditing; - const atrzSeLabel = row.atrzSeComNm ?? findCodeLabel(atrzSeCodes, row.atrzSeComCd); + const atrzSeLabel = row.atrzSeComNm || findCodeLabel(atrzSeCodes, row.atrzSeComCd); return ( @@ -72,7 +72,7 @@ function SortableAplnRow({ {isEditing ? ( @@ -111,10 +111,22 @@ function SortableAplnRow({ ) : ( <> - - diff --git a/apps/as/src/pages/checklist-management/model/cklst-artcl-columns.tsx b/apps/as/src/pages/checklist-management/model/cklst-artcl-columns.tsx index c356d9e04..cc519945e 100644 --- a/apps/as/src/pages/checklist-management/model/cklst-artcl-columns.tsx +++ b/apps/as/src/pages/checklist-management/model/cklst-artcl-columns.tsx @@ -41,7 +41,7 @@ export function createCklstArtclColumns({ onEdit, onDelete }: CreateCklstArtclCo -
diff --git a/apps/as/src/pages/checklist-management/model/cklst-mng-columns.tsx b/apps/as/src/pages/checklist-management/model/cklst-mng-columns.tsx index 0e0ed52a7..fd74d47ce 100644 --- a/apps/as/src/pages/checklist-management/model/cklst-mng-columns.tsx +++ b/apps/as/src/pages/checklist-management/model/cklst-mng-columns.tsx @@ -54,7 +54,7 @@ export function createCklstMngColumns({ onEdit, onDelete }: CreateCklstMngColumn - diff --git a/apps/as/src/pages/checklist-management/ui/cklst-artcl-list.tsx b/apps/as/src/pages/checklist-management/ui/cklst-artcl-list.tsx index 3f4bdd3f5..480ab9508 100644 --- a/apps/as/src/pages/checklist-management/ui/cklst-artcl-list.tsx +++ b/apps/as/src/pages/checklist-management/ui/cklst-artcl-list.tsx @@ -100,7 +100,12 @@ function CklstArtclList({ cklstSn }: CklstArtclListProps) {

점검항목 관리

-
diff --git a/apps/as/src/pages/checklist-management/ui/cklst-mng-form-dialog.tsx b/apps/as/src/pages/checklist-management/ui/cklst-mng-form-dialog.tsx index 8ff919256..3efa22ba9 100644 --- a/apps/as/src/pages/checklist-management/ui/cklst-mng-form-dialog.tsx +++ b/apps/as/src/pages/checklist-management/ui/cklst-mng-form-dialog.tsx @@ -102,7 +102,7 @@ function CklstMngFormDialog({
setValue("docSeComCd", v)} placeholder="문서구분 선택" @@ -112,7 +112,7 @@ function CklstMngFormDialog({
setValue("taskSeComCd", v)} placeholder="업무구분 선택" diff --git a/apps/as/src/pages/checklist-management/ui/cklst-mng-list.tsx b/apps/as/src/pages/checklist-management/ui/cklst-mng-list.tsx index a6a3eaa0e..921295ac2 100644 --- a/apps/as/src/pages/checklist-management/ui/cklst-mng-list.tsx +++ b/apps/as/src/pages/checklist-management/ui/cklst-mng-list.tsx @@ -123,7 +123,7 @@ function CklstMngList({ selectedCklstSn, onSelectCklst }: CklstMngListProps) {

점검표 관리

-
diff --git a/apps/as/src/pages/checklist-management/ui/cklst-preview-dialog.tsx b/apps/as/src/pages/checklist-management/ui/cklst-preview-dialog.tsx index 42f2edcf8..c6c1f3746 100644 --- a/apps/as/src/pages/checklist-management/ui/cklst-preview-dialog.tsx +++ b/apps/as/src/pages/checklist-management/ui/cklst-preview-dialog.tsx @@ -67,6 +67,7 @@ function CklstPreviewDialog({ {displayRows.map(({ row, depth }) => { const branchPrefix = formatTreeBranchPrefix(depth); + const showRadios = row.bchckUseYn !== "N"; if (row.useYn === "N") return null; @@ -82,16 +83,20 @@ function CklstPreviewDialog({ {row.rvwCn ?? "—"} - -
- - -
-
- - -
-
+ {showRadios ? ( + +
+ + +
+
+ + +
+
+ ) : ( + "—" + )}
); diff --git a/apps/as/src/pages/checklist-management/ui/cklst-rvw-grid.tsx b/apps/as/src/pages/checklist-management/ui/cklst-rvw-grid.tsx index 5ceb5add4..7b44ba2b5 100644 --- a/apps/as/src/pages/checklist-management/ui/cklst-rvw-grid.tsx +++ b/apps/as/src/pages/checklist-management/ui/cklst-rvw-grid.tsx @@ -113,6 +113,7 @@ function CklstRvwGrid({ selectedCklst }: CklstRvwGridProps) { rvwCnLvNo: 1, rvwCn: "", useYn: "Y", + bchckUseYn: "Y", sortSeq: maxSort + 1, isNew: true, isDirty: true, @@ -145,6 +146,7 @@ function CklstRvwGrid({ selectedCklst }: CklstRvwGridProps) { rvwCnLvNo: parentLv + 1, rvwCn: "", useYn: "Y", + bchckUseYn: "Y", sortSeq: maxSort + 1, isNew: true, isDirty: true, @@ -189,6 +191,8 @@ function CklstRvwGrid({ selectedCklst }: CklstRvwGridProps) { const handleSave = async () => { if (!cklstSn) return; + if (!(await confirm("체크리스트 항목을 저장하시겠습니까?"))) return; + const dirty = getDirtyRows().sort((a, b) => (a.rvwCnLvNo ?? 1) - (b.rvwCnLvNo ?? 1)); for (const row of dirty) { @@ -198,8 +202,6 @@ function CklstRvwGrid({ selectedCklst }: CklstRvwGridProps) { return; } - if (!(await confirm("체크리스트 항목을 저장하시겠습니까?"))) return; - if (row.isNew) { await createMutation.mutateAsync({ cklstSn: row.cklstSn, @@ -209,7 +211,7 @@ function CklstRvwGrid({ selectedCklst }: CklstRvwGridProps) { rvwCn: row.rvwCn, useYn: row.useYn, sortSeq: row.sortSeq, - bchckUseYn: "Y", + bchckUseYn: row.bchckUseYn ?? "Y", }); } else if (row.cklstRvwSn != null) { await updateMutation.mutateAsync({ @@ -248,12 +250,11 @@ function CklstRvwGrid({ selectedCklst }: CklstRvwGridProps) { > 미리보기 -
diff --git a/apps/as/src/pages/cnstp-code-management/ui/cnstp-se-row.tsx b/apps/as/src/pages/cnstp-code-management/ui/cnstp-se-row.tsx index 397621a7c..090e7d2ba 100644 --- a/apps/as/src/pages/cnstp-code-management/ui/cnstp-se-row.tsx +++ b/apps/as/src/pages/cnstp-code-management/ui/cnstp-se-row.tsx @@ -41,7 +41,7 @@ function CnstpSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Cns onChange={(e) => onChange({ cnstpSeCd: e.target.value })} placeholder="코드" maxLength={10} - readOnly={!row.isNew} + disabled={!row.isNew} className="flex-1" />
@@ -85,12 +85,12 @@ function CnstpSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Cns type="button" size="sm" color="white" - disabled={row.isNew || !canAddChildRow()} + disabled={!canAddChildRow()} onClick={onAddChild} > 하위추가 - diff --git a/apps/as/src/pages/esntl-atch-file-mng/model/esntl-atch-file-columns.tsx b/apps/as/src/pages/esntl-atch-file-mng/model/esntl-atch-file-columns.tsx index ee3b04dda..ad5f32c62 100644 --- a/apps/as/src/pages/esntl-atch-file-mng/model/esntl-atch-file-columns.tsx +++ b/apps/as/src/pages/esntl-atch-file-mng/model/esntl-atch-file-columns.tsx @@ -68,10 +68,10 @@ export function createEsntlAtchFileColumns({ header: "관리", cell: ({ row }) => (
e.stopPropagation()}> - -
diff --git a/apps/as/src/pages/esntl-atch-file-mng/ui/esntl-atch-file-form-dialog.tsx b/apps/as/src/pages/esntl-atch-file-mng/ui/esntl-atch-file-form-dialog.tsx index 0f1faf8a1..16d6fed1c 100644 --- a/apps/as/src/pages/esntl-atch-file-mng/ui/esntl-atch-file-form-dialog.tsx +++ b/apps/as/src/pages/esntl-atch-file-mng/ui/esntl-atch-file-form-dialog.tsx @@ -94,7 +94,7 @@ function EsntlAtchFileFormDialog({
setValue("docSeComCd", v, { shouldValidate: true })} placeholder="문서구분 선택" diff --git a/apps/as/src/pages/esntl-atch-file-mng/ui/esntl-atch-file-list.tsx b/apps/as/src/pages/esntl-atch-file-mng/ui/esntl-atch-file-list.tsx index 661b3a822..62a728073 100644 --- a/apps/as/src/pages/esntl-atch-file-mng/ui/esntl-atch-file-list.tsx +++ b/apps/as/src/pages/esntl-atch-file-mng/ui/esntl-atch-file-list.tsx @@ -36,7 +36,7 @@ function EsntlAtchFileList({ onPaginationChange, }: EsntlAtchFileListProps) { const { confirm } = useConfirm(); - const { data: docSeCodes } = useCodeList("ASDOCSECD"); + const { data: docSeCodes } = useCodeList("ASDOCSE"); const [dialogMode, setDialogMode] = useState<"create" | "edit" | null>(null); const [editingRow, setEditingRow] = useState(null); diff --git a/apps/as/src/pages/esntl-atch-file-mng/ui/esntl-atch-file-mng-page.tsx b/apps/as/src/pages/esntl-atch-file-mng/ui/esntl-atch-file-mng-page.tsx index 425169983..3e5dbe9bb 100644 --- a/apps/as/src/pages/esntl-atch-file-mng/ui/esntl-atch-file-mng-page.tsx +++ b/apps/as/src/pages/esntl-atch-file-mng/ui/esntl-atch-file-mng-page.tsx @@ -32,7 +32,7 @@ const DEFAULT_SEARCH_PARAMS = { }; function EsntlAtchFileMngPage() { - const { data: docSeCodes, isPending: isDocSeLoading } = useCodeList("ASDOCSECD"); + const { data: docSeCodes, isPending: isDocSeLoading } = useCodeList("ASDOCSE"); const [docSeComCd, setDocSeComCd] = useState(ESNTL_ATCH_FILE_ALL_DOC_SE); const [pagination, setPagination] = useState({ diff --git a/apps/as/src/pages/instl-sncn-old/index.ts b/apps/as/src/pages/instl-sncn-old/index.ts new file mode 100644 index 000000000..570133ba4 --- /dev/null +++ b/apps/as/src/pages/instl-sncn-old/index.ts @@ -0,0 +1,4 @@ +export { CnfmListPage } from "./ui/cnfm-list-page"; +export { CnfmViewPage } from "./ui/cnfm-view-page"; +export { CnfmRegistPage } from "./ui/cnfm-regist-page"; +export { CnfmModifyPage } from "./ui/cnfm-modify-page"; diff --git a/apps/as/src/pages/instl-sncn/ui/cnfm-list-page.tsx b/apps/as/src/pages/instl-sncn-old/ui/cnfm-list-page.tsx similarity index 84% rename from apps/as/src/pages/instl-sncn/ui/cnfm-list-page.tsx rename to apps/as/src/pages/instl-sncn-old/ui/cnfm-list-page.tsx index 0f5d24a62..2b8c56174 100644 --- a/apps/as/src/pages/instl-sncn/ui/cnfm-list-page.tsx +++ b/apps/as/src/pages/instl-sncn-old/ui/cnfm-list-page.tsx @@ -9,8 +9,8 @@ import { dayjs } from "@/shared/lib"; import { useCodeList } from "@/entities/code"; import { INSTL_SNCN_REGISTER_SELECTION_KEY, - InstlSncnApi, type InstlSncnMngSearchParams, + instlSncnService, } from "@/entities/instl-sncn"; import "./cnfm-page.css"; @@ -78,14 +78,14 @@ function CnfmListPage() { useEffect(() => { if (!taskDialogOpen) return; if (selectedTaskSeComCd.length) return; - if (fnrsCodeList.length === 0) return; - // setSelectedTaskSeComCd([fnrsCodeList[0].lwpoCd]); + if (fnrsCodeList.length === 0) return; + // setSelectedTaskSeComCd([fnrsCodeList[0].lwpoCd]); }, [taskDialogOpen, selectedTaskSeComCd, fnrsCodeList]); const query = useQuery({ queryKey: ["instl-sncn-list", search, pageIndex, pageSize], queryFn: () => - InstlSncnApi.getPage({ + instlSncnService.getPage({ ...search, page: pageIndex, size: pageSize, @@ -120,13 +120,15 @@ function CnfmListPage() { // prepare arrays of selected codes and names const selectedCodes = selectedTaskSeComCd; - const selectedNames = selectedTaskSeComCd.map((code) => fnrsCodeList.find((item) => item.lwpoCd === code)?.lwpoCdNm || ""); + const selectedNames = selectedTaskSeComCd.map( + (code) => fnrsCodeList.find((item) => item.lwpoCd === code)?.lwpoCdNm || "", + ); sessionStorage.setItem( INSTL_SNCN_REGISTER_SELECTION_KEY, JSON.stringify({ taskSeComCdList: selectedCodes, - taskSeComCdNmList: selectedNames - }) + taskSeComCdNmList: selectedNames, + }), ); setTaskDialogOpen(false); router.push("/instl-sncn/register"); @@ -138,7 +140,9 @@ function CnfmListPage() {
- +
- +
- +
- +
@@ -198,11 +214,7 @@ function CnfmListPage() {

설치인가 목록

-
@@ -231,18 +245,22 @@ function CnfmListPage() { 담당자 해당기관 진행상황 - 시행일 + 신청일 {query.isLoading && ( - 조회 중입니다... + + 조회 중입니다... + )} {!query.isLoading && (query.data?.content?.length ?? 0) === 0 && ( - 조회 결과가 없습니다. + + 조회 결과가 없습니다. + )} {(query.data?.content ?? []).map((row) => { @@ -253,12 +271,12 @@ function CnfmListPage() { className="cnfm-row-clickable" onClick={() => router.push(`/instl-sncn/${row.instlSncnDocNo}`)} > - {row.sidoNm || "-"} - {row.sggNm || "-"} + {row.sido || "-"} + {row.gugun || "-"} {row.bizNm || "-"} {row.taskSeCdNm || row.taskSeComCd || "-"} - {row.chrgrNm || row.aplcntNm || "-"} - {row.instNm || row.aplcntOgdpNm || "-"} + {row.aplcntNm || "-"} + {row.envoSeCdNm || "-"}
@@ -267,10 +285,13 @@ function CnfmListPage() { index < currentStepIndex ? "completed" : index === currentStepIndex - ? "current" - : "pending"; + ? "current" + : "pending"; return ( - + {step.label} @@ -284,10 +305,13 @@ function CnfmListPage() { index < currentStepIndex ? "completed" : index === currentStepIndex - ? "current" - : "pending"; + ? "current" + : "pending"; return ( - + {step.label} @@ -378,7 +402,9 @@ function CnfmListPage() { checked={selectedTaskSeComCd.includes(item.lwpoCd)} onChange={() => { setSelectedTaskSeComCd((prev) => - prev.includes(item.lwpoCd) ? prev.filter((v) => v !== item.lwpoCd) : [...prev, item.lwpoCd], + prev.includes(item.lwpoCd) + ? prev.filter((v) => v !== item.lwpoCd) + : [...prev, item.lwpoCd], ); }} /> @@ -396,7 +422,7 @@ function CnfmListPage() { > 취소 -
- + * 사업기간 @@ -1283,7 +1283,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { - + * 사업내용 @@ -1296,26 +1296,30 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { - 구분 + 구분
{taskSeComCdNm}
- 신청 지자체 + 신청 지자체 - {userInfo?.enoNm ?? "\u00A0"} + {isModifyMode + ? `${currentDet?.sido ?? "-"} ${currentDet?.gugun ?? "-"}` + : (userInfo?.laeSeNm ?? "\u00A0")} - *사업구분 + + *사업구분 +
{[ - ["CNFM", "설치인가"], - ["APR", "설치승인"], - ["AFA", "폐지인가"], + ["1", "설치인가"], + ["2", "설치승인"], + ["3", "폐지승인"], ].map(([val, label]) => (