diff --git a/apps/as/app/(main)/(with-base-layout)/design-cnstn/[id]/page.tsx b/apps/as/app/(main)/(with-base-layout)/design-cnstn/[id]/page.tsx index 1b0fc983c..f603f8e39 100644 --- a/apps/as/app/(main)/(with-base-layout)/design-cnstn/[id]/page.tsx +++ b/apps/as/app/(main)/(with-base-layout)/design-cnstn/[id]/page.tsx @@ -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 ; + return ; } diff --git a/apps/as/app/(main)/(with-base-layout)/fnrs-cnsltn/[id]/page.tsx b/apps/as/app/(main)/(with-base-layout)/fnrs-cnsltn/[id]/page.tsx index ec9a3911e..0d0df0607 100644 --- a/apps/as/app/(main)/(with-base-layout)/fnrs-cnsltn/[id]/page.tsx +++ b/apps/as/app/(main)/(with-base-layout)/fnrs-cnsltn/[id]/page.tsx @@ -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 ; + return ; } diff --git a/apps/as/app/(main)/(with-base-layout)/instl-sncn/[id]/page.tsx b/apps/as/app/(main)/(with-base-layout)/instl-sncn/[id]/page.tsx index 89cd7a329..f89257bcc 100644 --- a/apps/as/app/(main)/(with-base-layout)/instl-sncn/[id]/page.tsx +++ b/apps/as/app/(main)/(with-base-layout)/instl-sncn/[id]/page.tsx @@ -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 ; + return ; } diff --git a/apps/as/app/(main)/(with-base-layout)/nti-hstry/page.tsx b/apps/as/app/(main)/(with-base-layout)/nti-hstry/page.tsx new file mode 100644 index 000000000..13df22b72 --- /dev/null +++ b/apps/as/app/(main)/(with-base-layout)/nti-hstry/page.tsx @@ -0,0 +1,3 @@ +import { NtiHstryListPage } from "@/pages/nti-hstry"; + +export default NtiHstryListPage; diff --git a/apps/as/app/(main)/(with-base-layout)/sewer-bsc-plan/[id]/page.tsx b/apps/as/app/(main)/(with-base-layout)/sewer-bsc-plan/[id]/page.tsx index 1b864a7c0..dec6502a8 100644 --- a/apps/as/app/(main)/(with-base-layout)/sewer-bsc-plan/[id]/page.tsx +++ b/apps/as/app/(main)/(with-base-layout)/sewer-bsc-plan/[id]/page.tsx @@ -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 ; + return ; } diff --git a/apps/as/docs/spec/nti-hstry/SPEC.md b/apps/as/docs/spec/nti-hstry/SPEC.md new file mode 100644 index 000000000..eb6fb5463 --- /dev/null +++ b/apps/as/docs/spec/nti-hstry/SPEC.md @@ -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` | `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 +``` diff --git a/apps/as/src/entities/noti/api/noti-service.ts b/apps/as/src/entities/noti/api/noti-service.ts new file mode 100644 index 000000000..649d6d17b --- /dev/null +++ b/apps/as/src/entities/noti/api/noti-service.ts @@ -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> { + const response = await client.get>(`${BASE}/as/my/list`, { + params, + }); + return response.data; +} + +async function readNoti(notiId: number): Promise { + const response = await client.put(`${BASE}/${notiId}/read`); + return response.data; +} + +export const notiService = { findMyAsNotiList, readNoti } satisfies NotiService; diff --git a/apps/as/src/entities/noti/api/queries.ts b/apps/as/src/entities/noti/api/queries.ts new file mode 100644 index 000000000..c314f23a7 --- /dev/null +++ b/apps/as/src/entities/noti/api/queries.ts @@ -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 }); + }, + }); +} diff --git a/apps/as/src/entities/noti/index.ts b/apps/as/src/entities/noti/index.ts new file mode 100644 index 000000000..dc57023cb --- /dev/null +++ b/apps/as/src/entities/noti/index.ts @@ -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"; diff --git a/apps/as/src/entities/noti/model/noti-interface.ts b/apps/as/src/entities/noti/model/noti-interface.ts new file mode 100644 index 000000000..d81f5fdca --- /dev/null +++ b/apps/as/src/entities/noti/model/noti-interface.ts @@ -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>; + readNoti(notiId: number): Promise; +} diff --git a/apps/as/src/entities/sewer-bsc-plan/api/sewer-bsc-plan-service.ts b/apps/as/src/entities/sewer-bsc-plan/api/sewer-bsc-plan-service.ts index 62aced93b..c8c0b298b 100644 --- a/apps/as/src/entities/sewer-bsc-plan/api/sewer-bsc-plan-service.ts +++ b/apps/as/src/entities/sewer-bsc-plan/api/sewer-bsc-plan-service.ts @@ -26,8 +26,8 @@ async function findPage( return response.data; } -async function findById(sewerBscPlanDocNo: string): Promise { - const response = await client.get(`${BASE}/${sewerBscPlanDocNo}`); +async function findById(sewerBscPlanDocNo: string): Promise { + const response = await client.get(`${BASE}/${sewerBscPlanDocNo}`); return response.data; } diff --git a/apps/as/src/entities/sewer-bsc-plan/model/sewer-bsc-plan-interface.ts b/apps/as/src/entities/sewer-bsc-plan/model/sewer-bsc-plan-interface.ts index 1ef4abb5c..0bab9d6f0 100644 --- a/apps/as/src/entities/sewer-bsc-plan/model/sewer-bsc-plan-interface.ts +++ b/apps/as/src/entities/sewer-bsc-plan/model/sewer-bsc-plan-interface.ts @@ -207,7 +207,7 @@ export interface SewerBscPlanDetResponseDto { /** Controller public 메서드명·시그니처와 동일 */ export interface SewerBscPlanService { findPage(params: SewerBscPlanSearchRequestDto): Promise>; - findById(sewerBscPlanDocNo: string): Promise; + findById(sewerBscPlanDocNo: string): Promise; save(body: SewerBscPlanSaveRequestDto): Promise; update( sewerBscPlanDocNo: string, diff --git a/apps/as/src/pages/design-cnstn/index.ts b/apps/as/src/pages/design-cnstn/index.ts index fcaaab3ed..8e1323aae 100644 --- a/apps/as/src/pages/design-cnstn/index.ts +++ b/apps/as/src/pages/design-cnstn/index.ts @@ -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"; diff --git a/apps/as/src/pages/design-cnstn/ui/design-detail-gate.tsx b/apps/as/src/pages/design-cnstn/ui/design-detail-gate.tsx new file mode 100644 index 000000000..723ae5f03 --- /dev/null +++ b/apps/as/src/pages/design-cnstn/ui/design-detail-gate.tsx @@ -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 ( + + + + ); +} + +export { DesignDetailGate }; diff --git a/apps/as/src/pages/design-cnstn/ui/design-list.tsx b/apps/as/src/pages/design-cnstn/ui/design-list.tsx index a23f2e9ea..f9652d833 100644 --- a/apps/as/src/pages/design-cnstn/ui/design-list.tsx +++ b/apps/as/src/pages/design-cnstn/ui/design-list.tsx @@ -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({ diff --git a/apps/as/src/pages/fnrs-cnsltn/index.ts b/apps/as/src/pages/fnrs-cnsltn/index.ts index 0193bd7c8..0531c9fb8 100644 --- a/apps/as/src/pages/fnrs-cnsltn/index.ts +++ b/apps/as/src/pages/fnrs-cnsltn/index.ts @@ -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"; diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-detail-gate.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-detail-gate.tsx new file mode 100644 index 000000000..153d491ff --- /dev/null +++ b/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-detail-gate.tsx @@ -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 ( + + + + ); +} + +export { FnrsDetailGate }; diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-list.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-list.tsx index 6f584074d..e09a86b1a 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-list.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-list.tsx @@ -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({ diff --git a/apps/as/src/pages/instl-sncn/index.ts b/apps/as/src/pages/instl-sncn/index.ts index 3957b0c66..0b48c115b 100644 --- a/apps/as/src/pages/instl-sncn/index.ts +++ b/apps/as/src/pages/instl-sncn/index.ts @@ -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"; diff --git a/apps/as/src/pages/instl-sncn/ui/instl-detail-gate.tsx b/apps/as/src/pages/instl-sncn/ui/instl-detail-gate.tsx new file mode 100644 index 000000000..321f92940 --- /dev/null +++ b/apps/as/src/pages/instl-sncn/ui/instl-detail-gate.tsx @@ -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 ( + + + + ); +} + +export { InstlSncnDetailGate }; diff --git a/apps/as/src/pages/instl-sncn/ui/instl-list.tsx b/apps/as/src/pages/instl-sncn/ui/instl-list.tsx index e54b41578..43981e896 100644 --- a/apps/as/src/pages/instl-sncn/ui/instl-list.tsx +++ b/apps/as/src/pages/instl-sncn/ui/instl-list.tsx @@ -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({ diff --git a/apps/as/src/pages/my-pbadms-sprt/lib/resolve-detail-path.ts b/apps/as/src/pages/my-pbadms-sprt/lib/resolve-detail-path.ts index f855d4394..d37d453cb 100644 --- a/apps/as/src/pages/my-pbadms-sprt/lib/resolve-detail-path.ts +++ b/apps/as/src/pages/my-pbadms-sprt/lib/resolve-detail-path.ts @@ -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; diff --git a/apps/as/src/pages/my-pbadms-sprt/ui/my-pbadms-sprt-list.tsx b/apps/as/src/pages/my-pbadms-sprt/ui/my-pbadms-sprt-list.tsx index 29cf221c4..06e64ee55 100644 --- a/apps/as/src/pages/my-pbadms-sprt/ui/my-pbadms-sprt-list.tsx +++ b/apps/as/src/pages/my-pbadms-sprt/ui/my-pbadms-sprt-list.tsx @@ -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); }; diff --git a/apps/as/src/pages/nti-hstry/index.ts b/apps/as/src/pages/nti-hstry/index.ts new file mode 100644 index 000000000..4df9ff6e9 --- /dev/null +++ b/apps/as/src/pages/nti-hstry/index.ts @@ -0,0 +1 @@ +export { NtiHstryListPage } from "./ui/nti-hstry-list-page"; diff --git a/apps/as/src/pages/nti-hstry/lib/resolve-noti-status.ts b/apps/as/src/pages/nti-hstry/lib/resolve-noti-status.ts new file mode 100644 index 000000000..19ff1f3fa --- /dev/null +++ b/apps/as/src/pages/nti-hstry/lib/resolve-noti-status.ts @@ -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; + actionLabel: string; +} + +function daysSince(regDt: string) { + return dayjs().startOf("day").diff(dayjs(regDt).startOf("day"), "day"); +} + +export function resolveNotiStatus( + row: Pick, +): 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: "처리하기", + }; +} diff --git a/apps/as/src/pages/nti-hstry/model/noti-table-columns.tsx b/apps/as/src/pages/nti-hstry/model/noti-table-columns.tsx new file mode 100644 index 000000000..24ddc6eaf --- /dev/null +++ b/apps/as/src/pages/nti-hstry/model/noti-table-columns.tsx @@ -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(); + +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 {status.label}; + }, + }), + columnHelper.display({ + id: "action", + header: "처리", + meta: { align: "center" }, + cell: ({ row }) => { + const status = resolveNotiStatus(row.original); + const label = status?.actionLabel ?? "처리하기"; + return ( +
event.stopPropagation()}> + +
+ ); + }, + }), + ]; +} diff --git a/apps/as/src/pages/nti-hstry/ui/nti-hstry-list-page.tsx b/apps/as/src/pages/nti-hstry/ui/nti-hstry-list-page.tsx new file mode 100644 index 000000000..bffec4476 --- /dev/null +++ b/apps/as/src/pages/nti-hstry/ui/nti-hstry-list-page.tsx @@ -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(defaultStartDate); + const [endDate, setEndDate] = useState(defaultEndDate); + const [docSeComCd, setDocSeComCd] = useState(""); + const [pagination, setPagination] = useState({ + pageIndex: DEFAULT_PAGE_INDEX, + pageSize: DEFAULT_PAGE_SIZE, + }); + const [searchParams, setSearchParams] = useState(buildDefaultSearchParams); + + const { data: docSeCodes, isPending: isDocSeLoading } = useCodeList("ASDOCSE"); + + const handleSearch = (event: React.FormEvent) => { + 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 ( +
+
+
+
+ + setStartDate(date ?? null)} + className="w-[12rem]" + /> + ~ + setEndDate(date ?? null)} + className="w-[12rem]" + /> +
+ +
+ + +
+ + +
+
+ + +
+ ); +} + +export { NtiHstryListPage }; diff --git a/apps/as/src/pages/nti-hstry/ui/nti-hstry-list.tsx b/apps/as/src/pages/nti-hstry/ui/nti-hstry-list.tsx new file mode 100644 index 000000000..be270e2d3 --- /dev/null +++ b/apps/as/src/pages/nti-hstry/ui/nti-hstry-list.tsx @@ -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 ( +
+
+

알림 이력 목록

+
+ {tableComponent} +
+ ); +} + +export { NtiHstryList }; diff --git a/apps/as/src/pages/sewer-bsc-plan/index.ts b/apps/as/src/pages/sewer-bsc-plan/index.ts index c77a4da99..f8329573d 100644 --- a/apps/as/src/pages/sewer-bsc-plan/index.ts +++ b/apps/as/src/pages/sewer-bsc-plan/index.ts @@ -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"; diff --git a/apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-detail-gate.tsx b/apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-detail-gate.tsx new file mode 100644 index 000000000..22e4ffdd5 --- /dev/null +++ b/apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-detail-gate.tsx @@ -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 ( + + + + ); +} + +export { SewerBscPlanDetailGate }; diff --git a/apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-list.tsx b/apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-list.tsx index b2f80834c..f5156c5b7 100644 --- a/apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-list.tsx +++ b/apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-list.tsx @@ -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({ diff --git a/apps/as/src/shared/config/api.ts b/apps/as/src/shared/config/api.ts index b164a88f6..594c51fdd 100644 --- a/apps/as/src/shared/config/api.ts +++ b/apps/as/src/shared/config/api.ts @@ -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", diff --git a/apps/as/src/widgets/pbadms-sprt/index.ts b/apps/as/src/widgets/pbadms-sprt/index.ts index 4746b9097..e891ec930 100644 --- a/apps/as/src/widgets/pbadms-sprt/index.ts +++ b/apps/as/src/widgets/pbadms-sprt/index.ts @@ -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, diff --git a/apps/as/src/widgets/pbadms-sprt/lib/can-edit-pbadms-doc.ts b/apps/as/src/widgets/pbadms-sprt/lib/can-edit-pbadms-doc.ts new file mode 100644 index 000000000..ba626a08c --- /dev/null +++ b/apps/as/src/widgets/pbadms-sprt/lib/can-edit-pbadms-doc.ts @@ -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 }; diff --git a/apps/as/src/widgets/pbadms-sprt/ui/pbadms-detail-gate.tsx b/apps/as/src/widgets/pbadms-sprt/ui/pbadms-detail-gate.tsx new file mode 100644 index 000000000..3d70d21c8 --- /dev/null +++ b/apps/as/src/widgets/pbadms-sprt/ui/pbadms-detail-gate.tsx @@ -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 ( +
+
+ + +
+
+ + +
+
+ + +
+
+ ); +} + +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 ; + + if (isError) return
{errorMessage}
; + + return children; +} + +export { PbadmsDetailGate }; +export type { PbadmsDetailGateProps };