[as] feat: 검토인력관리 페이지 구현

This commit is contained in:
2026-06-05 14:32:14 +09:00
parent 0947b76a6e
commit acaf2dd9f4
17 changed files with 845 additions and 126 deletions
@@ -0,0 +1 @@
export { RvwMnpwMngPage as default } from "@/pages/rvw-mnpw-mng";
@@ -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<T>` |
| 프론트 엔티티 | `@/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<RvwMnpwMngResponseDto>` | `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)
@@ -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,
};
}
@@ -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<ReturnType<typeof filterOgnzUserTree>>([]);
const [expandedOgnzSnIds, setExpandedOgnzSnIds] = useState<Set<number>>(new Set());
const [selectedUserId, setSelectedUserId] = useState<string>();
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}
/>
+1
View File
@@ -0,0 +1 @@
export { RvwMnpwMngPage } from "./ui/rvw-mnpw-mng-page";
@@ -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<string, RvwMnpwMngResponseDto[]> {
const grouped = Object.fromEntries(
blocks.map((block) => [block.atrzSeComCd, [] as RvwMnpwMngResponseDto[]]),
);
for (const item of items) {
grouped[item.atrzSeComCd]?.push(item);
}
return grouped;
}
@@ -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;
@@ -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<ReturnType<typeof filterOgnzUserTree>>([]);
const [expandedOgnzSnIds, setExpandedOgnzSnIds] = useState<Set<number>>(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<HTMLFormElement>) => {
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 (
<section className="card card-border mx-0 flex h-full flex-col pt-0">
<div className="card-header">
<h3 className="text-2xl font-semibold">조직도</h3>
</div>
<div className="flex flex-col gap-4 p-4">
<form onSubmit={handleSubmit} className="flex items-center gap-2">
<Input
className="!text-[1.3rem]"
placeholder="성명·부서 검색"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
/>
<Button type="submit">조회</Button>
<Button type="button" color="white" onClick={handleReset}>
초기화
</Button>
</form>
<div className="min-h-[32rem] flex-1 overflow-auto">
<OgnzUserTree
nodes={displayTree}
isPending={isPending}
isError={isError}
isSearchActive={isSearchActive}
expandedOgnzSnIds={expandedOgnzSnIds}
selectedUserId={selectedPerson?.userUuid}
onToggle={handleToggle}
onSelect={handleSelect}
/>
</div>
</div>
</section>
);
}
export { OgnzPanel };
@@ -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 (
<section className="card card-border mx-0 flex h-full flex-col pt-0">
<div className="card-header">
<h3 className="text-2xl font-semibold">인원 배치</h3>
</div>
<div className="flex flex-col gap-4 p-4">
{pageQuery.isPending ? (
<div className="flex flex-col gap-3">
<Skeleton className="h-[8rem]" />
<Skeleton className="h-[8rem]" />
<Skeleton className="h-[8rem]" />
<Skeleton className="h-[8rem]" />
</div>
) : pageQuery.isError ? (
<p className="py-8 text-center text-lg text-muted-foreground">
인원 배치 목록을 불러오지 못했습니다.
</p>
) : (
RVW_MNPW_BLOCKS.map((block) => (
<RvwMnpwBlock
key={block.id}
block={block}
items={groupedItems[block.atrzSeComCd] ?? []}
selectedPerson={selectedPerson}
/>
))
)}
</div>
</section>
);
}
export { RvwMnpwAsmtPanel };
@@ -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 (
<section className="rounded-md border border-border bg-background p-4">
<div className="mb-3 flex flex-wrap items-center justify-between gap-2">
<div className="flex items-center gap-2">
<h4 className="text-xl font-semibold">{block.title}</h4>
<Badge>{items.length}</Badge>
</div>
<Button
type="button"
size="sm"
color="green"
disabled={!selectedPerson || isAssigning}
onClick={handleAssign}
>
선택 인원 배치
</Button>
</div>
{items.length === 0 ? (
<p className="py-4 text-center text-lg text-muted-foreground">배치된 인원이 없습니다.</p>
) : (
<ul className="grid grid-cols-4 gap-2">
{items.map((item) => (
<li
key={`${item.atrzSeComCd}-${item.ognzSn}-${item.userUuid}`}
className="flex items-center justify-between gap-3 rounded-sm border border-border px-3 py-2"
>
<div className="min-w-0 flex-1">
{/* TBD: userUuidNm @CodeName 미적용 시 백엔드 enrichUserNames 결과만 표시 */}
<p className="truncate text-[1.4rem] font-medium text-label">
{item.userUuidNm || item.userUuid}
</p>
{/* TBD: 응답 DTO에 부서·직급 필드 없음 — ognzSn만 표시 */}
{/* <p className="text-[1.3rem] text-muted-foreground">조직 SN: {item.ognzSn}</p> */}
</div>
<Button
type="button"
size="sm"
color="white"
disabled={isRemoving}
onClick={() => handleRemove(item)}
>
제거
</Button>
</li>
))}
</ul>
)}
</section>
);
}
export { RvwMnpwBlock };
@@ -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<SelectedOgnzUser | null>(null);
return (
<div className="space-y-6 px-[1.8rem] pb-6">
{/* TBD: 역할별 CRUD 권한 — 메뉴·API 권한 연동 후 버튼 비활성 처리 */}
<div className="grid grid-cols-1 gap-6 xl:grid-cols-[minmax(280px,1fr)_minmax(0,2.5fr)]">
<OgnzPanel selectedPerson={selectedPerson} onSelectPerson={setSelectedPerson} />
<RvwMnpwAsmtPanel selectedPerson={selectedPerson} />
</div>
</div>
);
}
export { RvwMnpwMngPage };
@@ -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";
@@ -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<OgnzUserTreeResponseDto, "ognzSn" | "ognzNm">,
): SelectedOgnzUser {
return {
userUuid: user.userUnqId,
userNm: user.userNm,
ognzSn: node.ognzSn,
ognzNm: node.ognzNm,
jbgdSeCd: user.jbgdSeCd || undefined,
jbpsSeCd: user.jbpsSeCd || undefined,
};
}
@@ -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<number>;
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({
<CollapsibleContent>
{node.users.map((user) => {
const isSelected = selectedUserId === user.userUnqId;
const selectedUser = toSelectedOgnzUser(user, node);
return (
<div
@@ -78,21 +80,23 @@ function OgnzUserTreeNode({
style={{ paddingInlineStart: indent + 24 }}
className={cn(
"group flex cursor-pointer items-center justify-between gap-2 rounded-sm p-2 hover:bg-accent",
isSelected && "bg-accent",
isSelected && "!border !border-primary !bg-primary-transparent",
)}
onClick={() => onSelect(toReviewerOption(user, node.ognzNm))}
onClick={() => onSelect(selectedUser)}
>
<span className="text-[1.4rem] text-label">{user.userNm}</span>
<Button
type="button"
size="sm"
onClick={(e) => {
e.stopPropagation();
onSelect(toReviewerOption(user, node.ognzNm));
}}
>
선택
</Button>
{showSelectButton ? (
<Button
type="button"
size="sm"
onClick={(e) => {
e.stopPropagation();
onSelect(selectedUser);
}}
>
선택
</Button>
) : null}
</div>
);
})}
@@ -104,6 +108,7 @@ function OgnzUserTreeNode({
depth={depth + 1}
expandedOgnzSnIds={expandedOgnzSnIds}
selectedUserId={selectedUserId}
showSelectButton={showSelectButton}
onToggle={onToggle}
onSelect={onSelect}
/>
@@ -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<number>;
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 (
<p className="py-8 text-center text-lg text-muted-foreground">
등록된 조직/사용자가 없습니다.
{isSearchActive ? "조회된 인원이 없습니다." : "등록된 조직/사용자가 없습니다."}
</p>
);
}
@@ -60,6 +64,7 @@ function OgnzUserTree({
node={node}
expandedOgnzSnIds={expandedOgnzSnIds}
selectedUserId={selectedUserId}
showSelectButton={showSelectButton}
onToggle={onToggle}
onSelect={onSelect}
/>
+67 -97
View File
@@ -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: