Merge branch 'iipbs/as/dev'
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { DesignViewPage } from "@/pages/design-cnstn";
|
||||
import { DesignDetailGate } from "@/pages/design-cnstn";
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
@@ -6,5 +6,5 @@ interface PageProps {
|
||||
|
||||
export default async function Page({ params }: PageProps) {
|
||||
const { id } = await params;
|
||||
return <DesignViewPage id={id} />;
|
||||
return <DesignDetailGate id={id} />;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FnrsViewPage } from "@/pages/fnrs-cnsltn";
|
||||
import { FnrsDetailGate } from "@/pages/fnrs-cnsltn";
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
@@ -6,5 +6,5 @@ interface PageProps {
|
||||
|
||||
export default async function Page({ params }: PageProps) {
|
||||
const { id } = await params;
|
||||
return <FnrsViewPage id={id} />;
|
||||
return <FnrsDetailGate id={id} />;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { InstlViewPage } from "@/pages/instl-sncn";
|
||||
import { InstlSncnDetailGate } from "@/pages/instl-sncn";
|
||||
|
||||
export default async function InstlSncnDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
return <InstlViewPage id={id} />;
|
||||
return <InstlSncnDetailGate id={id} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { NtiHstryListPage } from "@/pages/nti-hstry";
|
||||
|
||||
export default NtiHstryListPage;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SewerBscPlanViewPage } from "@/pages/sewer-bsc-plan";
|
||||
import { SewerBscPlanDetailGate } from "@/pages/sewer-bsc-plan";
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
@@ -9,5 +9,5 @@ export default async function Page({ params, searchParams }: PageProps) {
|
||||
const { id } = await params;
|
||||
const { chgCycl } = await searchParams;
|
||||
const propChgCycl = chgCycl ? Number(chgCycl) : undefined;
|
||||
return <SewerBscPlanViewPage id={id} propChgCycl={propChgCycl} />;
|
||||
return <SewerBscPlanDetailGate id={id} propChgCycl={propChgCycl} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
# 화면설계서: 알림이력
|
||||
|
||||
## 1. 기본 정보
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ----------- | -------------------------------------------- |
|
||||
| 버전 | 1.4 |
|
||||
| 프로세스명 | UI설계 |
|
||||
| 화면명 | 알림이력 |
|
||||
| 화면 ID | AS_SC0205011 |
|
||||
| 요구사항 ID | NSUR-001-001 |
|
||||
| 화면 유형 | [v] 조회 [ ] 입력 [ ] 수정 [ ] 삭제 [ ] 출력 |
|
||||
| 메뉴 경로 | 메인 > 행정지원서비스 > 알림이력 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 화면 목적
|
||||
|
||||
로그인한 사용자가 **본인에게 발송된 행정지원(ADSS) 알림**을 기간·구분으로 조회한다. 목록의 처리 버튼으로 해당 알림을 읽음 처리한 뒤, 연계된 업무 화면으로 이동한다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 화면 구성
|
||||
|
||||
### 3-1. 전체 레이아웃
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 브레드크럼: 메인 > 행정지원서비스 > 알림이력 │
|
||||
│ 페이지 타이틀: 알림이력 │
|
||||
├─────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ [영역 A] 검색 필터 │
|
||||
│ 기간 [시작일] ~ [종료일] 구분 ▼ [검색] │
|
||||
├─────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ [영역 B] 알림 이력 목록 │
|
||||
│ 타이틀: 알림 이력 목록 N개 ▼ │
|
||||
│ ┌────┬──────────────┬────────────────┬──────────┬────────┬────────┐ │
|
||||
│ │ No │ 알림일시 │ 알림내용 │ 연계업무 │ 상태 │ 처리 │ │
|
||||
│ ├────┼──────────────┼────────────────┼──────────┼────────┼────────┤ │
|
||||
│ │ … │ regDt │ notiTtl │ docSeComNm│ Badge │ 버튼 │ │
|
||||
│ └────┴──────────────┴────────────────┴──────────┴────────┴────────┘ │
|
||||
│ [페이지네이션] │
|
||||
└─────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3-2. 화면 데이터 흐름
|
||||
|
||||
```
|
||||
BEGIN
|
||||
|
||||
// 최초 진입
|
||||
// 기간 기본값: 오늘 기준 최근 1개월 (시작일 = today-1M, 종료일 = today)
|
||||
CALL findMyAsNotiList(NotiSearchDto + pageable)
|
||||
DISPLAY 알림 이력 목록 그리드 (영역 B)
|
||||
|
||||
ON [검색] 버튼 CLICK DO
|
||||
CALL findMyAsNotiList(영역 A 조건 + page=0)
|
||||
DISPLAY 알림 이력 목록 그리드 (영역 B)
|
||||
END ON
|
||||
|
||||
ON 페이지네이션 CHANGE DO
|
||||
CALL findMyAsNotiList(영역 A 조건 + page/size)
|
||||
DISPLAY 알림 이력 목록 그리드 (영역 B)
|
||||
END ON
|
||||
|
||||
ON 영역 B [처리] 버튼 CLICK DO
|
||||
CALL readNoti(notiId)
|
||||
// UI설계: 해당 알림에 연계된 URL로 이동
|
||||
NAVIGATE notiUrl
|
||||
END ON
|
||||
|
||||
END
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 영역 A — 검색 필터
|
||||
|
||||
구성: **기간 · 구분** (+ 검색).
|
||||
|
||||
서버 `findMyAsNotiList` 고정 정책 (클라이언트 미전달):
|
||||
|
||||
- `recvUnqId` = 로그인 사용자 UUID
|
||||
- `delYn` = `N`
|
||||
- `notiType` = `ADSS` (행정지원 전용 API)
|
||||
|
||||
### 4-1. 필드 정의
|
||||
|
||||
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
|
||||
| ---------- | ---------- | ---- | --------------------------- | ----------------- | ------------------------------------------------------------------------------------------------ |
|
||||
| 기간(시작) | DatePicker | N | 오늘 − 1개월 (`YYYY-MM-DD`) | `startDt` | `NotiSearchDto.startDt` |
|
||||
| 기간(종료) | DatePicker | N | 오늘 (`YYYY-MM-DD`) | `endDt` | `NotiSearchDto.endDt` |
|
||||
| 구분 | Select | N | 전체(미선택) | `docSeComCd` | `INSTL` / `FNRS` / `SEWER` / `CNFM` / `PWRK` (또는 단축 `I`/`F`/`S`/`C`/`P`). 공통코드 `ASDOCSE` |
|
||||
|
||||
### 4-2. 버튼
|
||||
|
||||
| 버튼명 | 동작 |
|
||||
| ------ | -------------------------------------------------------------- |
|
||||
| 검색 | 영역 A 조건으로 `findMyAsNotiList` 호출 → 영역 B 갱신 (page=0) |
|
||||
|
||||
### 4-3. 콤보·기본값 주의
|
||||
|
||||
| 항목 | 내용 |
|
||||
| -------------- | ---------------------------------------------------------- |
|
||||
| 구분 = 전체 | `docSeComCd` 미전달. `notiType`은 서버가 `ADSS`로 고정 |
|
||||
| 구분 옵션 소스 | 공통코드 `ASDOCSE` (`docSeComCd` ↔ `docSeComNm`) |
|
||||
| 기간 | UI설계 상세설명: 기본 최근 1개월. `startDt`/`endDt`로 전달 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 영역 B — 알림 이력 목록
|
||||
|
||||
### 5-1. 그리드 컬럼
|
||||
|
||||
| 표시명 | Field | 비고 |
|
||||
| -------- | ------------ | --------------------------------------------------------------- |
|
||||
| No | (row index) | 페이지 오프셋 반영 |
|
||||
| 알림일시 | `regDt` | 표시 `YYYY-MM-DD HH:mm` (응답 `yyyy-MM-dd HH:mm:ss`) |
|
||||
| 알림내용 | `notiTtl` | 제목 문자열. UI 샘플은 `[YYYY-MM-DD] …` 접두 |
|
||||
| 연계업무 | `docSeComNm` | 문서구분 공통코드명 (`docSeComCd` + `@CodeName` `ASDOCSE`) |
|
||||
| 상태 | (derived) | `Badge` 렌더링. 판정 규칙은 §5-1-1 |
|
||||
| 처리 | (action) | 상태별 버튼 라벨. 클릭 시 `readNoti` 후 `notiUrl`로 이동 (§5-3) |
|
||||
|
||||
#### 5-1-1. 상태 UI 케이스 (`@common/components` `Badge`)
|
||||
|
||||
판정에 사용: `notiPrcsCmptnYn`, `notReadYn`, `regDt` (오늘 기준 경과일).
|
||||
|
||||
| 조건 | 라벨 | Badge |
|
||||
| ------------------------------------------------------------------------------ | -------- | ----------------------------- |
|
||||
| `notiPrcsCmptnYn` = `Y` | 처리완료 | `variant="default"` (primary) |
|
||||
| `notiPrcsCmptnYn` = `N` 이고 `notReadYn` = `Y` 이고 `regDt`로부터 **7일 초과** | 처리지연 | `color="red"` |
|
||||
| `notiPrcsCmptnYn` = `N` 이고 `notReadYn` = `Y` 이고 `regDt`로부터 **7일 이내** | 처리중 | `color="green"` |
|
||||
| `notiPrcsCmptnYn` = `N` 이고 `notReadYn` = `N` 이고 `regDt`로부터 **3일 초과** | 지연 | `color="orange"` |
|
||||
| `notiPrcsCmptnYn` = `N` 이고 `notReadYn` = `N` 이고 `regDt`로부터 **3일 이내** | 미조회 | `color="yellow"` |
|
||||
|
||||
```
|
||||
IF notiPrcsCmptnYn = Y THEN
|
||||
DISPLAY Badge "처리완료" (primary / variant=default)
|
||||
ELSE IF notReadYn = Y THEN
|
||||
IF daysSince(regDt) > 7 THEN
|
||||
DISPLAY Badge "처리지연" (red)
|
||||
ELSE
|
||||
DISPLAY Badge "처리중" (green)
|
||||
END IF
|
||||
ELSE // notReadYn = N
|
||||
IF daysSince(regDt) > 3 THEN
|
||||
DISPLAY Badge "지연" (orange)
|
||||
ELSE
|
||||
DISPLAY Badge "미조회" (yellow)
|
||||
END IF
|
||||
END IF
|
||||
```
|
||||
|
||||
- `daysSince(regDt)`: 오늘 날짜 − `regDt` 일자 (일 단위)
|
||||
- `regDt`가 없으면 상태 Badge 표시 전략 TBD (미표시 또는 기본값)
|
||||
|
||||
응답에 있으나 그리드에 **표시하지 않음**:
|
||||
|
||||
| Field | 비고 |
|
||||
| ----------------- | ---------------------------------------------------------------------- |
|
||||
| `notiId` | 알림 ID — 행 키 |
|
||||
| `recvUnqId` | 수신 회원 UUID (서버가 로그인 사용자로 고정) |
|
||||
| `senderUnqId` | 발신 회원 UUID |
|
||||
| `notiType` | 알림 종류 (`ADSS`) |
|
||||
| `notiCn` | 알림 내용 (목록에서는 서버가 제외할 수 있음) |
|
||||
| `notReadYn` | 확인 여부 `Y`/`N`. 상태 Badge 판정에 사용 (§5-1-1) |
|
||||
| `notiUrl` | 처리 버튼 이동 URL. 컬럼 미표시 |
|
||||
| `notiLnkgUnqNo` | 알림연결고유번호 |
|
||||
| `docSeComCd` | 문서구분 공통코드. 연계업무 표시는 `docSeComNm` 사용 |
|
||||
| `notiIdntyDt` | 알림 확인 일시 |
|
||||
| `notiIdntyYn` | 알림 확인 여부 (`notReadYn`과 동일 의미). 상태 판정은 `notReadYn` 사용 |
|
||||
| `notiPrcsCmptnYn` | 알림 처리 완료 여부. 상태 Badge 판정에 사용 (§5-1-1) |
|
||||
| `notiPrcsCmptnDt` | 알림 처리 완료 일시 |
|
||||
|
||||
### 5-2. 목록 메타·페이지네이션
|
||||
|
||||
| 항목 | 매핑 |
|
||||
| ------------- | ------------------------------------------- |
|
||||
| 건수 / size ▼ | 상단 우측 `N개` 셀렉트 → `Pageable.size` |
|
||||
| page / size | `NotiSearchDto`의 `PageableParams` |
|
||||
| 페이지 컨트롤 | Spring `Page` 표준 (`number`, `totalPages`) |
|
||||
|
||||
### 5-3. 행 상호작용
|
||||
|
||||
| 동작 | 설명 |
|
||||
| -------------- | --------------------------------------------------------------------------- |
|
||||
| 처리 버튼 클릭 | `readNoti(notiId)`로 읽음 처리 후, 해당 알림에 연계된 URL(`notiUrl`)로 이동 |
|
||||
| 행 클릭 | UI설계에 없음. 처리 버튼만 명시 |
|
||||
|
||||
**처리 버튼 라벨** (상태 Badge §5-1-1 기준):
|
||||
|
||||
| 상태 | 버튼명 |
|
||||
| -------- | -------- |
|
||||
| 미조회 | 처리하기 |
|
||||
| 지연 | 처리하기 |
|
||||
| 처리중 | 처리하기 |
|
||||
| 처리지연 | 처리하기 |
|
||||
| 처리완료 | 상세보기 |
|
||||
|
||||
- `notiUrl`이 비어 있으면 이동하지 않음 (no-op). 읽음 처리는 `notiUrl` 유무와 관계없이 호출
|
||||
- 읽음 처리 성공 후 목록 쿼리(`notiBaseKey`)를 invalidate 하여 영역 B 상태를 갱신
|
||||
|
||||
---
|
||||
|
||||
## 6. API/DTO 명세
|
||||
|
||||
### 6-1. 공통
|
||||
|
||||
| 항목 | 값 |
|
||||
| --------------- | ---------------------------------------------------------------------------------------- |
|
||||
| Base prefix | `/cms-service/api/v1/noti` (`ENDPOINT.CMS_SERVICE.NOTI`) |
|
||||
| 인증 | 로그인 사용자 UUID — 서버 `SecurityUtils.getLoggedInUserUnqIdUUID()` (클라이언트 미전달) |
|
||||
| 클라이언트 모듈 | `@/entities/noti` — `notiService` / `useFindMyAsNotiList` / `useReadNoti` |
|
||||
|
||||
### 6-2. 로그인한 사용자 행정지원 알림 목록 — `findMyAsNotiList`
|
||||
|
||||
| 기능 | Method | URL | Request DTO | Response DTO | API |
|
||||
| ---------------------------------- | ------ | ------------------- | --------------------------------- | ------------------------------------- | ------------------------------------------------------ |
|
||||
| 로그인한 사용자 행정지원 알림 목록 | GET | `{BASE}/as/my/list` | Query: `NotiSearchDto` + Pageable | `PageResponse<NotiAsUserResponseDto>` | `notiService.findMyAsNotiList` / `useFindMyAsNotiList` |
|
||||
|
||||
**Request (`NotiSearchDto` + Pageable)**
|
||||
|
||||
| 필드 | 필수 | 설명 |
|
||||
| ------------------------ | ---- | --------------------------------------------------------------------------- |
|
||||
| `recvUnqId` | N | 수신 회원 UUID. 서버가 로그인 사용자로 덮어씀 |
|
||||
| `senderUnqId` | N | 발신 회원 UUID. 본 화면 필터에 없음 |
|
||||
| `notiType` | N | 알림 종류. 본 API는 서버가 `ADSS`로 고정 |
|
||||
| `notiTtlKeyword` | N | 알림 제목 키워드. 본 화면 필터에 없음 |
|
||||
| `notiCnKeyword` | N | 알림 내용 키워드. 본 화면 필터에 없음 |
|
||||
| `notReadYn` | N | 알림 확인 여부 (`Y`/`N`). 본 화면 검색 필터에 없음 |
|
||||
| `delYn` | N | 삭제 여부. 서버가 `N`으로 고정 |
|
||||
| `startDt` | N | 시작일자 `YYYY-MM-DD` — 영역 A 기간(시작) |
|
||||
| `endDt` | N | 종료일자 `YYYY-MM-DD` — 영역 A 기간(종료) |
|
||||
| `docSeComCd` | N | 문서 구분 공통 코드 (`INSTL`/`FNRS`/`SEWER`/`CNFM`/`PWRK` 등) — 영역 A 구분 |
|
||||
| `page` / `size` / `sort` | N | Spring Pageable |
|
||||
|
||||
**Response (`NotiAsUserResponseDto`)**
|
||||
|
||||
| 필드 | 타입 | 설명 |
|
||||
| ----------------- | ------- | ------------------------------------------------------- |
|
||||
| `notiId` | number | 알림 ID |
|
||||
| `recvUnqId` | string | 수신 회원 UUID |
|
||||
| `senderUnqId` | string | 발신 회원 UUID |
|
||||
| `notiType` | string | 알림 종류 (`ADSS`) |
|
||||
| `notiTtl` | string | 알림 제목 |
|
||||
| `notiCn` | string? | 알림 내용 (목록에서 제외될 수 있음) |
|
||||
| `notReadYn` | string? | 알림 확인 여부 (`Y`/`N`). 상태 Badge 판정 |
|
||||
| `regDt` | string? | 등록 일시 `yyyy-MM-dd HH:mm:ss`. 상태 Badge 경과일 기준 |
|
||||
| `notiUrl` | string? | 알림 URL 주소 |
|
||||
| `notiLnkgUnqNo` | string? | 알림연결고유번호 |
|
||||
| `docSeComCd` | string? | 문서구분 공통코드 |
|
||||
| `docSeComNm` | string? | 문서구분 공통코드명 (`ASDOCSE`) |
|
||||
| `notiIdntyDt` | string? | 알림 확인 일시 `yyyy-MM-dd HH:mm:ss` |
|
||||
| `notiIdntyYn` | string? | 알림 확인 여부 (`Y`/`N`) |
|
||||
| `notiPrcsCmptnYn` | string? | 알림 처리 완료 여부 (`Y`/`N`). 상태 Badge 판정 |
|
||||
| `notiPrcsCmptnDt` | string? | 알림 처리 완료 일시 `yyyy-MM-dd HH:mm:ss` |
|
||||
|
||||
### 6-3. 알림 읽음 처리 — `readNoti`
|
||||
|
||||
| 기능 | Method | URL | Request DTO | Response DTO | API |
|
||||
| -------------- | ------ | ---------------------- | ----------------------- | ---------------- | -------------------------------------- |
|
||||
| 알림 읽음 처리 | PUT | `{BASE}/{notiId}/read` | Path: `notiId` (`Long`) | `Long` (알림 ID) | `notiService.readNoti` / `useReadNoti` |
|
||||
|
||||
**Request**
|
||||
|
||||
| 필드 | 위치 | 필수 | 설명 |
|
||||
| -------- | ---- | ---- | ------- |
|
||||
| `notiId` | Path | Y | 알림 ID |
|
||||
|
||||
Body 없음.
|
||||
|
||||
**Response**
|
||||
|
||||
| 타입 | 설명 |
|
||||
| ------ | ------------------------------ |
|
||||
| number | 읽음 처리된 알림 ID (`notiId`) |
|
||||
|
||||
화면 매핑: 영역 B [처리] 버튼 클릭 시 호출. 성공 시 `notiBaseKey` invalidate → `findMyAsNotiList` 재조회.
|
||||
|
||||
---
|
||||
|
||||
## 7. UI 구현 가이드
|
||||
|
||||
### 7-1. 기술 스택·원칙
|
||||
|
||||
- UI: `@common/components` — DatePicker, Select, Button, DataTable, Pagination, **Badge**
|
||||
- 목록: `@tanstack/react-table` + `useDataTable` (기존 `my-pbadms-sprt-list` 패턴)
|
||||
- 데이터: React Query `useFindMyAsNotiList(params)`, `useReadNoti()`
|
||||
- 검색 조건·페이지: URL `searchParams` 동기화 권장 (`useSearchParamsState` 스킬)
|
||||
- 처리 컬럼: 행 클릭이 아니라 **버튼 클릭**으로 `readNoti` 후 `notiUrl` 이동
|
||||
- 상태 컬럼: §5-1-1 규칙으로 `Badge` 렌더링 (`notiPrcsCmptnYn` / `notReadYn` / `regDt`)
|
||||
- 연계업무: `docSeComNm` 표시
|
||||
|
||||
### 7-2. 앱·파일 배치
|
||||
|
||||
| 구분 | 경로 |
|
||||
| ------------ | --------------------------------------------------------------------- |
|
||||
| 라우트 | `app/(main)/(with-base-layout)/noti/page.tsx` |
|
||||
| 페이지 | `pages/noti/ui/noti-list-page.tsx` |
|
||||
| 목록 그리드 | `pages/noti/ui/noti-list.tsx` |
|
||||
| 테이블 컬럼 | `pages/noti/model/noti-table-columns.tsx` |
|
||||
| 상태 판정 | `pages/noti/lib/resolve-noti-status.ts` |
|
||||
| Entity API | `entities/noti/api/noti-service.ts` |
|
||||
| Entity Hook | `entities/noti/api/queries.ts` → `useFindMyAsNotiList`, `useReadNoti` |
|
||||
| Entity Model | `entities/noti/model/noti-interface.ts` |
|
||||
|
||||
화면 레이어(`pages/noti`)·Entity(`findMyAsNotiList`, `readNoti`) 구현 완료.
|
||||
|
||||
### 7-3. 영역별 UI 매핑
|
||||
|
||||
| 영역 | 컴포넌트 | 데이터 |
|
||||
| ---- | ------------------- | -------------------------------------------------------------- |
|
||||
| A | `noti-list-page` | 로컬 state → `NotiSearchDto` |
|
||||
| B | DataTable + columns | `useFindMyAsNotiList` → `content[]`. 처리 버튼 → `useReadNoti` |
|
||||
|
||||
### 7-4. 다이얼로그·라우팅
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ----------- | --------------------------------------------------------------------- |
|
||||
| 다이얼로그 | 없음 (목록 전용) |
|
||||
| 목록 라우트 | `/noti` |
|
||||
| 상세 이동 | 처리 버튼 → `readNoti(notiId)` 후 `notiUrl` (절대/상대 경로 형식 TBD) |
|
||||
|
||||
### 7-5. 데이터 흐름
|
||||
|
||||
```
|
||||
BEGIN
|
||||
|
||||
// 최초 진입
|
||||
SET 기간 = 최근 1개월 // startDt / endDt 전달
|
||||
CALL findMyAsNotiList
|
||||
DISPLAY 영역 B
|
||||
|
||||
ON [검색] CLICK DO
|
||||
CALL findMyAsNotiList // → 영역 B 갱신
|
||||
END ON
|
||||
|
||||
ON 페이지 CHANGE DO
|
||||
CALL findMyAsNotiList
|
||||
END ON
|
||||
|
||||
ON 영역 B [처리] 버튼 CLICK DO
|
||||
CALL readNoti(notiId) // → 영역 B 상태 갱신 (invalidate)
|
||||
IF NOT notiUrl THEN
|
||||
// no-op (읽음 처리는 수행됨)
|
||||
ELSE
|
||||
NAVIGATE notiUrl
|
||||
END IF
|
||||
END ON
|
||||
|
||||
END
|
||||
```
|
||||
@@ -0,0 +1,23 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { PageResponse } from "@/shared/model";
|
||||
|
||||
import type { NotiAsUserResponseDto, NotiSearchDto, NotiService } from "../model/noti-interface";
|
||||
|
||||
const BASE = ENDPOINT.CMS_SERVICE.NOTI;
|
||||
|
||||
async function findMyAsNotiList(
|
||||
params: NotiSearchDto,
|
||||
): Promise<PageResponse<NotiAsUserResponseDto>> {
|
||||
const response = await client.get<PageResponse<NotiAsUserResponseDto>>(`${BASE}/as/my/list`, {
|
||||
params,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function readNoti(notiId: number): Promise<number> {
|
||||
const response = await client.put<number>(`${BASE}/${notiId}/read`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const notiService = { findMyAsNotiList, readNoti } satisfies NotiService;
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type { NotiSearchDto } from "../model/noti-interface";
|
||||
|
||||
import { notiService } from "./noti-service";
|
||||
|
||||
export const notiBaseKey = [ENDPOINT.CMS_SERVICE.NOTI] as const;
|
||||
|
||||
export function useFindMyAsNotiList(params: NotiSearchDto) {
|
||||
return useQuery({
|
||||
queryKey: [...notiBaseKey, "findMyAsNotiList", params],
|
||||
queryFn: () => notiService.findMyAsNotiList(params),
|
||||
});
|
||||
}
|
||||
|
||||
export function useReadNoti() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (notiId: number) => notiService.readNoti(notiId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: notiBaseKey });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { notiService } from "./api/noti-service";
|
||||
export { notiBaseKey, useFindMyAsNotiList, useReadNoti } from "./api/queries";
|
||||
export type { NotiAsUserResponseDto, NotiSearchDto, NotiService } from "./model/noti-interface";
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { PageableParams, PageResponse } from "@/shared/model";
|
||||
|
||||
/** 백엔드 NotiAsUserResponseDto */
|
||||
export interface NotiAsUserResponseDto {
|
||||
notiId: number;
|
||||
recvUnqId: string;
|
||||
senderUnqId: string;
|
||||
notiType: string;
|
||||
notiTtl: string;
|
||||
notiCn?: string;
|
||||
notReadYn?: string;
|
||||
regDt?: string;
|
||||
notiUrl?: string;
|
||||
notiLnkgUnqNo?: string;
|
||||
docSeComCd?: string;
|
||||
docSeComNm?: string;
|
||||
notiIdntyDt?: string;
|
||||
notiIdntyYn?: string;
|
||||
notiPrcsCmptnYn?: string;
|
||||
notiPrcsCmptnDt?: string;
|
||||
}
|
||||
|
||||
/** 백엔드 NotiSearchDto + Pageable */
|
||||
export interface NotiSearchDto extends PageableParams {
|
||||
recvUnqId?: string;
|
||||
senderUnqId?: string;
|
||||
notiType?: string;
|
||||
notiTtlKeyword?: string;
|
||||
notiCnKeyword?: string;
|
||||
notReadYn?: string;
|
||||
delYn?: string;
|
||||
startDt?: string;
|
||||
endDt?: string;
|
||||
docSeComCd?: string;
|
||||
}
|
||||
|
||||
/** Controller public 메서드명·시그니처와 동일 */
|
||||
export interface NotiService {
|
||||
findMyAsNotiList(params: NotiSearchDto): Promise<PageResponse<NotiAsUserResponseDto>>;
|
||||
readNoti(notiId: number): Promise<number>;
|
||||
}
|
||||
@@ -26,8 +26,8 @@ async function findPage(
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function findById(sewerBscPlanDocNo: string): Promise<SewerBscPlanResponseDto> {
|
||||
const response = await client.get<SewerBscPlanResponseDto>(`${BASE}/${sewerBscPlanDocNo}`);
|
||||
async function findById(sewerBscPlanDocNo: string): Promise<SewerBscPlanDetResponseDto> {
|
||||
const response = await client.get<SewerBscPlanDetResponseDto>(`${BASE}/${sewerBscPlanDocNo}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
|
||||
@@ -207,7 +207,7 @@ export interface SewerBscPlanDetResponseDto {
|
||||
/** Controller public 메서드명·시그니처와 동일 */
|
||||
export interface SewerBscPlanService {
|
||||
findPage(params: SewerBscPlanSearchRequestDto): Promise<PageResponse<SewerBscPlanResponseDto>>;
|
||||
findById(sewerBscPlanDocNo: string): Promise<SewerBscPlanResponseDto>;
|
||||
findById(sewerBscPlanDocNo: string): Promise<SewerBscPlanDetResponseDto>;
|
||||
save(body: SewerBscPlanSaveRequestDto): Promise<SewerBscPlanResponseDto>;
|
||||
update(
|
||||
sewerBscPlanDocNo: string,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { DesignDetailGate } from "./ui/design-detail-gate";
|
||||
export { DesignEditPage } from "./ui/design-edit-page";
|
||||
export { DesignFormPage } from "./ui/design-form-page";
|
||||
export { DesignListPage } from "./ui/design-list-page";
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import { useUserStore } from "@/shared/lib";
|
||||
import { useDesignCnstnMng } from "@/entities/design-cnstn";
|
||||
import { canEditPbadmsDoc, PbadmsDetailGate } from "@/widgets/pbadms-sprt";
|
||||
|
||||
import { DesignViewPage } from "./design-view-page";
|
||||
|
||||
interface DesignDetailGateProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
function DesignDetailGate({ id }: DesignDetailGateProps) {
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const { data, isLoading, isError } = useDesignCnstnMng(id);
|
||||
const shouldRedirectToEdit = canEditPbadmsDoc({
|
||||
docSeComCd: "CNFM",
|
||||
prgrsSttsComCd: data?.detDto?.prgrsSttsComCd,
|
||||
rgtr: data?.detDto?.rgtr,
|
||||
loggedInUserUnqId: userInfo?.userUnqId,
|
||||
});
|
||||
|
||||
return (
|
||||
<PbadmsDetailGate
|
||||
isPending={!userInfo || isLoading}
|
||||
isError={isError}
|
||||
shouldRedirectToEdit={Boolean(userInfo) && shouldRedirectToEdit}
|
||||
editHref={`/design-cnstn/${id}/edit`}
|
||||
>
|
||||
<DesignViewPage id={id} />
|
||||
</PbadmsDetailGate>
|
||||
);
|
||||
}
|
||||
|
||||
export { DesignDetailGate };
|
||||
@@ -5,7 +5,7 @@ import { useRouter } from "next/navigation";
|
||||
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
|
||||
import { useDataTable, useUserStore } from "@/shared/lib";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import {
|
||||
type DesignCnstnMngResponseDto,
|
||||
type DesignCnstnMngSearchDto,
|
||||
@@ -24,7 +24,6 @@ interface DesignListProps {
|
||||
}
|
||||
|
||||
function DesignList({ searchParams, pagination, onPaginationChange }: DesignListProps) {
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const router = useRouter();
|
||||
const [isTaskSeDialogOpen, setIsTaskSeDialogOpen] = useState(false);
|
||||
|
||||
@@ -44,13 +43,7 @@ function DesignList({ searchParams, pagination, onPaginationChange }: DesignList
|
||||
const columns = useMemo(() => createDesignCnstnColumns(), []);
|
||||
|
||||
const handleRowClick = (row: DesignCnstnMngResponseDto) => {
|
||||
const canEdit = row.prgrsSttsComCd === "CV" && row.rgtr === userInfo?.userUnqId;
|
||||
|
||||
router.push(
|
||||
canEdit
|
||||
? `/design-cnstn/${row.designCnstnDocNo}/edit`
|
||||
: `/design-cnstn/${row.designCnstnDocNo}`,
|
||||
);
|
||||
router.push(`/design-cnstn/${row.designCnstnDocNo}`);
|
||||
};
|
||||
|
||||
const { tableComponent } = useDataTable({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { FnrsDetailGate } from "./ui/fnrs-detail-gate";
|
||||
export { FnrsEditPage } from "./ui/fnrs-edit-page";
|
||||
export { FnrsFormPage } from "./ui/fnrs-form-page";
|
||||
export { FnrsListPage } from "./ui/fnrs-list-page";
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import { useUserStore } from "@/shared/lib";
|
||||
import { useFnrsCnsltnMng } from "@/entities/fnrs-cnsltn";
|
||||
import { canEditPbadmsDoc, PbadmsDetailGate } from "@/widgets/pbadms-sprt";
|
||||
|
||||
import { FnrsViewPage } from "./fnrs-view-page";
|
||||
|
||||
interface FnrsDetailGateProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
function FnrsDetailGate({ id }: FnrsDetailGateProps) {
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const { data, isLoading, isError } = useFnrsCnsltnMng(id);
|
||||
const shouldRedirectToEdit = canEditPbadmsDoc({
|
||||
docSeComCd: "FNRS",
|
||||
prgrsSttsComCd: data?.detDto?.prgrsSttsComCd,
|
||||
rgtr: data?.detDto?.rgtr,
|
||||
loggedInUserUnqId: userInfo?.userUnqId,
|
||||
});
|
||||
|
||||
return (
|
||||
<PbadmsDetailGate
|
||||
isPending={!userInfo || isLoading}
|
||||
isError={isError}
|
||||
shouldRedirectToEdit={Boolean(userInfo) && shouldRedirectToEdit}
|
||||
editHref={`/fnrs-cnsltn/${id}/edit`}
|
||||
>
|
||||
<FnrsViewPage id={id} />
|
||||
</PbadmsDetailGate>
|
||||
);
|
||||
}
|
||||
|
||||
export { FnrsDetailGate };
|
||||
@@ -5,7 +5,7 @@ import { useRouter } from "next/navigation";
|
||||
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
|
||||
import { useDataTable, useUserStore } from "@/shared/lib";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import {
|
||||
FnrsCnsltnMngResponseDto,
|
||||
type FnrsCnsltnMngSearchDto,
|
||||
@@ -24,7 +24,6 @@ interface FnrsListProps {
|
||||
}
|
||||
|
||||
function FnrsList({ searchParams, pagination, onPaginationChange }: FnrsListProps) {
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const router = useRouter();
|
||||
const [isTaskSeDialogOpen, setIsTaskSeDialogOpen] = useState(false);
|
||||
|
||||
@@ -44,13 +43,7 @@ function FnrsList({ searchParams, pagination, onPaginationChange }: FnrsListProp
|
||||
const columns = useMemo(() => createFnrsCnsltnColumns(), []);
|
||||
|
||||
const handleRowClick = (row: FnrsCnsltnMngResponseDto) => {
|
||||
const canEdit =
|
||||
(row.prgrsSttsComCd === "SV" || row.prgrsSttsComCd === "SL") &&
|
||||
row.rgtr === userInfo?.userUnqId;
|
||||
|
||||
router.push(
|
||||
canEdit ? `/fnrs-cnsltn/${row.fnrsCnsltnDocNo}/edit` : `/fnrs-cnsltn/${row.fnrsCnsltnDocNo}`,
|
||||
);
|
||||
router.push(`/fnrs-cnsltn/${row.fnrsCnsltnDocNo}`);
|
||||
};
|
||||
|
||||
const { tableComponent } = useDataTable({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { InstlSncnDetailGate } from "./ui/instl-detail-gate";
|
||||
export { InstlEditPage } from "./ui/instl-edit-page";
|
||||
export { InstlFormPage } from "./ui/instl-form-page";
|
||||
export { InstlListPage } from "./ui/instl-list-page";
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import { useUserStore } from "@/shared/lib";
|
||||
import { useInstlSncnDetail } from "@/entities/instl-sncn";
|
||||
import { canEditPbadmsDoc, PbadmsDetailGate } from "@/widgets/pbadms-sprt";
|
||||
|
||||
import { InstlViewPage } from "./instl-view-page";
|
||||
|
||||
interface InstlSncnDetailGateProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
function InstlSncnDetailGate({ id }: InstlSncnDetailGateProps) {
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const { data, isLoading, isError } = useInstlSncnDetail(id);
|
||||
const shouldRedirectToEdit = canEditPbadmsDoc({
|
||||
docSeComCd: "INSTL",
|
||||
prgrsSttsComCd: data?.detDto.prgrsSttsComCd,
|
||||
rgtr: data?.detDto.rgtr,
|
||||
loggedInUserUnqId: userInfo?.userUnqId,
|
||||
});
|
||||
|
||||
return (
|
||||
<PbadmsDetailGate
|
||||
isPending={!userInfo || isLoading}
|
||||
isError={isError}
|
||||
shouldRedirectToEdit={Boolean(userInfo) && shouldRedirectToEdit}
|
||||
editHref={`/instl-sncn/${id}/edit`}
|
||||
>
|
||||
<InstlViewPage id={id} />
|
||||
</PbadmsDetailGate>
|
||||
);
|
||||
}
|
||||
|
||||
export { InstlSncnDetailGate };
|
||||
@@ -5,7 +5,7 @@ import { useRouter } from "next/navigation";
|
||||
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
|
||||
import { useDataTable, useUserStore } from "@/shared/lib";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import {
|
||||
type InstlSncnMngResponseDto,
|
||||
type InstlSncnMngSearchParams,
|
||||
@@ -24,7 +24,6 @@ interface InstlListProps {
|
||||
}
|
||||
|
||||
function InstlList({ searchParams, pagination, onPaginationChange }: InstlListProps) {
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const router = useRouter();
|
||||
const [isTaskSeDialogOpen, setIsTaskSeDialogOpen] = useState(false);
|
||||
|
||||
@@ -44,13 +43,7 @@ function InstlList({ searchParams, pagination, onPaginationChange }: InstlListPr
|
||||
const columns = useMemo(() => createInstlSncnColumns(), []);
|
||||
|
||||
const handleRowClick = (row: InstlSncnMngResponseDto) => {
|
||||
const canEdit =
|
||||
(row.prgrsSttsComCd === "SV" || row.prgrsSttsComCd === "SL") &&
|
||||
row.rgtr === userInfo?.userUnqId;
|
||||
|
||||
router.push(
|
||||
canEdit ? `/instl-sncn/${row.instlSncnDocNo}/edit` : `/instl-sncn/${row.instlSncnDocNo}`,
|
||||
);
|
||||
router.push(`/instl-sncn/${row.instlSncnDocNo}`);
|
||||
};
|
||||
|
||||
const { tableComponent } = useDataTable({
|
||||
|
||||
@@ -1,42 +1,19 @@
|
||||
import type { MyAdssDocResponseDto } from "@/entities/elapr";
|
||||
|
||||
function canEditByDocSe(
|
||||
docSeComCd: string,
|
||||
prgrsSttsComCd: string | undefined,
|
||||
rgtr: string | undefined,
|
||||
loggedInUserUnqId: string | undefined,
|
||||
): boolean {
|
||||
if (!loggedInUserUnqId || !rgtr || rgtr !== loggedInUserUnqId) return false;
|
||||
|
||||
if (docSeComCd === "CNFM") return prgrsSttsComCd === "CV";
|
||||
|
||||
if (docSeComCd === "INSTL" || docSeComCd === "FNRS" || docSeComCd === "SEWER")
|
||||
return prgrsSttsComCd === "SV" || prgrsSttsComCd === "SL";
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function resolveMyPbadmsSprtDetailPath(
|
||||
row: MyAdssDocResponseDto,
|
||||
loggedInUserUnqId: string | undefined,
|
||||
): string | null {
|
||||
const { docNo, docSeComCd, prgrsSttsComCd, rgtr, chgCycl } = row;
|
||||
function resolveMyPbadmsSprtDetailPath(row: MyAdssDocResponseDto): string | null {
|
||||
const { docNo, docSeComCd, chgCycl } = row;
|
||||
if (!docNo || !docSeComCd) return null;
|
||||
|
||||
const canEdit = canEditByDocSe(docSeComCd, prgrsSttsComCd, rgtr, loggedInUserUnqId);
|
||||
|
||||
switch (docSeComCd) {
|
||||
case "FNRS":
|
||||
return canEdit ? `/fnrs-cnsltn/${docNo}/edit` : `/fnrs-cnsltn/${docNo}`;
|
||||
return `/fnrs-cnsltn/${docNo}`;
|
||||
case "INSTL":
|
||||
return canEdit ? `/instl-sncn/${docNo}/edit` : `/instl-sncn/${docNo}`;
|
||||
return `/instl-sncn/${docNo}`;
|
||||
case "CNFM":
|
||||
return canEdit ? `/design-cnstn/${docNo}/edit` : `/design-cnstn/${docNo}`;
|
||||
return `/design-cnstn/${docNo}`;
|
||||
case "SEWER": {
|
||||
const cycl = chgCycl ?? 1;
|
||||
return canEdit
|
||||
? `/sewer-bsc-plan/${docNo}/edit?chgCycl=${cycl}`
|
||||
: `/sewer-bsc-plan/${docNo}?chgCycl=${cycl}`;
|
||||
return `/sewer-bsc-plan/${docNo}?chgCycl=${cycl}`;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useRouter } from "next/navigation";
|
||||
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
|
||||
import { useDataTable, useUserStore } from "@/shared/lib";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import {
|
||||
type ElaprMyDocSearchDto,
|
||||
type MyAdssDocResponseDto,
|
||||
@@ -28,7 +28,6 @@ function toOptionalParam(value: string | undefined) {
|
||||
|
||||
function MyPbadmsSprtList({ searchParams, pagination, onPaginationChange }: MyPbadmsSprtListProps) {
|
||||
const router = useRouter();
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
|
||||
const queryParams = useMemo(
|
||||
(): ElaprMyDocSearchDto => ({
|
||||
@@ -54,7 +53,7 @@ function MyPbadmsSprtList({ searchParams, pagination, onPaginationChange }: MyPb
|
||||
);
|
||||
|
||||
const handleRowClick = (row: MyAdssDocResponseDto) => {
|
||||
const path = resolveMyPbadmsSprtDetailPath(row, userInfo?.userUnqId);
|
||||
const path = resolveMyPbadmsSprtDetailPath(row);
|
||||
if (path) router.push(path);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { NtiHstryListPage } from "./ui/nti-hstry-list-page";
|
||||
@@ -0,0 +1,61 @@
|
||||
import { dayjs } from "@/shared/lib";
|
||||
import type { NotiAsUserResponseDto } from "@/entities/noti";
|
||||
import type { BadgeProps } from "@common/components";
|
||||
|
||||
export type NotiStatusLabel = "처리완료" | "처리지연" | "처리중" | "지연" | "미조회";
|
||||
|
||||
export interface NotiStatusUi {
|
||||
label: NotiStatusLabel;
|
||||
badgeProps: Pick<BadgeProps, "variant" | "color" | "size">;
|
||||
actionLabel: string;
|
||||
}
|
||||
|
||||
function daysSince(regDt: string) {
|
||||
return dayjs().startOf("day").diff(dayjs(regDt).startOf("day"), "day");
|
||||
}
|
||||
|
||||
export function resolveNotiStatus(
|
||||
row: Pick<NotiAsUserResponseDto, "notiPrcsCmptnYn" | "notReadYn" | "regDt">,
|
||||
): NotiStatusUi | null {
|
||||
if (row.notiPrcsCmptnYn === "Y") {
|
||||
return {
|
||||
label: "처리완료",
|
||||
badgeProps: { variant: "default", size: "lg" },
|
||||
actionLabel: "상세보기",
|
||||
};
|
||||
}
|
||||
|
||||
if (!row.regDt) return null;
|
||||
|
||||
const days = daysSince(row.regDt);
|
||||
const isRead = row.notReadYn === "Y";
|
||||
|
||||
if (isRead) {
|
||||
if (days > 7) {
|
||||
return {
|
||||
label: "처리지연",
|
||||
badgeProps: { color: "red", size: "lg" },
|
||||
actionLabel: "처리하기",
|
||||
};
|
||||
}
|
||||
return {
|
||||
label: "처리중",
|
||||
badgeProps: { color: "green", size: "lg" },
|
||||
actionLabel: "처리하기",
|
||||
};
|
||||
}
|
||||
|
||||
if (days > 3) {
|
||||
return {
|
||||
label: "지연",
|
||||
badgeProps: { color: "orange", size: "lg" },
|
||||
actionLabel: "처리하기",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
label: "미조회",
|
||||
badgeProps: { color: "yellow" },
|
||||
actionLabel: "처리하기",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
"use client";
|
||||
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
|
||||
import { dayjs } from "@/shared/lib";
|
||||
import type { NotiAsUserResponseDto } from "@/entities/noti";
|
||||
import { Badge, Button } from "@common/components";
|
||||
|
||||
import { resolveNotiStatus } from "../lib/resolve-noti-status";
|
||||
|
||||
const columnHelper = createColumnHelper<NotiAsUserResponseDto>();
|
||||
|
||||
function formatOptional(value: string | number | undefined | null) {
|
||||
if (value === undefined || value === null) return "-";
|
||||
const text = String(value).trim();
|
||||
return text ? text : "-";
|
||||
}
|
||||
|
||||
function formatDateTime(value: string | undefined) {
|
||||
return value ? dayjs(value).format("YYYY-MM-DD HH:mm") : "-";
|
||||
}
|
||||
|
||||
interface CreateNotiColumnsOptions {
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
isActionPending: boolean;
|
||||
onAction: (row: NotiAsUserResponseDto) => void;
|
||||
}
|
||||
|
||||
export function createNotiColumns({
|
||||
pageIndex,
|
||||
pageSize,
|
||||
isActionPending,
|
||||
onAction,
|
||||
}: CreateNotiColumnsOptions) {
|
||||
return [
|
||||
columnHelper.display({
|
||||
id: "no",
|
||||
header: "No",
|
||||
meta: { align: "center" },
|
||||
cell: ({ row }) => pageIndex * pageSize + row.index + 1,
|
||||
}),
|
||||
columnHelper.accessor("regDt", {
|
||||
header: "알림일시",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => formatDateTime(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("notiTtl", {
|
||||
header: "알림내용",
|
||||
meta: { align: "left" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
columnHelper.accessor("docSeComNm", {
|
||||
header: "연계업무",
|
||||
meta: { align: "center" },
|
||||
cell: ({ row }) => formatOptional(row.original.docSeComNm || row.original.docSeComCd),
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "status",
|
||||
header: "상태",
|
||||
meta: { align: "center" },
|
||||
cell: ({ row }) => {
|
||||
const status = resolveNotiStatus(row.original);
|
||||
if (!status) return "-";
|
||||
return <Badge {...status.badgeProps}>{status.label}</Badge>;
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "action",
|
||||
header: "처리",
|
||||
meta: { align: "center" },
|
||||
cell: ({ row }) => {
|
||||
const status = resolveNotiStatus(row.original);
|
||||
const label = status?.actionLabel ?? "처리하기";
|
||||
return (
|
||||
<div className="flex justify-center" onClick={(event) => event.stopPropagation()}>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="white"
|
||||
disabled={isActionPending}
|
||||
onClick={() => onAction(row.original)}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import type { PaginationState } from "@tanstack/react-table";
|
||||
|
||||
import { dayjs } from "@/shared/lib";
|
||||
import { useCodeList } from "@/entities/code";
|
||||
import type { NotiSearchDto } from "@/entities/noti";
|
||||
import {
|
||||
Button,
|
||||
InputDatePicker,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
|
||||
import { NtiHstryList } from "./nti-hstry-list";
|
||||
|
||||
const DEFAULT_PAGE_INDEX = 0;
|
||||
const DEFAULT_PAGE_SIZE = 10;
|
||||
const ALL_SELECT_VALUE = "__ALL__";
|
||||
|
||||
function defaultStartDate() {
|
||||
return dayjs().subtract(1, "month").toDate();
|
||||
}
|
||||
|
||||
function defaultEndDate() {
|
||||
return dayjs().toDate();
|
||||
}
|
||||
|
||||
function formatDateParam(date: Date | null | undefined) {
|
||||
return date ? dayjs(date).format("YYYY-MM-DD") : "";
|
||||
}
|
||||
|
||||
function buildDefaultSearchParams(): NotiSearchDto {
|
||||
return {
|
||||
startDt: formatDateParam(defaultStartDate()),
|
||||
endDt: formatDateParam(defaultEndDate()),
|
||||
docSeComCd: "",
|
||||
page: DEFAULT_PAGE_INDEX,
|
||||
size: DEFAULT_PAGE_SIZE,
|
||||
};
|
||||
}
|
||||
|
||||
function NtiHstryListPage() {
|
||||
const [startDate, setStartDate] = useState<Date | null>(defaultStartDate);
|
||||
const [endDate, setEndDate] = useState<Date | null>(defaultEndDate);
|
||||
const [docSeComCd, setDocSeComCd] = useState("");
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: DEFAULT_PAGE_INDEX,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
});
|
||||
const [searchParams, setSearchParams] = useState<NotiSearchDto>(buildDefaultSearchParams);
|
||||
|
||||
const { data: docSeCodes, isPending: isDocSeLoading } = useCodeList("ASDOCSE");
|
||||
|
||||
const handleSearch = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setPagination((prev) => ({ ...prev, pageIndex: DEFAULT_PAGE_INDEX }));
|
||||
setSearchParams({
|
||||
startDt: formatDateParam(startDate),
|
||||
endDt: formatDateParam(endDate),
|
||||
docSeComCd,
|
||||
page: DEFAULT_PAGE_INDEX,
|
||||
size: pagination.pageSize,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4 px-[1.8rem] pb-6">
|
||||
<section className="card card-border mx-0 bg-background">
|
||||
<form className="flex flex-wrap items-center gap-4 p-2" onSubmit={handleSearch}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="whitespace-nowrap">기간</Label>
|
||||
<InputDatePicker
|
||||
value={startDate}
|
||||
onChange={(date) => setStartDate(date ?? null)}
|
||||
className="w-[12rem]"
|
||||
/>
|
||||
<span className="text-muted-foreground">~</span>
|
||||
<InputDatePicker
|
||||
value={endDate}
|
||||
onChange={(date) => setEndDate(date ?? null)}
|
||||
className="w-[12rem]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="docSeComCd" className="whitespace-nowrap">
|
||||
구분
|
||||
</Label>
|
||||
<Select
|
||||
value={docSeComCd || ALL_SELECT_VALUE}
|
||||
onValueChange={(value) => setDocSeComCd(value === ALL_SELECT_VALUE ? "" : value)}
|
||||
disabled={isDocSeLoading}
|
||||
>
|
||||
<SelectTrigger id="docSeComCd" className="w-[12rem]">
|
||||
<SelectValue placeholder={isDocSeLoading ? "로딩 중…" : "전체"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={ALL_SELECT_VALUE}>전체</SelectItem>
|
||||
{docSeCodes?.map((item) => (
|
||||
<SelectItem key={item.lwpoCd} value={item.lwpoCd}>
|
||||
{item.lwpoCdNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Button type="submit">검색</Button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<NtiHstryList
|
||||
searchParams={searchParams}
|
||||
pagination={pagination}
|
||||
onPaginationChange={setPagination}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { NtiHstryListPage };
|
||||
@@ -0,0 +1,114 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import {
|
||||
type NotiAsUserResponseDto,
|
||||
type NotiSearchDto,
|
||||
useFindMyAsNotiList,
|
||||
useReadNoti,
|
||||
} from "@/entities/noti";
|
||||
|
||||
import { createNotiColumns } from "../model/noti-table-columns";
|
||||
|
||||
interface NtiHstryListProps {
|
||||
searchParams: NotiSearchDto;
|
||||
pagination: PaginationState;
|
||||
onPaginationChange: (pagination: PaginationState) => void;
|
||||
}
|
||||
|
||||
function toOptionalParam(value: string | undefined) {
|
||||
const trimmed = value?.trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
|
||||
function NtiHstryList({ searchParams, pagination, onPaginationChange }: NtiHstryListProps) {
|
||||
const router = useRouter();
|
||||
const { mutateAsync: readNoti, isPending: isReadPending } = useReadNoti();
|
||||
|
||||
const queryParams = useMemo(
|
||||
(): NotiSearchDto => ({
|
||||
startDt: toOptionalParam(searchParams.startDt),
|
||||
endDt: toOptionalParam(searchParams.endDt),
|
||||
docSeComCd: toOptionalParam(searchParams.docSeComCd),
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
sort: searchParams.sort,
|
||||
}),
|
||||
[searchParams, pagination],
|
||||
);
|
||||
|
||||
const pageQuery = useFindMyAsNotiList(queryParams);
|
||||
|
||||
const resolveNotiUrl = (row: NotiAsUserResponseDto) => {
|
||||
const notiUrl = row.notiUrl?.trim();
|
||||
if (!notiUrl) return null;
|
||||
|
||||
if (row.docSeComCd === "SEWER") {
|
||||
const parts = notiUrl.split("/");
|
||||
if (parts.length > 1) {
|
||||
parts.pop();
|
||||
return parts.join("/");
|
||||
}
|
||||
}
|
||||
|
||||
return notiUrl;
|
||||
};
|
||||
|
||||
const handleAction = useCallback(
|
||||
async (row: NotiAsUserResponseDto) => {
|
||||
if (row.notReadYn === "N") await readNoti(row.notiId);
|
||||
|
||||
const notiUrl = resolveNotiUrl(row);
|
||||
if (!notiUrl) return;
|
||||
|
||||
router.push(notiUrl);
|
||||
},
|
||||
[readNoti, router],
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
createNotiColumns({
|
||||
pageIndex: pagination.pageIndex,
|
||||
pageSize: pagination.pageSize,
|
||||
isActionPending: isReadPending,
|
||||
onAction: (row) => {
|
||||
void handleAction(row);
|
||||
},
|
||||
}),
|
||||
[pagination.pageIndex, pagination.pageSize, isReadPending, handleAction],
|
||||
);
|
||||
|
||||
const { tableComponent } = useDataTable({
|
||||
query: pageQuery,
|
||||
tableOptions: {
|
||||
columns,
|
||||
state: { pagination },
|
||||
onPaginationChange: (updater) => {
|
||||
const next = typeof updater === "function" ? updater(pagination) : updater;
|
||||
onPaginationChange(next);
|
||||
},
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
},
|
||||
dataTableOptions: {
|
||||
viewPagination: true,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<section className="card card-border mx-0 bg-background pb-8 pt-0">
|
||||
<div className="card-header flex items-center justify-between">
|
||||
<h3 className="text-2xl font-semibold">알림 이력 목록</h3>
|
||||
</div>
|
||||
{tableComponent}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export { NtiHstryList };
|
||||
@@ -1,3 +1,4 @@
|
||||
export { SewerBscPlanDetailGate } from "./ui/sewer-bsc-plan-detail-gate";
|
||||
export { SewerBscPlanEditPage } from "./ui/sewer-bsc-plan-edit-page";
|
||||
export { SewerBscPlanFormPage } from "./ui/sewer-bsc-plan-form-page";
|
||||
export { SewerBscPlanListPage } from "./ui/sewer-bsc-plan-list-page";
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import { useUserStore } from "@/shared/lib";
|
||||
import { useSewerBscPlan } from "@/entities/sewer-bsc-plan";
|
||||
import { canEditPbadmsDoc, PbadmsDetailGate } from "@/widgets/pbadms-sprt";
|
||||
|
||||
import { SewerBscPlanViewPage } from "./sewer-bsc-plan-view-page";
|
||||
|
||||
interface SewerBscPlanDetailGateProps {
|
||||
id: string;
|
||||
propChgCycl?: number;
|
||||
}
|
||||
|
||||
function SewerBscPlanDetailGate({ id, propChgCycl }: SewerBscPlanDetailGateProps) {
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const { data, isLoading, isError } = useSewerBscPlan(id);
|
||||
const shouldRedirectToEdit = canEditPbadmsDoc({
|
||||
docSeComCd: "SEWER",
|
||||
prgrsSttsComCd: data?.detDto?.prgrsSttsComCd,
|
||||
rgtr: data?.detDto?.rgtr,
|
||||
loggedInUserUnqId: userInfo?.userUnqId,
|
||||
});
|
||||
const chgCycl = propChgCycl ?? data?.activeDtlDto?.chgCycl;
|
||||
|
||||
return (
|
||||
<PbadmsDetailGate
|
||||
isPending={!userInfo || isLoading}
|
||||
isError={isError}
|
||||
shouldRedirectToEdit={Boolean(userInfo) && shouldRedirectToEdit}
|
||||
editHref={`/sewer-bsc-plan/${id}/edit?chgCycl=${chgCycl}`}
|
||||
>
|
||||
<SewerBscPlanViewPage id={id} propChgCycl={chgCycl} />
|
||||
</PbadmsDetailGate>
|
||||
);
|
||||
}
|
||||
|
||||
export { SewerBscPlanDetailGate };
|
||||
@@ -5,7 +5,7 @@ import { useRouter } from "next/navigation";
|
||||
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
|
||||
import { useDataTable, useUserStore } from "@/shared/lib";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import {
|
||||
type SewerBscPlanResponseDto,
|
||||
type SewerBscPlanSearchRequestDto,
|
||||
@@ -27,7 +27,6 @@ interface SewerBscPlanListProps {
|
||||
}
|
||||
|
||||
function SewerBscPlanList({ searchParams, pagination, onPaginationChange }: SewerBscPlanListProps) {
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const router = useRouter();
|
||||
const [isApplyDialogOpen, setIsApplyDialogOpen] = useState(false);
|
||||
|
||||
@@ -48,15 +47,7 @@ function SewerBscPlanList({ searchParams, pagination, onPaginationChange }: Sewe
|
||||
const columns = useMemo(() => createSewerBscPlanColumns(), []);
|
||||
|
||||
const handleRowClick = (row: SewerBscPlanResponseDto) => {
|
||||
const canEdit =
|
||||
(row.prgrsSttsComCd === "SV" || row.prgrsSttsComCd === "SL") &&
|
||||
row.rgtr === userInfo?.userUnqId;
|
||||
|
||||
router.push(
|
||||
canEdit
|
||||
? `/sewer-bsc-plan/${row.sewerBscPlanDocNo}/edit?chgCycl=${row.chgCycl}`
|
||||
: `/sewer-bsc-plan/${row.sewerBscPlanDocNo}?chgCycl=${row.chgCycl}`,
|
||||
);
|
||||
router.push(`/sewer-bsc-plan/${row.sewerBscPlanDocNo}?chgCycl=${row.chgCycl}`);
|
||||
};
|
||||
|
||||
const { tableComponent } = useDataTable({
|
||||
|
||||
@@ -40,6 +40,7 @@ export const ENDPOINT = {
|
||||
MENU_ROLES: "/cms-service/api/v1/menu-roles",
|
||||
MENU_AUTHRTS: "/cms-service/api/v1/menu-authrts",
|
||||
USER_ORGANIZATION: "/cms-service/api/v1/user-ognz",
|
||||
NOTI: "/cms-service/api/v1/noti",
|
||||
},
|
||||
STATVERIFY_SERVICE: {
|
||||
ST_ANLS_MST: "/statverify-service/api/v1/st-anls-mst",
|
||||
|
||||
@@ -11,6 +11,7 @@ export {
|
||||
recalculateBizCstTotals,
|
||||
} from "./lib/business-cost-form-utils";
|
||||
export { buildCnstpScaleItems, recalculateBzScaleTotals } from "./lib/business-scale-form-utils";
|
||||
export { canEditPbadmsDoc, type CanEditPbadmsDocParams } from "./lib/can-edit-pbadms-doc";
|
||||
export {
|
||||
buildChecklistTabs,
|
||||
isFormChecklistComplete,
|
||||
@@ -34,6 +35,7 @@ export { BusinessScaleFormSection } from "./ui/business-scale-form-section";
|
||||
export { ChecklistFormSection } from "./ui/checklist-form-section";
|
||||
export { ContactInfoFormSection } from "./ui/contact-info-form-section";
|
||||
export { FnrsSharFormSection } from "./ui/fnrs-shar-form-section";
|
||||
export { PbadmsDetailGate, type PbadmsDetailGateProps } from "./ui/pbadms-detail-gate";
|
||||
export {
|
||||
PbadmsTaskApplyDialog,
|
||||
type PbadmsTaskApplySelection,
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
interface CanEditPbadmsDocParams {
|
||||
docSeComCd: string;
|
||||
prgrsSttsComCd: string | undefined;
|
||||
rgtr: string | undefined;
|
||||
loggedInUserUnqId: string | undefined;
|
||||
}
|
||||
|
||||
function canEditPbadmsDoc({
|
||||
docSeComCd,
|
||||
prgrsSttsComCd,
|
||||
rgtr,
|
||||
loggedInUserUnqId,
|
||||
}: CanEditPbadmsDocParams): boolean {
|
||||
if (!loggedInUserUnqId || !rgtr || rgtr !== loggedInUserUnqId) return false;
|
||||
|
||||
if (docSeComCd === "CNFM") return prgrsSttsComCd === "CV";
|
||||
|
||||
if (docSeComCd === "INSTL" || docSeComCd === "FNRS" || docSeComCd === "SEWER")
|
||||
return prgrsSttsComCd === "SV" || prgrsSttsComCd === "SL";
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export { canEditPbadmsDoc };
|
||||
export type { CanEditPbadmsDocParams };
|
||||
@@ -0,0 +1,59 @@
|
||||
"use client";
|
||||
|
||||
import { type ReactNode, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { Skeleton } from "@common/components";
|
||||
|
||||
interface PbadmsDetailGateProps {
|
||||
isPending: boolean;
|
||||
isError: boolean;
|
||||
shouldRedirectToEdit: boolean;
|
||||
editHref: string;
|
||||
children: ReactNode;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
function DetailSkeleton() {
|
||||
return (
|
||||
<div className="mt-6 flex flex-col gap-4 px-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-[4rem] rounded-xl" />
|
||||
<Skeleton className="h-[16rem] rounded-xl" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-[4rem] rounded-xl" />
|
||||
<Skeleton className="h-[16rem] rounded-xl" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-[4rem] rounded-xl" />
|
||||
<Skeleton className="h-[32rem] rounded-xl" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PbadmsDetailGate({
|
||||
isPending,
|
||||
isError,
|
||||
shouldRedirectToEdit,
|
||||
editHref,
|
||||
children,
|
||||
errorMessage = "상세 정보를 찾을 수 없습니다.",
|
||||
}: PbadmsDetailGateProps) {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldRedirectToEdit) return;
|
||||
router.replace(editHref);
|
||||
}, [editHref, router, shouldRedirectToEdit]);
|
||||
|
||||
if (isPending || shouldRedirectToEdit) return <DetailSkeleton />;
|
||||
|
||||
if (isError) return <div className="mt-6 text-[1.4rem]">{errorMessage}</div>;
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
export { PbadmsDetailGate };
|
||||
export type { PbadmsDetailGateProps };
|
||||
Reference in New Issue
Block a user