From acaf2dd9f450cd0ad3cc72bce80b2b86390cbef3 Mon Sep 17 00:00:00 2001 From: bicochan Date: Fri, 5 Jun 2026 14:32:14 +0900 Subject: [PATCH] =?UTF-8?q?[as]=20feat:=20=EA=B2=80=ED=86=A0=EC=9D=B8?= =?UTF-8?q?=EB=A0=A5=EA=B4=80=EB=A6=AC=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(with-base-layout)/rvw-mnpw-mng/page.tsx | 1 + .../docs/review-manpower-management/SPEC.md | 370 ++++++++++++++++++ .../model/reviewer-option.ts | 8 +- .../ui/aprvr-picker-dialog.tsx | 23 +- apps/as/src/pages/rvw-mnpw-mng/index.ts | 1 + .../lib/group-rvw-mnpw-by-atrz-se.ts | 18 + .../model/rvw-mnpw-block-config.ts | 16 + .../src/pages/rvw-mnpw-mng/ui/ognz-panel.tsx | 98 +++++ .../rvw-mnpw-mng/ui/rvw-mnpw-asmt-panel.tsx | 63 +++ .../pages/rvw-mnpw-mng/ui/rvw-mnpw-block.tsx | 111 ++++++ .../rvw-mnpw-mng/ui/rvw-mnpw-mng-page.tsx | 24 ++ apps/as/src/widgets/ognz-user-tree/index.ts | 4 + .../lib/filter-ognz-user-tree.ts | 0 .../model/selected-ognz-user.ts | 24 ++ .../ui/ognz-user-tree-node.tsx | 35 +- .../ognz-user-tree}/ui/ognz-user-tree.tsx | 11 +- pnpm-lock.yaml | 164 ++++---- 17 files changed, 845 insertions(+), 126 deletions(-) create mode 100644 apps/as/app/(with-base-layout)/rvw-mnpw-mng/page.tsx create mode 100644 apps/as/docs/review-manpower-management/SPEC.md create mode 100644 apps/as/src/pages/rvw-mnpw-mng/index.ts create mode 100644 apps/as/src/pages/rvw-mnpw-mng/lib/group-rvw-mnpw-by-atrz-se.ts create mode 100644 apps/as/src/pages/rvw-mnpw-mng/model/rvw-mnpw-block-config.ts create mode 100644 apps/as/src/pages/rvw-mnpw-mng/ui/ognz-panel.tsx create mode 100644 apps/as/src/pages/rvw-mnpw-mng/ui/rvw-mnpw-asmt-panel.tsx create mode 100644 apps/as/src/pages/rvw-mnpw-mng/ui/rvw-mnpw-block.tsx create mode 100644 apps/as/src/pages/rvw-mnpw-mng/ui/rvw-mnpw-mng-page.tsx create mode 100644 apps/as/src/widgets/ognz-user-tree/index.ts rename apps/as/src/{pages/approval-line-management => widgets/ognz-user-tree}/lib/filter-ognz-user-tree.ts (100%) create mode 100644 apps/as/src/widgets/ognz-user-tree/model/selected-ognz-user.ts rename apps/as/src/{pages/approval-line-management => widgets/ognz-user-tree}/ui/ognz-user-tree-node.tsx (75%) rename apps/as/src/{pages/approval-line-management => widgets/ognz-user-tree}/ui/ognz-user-tree.tsx (76%) 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/src/pages/approval-line-management/model/reviewer-option.ts b/apps/as/src/pages/approval-line-management/model/reviewer-option.ts index 569646744..3b4658acd 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 { SelectedOgnzUser } from "@/widgets/ognz-user-tree"; export interface ReviewerOption { uuid: string; @@ -6,10 +6,10 @@ export interface ReviewerOption { deptNm: string; } -export function toReviewerOption(user: OgnzUserTreeUserDto, ognzNm: string): ReviewerOption { +export function fromSelectedOgnzUser(user: SelectedOgnzUser): ReviewerOption { return { - uuid: user.userUnqId, + uuid: user.userUuid, name: user.userNm, - deptNm: ognzNm, + deptNm: user.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..7fbab9f17 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 @@ -3,6 +3,12 @@ import { useCallback, useEffect, useState } from "react"; import { useOgnzUserTree } from "@/entities/user-organization"; +import { + collectExpandedOgnzSnIds, + filterOgnzUserTree, + OgnzUserTree, + type SelectedOgnzUser, +} from "@/widgets/ognz-user-tree"; import { Button, Dialog, @@ -13,10 +19,7 @@ import { Input, } from "@common/components"; -import { collectExpandedOgnzSnIds, filterOgnzUserTree } from "../lib/filter-ognz-user-tree"; -import type { ReviewerOption } from "../model/reviewer-option"; - -import { OgnzUserTree } from "./ognz-user-tree"; +import { fromSelectedOgnzUser, type ReviewerOption } from "../model/reviewer-option"; interface AprvrPickerDialogProps { open: boolean; @@ -29,6 +32,7 @@ function AprvrPickerDialog({ open, onOpenChange, onSelect }: AprvrPickerDialogPr const [displayTree, setDisplayTree] = useState>([]); const [expandedOgnzSnIds, setExpandedOgnzSnIds] = useState>(new Set()); const [selectedUserId, setSelectedUserId] = useState(); + const [isSearchActive, setIsSearchActive] = useState(false); const { data, isPending, isError } = useOgnzUserTree({ enabled: open }); @@ -36,6 +40,7 @@ function AprvrPickerDialog({ open, onOpenChange, onSelect }: AprvrPickerDialogPr setKeyword(""); setSelectedUserId(undefined); setExpandedOgnzSnIds(new Set()); + setIsSearchActive(false); setDisplayTree(data ?? []); }, [data]); @@ -44,6 +49,7 @@ function AprvrPickerDialog({ open, onOpenChange, onSelect }: AprvrPickerDialogPr setKeyword(""); setSelectedUserId(undefined); setExpandedOgnzSnIds(new Set()); + setIsSearchActive(false); return; } @@ -66,15 +72,16 @@ function AprvrPickerDialog({ open, onOpenChange, onSelect }: AprvrPickerDialogPr const filtered = filterOgnzUserTree(data ?? [], keyword); setDisplayTree(filtered); setExpandedOgnzSnIds(collectExpandedOgnzSnIds(filtered)); + setIsSearchActive(keyword.trim().length > 0); }; const handleReset = () => { resetState(); }; - const handleSelect = (reviewer: ReviewerOption) => { - setSelectedUserId(reviewer.uuid); - onSelect(reviewer); + const handleSelect = (user: SelectedOgnzUser) => { + setSelectedUserId(user.userUuid); + onSelect(fromSelectedOgnzUser(user)); onOpenChange(false); }; @@ -108,8 +115,10 @@ function AprvrPickerDialog({ open, onOpenChange, onSelect }: AprvrPickerDialogPr nodes={displayTree} isPending={isPending} isError={isError} + isSearchActive={isSearchActive} expandedOgnzSnIds={expandedOgnzSnIds} selectedUserId={selectedUserId} + showSelectButton onToggle={handleToggle} onSelect={handleSelect} /> diff --git a/apps/as/src/pages/rvw-mnpw-mng/index.ts b/apps/as/src/pages/rvw-mnpw-mng/index.ts new file mode 100644 index 000000000..2b0f5d435 --- /dev/null +++ b/apps/as/src/pages/rvw-mnpw-mng/index.ts @@ -0,0 +1 @@ +export { RvwMnpwMngPage } from "./ui/rvw-mnpw-mng-page"; diff --git a/apps/as/src/pages/rvw-mnpw-mng/lib/group-rvw-mnpw-by-atrz-se.ts b/apps/as/src/pages/rvw-mnpw-mng/lib/group-rvw-mnpw-by-atrz-se.ts new file mode 100644 index 000000000..a2777d29f --- /dev/null +++ b/apps/as/src/pages/rvw-mnpw-mng/lib/group-rvw-mnpw-by-atrz-se.ts @@ -0,0 +1,18 @@ +import type { RvwMnpwMngResponseDto } from "@/entities/rvw-mnpw"; + +import type { RvwMnpwBlockConfig } from "../model/rvw-mnpw-block-config"; + +export function groupRvwMnpwByAtrzSe( + items: RvwMnpwMngResponseDto[], + blocks: RvwMnpwBlockConfig[], +): Record { + const grouped = Object.fromEntries( + blocks.map((block) => [block.atrzSeComCd, [] as RvwMnpwMngResponseDto[]]), + ); + + for (const item of items) { + grouped[item.atrzSeComCd]?.push(item); + } + + return grouped; +} diff --git a/apps/as/src/pages/rvw-mnpw-mng/model/rvw-mnpw-block-config.ts b/apps/as/src/pages/rvw-mnpw-mng/model/rvw-mnpw-block-config.ts new file mode 100644 index 000000000..30dff1f37 --- /dev/null +++ b/apps/as/src/pages/rvw-mnpw-mng/model/rvw-mnpw-block-config.ts @@ -0,0 +1,16 @@ +export interface RvwMnpwBlockConfig { + id: string; + title: string; + atrzSeComCd: string; +} + +/** 결재선 관리 validators.ts ATRZ 코드와 동일. ATRZSE / ATRZSECD 배포 환경 코드표 최종 확인 필요 */ +export const RVW_MNPW_BLOCKS: RvwMnpwBlockConfig[] = [ + { id: "receiver", title: "접수", atrzSeComCd: "ATRZ00" }, + // TBD: ATRZ02(검토2) 별도 블록 필요 여부 — 현재 ATRZ01만 검토 블록으로 사용 + { id: "reviewer", title: "검토", atrzSeComCd: "ATRZ01" }, + { id: "approver", title: "승인", atrzSeComCd: "ATRZ03" }, + { id: "final", title: "최종승인", atrzSeComCd: "ATRZ99" }, +]; + +export const RVW_MNPW_PAGE_SIZE = 500; diff --git a/apps/as/src/pages/rvw-mnpw-mng/ui/ognz-panel.tsx b/apps/as/src/pages/rvw-mnpw-mng/ui/ognz-panel.tsx new file mode 100644 index 000000000..172f9718b --- /dev/null +++ b/apps/as/src/pages/rvw-mnpw-mng/ui/ognz-panel.tsx @@ -0,0 +1,98 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; + +import { useOgnzUserTree } from "@/entities/user-organization"; +import { + collectExpandedOgnzSnIds, + filterOgnzUserTree, + OgnzUserTree, + type SelectedOgnzUser, +} from "@/widgets/ognz-user-tree"; +import { Button, Input } from "@common/components"; + +interface OgnzPanelProps { + selectedPerson: SelectedOgnzUser | null; + onSelectPerson: (person: SelectedOgnzUser) => void; +} + +function OgnzPanel({ selectedPerson, onSelectPerson }: OgnzPanelProps) { + const [keyword, setKeyword] = useState(""); + const [displayTree, setDisplayTree] = useState>([]); + const [expandedOgnzSnIds, setExpandedOgnzSnIds] = useState>(new Set()); + const [isSearchActive, setIsSearchActive] = useState(false); + + const { data, isPending, isError } = useOgnzUserTree(); + + useEffect(() => { + setDisplayTree(data ?? []); + }, [data]); + + const handleToggle = useCallback((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(); + + const filtered = filterOgnzUserTree(data ?? [], keyword); + setDisplayTree(filtered); + setExpandedOgnzSnIds(collectExpandedOgnzSnIds(filtered)); + setIsSearchActive(keyword.trim().length > 0); + }; + + const handleReset = () => { + setKeyword(""); + setDisplayTree(data ?? []); + setExpandedOgnzSnIds(new Set()); + setIsSearchActive(false); + }; + + const handleSelect = (person: SelectedOgnzUser) => { + if (selectedPerson?.userUuid === person.userUuid) return; + onSelectPerson(person); + }; + + return ( +
+
+

조직도

+
+ +
+
+ setKeyword(e.target.value)} + /> + + +
+ +
+ +
+
+
+ ); +} + +export { OgnzPanel }; diff --git a/apps/as/src/pages/rvw-mnpw-mng/ui/rvw-mnpw-asmt-panel.tsx b/apps/as/src/pages/rvw-mnpw-mng/ui/rvw-mnpw-asmt-panel.tsx new file mode 100644 index 000000000..f9335e63e --- /dev/null +++ b/apps/as/src/pages/rvw-mnpw-mng/ui/rvw-mnpw-asmt-panel.tsx @@ -0,0 +1,63 @@ +"use client"; + +import { useMemo } from "react"; + +import { useRvwMnpwMngPage } from "@/entities/rvw-mnpw"; +import type { SelectedOgnzUser } from "@/widgets/ognz-user-tree"; +import { Skeleton } from "@common/components"; + +import { groupRvwMnpwByAtrzSe } from "../lib/group-rvw-mnpw-by-atrz-se"; +import { RVW_MNPW_BLOCKS, RVW_MNPW_PAGE_SIZE } from "../model/rvw-mnpw-block-config"; + +import { RvwMnpwBlock } from "./rvw-mnpw-block"; + +interface RvwMnpwAsmtPanelProps { + selectedPerson: SelectedOgnzUser | null; +} + +function RvwMnpwAsmtPanel({ selectedPerson }: RvwMnpwAsmtPanelProps) { + const pageQuery = useRvwMnpwMngPage({ + page: 0, + size: RVW_MNPW_PAGE_SIZE, + sort: "atrzSeComCd,asc", + }); + + const groupedItems = useMemo( + () => groupRvwMnpwByAtrzSe(pageQuery.data?.content ?? [], RVW_MNPW_BLOCKS), + [pageQuery.data?.content], + ); + + return ( +
+
+

인원 배치

+
+ +
+ {pageQuery.isPending ? ( +
+ + + + +
+ ) : pageQuery.isError ? ( +

+ 인원 배치 목록을 불러오지 못했습니다. +

+ ) : ( + RVW_MNPW_BLOCKS.map((block) => ( + + )) + )} +
+
+ ); +} + +export { RvwMnpwAsmtPanel }; diff --git a/apps/as/src/pages/rvw-mnpw-mng/ui/rvw-mnpw-block.tsx b/apps/as/src/pages/rvw-mnpw-mng/ui/rvw-mnpw-block.tsx new file mode 100644 index 000000000..e03918de6 --- /dev/null +++ b/apps/as/src/pages/rvw-mnpw-mng/ui/rvw-mnpw-block.tsx @@ -0,0 +1,111 @@ +"use client"; + +import { useConfirm } from "@frontend-opensource/use-react-hooks"; + +import { useAlertStore } from "@/shared/lib"; +import type { RvwMnpwMngResponseDto } from "@/entities/rvw-mnpw"; +import { useDeleteRvwMnpwMng, useSaveRvwMnpwMng } from "@/entities/rvw-mnpw"; +import type { SelectedOgnzUser } from "@/widgets/ognz-user-tree"; +import { Badge, Button } from "@common/components"; + +import type { RvwMnpwBlockConfig } from "../model/rvw-mnpw-block-config"; + +interface RvwMnpwBlockProps { + block: RvwMnpwBlockConfig; + items: RvwMnpwMngResponseDto[]; + selectedPerson: SelectedOgnzUser | null; +} + +function RvwMnpwBlock({ block, items, selectedPerson }: RvwMnpwBlockProps) { + const { confirm } = useConfirm(); + const alert = useAlertStore((state) => state.setMessage); + const saveMutation = useSaveRvwMnpwMng(); + const deleteMutation = useDeleteRvwMnpwMng(); + + const handleAssign = async () => { + if (!selectedPerson) return; + + try { + await saveMutation.mutateAsync({ + atrzSeComCd: block.atrzSeComCd, + ognzSn: selectedPerson.ognzSn, + userUuid: selectedPerson.userUuid, + }); + alert("배치되었습니다."); + } catch (error) { + const message = error instanceof Error ? error.message : "배치에 실패했습니다."; + alert(message); + } + }; + + const handleRemove = async (item: RvwMnpwMngResponseDto) => { + if (!(await confirm("해당 인원을 배치에서 제거하시겠습니까?"))) return; + + try { + await deleteMutation.mutateAsync({ + atrzSeComCd: item.atrzSeComCd, + ognzSn: item.ognzSn, + userUuid: item.userUuid, + }); + } catch (error) { + const message = error instanceof Error ? error.message : "제거에 실패했습니다."; + alert(message); + } + }; + + const isAssigning = saveMutation.isPending; + const isRemoving = deleteMutation.isPending; + + return ( +
+
+
+

{block.title}

+ {items.length} +
+ +
+ + {items.length === 0 ? ( +

배치된 인원이 없습니다.

+ ) : ( +
    + {items.map((item) => ( +
  • +
    + {/* TBD: userUuidNm @CodeName 미적용 시 백엔드 enrichUserNames 결과만 표시 */} +

    + {item.userUuidNm || item.userUuid} +

    + {/* TBD: 응답 DTO에 부서·직급 필드 없음 — ognzSn만 표시 */} + {/*

    조직 SN: {item.ognzSn}

    */} +
    + +
  • + ))} +
+ )} +
+ ); +} + +export { RvwMnpwBlock }; diff --git a/apps/as/src/pages/rvw-mnpw-mng/ui/rvw-mnpw-mng-page.tsx b/apps/as/src/pages/rvw-mnpw-mng/ui/rvw-mnpw-mng-page.tsx new file mode 100644 index 000000000..3e4234dcc --- /dev/null +++ b/apps/as/src/pages/rvw-mnpw-mng/ui/rvw-mnpw-mng-page.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { useState } from "react"; + +import type { SelectedOgnzUser } from "@/widgets/ognz-user-tree"; + +import { OgnzPanel } from "./ognz-panel"; +import { RvwMnpwAsmtPanel } from "./rvw-mnpw-asmt-panel"; + +function RvwMnpwMngPage() { + const [selectedPerson, setSelectedPerson] = useState(null); + + return ( +
+ {/* TBD: 역할별 CRUD 권한 — 메뉴·API 권한 연동 후 버튼 비활성 처리 */} +
+ + +
+
+ ); +} + +export { RvwMnpwMngPage }; diff --git a/apps/as/src/widgets/ognz-user-tree/index.ts b/apps/as/src/widgets/ognz-user-tree/index.ts new file mode 100644 index 000000000..e16df59e3 --- /dev/null +++ b/apps/as/src/widgets/ognz-user-tree/index.ts @@ -0,0 +1,4 @@ +export { collectExpandedOgnzSnIds, filterOgnzUserTree } from "./lib/filter-ognz-user-tree"; +export { type SelectedOgnzUser, toSelectedOgnzUser } from "./model/selected-ognz-user"; +export { OgnzUserTree } from "./ui/ognz-user-tree"; +export { OgnzUserTreeNode } from "./ui/ognz-user-tree-node"; diff --git a/apps/as/src/pages/approval-line-management/lib/filter-ognz-user-tree.ts b/apps/as/src/widgets/ognz-user-tree/lib/filter-ognz-user-tree.ts similarity index 100% rename from apps/as/src/pages/approval-line-management/lib/filter-ognz-user-tree.ts rename to apps/as/src/widgets/ognz-user-tree/lib/filter-ognz-user-tree.ts diff --git a/apps/as/src/widgets/ognz-user-tree/model/selected-ognz-user.ts b/apps/as/src/widgets/ognz-user-tree/model/selected-ognz-user.ts new file mode 100644 index 000000000..98e305e7e --- /dev/null +++ b/apps/as/src/widgets/ognz-user-tree/model/selected-ognz-user.ts @@ -0,0 +1,24 @@ +import type { OgnzUserTreeResponseDto, OgnzUserTreeUserDto } from "@/entities/user-organization"; + +export interface SelectedOgnzUser { + userUuid: string; + userNm: string; + ognzSn: number; + ognzNm: string; + jbgdSeCd?: string; + jbpsSeCd?: string; +} + +export function toSelectedOgnzUser( + user: OgnzUserTreeUserDto, + node: Pick, +): SelectedOgnzUser { + return { + userUuid: user.userUnqId, + userNm: user.userNm, + ognzSn: node.ognzSn, + ognzNm: node.ognzNm, + jbgdSeCd: user.jbgdSeCd || undefined, + jbpsSeCd: user.jbpsSeCd || undefined, + }; +} diff --git a/apps/as/src/pages/approval-line-management/ui/ognz-user-tree-node.tsx b/apps/as/src/widgets/ognz-user-tree/ui/ognz-user-tree-node.tsx similarity index 75% rename from apps/as/src/pages/approval-line-management/ui/ognz-user-tree-node.tsx rename to apps/as/src/widgets/ognz-user-tree/ui/ognz-user-tree-node.tsx index 193cc24f2..9a8b9fd48 100644 --- a/apps/as/src/pages/approval-line-management/ui/ognz-user-tree-node.tsx +++ b/apps/as/src/widgets/ognz-user-tree/ui/ognz-user-tree-node.tsx @@ -11,16 +11,16 @@ import { CollapsibleTrigger, } from "@common/components"; -import type { ReviewerOption } from "../model/reviewer-option"; -import { toReviewerOption } from "../model/reviewer-option"; +import { type SelectedOgnzUser, toSelectedOgnzUser } from "../model/selected-ognz-user"; interface OgnzUserTreeNodeProps { node: OgnzUserTreeResponseDto; depth?: number; expandedOgnzSnIds: Set; selectedUserId?: string; + showSelectButton?: boolean; onToggle: (ognzSn: number, open: boolean) => void; - onSelect: (reviewer: ReviewerOption) => void; + onSelect: (user: SelectedOgnzUser) => void; } function OgnzUserTreeNode({ @@ -28,6 +28,7 @@ function OgnzUserTreeNode({ depth = 0, expandedOgnzSnIds, selectedUserId, + showSelectButton = false, onToggle, onSelect, }: OgnzUserTreeNodeProps) { @@ -71,6 +72,7 @@ function OgnzUserTreeNode({ {node.users.map((user) => { const isSelected = selectedUserId === user.userUnqId; + const selectedUser = toSelectedOgnzUser(user, node); return (
onSelect(toReviewerOption(user, node.ognzNm))} + onClick={() => onSelect(selectedUser)} > {user.userNm} - + {showSelectButton ? ( + + ) : null}
); })} @@ -104,6 +108,7 @@ function OgnzUserTreeNode({ depth={depth + 1} expandedOgnzSnIds={expandedOgnzSnIds} selectedUserId={selectedUserId} + showSelectButton={showSelectButton} onToggle={onToggle} onSelect={onSelect} /> diff --git a/apps/as/src/pages/approval-line-management/ui/ognz-user-tree.tsx b/apps/as/src/widgets/ognz-user-tree/ui/ognz-user-tree.tsx similarity index 76% rename from apps/as/src/pages/approval-line-management/ui/ognz-user-tree.tsx rename to apps/as/src/widgets/ognz-user-tree/ui/ognz-user-tree.tsx index 226e878f5..968e9526c 100644 --- a/apps/as/src/pages/approval-line-management/ui/ognz-user-tree.tsx +++ b/apps/as/src/widgets/ognz-user-tree/ui/ognz-user-tree.tsx @@ -3,7 +3,7 @@ import type { OgnzUserTreeResponseDto } from "@/entities/user-organization"; import { Skeleton } from "@common/components"; -import type { ReviewerOption } from "../model/reviewer-option"; +import type { SelectedOgnzUser } from "../model/selected-ognz-user"; import { OgnzUserTreeNode } from "./ognz-user-tree-node"; @@ -11,18 +11,22 @@ interface OgnzUserTreeProps { nodes: OgnzUserTreeResponseDto[]; isPending?: boolean; isError?: boolean; + isSearchActive?: boolean; expandedOgnzSnIds: Set; selectedUserId?: string; + showSelectButton?: boolean; onToggle: (ognzSn: number, open: boolean) => void; - onSelect: (reviewer: ReviewerOption) => void; + onSelect: (user: SelectedOgnzUser) => void; } function OgnzUserTree({ nodes, isPending, isError, + isSearchActive = false, expandedOgnzSnIds, selectedUserId, + showSelectButton = false, onToggle, onSelect, }: OgnzUserTreeProps) { @@ -47,7 +51,7 @@ function OgnzUserTree({ if (nodes.length === 0) { return (

- 등록된 조직/사용자가 없습니다. + {isSearchActive ? "조회된 인원이 없습니다." : "등록된 조직/사용자가 없습니다."}

); } @@ -60,6 +64,7 @@ function OgnzUserTree({ node={node} expandedOgnzSnIds={expandedOgnzSnIds} selectedUserId={selectedUserId} + showSelectButton={showSelectButton} onToggle={onToggle} onSelect={onSelect} /> diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f8919f1d1..b20d42ab4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,8 +92,8 @@ importers: specifier: ^1.11.13 version: 1.11.20 next: - specifier: 15.3.8 - version: 15.3.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: 15.5.18 + version: 15.5.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react-hook-form: specifier: ^7.54.2 version: 7.72.0(react@19.2.4) @@ -103,10 +103,10 @@ importers: devDependencies: eslint: specifier: ^9.0.0 - version: 9.39.4(jiti@2.6.1) + version: 9.39.4(jiti@1.21.7) eslint-config-next: specifier: 15.3.8 - version: 15.3.8(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + version: 15.3.8(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) postcss: specifier: ^8.4.49 version: 8.5.8 @@ -180,8 +180,8 @@ importers: specifier: ^0.474.0 version: 0.474.0(react@19.2.4) next: - specifier: 15.3.8 - version: 15.3.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: 15.5.18 + version: 15.5.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react-hook-form: specifier: ^7.54.2 version: 7.72.0(react@19.2.4) @@ -200,13 +200,13 @@ importers: version: 3.0.6 eslint: specifier: ^9.0.0 - version: 9.39.4(jiti@1.21.7) + version: 9.39.4(jiti@2.6.1) eslint-config-next: specifier: 15.3.8 - version: 15.3.8(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + version: 15.3.8(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) eslint-plugin-react-hooks: specifier: ^5.0.0 - version: 5.2.0(eslint@9.39.4(jiti@1.21.7)) + version: 5.2.0(eslint@9.39.4(jiti@2.6.1)) postcss: specifier: ^8.4.49 version: 8.5.8 @@ -313,8 +313,8 @@ importers: specifier: ^0.474.0 version: 0.474.0(react@19.2.4) next: - specifier: 15.3.8 - version: 15.3.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: 15.5.18 + version: 15.5.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next-qrcode: specifier: ^2.5.1 version: 2.5.1(react@19.2.4) @@ -443,8 +443,8 @@ importers: specifier: ^3.0.5 version: 3.0.5 next: - specifier: 15.3.8 - version: 15.3.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: 15.5.18 + version: 15.5.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -594,8 +594,8 @@ importers: specifier: ^0.474.0 version: 0.474.0(react@19.2.4) next: - specifier: 15.3.8 - version: 15.3.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: 15.5.18 + version: 15.5.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next-qrcode: specifier: ^2.5.1 version: 2.5.1(react@19.2.4) @@ -712,8 +712,8 @@ importers: specifier: ^0.474.0 version: 0.474.0(react@19.2.4) next: - specifier: 15.3.8 - version: 15.3.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: 15.5.18 + version: 15.5.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4) picocolors: specifier: ^1.1.1 version: 1.1.1 @@ -812,8 +812,8 @@ importers: specifier: ^0.474.0 version: 0.474.0(react@19.2.4) next: - specifier: 15.3.8 - version: 15.3.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: 15.5.18 + version: 15.5.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react-hook-form: specifier: ^7.54.2 version: 7.72.0(react@19.2.4) @@ -897,8 +897,8 @@ importers: specifier: ^3.0.5 version: 3.0.5 next: - specifier: 15.3.8 - version: 15.3.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: 15.5.18 + version: 15.5.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4) ol: specifier: ^7.3.0 version: 7.5.2 @@ -1003,8 +1003,8 @@ importers: specifier: ^2.30.1 version: 2.30.1 next: - specifier: 15.3.8 - version: 15.3.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: 15.5.18 + version: 15.5.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4) ol: specifier: ^7.3.0 version: 7.5.2 @@ -1193,8 +1193,8 @@ importers: specifier: ^0.474.0 version: 0.474.0(react@19.2.4) next: - specifier: 15.3.8 - version: 15.3.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: 15.5.18 + version: 15.5.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next-qrcode: specifier: ^2.5.1 version: 2.5.1(react@19.2.4) @@ -1341,8 +1341,8 @@ importers: specifier: ^2.30.1 version: 2.30.1 next: - specifier: 15.3.8 - version: 15.3.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: 15.5.18 + version: 15.5.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4) ol: specifier: ^7.3.0 version: 7.5.2 @@ -3397,10 +3397,10 @@ packages: integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==, } - "@next/env@15.3.8": + "@next/env@15.5.18": resolution: { - integrity: sha512-SAfHg0g91MQVMPioeFeDjE+8UPF3j3BvHjs8ZKJAUz1BG7eMPvfCKOAgNWJ6s1MLNeP6O2InKQRTNblxPWuq+Q==, + integrity: sha512-hAV85Ckd9QR6RvH04MEKwsfLTksvFpO47j9xwtoIuvuPnlwecpSi+uZTtm8HirVbtlI2Fnz//xpcSTjFdyJk+g==, } "@next/eslint-plugin-next@15.3.8": @@ -3409,73 +3409,73 @@ packages: integrity: sha512-CKpvDZhNXPUjUHmjHddcsqjwj46oQblFms2hr2uA79C0kVTiSXQEQxkawUsDNSofHhTI31IbSeBdVyuk/U1nKw==, } - "@next/swc-darwin-arm64@15.3.5": + "@next/swc-darwin-arm64@15.5.18": resolution: { - integrity: sha512-lM/8tilIsqBq+2nq9kbTW19vfwFve0NR7MxfkuSUbRSgXlMQoJYg+31+++XwKVSXk4uT23G2eF/7BRIKdn8t8w==, + integrity: sha512-w0WvQf1n+txiwns/9pwIQteCJpZTbxzO2SE0FLcwuD4v0WEh1JPOjdyxWL21XwJsdpx8cFRjyzxzCS/siP7HcQ==, } engines: { node: ">= 10" } cpu: [arm64] os: [darwin] - "@next/swc-darwin-x64@15.3.5": + "@next/swc-darwin-x64@15.5.18": resolution: { - integrity: sha512-WhwegPQJ5IfoUNZUVsI9TRAlKpjGVK0tpJTL6KeiC4cux9774NYE9Wu/iCfIkL/5J8rPAkqZpG7n+EfiAfidXA==, + integrity: sha512-znn71QmDuxm+BOaglihMZfvyySMnNljkVIY5Z2TCssBmm+WqL6c19VhtH5ktFkHa8EZ2bnTUpcNcmNSQsg67og==, } engines: { node: ">= 10" } cpu: [x64] os: [darwin] - "@next/swc-linux-arm64-gnu@15.3.5": + "@next/swc-linux-arm64-gnu@15.5.18": resolution: { - integrity: sha512-LVD6uMOZ7XePg3KWYdGuzuvVboxujGjbcuP2jsPAN3MnLdLoZUXKRc6ixxfs03RH7qBdEHCZjyLP/jBdCJVRJQ==, + integrity: sha512-yPPe5MNL+igZUa+OsqQJisqSfh6oarIuA1Q0BDxljGJhRQyZeP+WRHh7rs/jZUGMh5aY0YdIjXZG0VohkKkUdw==, } engines: { node: ">= 10" } cpu: [arm64] os: [linux] - "@next/swc-linux-arm64-musl@15.3.5": + "@next/swc-linux-arm64-musl@15.5.18": resolution: { - integrity: sha512-k8aVScYZ++BnS2P69ClK7v4nOu702jcF9AIHKu6llhHEtBSmM2zkPGl9yoqbSU/657IIIb0QHpdxEr0iW9z53A==, + integrity: sha512-glaCczEWIrHsokFZ3pP08U4BpKxwIdnT+txdOM32OBgpL9Yw4aqx8NejmgtZQZOdstQ5f0L3CasIZudzCuD+nw==, } engines: { node: ">= 10" } cpu: [arm64] os: [linux] - "@next/swc-linux-x64-gnu@15.3.5": + "@next/swc-linux-x64-gnu@15.5.18": resolution: { - integrity: sha512-2xYU0DI9DGN/bAHzVwADid22ba5d/xrbrQlr2U+/Q5WkFUzeL0TDR963BdrtLS/4bMmKZGptLeg6282H/S2i8A==, + integrity: sha512-oUfg2EgJmU3R0OCOWiokGFUTvZiPfXtriXiuF3YNxRoROCdgvTedHIzYoeKH34gsZxS/V7mHbfq2hpAHwhH1/A==, } engines: { node: ">= 10" } cpu: [x64] os: [linux] - "@next/swc-linux-x64-musl@15.3.5": + "@next/swc-linux-x64-musl@15.5.18": resolution: { - integrity: sha512-TRYIqAGf1KCbuAB0gjhdn5Ytd8fV+wJSM2Nh2is/xEqR8PZHxfQuaiNhoF50XfY90sNpaRMaGhF6E+qjV1b9Tg==, + integrity: sha512-JLxSP3KTd9iu/bvUMQxH7RJo9xKSHf55/6RPE4a6FTSZygGn7uvZbCej0AHXydwkggQGSD9UddSjwv6Xz5ESfA==, } engines: { node: ">= 10" } cpu: [x64] os: [linux] - "@next/swc-win32-arm64-msvc@15.3.5": + "@next/swc-win32-arm64-msvc@15.5.18": resolution: { - integrity: sha512-h04/7iMEUSMY6fDGCvdanKqlO1qYvzNxntZlCzfE8i5P0uqzVQWQquU1TIhlz0VqGQGXLrFDuTJVONpqGqjGKQ==, + integrity: sha512-ir1v7enP52K2HNz3tQQvwF+x7VNxBk1ciiZ18WBPvxf4C59IqdfmHPJYK3vH7rSxpuCVw/8C712wTXNAtEp+NA==, } engines: { node: ">= 10" } cpu: [arm64] os: [win32] - "@next/swc-win32-x64-msvc@15.3.5": + "@next/swc-win32-x64-msvc@15.5.18": resolution: { - integrity: sha512-5fhH6fccXxnX2KhllnGhkYMndhOiLOLEiVGYjP2nizqeGWkN10sA9taATlXwake2E2XMvYZjjz0Uj7T0y+z1yw==, + integrity: sha512-LIu5me6QTANCd25E7I5uIEfvgQ06RK7tvHAbYo3zCb3VpxQEPvMcSpd87NwUABDT6MbGPdEGR5VRiK4PPTJhQg==, } engines: { node: ">= 10" } cpu: [x64] @@ -4776,12 +4776,6 @@ packages: peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - "@swc/counter@0.1.3": - resolution: - { - integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==, - } - "@swc/helpers@0.5.15": resolution: { @@ -6021,13 +6015,6 @@ packages: peerDependencies: esbuild: ">=0.18" - busboy@1.6.0: - resolution: - { - integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==, - } - engines: { node: ">=10.16.0" } - cac@6.7.14: resolution: { @@ -8743,16 +8730,16 @@ packages: react: ^19.2.1 react-dom: ^19.2.1 - next@15.3.8: + next@15.5.18: resolution: { - integrity: sha512-L+4c5Hlr84fuaNADZbB9+ceRX9/CzwxJ+obXIGHupboB/Q1OLbSUapFs4bO8hnS/E6zV/JDX7sG1QpKVR2bguA==, + integrity: sha512-eKL8zUJkX9Y5lE+RX/2YJoItVdGlIscyVyboeD9wSpp0PaGqjoA4tTpT2qPqz9ax+5IzGESyLSeZ/RCwbSZ2uQ==, } engines: { node: ^18.18.0 || ^19.8.0 || >= 20.0.0 } hasBin: true peerDependencies: "@opentelemetry/api": ^1.1.0 - "@playwright/test": ^1.41.2 + "@playwright/test": ^1.51.1 babel-plugin-react-compiler: "*" react: ^19.2.1 react-dom: ^19.2.1 @@ -10137,13 +10124,6 @@ packages: prettier: optional: true - streamsearch@1.1.0: - resolution: - { - integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==, - } - engines: { node: ">=10.0.0" } - string-argv@0.3.2: resolution: { @@ -11946,34 +11926,34 @@ snapshots: "@tybys/wasm-util": 0.10.1 optional: true - "@next/env@15.3.8": {} + "@next/env@15.5.18": {} "@next/eslint-plugin-next@15.3.8": dependencies: fast-glob: 3.3.1 - "@next/swc-darwin-arm64@15.3.5": + "@next/swc-darwin-arm64@15.5.18": optional: true - "@next/swc-darwin-x64@15.3.5": + "@next/swc-darwin-x64@15.5.18": optional: true - "@next/swc-linux-arm64-gnu@15.3.5": + "@next/swc-linux-arm64-gnu@15.5.18": optional: true - "@next/swc-linux-arm64-musl@15.3.5": + "@next/swc-linux-arm64-musl@15.5.18": optional: true - "@next/swc-linux-x64-gnu@15.3.5": + "@next/swc-linux-x64-gnu@15.5.18": optional: true - "@next/swc-linux-x64-musl@15.3.5": + "@next/swc-linux-x64-musl@15.5.18": optional: true - "@next/swc-win32-arm64-msvc@15.3.5": + "@next/swc-win32-arm64-msvc@15.5.18": optional: true - "@next/swc-win32-x64-msvc@15.3.5": + "@next/swc-win32-x64-msvc@15.5.18": optional: true "@nodelib/fs.scandir@2.1.5": @@ -12904,8 +12884,6 @@ snapshots: dependencies: storybook: 8.6.18(prettier@3.8.1) - "@swc/counter@0.1.3": {} - "@swc/helpers@0.5.15": dependencies: tslib: 2.8.1 @@ -13821,10 +13799,6 @@ snapshots: esbuild: 0.24.2 load-tsconfig: 0.2.5 - busboy@1.6.0: - dependencies: - streamsearch: 1.1.0 - cac@6.7.14: {} call-bind-apply-helpers@1.0.2: @@ -15642,26 +15616,24 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - next@15.3.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + next@15.5.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - "@next/env": 15.3.8 - "@swc/counter": 0.1.3 + "@next/env": 15.5.18 "@swc/helpers": 0.5.15 - busboy: 1.6.0 caniuse-lite: 1.0.30001781 postcss: 8.4.31 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) styled-jsx: 5.1.6(react@19.2.4) optionalDependencies: - "@next/swc-darwin-arm64": 15.3.5 - "@next/swc-darwin-x64": 15.3.5 - "@next/swc-linux-arm64-gnu": 15.3.5 - "@next/swc-linux-arm64-musl": 15.3.5 - "@next/swc-linux-x64-gnu": 15.3.5 - "@next/swc-linux-x64-musl": 15.3.5 - "@next/swc-win32-arm64-msvc": 15.3.5 - "@next/swc-win32-x64-msvc": 15.3.5 + "@next/swc-darwin-arm64": 15.5.18 + "@next/swc-darwin-x64": 15.5.18 + "@next/swc-linux-arm64-gnu": 15.5.18 + "@next/swc-linux-arm64-musl": 15.5.18 + "@next/swc-linux-x64-gnu": 15.5.18 + "@next/swc-linux-x64-musl": 15.5.18 + "@next/swc-win32-arm64-msvc": 15.5.18 + "@next/swc-win32-x64-msvc": 15.5.18 sharp: 0.34.5 transitivePeerDependencies: - "@babel/core" @@ -16547,8 +16519,6 @@ snapshots: - supports-color - utf-8-validate - streamsearch@1.1.0: {} - string-argv@0.3.2: {} string-width@4.2.3: