Merge branch 'refs/heads/main' into feature/0612_sentry_fr_merge
# Conflicts: # apps/as/next.config.ts
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
import { DashboardPage } from "@/pages/dashboard/ui/dashboard-page";
|
||||
|
||||
export default DashboardPage;
|
||||
@@ -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 { MyPbadmsSprtListPage } from "@/pages/my-pbadms-sprt";
|
||||
|
||||
export default MyPbadmsSprtListPage;
|
||||
@@ -0,0 +1,3 @@
|
||||
import { NtiHstryListPage } from "@/pages/nti-hstry";
|
||||
|
||||
export default NtiHstryListPage;
|
||||
@@ -1 +0,0 @@
|
||||
export { MainPage as default } from "@/pages/home";
|
||||
@@ -1,17 +1,13 @@
|
||||
import Link from "next/link";
|
||||
import { SewerBscPlanDetailGate } from "@/pages/sewer-bsc-plan";
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
searchParams: Promise<{ chgCycl?: string }>;
|
||||
}
|
||||
|
||||
export default async function Page({ params }: PageProps) {
|
||||
export default async function Page({ params, searchParams }: PageProps) {
|
||||
const { id } = await params;
|
||||
return (
|
||||
<div className="mt-6 px-4 text-[1.4rem]">
|
||||
하수도기본계획 상세({id})는 후속 구현 예정입니다.{" "}
|
||||
<Link className="underline" href={`/sewer-bsc-plan/${id}/edit`}>
|
||||
수정 화면으로 이동
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
const { chgCycl } = await searchParams;
|
||||
const propChgCycl = chgCycl ? Number(chgCycl) : undefined;
|
||||
return <SewerBscPlanDetailGate id={id} propChgCycl={propChgCycl} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { Metadata } from "next";
|
||||
import Image from "next/image";
|
||||
|
||||
import "@/app/styles/globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "디지털 행정지원 시스템",
|
||||
description: "페이지가 존재하지 않습니다. 관리자에게 문의하여 주십시오.",
|
||||
};
|
||||
|
||||
export default function GlobalNotFound() {
|
||||
return (
|
||||
<html lang="ko">
|
||||
<body>
|
||||
<main className="flex min-h-screen flex-col items-center justify-center bg-[#f0f4f8] px-4 py-16">
|
||||
<div className="mb-16 text-center">
|
||||
<h1 className="text-[3.2rem] font-bold leading-snug tracking-tight">
|
||||
<span className="text-primary">디지털 행정지원 시스템</span>에 오신 것을 환영합니다.
|
||||
</h1>
|
||||
<p className="mt-6 text-[2rem] text-[#666]">오류가 발생하였습니다.</p>
|
||||
</div>
|
||||
|
||||
<section className="card card-border flex w-full max-w-[72rem] flex-col items-center !rounded-2xl !py-16">
|
||||
<Image
|
||||
src="/images/magnifying-glass-with-warning.png"
|
||||
alt=""
|
||||
width={280}
|
||||
height={280}
|
||||
className="size-[28rem] object-contain"
|
||||
/>
|
||||
<p className="mt-4 text-[2.4rem] font-bold text-[#444]">페이지가 존재하지 않습니다.</p>
|
||||
<p className="mt-2 text-[1.6rem] text-[#666]">관리자에게 문의하여 주십시오</p>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
# 재원협의·실무협의 프론트엔드 개발 가이드
|
||||
|
||||
> **대상 앱**: `frontend/apps/as`
|
||||
> **작성일**: 2026-08-27
|
||||
> **개정일**: 2026-09-07
|
||||
> **버전**: 2.0
|
||||
> **Process**: PR-AS-003 (재원협의 관리 — 실무협의 및 AI 검증 통합)
|
||||
>
|
||||
> **SoT (2026-09-03 완결)**:
|
||||
>
|
||||
> - 프로세스·상태·버튼: `03.재원협의_실무협의_프로세스_통합_v완결.md`
|
||||
> - REST 설계: `05.재원협의_상태별_endpoint_설계.md`
|
||||
> - 공통코드: `FNRSSTTS` (`SV, ST, LE, FV, ER, SL, AC, TE, TP, RM, RB, RC, RA, AV, RK, RR, EP, TA`)
|
||||
>
|
||||
> **백엔드 서비스**:
|
||||
>
|
||||
> - `as-service` — `/api/v1/fnrs-cnsltn-mng`, `/api/v1/pwrk-cnsltn-mng`, `/api/v1/pwrk-cnsltn-doc-crt-rslt`, `/api/v1/dcrag-to`
|
||||
> - `as-elapr-service` — `/api/v1/elapr-dmnd`, `/api/v1/elapr-prgrs-stts`, `/api/v1/elapr-apln`
|
||||
>
|
||||
> **현재 프론트 상태**: 재원협의(`fnrs-cnsltn`)·전자결재(`elapr`)·DC RAG TO(`dcrag-to`)는 존재한다. 실무협의(`pwrk-cnsltn-`\*) 엔티티·화면은 **미구현**. 버튼 매트릭스·상태코드는 **v2.1 잔존**이며 v3.0(`FNRSSTTS`)과 불일치한다. [9절](#9-현재-코드-vs-to-be-갭) 참고.
|
||||
|
||||
---
|
||||
|
||||
## 1. 문서 목적
|
||||
|
||||
재원협의 4대 주체(기초·광역·환경청·공단)의 **상태 전이·버튼·API**를 `apps/as`에 구현할 때 쓰는 프론트엔드 가이드이다.
|
||||
|
||||
v1.0은 공단 접수자의 **자료검증 → 초안생성 → 웹기안기**만 다뤘다. v2.0은 2026-09-03 완결 프로세스 기준으로 **신청부터 최종종결(**`TA`**)까지**, 실무협의·AI 3파이프라인·결재선·보완 루프를 포함한다.
|
||||
|
||||
상세 표·시퀀스·DTO는 본문에 넣지 않고 레퍼런스로 분리한다. 구현 중에는 본문의 규칙만 보고, 표가 필요하면 해당 파일을 연다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 문서 맵
|
||||
|
||||
| 파일 | 내용 | 언제 보는가 |
|
||||
| -------------------------------------------------------- | ------------------------------------------------------------------------- | ----------------------- |
|
||||
| [development-guide.md](./development-guide.md) (본 문서) | 도메인, 핵심 규칙, 화면 흐름, 갭, 구현 순서 | 일상 구현 |
|
||||
| [ref-process-status.md](./ref-process-status.md) | 4-Lane 시퀀스, Activity, `FNRSSTTS`, 보완 3루프, AI 타임아웃, 결재선 복사 | 상태 전이·폴링 UX |
|
||||
| [ref-screen-buttons.md](./ref-screen-buttons.md) | 38행 화면·역할·버튼 매트릭스, 버튼 가시성 | `button-config.ts` 개편 |
|
||||
| [ref-endpoint.md](./ref-endpoint.md) | 버튼→API 총괄, 단계별 입출력, FE 호출 금지 API, Gap Analysis | 엔티티·service 매핑 |
|
||||
| [ref-frontend-entities.md](./ref-frontend-entities.md) | ENDPOINT, 슬라이스, Service 시그니처, 폴링, 웹기안기, DTO | 엔티티 레이어 구현 |
|
||||
| [ref-webhwp-api.md](./ref-webhwp-api.md) | 웹한글기안기 v3 `HwpCtrl` Properties/Methods, Open/SaveAs | 기안기 위젯 연동 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 도메인 개요
|
||||
|
||||
실무협의서(PWRK)는 재원협의서(FNRS)의 **하위 문서**이다. 재원협의 1건당 실무협의서는 **1건만** 생성된다.
|
||||
|
||||
| 문서 | 번호 예시 | 필드 |
|
||||
| -------- | ----------------- | ----------------------------- |
|
||||
| 재원협의 | `F-20260824-0001` | `fnrsCnsltnDocNo` / `upDocNo` |
|
||||
| 실무협의 | `P-20260827-0001` | `pwrkCnsltnDocNo` |
|
||||
|
||||
진행상태는 **재원협의 마스터** `tb_as_fnrs_cnsltn_mng.prgrs_stts_com_cd` (`FNRSSTTS`)가 SSOT이다. 실무협의 문서의 `prgrsSttsComCd`는 연계 표시용이며, 화면 버튼은 재원협의 상태로 분기한다.
|
||||
|
||||
### 3-1. 4대 주체
|
||||
|
||||
| 역할 | 코드 | 화면 소유 | 핵심 구간 |
|
||||
| --------------- | ------------- | -------------- | ------------------------------------ |
|
||||
| 기초자치단체 | `ROLE_USER05` | 작성/수정/상세 | 신청, 3종 보완, 조치결과, 최종확인 |
|
||||
| 광역자치단체 | `ROLE_USER04` | 상세 | 경유 제출, 결과 전달 |
|
||||
| 유역·지방환경청 | `ROLE_USER03` | 상세 | 의뢰/자체종결, 1·2차 회신 |
|
||||
| 한국환경공단 | `ROLE_USER01` | 상세 + 결재선 | 파일검증, 검토자지정, 1·2차 기술검토 |
|
||||
|
||||
### 3-2. 핵심 식별자
|
||||
|
||||
| 필드 | 의미 | 비고 |
|
||||
| ----------------- | ---------------- | ----------------------------------------------------------- |
|
||||
| `fnrsCnsltnDocNo` | 재원협의문서번호 | 상태 전이·목록 PK |
|
||||
| `pwrkCnsltnDocNo` | 실무협의문서번호 | `P-YYYYMMDD-NNNN`. 검토자지정(`ER→AC`) 시 발급 |
|
||||
| `upDocNo` | 상위문서번호 | 재원협의 번호. `upDocSeComCd = "FNRS"` |
|
||||
| `crtDmndUuid` | 생성요청 UUID | 초안 폴링·웹기안기·다운로드 키 |
|
||||
| `verSn` | 버전일련번호 | 복합 PK `(crtDmndUuid, verSn)`. 저장 시 서버가 `+1` |
|
||||
| `orgnlOtptDocCn` | HWP 바이너리 | 목록/상세 응답은 항상 `null`. 다운로드·`webhwp/open`만 사용 |
|
||||
| `elaprCycl` | 결재 차수 | 1차 `TE`, 2차 `RK`. 금액검증 완료 후 1차 결재선 복사 |
|
||||
|
||||
### 3-3. IIPBS 약어
|
||||
|
||||
| 한글 | 약어 | 필드 예 |
|
||||
| ---- | ------------ | --------------------------- |
|
||||
| 재원 | FNRS | `fnrsCnsltnDocNo` |
|
||||
| 실무 | PWRK | `pwrkCnsltnDocNo` |
|
||||
| 협의 | CNSLTN | `fnrs-cnsltn-mng` |
|
||||
| 생성 | CRT | `crtDmndUuid` |
|
||||
| 요청 | DMND | `crtDmndUuid`, `elapr-dmnd` |
|
||||
| 검증 | VRFC | `vrfcSttsComCd` |
|
||||
| 보완 | SPLMNT | `splmntDmndList` |
|
||||
| 결재 | ELAPR / ATRZ | `elaprCycl`, `atrzSeComCd` |
|
||||
|
||||
---
|
||||
|
||||
## 4. 상태코드 한눈에 보기
|
||||
|
||||
상위코드 `FNRSSTTS`. v1.0의 `PWRKSTTS`(`DV`, `AR`, `TR` 등)는 **폐기**한다.
|
||||
|
||||
정방향:
|
||||
|
||||
```
|
||||
SV → ST → LE → FV → ER → AC → TE → TP → RM → RB → RC → RA → AV → RK → RR → EP → TA
|
||||
```
|
||||
|
||||
자체종결: `LE → EP → TA`
|
||||
보완: `ER|TE|RK → SL` 후 직전 상태로 복귀.
|
||||
|
||||
| 코드 | 명칭 | FE에서 할 일 |
|
||||
| ---- | ---------------------- | ------------------------------------------------- |
|
||||
| `SV` | 저장 | 기초 작성화면 저장/신청/삭제 |
|
||||
| `ST` | 신청완료 | 광역 환경청제출 |
|
||||
| `LE` | 환경청 검토 | 자체종결(`EP`) 또는 기술검토 의뢰(`FV`) |
|
||||
| `FV` | 파일검증 | AI 진행 표시. **30분** 후 `ER` Fallback |
|
||||
| `ER` | 기술검토 의뢰 | 공단 접수자: 파일보완 / 수동파일검증 / 검토자지정 |
|
||||
| `SL` | 보완요청 | 기초 수정화면. 복귀 대상은 이력의 직전 상태 |
|
||||
| `AC` | 초안생성 | AI 진행 표시. **30분** 후 `TE` Fallback |
|
||||
| `TE` | 기술검토 | 웹기안기·검토완료·보완. 결재선 1차 |
|
||||
| `TP` | 기술검토 회신 | 환경청 → 광역 `RM` |
|
||||
| `RM` | 재원협의결과회신 | 광역 → 기초 `RB` |
|
||||
| `RB` | 지자체회신 | 기초 조치결과 저장/협의회신 |
|
||||
| `RC` | 조치결과서 제출 | 광역 → 환경청 `RA` |
|
||||
| `RA` | 조치결과서제출(환경청) | 환경청 2차 의뢰 `AV` |
|
||||
| `AV` | 금액검증 | AI 진행 표시. **30분** 후 `RK` Fallback |
|
||||
| `RK` | 조치결과서검토 | 웹기안기·금액검증 재실행·보완. 결재선 2차 |
|
||||
| `RR` | 조치결과서검토 회신 | 환경청 → 광역 `EP` |
|
||||
| `EP` | 검토결과 회신 | 광역 → 기초 `TA` |
|
||||
| `TA` | 검토결과 회신(종료) | 기초 조회만. **고시(**`TB`**) 아님** |
|
||||
|
||||
코드 의미·구버전 대조·보완 루프·AI Fallback은 [ref-process-status.md](./ref-process-status.md).
|
||||
|
||||
---
|
||||
|
||||
## 5. 프론트가 지켜야 할 핵심 규칙
|
||||
|
||||
1. **상태 SSOT는** `FNRSSTTS`. 버튼 가시성은 `(dtlInstCd, prgrsSttsComCd, atrzSeComCd)`로만 계산한다. 공단은 결재선 구분(`ATRZ01` 접수자, `ATRZ02` 검토자, `ATRZ03` 승인자, `ATRZ99` 최종승인자)을 추가로 본다.
|
||||
2. **AI 콜백 API는 화면에서 호출하지 않는다.** `POST /dcrag/documents/verify-result`, `POST /dcrag/draft/complete`, `POST /dcrag/cost/verify-result`는 서버 전용이다.
|
||||
3. **초안생성 1차 트리거는 검토자지정이다.** `POST /elapr-dmnd` (`ER → AC`). 예전처럼 `POST /pwrk-cnsltn-mng`로 초안을 시작하지 않는다.
|
||||
4. **기술검토 의뢰는** `LE → FV` **전용 API** `POST /fnrs-cnsltn-mng/{docNo}/request-examine`. 현재 코드의 `updatePrgrsStts(ER)` 직행은 폐기.
|
||||
5. **수동 AI 재실행은 상태를 바꾸지 않는다.**
|
||||
|
||||
- `ER` 파일검증 → `POST /dcrag-to/documents/transfer`
|
||||
- `TE` 초안생성 → `POST /dcrag-to/documents/draft/generate` (타임아웃·실패 시에만)
|
||||
- `RK` 금액검증 → `POST /dcrag-to/documents/cost/verify`
|
||||
|
||||
6. **진행상태만 바꾸는 버튼은** `POST /fnrs-cnsltn-mng/doc/updatePrgrsStts`**를 재사용한다.** (`ST→LE`, `RM→RB`, `RC→RA`, `RR→EP`, `EP→TA`)
|
||||
7. **보완요청/보완완료는 공통 API** `reqsupplement` / `submitsupplement`. 복귀 상태(`ER`/`TE`/`RK`)는 서버가 이력에서 결정한다. 프론트는 직전 상태를 추측해 보내지 않는다.
|
||||
8. **결재 승인/회신은** `PUT /elapr-prgrs-stts/{id}/process` **하나**. 1차 최종승인자 → `TP`, 2차 → `RR`은 서버가 Feign으로 처리한다.
|
||||
9. **2차 결재선 복사는 백엔드**. `POST /elapr-apln/copy-line`은 `AV→RK` 콜백에서 호출한다. 프론트는 `RK`에서 2차 결재선을 **조회·표시**만 한다.
|
||||
10. **웹기안기는 `webhwp/open` 응답을 axios `blob`으로 조회한 뒤 `File`로 감싸 `HwpCtrl.Open`에 전달한다.** `blob:` Object URL과 `orgnlOtptDocCn`은 쓰지 않는다.
|
||||
|
||||
---
|
||||
|
||||
## 6. 화면 데이터 흐름
|
||||
|
||||
재원협의 **목록 → 상세/작성**이 본체다. 실무협의 목록은 공단 검토 보조 화면이다.
|
||||
|
||||
```
|
||||
BEGIN
|
||||
CALL fnrsCnsltnMngService.findPage(...)
|
||||
ON 행 CLICK → /fnrs-cnsltn/{fnrsCnsltnDocNo}
|
||||
|
||||
CALL findById(fnrsCnsltnDocNo)
|
||||
COMPUTE buttons = getButtonConfigs(dtlInstCd, prgrsSttsComCd, atrzSeComCdList)
|
||||
|
||||
// ----- 기초 ROLE_USER05 -----
|
||||
ON [저장] (빈값/SV) → saveApply(prgrsSttsComCd: SV)
|
||||
ON [신청완료] (SV) → saveApply(prgrsSttsComCd: ST)
|
||||
ON [삭제] (SV) → remove
|
||||
ON [파일보완완료] (SL←ER) → submitsupplement
|
||||
ON [보완완료] (SL←TE|RK) → submitsupplement
|
||||
ON [저장] (RB) → submit-actn-result { isSubmit: false }
|
||||
ON [협의회신] (RB) → submit-actn-result { isSubmit: true }
|
||||
ON [결과회신] (TA) → API 없음 (조회)
|
||||
|
||||
// ----- 광역 ROLE_USER04 -----
|
||||
ON [환경청제출] (ST) → updatePrgrsStts(LE)
|
||||
ON [지자체회신] (RM) → updatePrgrsStts(RB)
|
||||
ON [조치결과서제출] (RC) → updatePrgrsStts(RA)
|
||||
ON [결과회신] (EP) → updatePrgrsStts(TA)
|
||||
|
||||
// ----- 환경청 ROLE_USER03 -----
|
||||
ON [결과회신] (LE 자체종결) → tech-review → EP
|
||||
ON [기술검토 의뢰] (LE) → request-examine → FV → START POLL FV
|
||||
ON [재원협의 결과 회신] (TP) → tech-review → RM
|
||||
ON [조치결과서 검토요청] (RA) → request-actn-review → AV → START POLL AV
|
||||
ON [결과회신] (RR) → updatePrgrsStts(EP)
|
||||
|
||||
// ----- 공단 ROLE_USER01 -----
|
||||
ON [파일보완] (ER, ATRZ01) → GET file-vrfc-results → 팝업 → reqsupplement
|
||||
ON [파일검증] (ER, ATRZ01) → dcrag-to transfer (상태 유지)
|
||||
ON [검토자지정] (ER, ATRZ01) → POST elapr-dmnd (docPrgrsSttsComCd: AC) → START POLL AC
|
||||
ON [실무협의서관리] (TE|RK) → GET webhwp/open (blob) → HwpCtrl.Open(File)
|
||||
ON [실무협의서생성] (TE, 실패/타임아웃) → dcrag-to draft/generate
|
||||
ON [검토결과조회] (TE) → pwrk-cnsltn-mng findById | findByUpDocNo
|
||||
ON [저장] (TE|RK) → pwrk-cnsltn-mng save-apply
|
||||
ON [검토완료] → elapr-prgrs-stts process (상신)
|
||||
ON [보완요청] (TE|RK) → 보완 팝업 → reqsupplement
|
||||
ON [금액검증] (RK) → dcrag-to cost/verify
|
||||
ON [결재(승인)] → process (중간)
|
||||
ON [결재(회신)] → process (최종 → TP 또는 RR)
|
||||
|
||||
// ----- AI 진행 중 (FV / AC / AV) -----
|
||||
POLL 상태 또는 draft status. 30분 안내. 종료 후 상세 invalidate
|
||||
END
|
||||
```
|
||||
|
||||
38행 버튼 표는 [ref-screen-buttons.md](./ref-screen-buttons.md). API 상세는 [ref-endpoint.md](./ref-endpoint.md).
|
||||
|
||||
---
|
||||
|
||||
## 7. 버튼 → API 축약
|
||||
|
||||
프론트가 치는 것만. 콜백·스케줄러는 제외.
|
||||
|
||||
| 주체 | 버튼 | From → To | Method & URI |
|
||||
| --------- | -------------------------- | ----------------------------- | --------------------------------------------------------------- | --- | ------------------------------------------------ |
|
||||
| 기초 | 저장 / 신청완료 | →`SV` / `SV→ST` | `POST /fnrs-cnsltn-mng/save-apply` |
|
||||
| 기초 | 삭제 | `SV` | `DELETE /fnrs-cnsltn-mng/{docNo}` |
|
||||
| 광역 | 환경청제출 | `ST→LE` | `POST /fnrs-cnsltn-mng/doc/updatePrgrsStts` |
|
||||
| 환경청 | 결과회신(자체종결) | `LE→EP` | `POST /fnrs-cnsltn-mng/{docNo}/tech-review` |
|
||||
| 환경청 | 기술검토 의뢰 | `LE→FV` | **신규** `POST /fnrs-cnsltn-mng/{docNo}/request-examine` |
|
||||
| 공단 접수 | 파일보완 팝업 | `ER` | `GET /fnrs-cnsltn-mng/{docNo}/file-vrfc-results` |
|
||||
| 공단 접수 | 파일보완요청 | `ER→SL` | `POST /fnrs-cnsltn-mng/{docNo}/reqsupplement` |
|
||||
| 공단 접수 | 파일검증 재실행 | `ER` 유지 | `POST /dcrag-to/documents/transfer` |
|
||||
| 공단 접수 | 검토자지정 | `ER→AC` | `POST /elapr-dmnd` |
|
||||
| 공단 검토 | 실무협의서관리 | `TE`/`RK` | `GET .../pwrk-cnsltn-doc-crt-rslt/{uuid}/webhwp/open` |
|
||||
| 공단 검토 | 실무협의서생성 | `TE` 유지 | `POST /dcrag-to/documents/draft/generate` |
|
||||
| 공단 검토 | 저장 | `TE`/`RK` | `POST /pwrk-cnsltn-mng/save-apply` |
|
||||
| 공단 | 검토완료 / 결재(승인·회신) | `TE` 유지 / `TE→TP` / `RK→RR` | `PUT /elapr-prgrs-stts/{id}/process` |
|
||||
| 공단 검토 | 보완요청 | `TE→SL` / `RK→SL` | `POST /fnrs-cnsltn-mng/{docNo}/reqsupplement` |
|
||||
| 기초 | 파일보완완료 / 보완완료 | `SL→ER | TE | RK` | `POST /fnrs-cnsltn-mng/{docNo}/submitsupplement` |
|
||||
| 환경청 | 재원협의 결과 회신 | `TP→RM` | `POST /fnrs-cnsltn-mng/{docNo}/tech-review` |
|
||||
| 광역 | 지자체회신 | `RM→RB` | `updatePrgrsStts` |
|
||||
| 기초 | 저장(조치) / 협의회신 | `RB` / `RB→RC` | **신규** `POST /pwrk-cnsltn-mng/{pwrkDocNo}/submit-actn-result` |
|
||||
| 광역 | 조치결과서제출 | `RC→RA` | `updatePrgrsStts` |
|
||||
| 환경청 | 조치결과서 검토요청 | `RA→AV` | **신규** `POST /fnrs-cnsltn-mng/{docNo}/request-actn-review` |
|
||||
| 공단 검토 | 금액검증 재실행 | `RK` 유지 | `POST /dcrag-to/documents/cost/verify` |
|
||||
| 환경청 | 결과회신 | `RR→EP` | `updatePrgrsStts` |
|
||||
| 광역 | 결과회신 | `EP→TA` | `updatePrgrsStts` |
|
||||
| 웹기안기 | 저장 | 버전 +1 | `POST /pwrk-cnsltn-doc-crt-rslt` multipart |
|
||||
| 공통 | 다운로드 | — | `GET /pwrk-cnsltn-doc-crt-rslt/{uuid}/download` |
|
||||
|
||||
신규 4개: `request-examine`, `submit-actn-result`, `request-actn-review`, (백엔드) `elapr-apln/copy-line`.
|
||||
|
||||
---
|
||||
@@ -0,0 +1,336 @@
|
||||
# 레퍼런스 — 상태별 Endpoint
|
||||
|
||||
> 상위: [개발가이드.md](./개발가이드.md)
|
||||
> SoT: `05.재원협의_상태별_endpoint_설계.md` (v1.0, 2026-09-03)
|
||||
> Gateway: `/as-service`, `/as-elapr-service`
|
||||
> Auth: `Authorization`, `X-Site-Id`, `token-id` (기존 `client` 인터셉터)
|
||||
> 날짜: `yyyy-MM-dd HH:mm:ss`
|
||||
|
||||
프론트가 치는 API만 상세히 적는다. AI 콜백·스케줄러·Feign은 [§6 호출 금지](#6-프론트-호출-금지).
|
||||
|
||||
---
|
||||
|
||||
## 1. 총괄 매트릭스 (38 + WebHWP)
|
||||
|
||||
| 순번 | 버튼 | From → To | 서비스 | Method & URI | 구분 |
|
||||
| :---: | --- | --- | --- | --- | --- |
|
||||
| 1 | 저장 | →`SV` | as | `POST /api/v1/fnrs-cnsltn-mng/save-apply` | 기존 |
|
||||
| 2 | 신청완료 | `SV→ST` | as | 동일 (`prgrsSttsComCd=ST`) | 기존 |
|
||||
| 3 | 삭제 | `SV` | as | `DELETE /api/v1/fnrs-cnsltn-mng/{docNo}` | 기존 |
|
||||
| 4 | 환경청제출 | `ST→LE` | as | `POST /api/v1/fnrs-cnsltn-mng/doc/updatePrgrsStts` | 기존 |
|
||||
| 5 | 결과회신(자체종결) | `LE→EP` | as | `POST /api/v1/fnrs-cnsltn-mng/{docNo}/tech-review` | 기존 |
|
||||
| 6 | 기술검토 의뢰 | `LE→FV` | as | `POST /api/v1/fnrs-cnsltn-mng/{docNo}/request-examine` | **신규** |
|
||||
| 7 | 파일검증 콜백 | `FV→ER` | as | `POST /api/v1/dcrag/documents/verify-result` | 서버 |
|
||||
| 8 | 파일보완 팝업 | `ER` | as | `GET /api/v1/fnrs-cnsltn-mng/{docNo}/file-vrfc-results` | 기존 |
|
||||
| 9 | 파일보완요청 | `ER→SL` | as | `POST /api/v1/fnrs-cnsltn-mng/{docNo}/reqsupplement` | 기존 |
|
||||
| 10 | 파일검증 재실행 | `ER` | as | `POST /api/v1/dcrag-to/documents/transfer` | 기존 |
|
||||
| 11 | 검토자지정 | `ER→AC` | elapr | `POST /api/v1/elapr-dmnd` | 기존 |
|
||||
| 12 | 초안 콜백 | `AC→TE` | as | `POST /api/v1/dcrag/draft/complete` | 서버 |
|
||||
| 13 | 실무협의서관리 | `TE` | as | `GET /api/v1/pwrk-cnsltn-mng/updocno/{upDocNo}` | 기존 |
|
||||
| 14 | 검토결과조회 | `TE` | as | `GET /api/v1/pwrk-cnsltn-mng/{pwrkDocNo}` | 기존 |
|
||||
| 15 | 실무협의서생성 | `TE` | as | `POST /api/v1/dcrag-to/documents/draft/generate` | 기존 |
|
||||
| 16 | 저장(실무) | `TE` | as | `POST /api/v1/pwrk-cnsltn-mng/save-apply` | 기존 |
|
||||
| 17 | 검토완료 | `TE` | elapr | `PUT /api/v1/elapr-prgrs-stts/{id}/process` | 기존 |
|
||||
| 18 | 보완요청 | `TE→SL` | as | `POST .../reqsupplement` | 기존 |
|
||||
| 19 | 결재(승인) | `TE` | elapr | `PUT .../process` | 기존 |
|
||||
| 20 | 결재(회신) | `TE→TP` | elapr | `PUT .../process` | 기존 |
|
||||
| 21 | 파일보완완료 | `SL→ER` | as | `POST .../submitsupplement` | 기존 |
|
||||
| 22 | 보완완료(기술) | `SL→TE` | as | 동일 | 기존 |
|
||||
| 23 | 보완완료(조치) | `SL→RK` | as | 동일 | 기존 |
|
||||
| 24 | 재원협의 결과 회신 | `TP→RM` | as | `POST .../{docNo}/tech-review` | 기존 |
|
||||
| 25 | 지자체회신 | `RM→RB` | as | `POST .../doc/updatePrgrsStts` | 기존 |
|
||||
| 26 | 저장(조치결과) | `RB` | as | `POST /api/v1/pwrk-cnsltn-mng/{pwrkDocNo}/submit-actn-result` | **신규** |
|
||||
| 27 | 협의회신 | `RB→RC` | as | 동일 (`isSubmit=true`) | **신규** |
|
||||
| 28 | 조치결과서제출 | `RC→RA` | as | `updatePrgrsStts` | 기존 |
|
||||
| 29 | 조치결과서 검토요청 | `RA→AV` | as | `POST /api/v1/fnrs-cnsltn-mng/{docNo}/request-actn-review` | **신규** |
|
||||
| 30 | 금액검증 콜백 | `AV→RK` | as | `POST /api/v1/dcrag/cost/verify-result` | 서버 |
|
||||
| 31 | 실무협의서관리 | `RK` | as | `GET /pwrk-cnsltn-mng/{pwrkDocNo}` | 기존 |
|
||||
| 32 | 금액검증 재실행 | `RK` | as | `POST /api/v1/dcrag-to/documents/cost/verify` | 기존 |
|
||||
| 33 | 보완요청(금액) | `RK→SL` | as | `reqsupplement` | 기존 |
|
||||
| 34 | 결재(승인) | `RK` | elapr | `process` | 기존 |
|
||||
| 35 | 결재(회신) | `RK→RR` | elapr | `process` | 기존 |
|
||||
| 36 | 결과회신 | `RR→EP` | as | `updatePrgrsStts` | 기존 |
|
||||
| 37 | 결과회신 | `EP→TA` | as | `updatePrgrsStts` | 기존 |
|
||||
| 38 | 결과회신(확인) | `TA` | — | 없음 | — |
|
||||
| W1 | 웹기안기 Open | — | as | `GET /api/v1/pwrk-cnsltn-doc-crt-rslt/{uuid}/webhwp/open` | 기존 |
|
||||
| W2 | HWPX 저장 | — | as | `POST /api/v1/pwrk-cnsltn-doc-crt-rslt` multipart | 기존 |
|
||||
| W3 | 다운로드 | — | as | `GET /api/v1/pwrk-cnsltn-doc-crt-rslt/{uuid}/download` | 기존 |
|
||||
| E-N | 결재선 복사 | `AV→RK` 부수 | elapr | `POST /api/v1/elapr-apln/copy-line` | **신규·서버** |
|
||||
|
||||
조회 공통: `GET /fnrs-cnsltn-mng`, `GET /fnrs-cnsltn-mng/{docNo}`, `GET /pwrk-cnsltn-mng`, `GET /dcrag-to/documents/draft/status/{crtDmndUuid}`.
|
||||
|
||||
---
|
||||
|
||||
## 2. 공통 요청
|
||||
|
||||
### `DocPrgrsUpdateRequestDto`
|
||||
|
||||
`POST /fnrs-cnsltn-mng/doc/updatePrgrsStts` — `ST→LE`, `RM→RB`, `RC→RA`, `RR→EP`, `EP→TA`.
|
||||
|
||||
```json
|
||||
{
|
||||
"docNo": "F-20260824-0001",
|
||||
"docSeComCd": "FNRS",
|
||||
"prgrsSttsComCd": "LE",
|
||||
"rgtrUuid": "optional-uuid",
|
||||
"rmrk": "optional"
|
||||
}
|
||||
```
|
||||
|
||||
응답: `204/200` void. 프론트 엔티티: 기존 `changePrgrsSttsComCd`.
|
||||
|
||||
### `FnrsCnsltnApplySaveRequestDto`
|
||||
|
||||
`save-apply`, `tech-review`, `reqsupplement`, `submitsupplement`가 공유. 핵심은 `fnrsCnsltnMng.prgrsSttsComCd`와 중첩 리스트(`bzScaleList`, `bizCstList`, `cklstResultList`, `esntlAtchFileList`, `splmntDmndList`).
|
||||
|
||||
---
|
||||
|
||||
## 3. 단계별 FE API
|
||||
|
||||
### 3-1. `SV → ST → LE`
|
||||
|
||||
**저장 / 신청완료** — `POST /fnrs-cnsltn-mng/save-apply`
|
||||
권한: `ROLE_USER05`. 임시저장은 `prgrsSttsComCd=SV`, 신청은 `ST`.
|
||||
응답: `string` (문서번호). 기존 `fnrsCnsltnMngService.saveApply`.
|
||||
|
||||
**삭제** — `DELETE /fnrs-cnsltn-mng/{docNo}` (`SV`만).
|
||||
|
||||
**환경청제출** — `updatePrgrsStts` `LE`. 권한: `ROLE_USER04`.
|
||||
|
||||
### 3-2. `LE → FV | EP`
|
||||
|
||||
**자체종결** — `POST /fnrs-cnsltn-mng/{docNo}/tech-review`
|
||||
Body: `FnrsCnsltnApplySaveRequestDto` (환경청 의견·체크리스트). `LE→EP`.
|
||||
기존 `techReview`. 현재 화면은 `TP`에서도 이 API를 쓰며 목표 상태를 `EP`로 넣고 있다 → **`TP`는 `RM`으로 바꿔야 함**.
|
||||
|
||||
**기술검토 의뢰 (신규)** — `POST /fnrs-cnsltn-mng/{docNo}/request-examine`
|
||||
|
||||
```json
|
||||
{ "prgrsSttsComCd": "FV" }
|
||||
```
|
||||
|
||||
응답: `string` (docNo). `LE→FV` + AI 파일검증 + 30분 타이머(서버).
|
||||
프론트: `fnrsCnsltnMngService.requestExamine(docNo, body)` 추가. **`updatePrgrsStts("ER")` 금지.**
|
||||
|
||||
### 3-3. `FV → ER → SL → ER`
|
||||
|
||||
파일검증 콜백은 서버. FE는 `FV`에서 상세 refetch.
|
||||
|
||||
**검증결과 목록** — `GET /fnrs-cnsltn-mng/{fnrsCnsltnDocNo}/file-vrfc-results`
|
||||
응답: `FnrsCnsltnFileVrfcRsltResponseDto[]`
|
||||
|
||||
| 필드 | 설명 |
|
||||
| --- | --- |
|
||||
| `fileSeComCd` | `ESSENTIAL` / `ADDITIONAL` |
|
||||
| `fileSeNm` | 구분명 |
|
||||
| `esntlAtchFileId` | 필수첨부 ID |
|
||||
| `atchFileNm` | 파일명 |
|
||||
| `vrfcRsltComCd` | `PASS` / `FAIL` / `WARN` |
|
||||
| `vrfcOpnn` | 검증의견 |
|
||||
|
||||
엔티티에 `findFileVrfcResults` 추가. 첨부 행 배지는 상세 `esntlFileList`/`etcFileList`의 `vrfcSttsComCd`, `vrfcRsltCn`, `vrfcDt`도 사용 (타입 확장 필요).
|
||||
|
||||
**파일보완요청** — `POST .../{docNo}/reqsupplement` (`ER→SL`).
|
||||
**파일보완완료** — `POST .../{docNo}/submitsupplement` (`SL→ER`). 복귀는 서버.
|
||||
|
||||
**파일검증 재실행** — `POST /dcrag-to/documents/transfer`
|
||||
|
||||
```json
|
||||
{ "fnrsCnsltnDocNo": "F-20260824-0001", "taskSeComCd": "SF", "taskSeCdNm": "하수처리시설" }
|
||||
```
|
||||
|
||||
응답: `{ resultCode, message, requestId }`. 상태 유지. 기존 `dcRagApiToService.requestDocumentTransfer`.
|
||||
|
||||
### 3-4. `ER → AC → TE → TP`
|
||||
|
||||
**검토자지정** — `POST /elapr-dmnd`
|
||||
기존 `elaprDmndService.save`.
|
||||
|
||||
| 필드 | 값 |
|
||||
| --- | --- |
|
||||
| `elaprDocNo` | 재원협의 번호 |
|
||||
| `elaprCycl` | `1` |
|
||||
| `docPrgrsSttsComCd` | **`AC`** (현행은 현재 상태 `ER`를 넣고 있음 → 수정) |
|
||||
| `elaprPrgrsSttsList[].atrzOrdr` | 1 검토자, 2 승인자, 3 최종 |
|
||||
|
||||
서버가 실무협의 문서 생성 + RAG 초안 트리거 + 30분 타이머. 응답의 문서번호/`crtDmndUuid`가 있으면 폴링 키로 쓴다. 없으면 `findByUpDocNo`로 복원.
|
||||
|
||||
**초안 상태 폴링** — `GET /dcrag-to/documents/draft/status/{crtDmndUuid}`
|
||||
기존 `getDraftGenerationStatus`. 주기 5~10초. 종료 `COMPLETED`/`FAILED`.
|
||||
|
||||
**수동 초안생성** — `POST /dcrag-to/documents/draft/generate` (`TE` 유지)
|
||||
|
||||
```json
|
||||
{
|
||||
"fnrsCnsltnDocNo": "F-...",
|
||||
"pwrkCnsltnDocNo": "P-...",
|
||||
"crtDmndUuid": "optional"
|
||||
}
|
||||
```
|
||||
|
||||
202 + `DcRagDraftGenerateResponseDto` (`crtDmndUuid`, `crtSttsComCd`, `estimatedMinutes`).
|
||||
v1.0 가이드의 “이 API를 프론트가 치면 안 된다”는 **폐기**. 1차 트리거는 검토자지정이고, 이 API는 Fallback 전용.
|
||||
|
||||
**실무 저장** — `POST /pwrk-cnsltn-mng/save-apply` → `string`.
|
||||
**보완요청/완료** — fnrs `reqsupplement` / `submitsupplement`.
|
||||
|
||||
**결재** — `PUT /elapr-prgrs-stts/{elaprPrgrsId}/process`
|
||||
|
||||
```json
|
||||
{
|
||||
"atrzPrgrsSttsComCd": "ATRZ_PRGRS_003",
|
||||
"atrzOpnnCn": "의견",
|
||||
"finalApprovalYn": false
|
||||
}
|
||||
```
|
||||
|
||||
반려: `ATRZ_PRGRS_004`. 기존 `elaprPrgrsSttsService.process`. 최종 시 서버가 `TP`/`RR` 전이.
|
||||
|
||||
### 3-5. `TP → RM → RB → RC`
|
||||
|
||||
**환경청 1차 회신** — `tech-review` (`TP→RM`).
|
||||
**광역 지자체회신** — `updatePrgrsStts` `RB`.
|
||||
|
||||
**조치결과 저장/제출 (신규)** — `POST /pwrk-cnsltn-mng/{pwrkDocNo}/submit-actn-result`
|
||||
|
||||
```json
|
||||
{
|
||||
"isSubmit": false,
|
||||
"actnList": [
|
||||
{
|
||||
"rvwOpnnSn": 1,
|
||||
"actnRsltCn": "수용 — 관로 연장 재산정",
|
||||
"atchFileGroupId": "optional"
|
||||
}
|
||||
],
|
||||
"totActnSummary": "제출 시 필수",
|
||||
"muniDocNo": "제출 시 필수"
|
||||
}
|
||||
```
|
||||
|
||||
- `isSubmit: false` → `RB` 유지
|
||||
- `isSubmit: true` → `RB→RC`
|
||||
응답: `string` (pwrkDocNo). DTO명 `PwrkActnResultSubmitDto`.
|
||||
|
||||
### 3-6. `RC → RA → AV → RK`
|
||||
|
||||
**광역 제출** — `updatePrgrsStts` `RA`.
|
||||
|
||||
**2차 의뢰 (신규)** — `POST /fnrs-cnsltn-mng/{docNo}/request-actn-review`
|
||||
|
||||
```json
|
||||
{
|
||||
"envoDocNo": "환경청 공문번호",
|
||||
"specialRqstCn": "중점 검증 요청 (선택)"
|
||||
}
|
||||
```
|
||||
|
||||
`RA→AV`. 응답: `string` (pwrkDocNo). DTO명 `EnvoActnReviewReqDto`.
|
||||
|
||||
금액검증 콜백은 서버. 콜백 후 서버가 `copy-line` 호출.
|
||||
|
||||
**금액검증 재실행** — `POST /dcrag-to/documents/cost/verify`
|
||||
|
||||
```json
|
||||
{
|
||||
"fnrsCnsltnDocNo": "F-...",
|
||||
"pwrkCnsltnDocNo": "P-..."
|
||||
}
|
||||
```
|
||||
|
||||
202. 기존 `requestCostVerification`. `RK` 유지.
|
||||
|
||||
### 3-7. `RK → RR | SL`
|
||||
|
||||
보완은 3-4와 동일 API. 팝업만 금액 체크.
|
||||
결재 `process` 2차 최종 → 서버가 `RR`.
|
||||
|
||||
### 3-8. `RR → EP → TA`
|
||||
|
||||
둘 다 `updatePrgrsStts`. `TA` 확인은 API 없음.
|
||||
|
||||
---
|
||||
|
||||
## 4. 웹기안기
|
||||
|
||||
설계서 기준 **기존**. 구현 시 컨트롤러 존재 여부를 확인한다.
|
||||
|
||||
| ID | 용도 | URI | 응답 |
|
||||
| --- | --- | --- | --- |
|
||||
| W1 | 웹기안기 Open | `GET .../pwrk-cnsltn-doc-crt-rslt/{crtDmndUuid}/webhwp/open` | `inline` HWPX blob → `HwpCtrl.Open(File)` |
|
||||
| W2 | 편집 저장 | `POST .../pwrk-cnsltn-doc-crt-rslt` multipart `file` + `crtDmndUuid` | 201, `verSn+1` |
|
||||
| W3 | PC 저장 | `GET .../{crtDmndUuid}/download` | `attachment` blob |
|
||||
|
||||
버전 지정: `GET .../{crtDmndUuid}/{verSn}/webhwp/open`, `.../{verSn}/download`.
|
||||
`/{uuid}/latest`, `/{uuid}/download`가 `/{uuid}/{verSn}`보다 우선. path를 섞지 말 것.
|
||||
|
||||
다운로드:
|
||||
|
||||
- axios `responseType: "blob"`
|
||||
- 파일명: `Content-Disposition` `filename*=UTF-8''...` (`parseFilenameFromResponse`)
|
||||
- 서버 파일명: `재원협의 실무협의서({bizNm})_v{verSn}.hwp`
|
||||
- Content-Type은 `application/x-hwp` 또는 `application/haansofthwpx`일 수 있음. 타입에 의존하지 말 것
|
||||
|
||||
Open은 axios `responseType: "blob"`으로 인증된 API를 호출하고, 응답 Blob을 HWPX `File`로 감싸 `HwpCtrl.Open`에 전달한다. `blob:` Object URL은 사용하지 않는다.
|
||||
|
||||
multipart `Content-Type` 헤더를 수동 지정하지 않는다 (boundary). JSON `saveJson`은 UI에서 쓰지 않는다.
|
||||
|
||||
---
|
||||
|
||||
## 5. 전자결재 (`as-elapr-service`)
|
||||
|
||||
| API | URI | FE |
|
||||
| --- | --- | --- |
|
||||
| 검토자지정·1차 결재요청 | `POST /elapr-dmnd` | O |
|
||||
| 결재 처리 | `PUT /elapr-prgrs-stts/{id}/process` | O |
|
||||
| 결재선 복사 | `POST /elapr-apln/copy-line` | **X (서버)** |
|
||||
|
||||
`copy-line` body (참고만):
|
||||
|
||||
```json
|
||||
{
|
||||
"docNo": "P-20260827-0001",
|
||||
"sourceElaprCycl": 1,
|
||||
"targetElaprCycl": 2,
|
||||
"rgtrUuid": "system-uuid"
|
||||
}
|
||||
```
|
||||
|
||||
`RK`에서 2차 결재선은 기존 `findDocPrgrsByDocNo` / `elaprCycl=2` 조회로 표시.
|
||||
|
||||
---
|
||||
|
||||
## 6. 프론트 호출 금지
|
||||
|
||||
| URI | 이유 |
|
||||
| --- | --- |
|
||||
| `POST /dcrag/documents/verify-result` | AI→AS 콜백 |
|
||||
| `POST /dcrag/draft/complete` | AI→AS 콜백 |
|
||||
| `POST /dcrag/cost/verify-result` | AI→AS 콜백. 여기서 copy-line도 호출 |
|
||||
| `POST /elapr-apln/copy-line` | 콜백 Feign |
|
||||
| `POST /pwrk-cnsltn-mng` (`save` 초안 트리거) | v3.0 1차 트리거는 `elapr-dmnd`. 컨트롤러가 남아 있어도 화면에서 쓰지 않음 |
|
||||
|
||||
`POST /dcrag-to/documents/draft/generate`는 **Fallback에서만** 허용.
|
||||
|
||||
---
|
||||
|
||||
## 7. Gap Analysis (프론트 엔티티)
|
||||
|
||||
### 이미 있는 것
|
||||
|
||||
`fnrsCnsltnMngService`: `findPage`, `findById`, `save`, `saveApply`, `update`, `techReview`, `submitSupplement`, `submitReqSupplement`, `remove`, `changePrgrsSttsComCd`
|
||||
`dcRagApiToService`: `requestDocumentTransfer`, `requestDraftGeneration`, `getDraftGenerationStatus`, `requestCostVerification`
|
||||
`elaprDmndService.save`, `elaprPrgrsSttsService.process`
|
||||
|
||||
### 추가할 것
|
||||
|
||||
| 메서드 | URI |
|
||||
| --- | --- |
|
||||
| `requestExamine` | `POST /fnrs-cnsltn-mng/{id}/request-examine` |
|
||||
| `findFileVrfcResults` | `GET /fnrs-cnsltn-mng/{id}/file-vrfc-results` |
|
||||
| `requestActnReview` | `POST /fnrs-cnsltn-mng/{id}/request-actn-review` |
|
||||
| `pwrkCnsltnMngService.*` | 슬라이스 신규 |
|
||||
| `submitActnResult` | `POST /pwrk-cnsltn-mng/{id}/submit-actn-result` |
|
||||
| `pwrkCnsltnDocCrtRsltService.*` | 슬라이스 신규 |
|
||||
|
||||
`copyLine`은 프론트 서비스에 넣지 않는다.
|
||||
@@ -0,0 +1,347 @@
|
||||
# 레퍼런스 — 프론트 엔티티·DTO
|
||||
|
||||
> 상위: [개발가이드.md](./개발가이드.md)
|
||||
> 엔티티 규칙: `frontend/.cursor/skills/backend-controller-to-entity`
|
||||
|
||||
---
|
||||
|
||||
## 1. ENDPOINT
|
||||
|
||||
`apps/as/src/shared/config/api.ts`
|
||||
|
||||
```ts
|
||||
AS_SERVICE: {
|
||||
// 기존
|
||||
FNRS_CNSLTN_MNG: "/as-service/api/v1/fnrs-cnsltn-mng",
|
||||
DCRAG_TO: "/as-service/api/v1/dcrag-to",
|
||||
// 추가
|
||||
PWRK_CNSLTN_MNG: "/as-service/api/v1/pwrk-cnsltn-mng",
|
||||
PWRK_CNSLTN_DOC_CRT_RSLT: "/as-service/api/v1/pwrk-cnsltn-doc-crt-rslt",
|
||||
}
|
||||
|
||||
AS_ELAPR_SERVICE: {
|
||||
ELAPR_TMPLT: "/as-elapr-service/api/v1/elapr-tmplt",
|
||||
ELAPR_APLN: "/as-elapr-service/api/v1/elapr-apln",
|
||||
ELAPR_DMND: "/as-elapr-service/api/v1/elapr-dmnd",
|
||||
ELAPR_PRGRS_STTS: "/as-elapr-service/api/v1/elapr-prgrs-stts",
|
||||
}
|
||||
```
|
||||
|
||||
`copy-line`은 `ELAPR_APLN` 하위이나 FE에서 호출하지 않음.
|
||||
|
||||
---
|
||||
|
||||
## 2. 슬라이스 배치
|
||||
|
||||
```
|
||||
entities/fnrs-cnsltn/ # 확장
|
||||
entities/dcrag-to/ # 구현됨
|
||||
entities/elapr/ # 구현됨
|
||||
entities/pwrk-cnsltn-mng/ # 신규
|
||||
entities/pwrk-cnsltn-doc-crt-rslt/ # 신규
|
||||
```
|
||||
|
||||
```
|
||||
entities/pwrk-cnsltn-mng/
|
||||
├── model/
|
||||
│ ├── pwrk-cnsltn-mng-interface.ts
|
||||
│ └── pwrk-cnsltn-mng-payload-schema.ts
|
||||
├── api/
|
||||
│ ├── pwrk-cnsltn-mng-service.ts
|
||||
│ └── queries.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
`submit-actn-result` Request는 zod SSOT (`PwrkActnResultSubmitDto`).
|
||||
`request-examine` / `request-actn-review`도 fnrs payload-schema에 추가.
|
||||
|
||||
라우트:
|
||||
|
||||
| 화면 | 경로 |
|
||||
| --- | --- |
|
||||
| 재원협의 목록/상세/작성 | 기존 `app/(main)/(with-base-layout)/fnrs-cnsltn/...` |
|
||||
| 실무협의 목록 | `.../pwrk-cnsltn/page.tsx` (선택) |
|
||||
| 실무협의 상세 | `.../pwrk-cnsltn/[id]/page.tsx` (선택) |
|
||||
| 페이지 UI | `pages/fnrs-cnsltn/`, `pages/pwrk-cnsltn/` |
|
||||
|
||||
---
|
||||
|
||||
## 3. Service 시그니처
|
||||
|
||||
`fnrsCnsltnMngService`에 추가:
|
||||
|
||||
```ts
|
||||
requestExamine(docNo: string, body: FnrsExamineRequestDto): Promise<string>;
|
||||
findFileVrfcResults(fnrsCnsltnDocNo: string): Promise<FnrsCnsltnFileVrfcRsltResponseDto[]>;
|
||||
requestActnReview(docNo: string, body: EnvoActnReviewReqDto): Promise<string>;
|
||||
```
|
||||
|
||||
실무협의:
|
||||
|
||||
```ts
|
||||
export interface PwrkCnsltnMngService {
|
||||
findPage(searchDto: PwrkCnsltnMngSearchDto): Promise<PageResponse<PwrkCnsltnMngResponseDto>>;
|
||||
findById(pwrkCnsltnDocNo: string): Promise<PwrkCnsltnMngDetResponseDto>;
|
||||
findByUpDocNo(upDocNo: string): Promise<PwrkCnsltnMngDetResponseDto>;
|
||||
saveApply(body: PwrkCnsltnApplySaveRequestDto): Promise<string>;
|
||||
submitActnResult(
|
||||
pwrkCnsltnDocNo: string,
|
||||
body: PwrkActnResultSubmitDto,
|
||||
): Promise<string>;
|
||||
update(pwrkCnsltnDocNo: string, body: PwrkCnsltnMngUpdateRequestDto): Promise<string>;
|
||||
remove(pwrkCnsltnDocNo: string): Promise<void>;
|
||||
changePrgrsSttsComCd(body: DocPrgrsUpdateRequestDto): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
v1.0의 `save(): PwrkCnsltnMngSaveResponseDto`(초안 트리거)는 화면에서 사용하지 않는다. 컨트롤러가 남아 있으면 엔티티에 넣더라도 UI 연결 금지.
|
||||
|
||||
생성결과:
|
||||
|
||||
```ts
|
||||
export interface PwrkCnsltnDocCrtRsltService {
|
||||
findPage(
|
||||
searchDto: PwrkCnsltnDocCrtRsltSearchDto,
|
||||
): Promise<PageResponse<PwrkCnsltnDocCrtRsltResponseDto>>;
|
||||
findById(crtDmndUuid: string, verSn: number): Promise<PwrkCnsltnDocCrtRsltResponseDto>;
|
||||
findLatestByCrtDmndUuid(crtDmndUuid: string): Promise<PwrkCnsltnDocCrtRsltResponseDto>;
|
||||
openWebHwpLatest(crtDmndUuid: string): Promise<AxiosResponse<Blob>>;
|
||||
download(crtDmndUuid: string, verSn: number): Promise<AxiosResponse<Blob>>;
|
||||
downloadLatest(crtDmndUuid: string): Promise<AxiosResponse<Blob>>;
|
||||
saveMultipart(formData: FormData): Promise<PwrkCnsltnDocCrtRsltResponseDto>;
|
||||
updateMultipart(
|
||||
crtDmndUuid: string,
|
||||
verSn: number,
|
||||
formData: FormData,
|
||||
): Promise<PwrkCnsltnDocCrtRsltResponseDto>;
|
||||
remove(crtDmndUuid: string, verSn: number): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
`webhwp/open`은 인증된 axios 요청으로 Blob을 조회한다:
|
||||
|
||||
```ts
|
||||
async function openWebHwpLatest(crtDmndUuid: string) {
|
||||
return client.get<Blob>(`${BASE}/${crtDmndUuid}/webhwp/open`, {
|
||||
responseType: "blob",
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
응답 Blob은 HWPX `File`로 감싸 `HwpCtrl.Open(file, "HWPX", ...)`에 전달한다. `blob:` Object URL은 사용하지 않는다.
|
||||
|
||||
DC RAG TO (구현됨, 시그니처 유지):
|
||||
|
||||
```ts
|
||||
requestDocumentTransfer(body: DcRagDocumentTransferRequestDto): Promise<DcRagCommonResponseDto>;
|
||||
requestDraftGeneration(body: DcRagDraftGenerateRequestDto): Promise<DcRagDraftGenerateResponseDto>;
|
||||
getDraftGenerationStatus(crtDmndUuid: string): Promise<DcRagDraftStatusResponseDto>;
|
||||
requestCostVerification(body: DcRagCostVerifyRequestDto): Promise<DcRagCommonResponseDto>;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. DTO
|
||||
|
||||
타입명 = 백엔드 클래스명. Save/Apply는 zod.
|
||||
|
||||
### 4-1. 재원협의 추가
|
||||
|
||||
**`FnrsExamineRequestDto`**
|
||||
|
||||
| 필드 | 필수 | 값 |
|
||||
| --- | --- | --- |
|
||||
| `prgrsSttsComCd` | N | 기본 `FV` |
|
||||
|
||||
**`EnvoActnReviewReqDto`**
|
||||
|
||||
| 필드 | 필수 |
|
||||
| --- | --- |
|
||||
| `envoDocNo` | Y |
|
||||
| `specialRqstCn` | N |
|
||||
|
||||
**`FnrsCnsltnFileVrfcRsltResponseDto`**: [ref-endpoint.md](./ref-endpoint.md) §3-3.
|
||||
|
||||
첨부 검증 배지용으로 `AtflMngClientResponseDto` / `EsntlAtchFileMngTreeResponseDto`에 `vrfcDt`, `vrfcRsltCn`, `vrfcSttsComCd` 확장.
|
||||
|
||||
### 4-2. `PwrkActnResultSubmitDto` (zod)
|
||||
|
||||
| 필드 | 필수 | 비고 |
|
||||
| --- | --- | --- |
|
||||
| `isSubmit` | Y | `false` 저장, `true` 협의회신 |
|
||||
| `actnList` | Y | |
|
||||
| `actnList[].rvwOpnnSn` | Y | |
|
||||
| `actnList[].actnRsltCn` | Y | |
|
||||
| `actnList[].atchFileGroupId` | N | |
|
||||
| `totActnSummary` | 제출 시 | |
|
||||
| `muniDocNo` | 제출 시 | |
|
||||
|
||||
### 4-3. `PwrkCnsltnMng*`
|
||||
|
||||
**Search**: `pwrkCnsltnDocNo`, `upDocNo`, `upDocSeComCd`, `prgrsSttsComCd`, `admdstCd`, `envoSeComCd`, `taskSeComCd`, `bizNm`
|
||||
|
||||
**DetResponse 중첩**
|
||||
|
||||
- `cnsltnList`: 협의내역 (`rvwOpnnSn`, `rvwOpnnClsfComCd`, `cnsltnCn`, `rfltYn`, …)
|
||||
- `pjtcoAjmtList`: 사업비조정
|
||||
- `crtDmndList[].crtRslt`: 생성요청 + 최신 결과
|
||||
- `splmntDmndList`: 보완요청
|
||||
- `sttsChgHstryList`: 상태변경이력 — `SL` 복귀 라벨에 사용
|
||||
- `trsmFileList`: 전송파일 (Java Det DTO 확인 후 반영)
|
||||
|
||||
마스터 조인: `bizNm`, `admdstCd`, `envoSeComCd`, `taskSeComCd`, `aplcntNm` 등.
|
||||
|
||||
`crtRslt.orgnlOtptDocCn`은 항상 `null`.
|
||||
|
||||
**상세 예**
|
||||
|
||||
```json
|
||||
{
|
||||
"pwrkCnsltnDocNo": "P-20260827-0001",
|
||||
"upDocNo": "F-20260824-0001",
|
||||
"upDocSeComCd": "FNRS",
|
||||
"prgrsSttsComCd": "TE",
|
||||
"cnsltnList": [],
|
||||
"pjtcoAjmtList": [],
|
||||
"crtDmndList": [
|
||||
{
|
||||
"crtDmndUuid": "d0545bef-735f-4d7b-a781-88e96e03577a",
|
||||
"pwrkCnsltnDocNo": "P-20260827-0001",
|
||||
"rqstrUuid": "...",
|
||||
"rqstrNm": "관리자",
|
||||
"crtSttsComCd": "C",
|
||||
"crtRslt": {
|
||||
"crtDmndUuid": "d0545bef-735f-4d7b-a781-88e96e03577a",
|
||||
"verSn": 1,
|
||||
"rgtrNm": "관리자",
|
||||
"orgnlOtptDocCn": null,
|
||||
"crtCmptnDt": "2026-08-27 10:47:01"
|
||||
}
|
||||
}
|
||||
],
|
||||
"splmntDmndList": [],
|
||||
"sttsChgHstryList": []
|
||||
}
|
||||
```
|
||||
|
||||
### 4-4. `PwrkCnsltnDocCrtRslt*`
|
||||
|
||||
복합 PK: `crtDmndUuid` + `verSn`
|
||||
|
||||
**Response**
|
||||
|
||||
| 필드 | 타입 | 비고 |
|
||||
| --- | --- | --- |
|
||||
| `crtDmndUuid` | `string` | |
|
||||
| `verSn` | `number` | |
|
||||
| `rgtrUuid` / `rgtrNm` | `string` | |
|
||||
| `orgnlOtptDocCn` | `null` | 목록/상세 미포함 |
|
||||
| `crtCmptnDt` | `string` | |
|
||||
|
||||
**Search** (`PageableParams`): `crtDmndUuid`(이력 조회 시 필수), `pwrkCnsltnDocNo`, `verSn`, `rgtrUuid`
|
||||
|
||||
```http
|
||||
GET /as-service/api/v1/pwrk-cnsltn-doc-crt-rslt?page=0&size=10&crtDmndUuid={uuid}&sort=verSn,desc
|
||||
```
|
||||
|
||||
**Save (zod)**: `crtDmndUuid` 필수. `rgtrUuid` 생략 시 서버 로그인 사용자. `file`은 multipart part (DTO 필드 아님).
|
||||
|
||||
### 4-5. DC RAG (구현됨)
|
||||
|
||||
```ts
|
||||
interface DcRagDocumentTransferRequestDto {
|
||||
fnrsCnsltnDocNo: string;
|
||||
taskSeComCd?: string;
|
||||
taskSeCdNm?: string;
|
||||
}
|
||||
|
||||
interface DcRagDraftStatusResponseDto {
|
||||
crtDmndUuid: string;
|
||||
status: DraftStatus;
|
||||
statusName?: string;
|
||||
progressPercent?: number;
|
||||
events?: EventInfoDto[];
|
||||
startedDt?: string;
|
||||
estimatedCompletionDt?: string;
|
||||
errorMessage?: string | null;
|
||||
}
|
||||
|
||||
type DraftStatus =
|
||||
| "REQUESTED"
|
||||
| "DOWNLOADING"
|
||||
| "ANALYZING"
|
||||
| "GENERATING"
|
||||
| "COMPLETED"
|
||||
| "FAILED";
|
||||
```
|
||||
|
||||
`estimatedMinutes`는 초안 응답에 있을 수 있으나 UX 안내는 **30분 Fallback**을 따른다.
|
||||
|
||||
---
|
||||
|
||||
## 5. 폴링 훅
|
||||
|
||||
초안 (`AC` 또는 `TE`이면서 생성 중):
|
||||
|
||||
```ts
|
||||
useQuery({
|
||||
queryKey: [...dcragToKeys.draftStatus(crtDmndUuid)],
|
||||
queryFn: () => dcRagApiToService.getDraftGenerationStatus(crtDmndUuid),
|
||||
enabled: Boolean(crtDmndUuid) && isPolling,
|
||||
refetchInterval: (query) => {
|
||||
const status = query.state.data?.status;
|
||||
if (status === "COMPLETED" || status === "FAILED") return false;
|
||||
return 5_000;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
`COMPLETED` 시 `fnrs-cnsltn` 상세·`pwrk-cnsltn-mng` `findByUpDocNo` invalidate.
|
||||
|
||||
`FV`/`AV`는 `findById`를 5~10초 refetch. `prgrsSttsComCd`가 `ER`/`RK`가 되면 중단. 시작 시각 기준 30분 경과 안내 문구.
|
||||
|
||||
`crtDmndUuid` 복원: `findByUpDocNo(fnrsCnsltnDocNo).crtDmndList[0]`.
|
||||
|
||||
---
|
||||
|
||||
## 6. 다운로드
|
||||
|
||||
```ts
|
||||
const response = await client.get<Blob>(url, { responseType: "blob" });
|
||||
const filename = parseFilenameFromResponse(response);
|
||||
const objectUrl = window.URL.createObjectURL(response.data);
|
||||
```
|
||||
|
||||
기존 `attachService`와 동일.
|
||||
|
||||
---
|
||||
|
||||
## 7. 실무협의 목록 컬럼 (선택 화면)
|
||||
|
||||
| 표시명 | Field |
|
||||
| --- | --- |
|
||||
| 실무협의번호 | `pwrkCnsltnDocNo` |
|
||||
| 재원협의번호 | `upDocNo` |
|
||||
| 진행상태 | `prgrsSttsComCdNm` |
|
||||
| 사업명 | `bizNm` |
|
||||
| 시도/구군 | `sido` / `gugun` |
|
||||
| 신청자 | `aplcntNm` |
|
||||
| 등록일 | `regDt` |
|
||||
|
||||
행 클릭 → `/pwrk-cnsltn/{pwrkCnsltnDocNo}` 또는 재원협의 상세로 이동 (화면설계 확정 후).
|
||||
|
||||
---
|
||||
|
||||
## 8. `elapr-dmnd` 저장 시 주의
|
||||
|
||||
현행 `floating-action-buttons.tsx`:
|
||||
|
||||
```ts
|
||||
elaprCycl: data.detDto?.prgrsSttsComCd === "RK" ? 2 : 1,
|
||||
docPrgrsSttsComCd: data.detDto?.prgrsSttsComCd, // ER
|
||||
```
|
||||
|
||||
To-Be:
|
||||
|
||||
- 검토자지정은 **`ER`에서만**. `elaprCycl: 1`, `docPrgrsSttsComCd: "AC"`
|
||||
- 2차 결재선은 서버 `copy-line`. `RK`에서 검토자지정 팝업을 다시 열지 않음
|
||||
@@ -0,0 +1,246 @@
|
||||
# 레퍼런스 — 프로세스·상태 전이
|
||||
|
||||
> 상위: [개발가이드.md](./개발가이드.md)
|
||||
> SoT: `03.재원협의_실무협의_프로세스_통합_v완결.md` (v3.0, 2026-09-03)
|
||||
|
||||
---
|
||||
|
||||
## 1. 프로세스 개요
|
||||
|
||||
| 항목 | 내용 |
|
||||
| --- | --- |
|
||||
| Process No. | PR-AS-003 |
|
||||
| 메뉴 | 행정지원 > 2.2.2 재원협의 > 2.2.2.1 재원협의 관리 |
|
||||
| 목적 | 기초 → 광역(경유) → 환경청(승인) → 공단(기술검토/AI) 4자 협의로 사업규모·국비·분담금을 확정 |
|
||||
|
||||
2-Pass:
|
||||
|
||||
- **Pass 1** 기술 타당성: `SV→ST→LE→FV→ER→AC→TE→TP→RM→RB`
|
||||
- **Pass 2** 조치결과·금액: `RB→RC→RA→AV→RK→RR→EP→TA`
|
||||
|
||||
자체종결(기술검토 생략): `LE→EP→TA`
|
||||
|
||||
---
|
||||
|
||||
## 2. 4-Lane 시퀀스
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
actor 기초 as 기초 (ROLE_USER05)
|
||||
actor 광역 as 광역 (ROLE_USER04)
|
||||
actor 환경청 as 환경청 (ROLE_USER03)
|
||||
actor 공단 as 공단 (ROLE_USER01)
|
||||
actor AI as AI 엔진
|
||||
|
||||
rect rgb(240, 248, 255)
|
||||
기초->>기초: [저장] SV
|
||||
기초->>광역: [신청완료] ST
|
||||
광역->>환경청: [환경청제출] LE
|
||||
end
|
||||
|
||||
rect rgb(255, 250, 240)
|
||||
alt 자체종결
|
||||
환경청->>광역: [결과회신] LE → EP
|
||||
광역->>기초: [결과회신] EP → TA
|
||||
else 기술검토 의뢰
|
||||
환경청->>AI: [기술검토 의뢰] LE → FV
|
||||
end
|
||||
end
|
||||
|
||||
rect rgb(245, 255, 245)
|
||||
AI-->>공단: 파일검증 콜백 FV → ER
|
||||
Note over 공단: 30분 미응답 시 ER + 수동 [파일검증]
|
||||
alt 파일 미비
|
||||
공단->>기초: [파일보완] ER → SL
|
||||
기초->>공단: [파일보완완료] SL → ER
|
||||
else 적격
|
||||
공단->>AI: [검토자지정] ER → AC
|
||||
end
|
||||
end
|
||||
|
||||
rect rgb(255, 245, 245)
|
||||
AI-->>공단: 초안 콜백 AC → TE
|
||||
Note over 공단: 30분 미응답 시 TE + 수동 [실무협의서생성]
|
||||
alt 설계 미비
|
||||
공단->>기초: [보완요청] TE → SL
|
||||
기초->>공단: [보완완료] SL → TE
|
||||
else 검토 완료
|
||||
공단->>공단: [검토완료] 상신, [결재(승인)] TE 유지
|
||||
공단->>환경청: [결재(회신)] TE → TP
|
||||
end
|
||||
end
|
||||
|
||||
rect rgb(250, 240, 255)
|
||||
환경청->>광역: [재원협의 결과 회신] TP → RM
|
||||
광역->>기초: [지자체회신] RM → RB
|
||||
기초->>광역: [협의회신] RB → RC
|
||||
end
|
||||
|
||||
rect rgb(240, 255, 255)
|
||||
광역->>환경청: [조치결과서제출] RC → RA
|
||||
환경청->>AI: [조치결과서 검토요청] RA → AV
|
||||
AI-->>공단: 금액검증 콜백 AV → RK
|
||||
Note over 공단: 30분 미응답 시 RK + 수동 [금액검증]
|
||||
Note over 공단: AV→RK 시 1차 결재선 → 2차 복사 (서버)
|
||||
end
|
||||
|
||||
rect rgb(255, 255, 230)
|
||||
alt 소명 미비
|
||||
공단->>기초: [보완요청] RK → SL
|
||||
기초->>공단: [보완완료] SL → RK
|
||||
else 확정
|
||||
공단->>환경청: [결재(회신)] RK → RR
|
||||
end
|
||||
end
|
||||
|
||||
rect rgb(255, 240, 240)
|
||||
환경청->>광역: [결과회신] RR → EP
|
||||
광역->>기초: [결과회신] EP → TA
|
||||
end
|
||||
```
|
||||
|
||||
거버넌스:
|
||||
|
||||
```
|
||||
기초 (USER05) --ST--> 광역 (USER04) --LE--> 환경청 (USER03)
|
||||
기초 <-----TA------ 광역 <-----EP/RM------ 환경청
|
||||
환경청 --FV/ER--> 공단 (USER01) --TP/RR--> 환경청
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Activity 정의 (14)
|
||||
|
||||
| No. | Activity | 주체 | From → To | FE 포인트 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1 | 재원협의 조회 | 기초 | — | 기본계획·자가점검 대조 |
|
||||
| 2 | 등록 및 제출 | 기초·광역 | `SV→ST`, 광역 `LE` | `save-apply`, `updatePrgrsStts` |
|
||||
| 3 | 접수 및 의뢰 | 환경청 | `LE→FV` / `EP` | 의뢰 vs 자체종결 분기 |
|
||||
| 4 | AI 파일검증 | 공단·AI | `FV→ER` / `SL` | 30분 Fallback, 파일보완 팝업 |
|
||||
| 5 | 자체종결 회신 | 환경청·광역 | `LE→EP→TA` | 공단 레인 없음 |
|
||||
| 6 | 검토자지정·초안 | 공단·AI | `ER→AC→TE` | `elapr-dmnd`, 초안 폴링 |
|
||||
| 7 | 1차 기술검토 | 공단 결재선 | `TE` / `SL` | 웹기안기, HWP 파싱은 서버 |
|
||||
| 8 | 기술검토 회신 | 공단 최종 | `TE→TP` | `process` 최종 |
|
||||
| 9 | 검토결과 회신 | 환경청·광역 | `TP→RM→RB` | 1차 회신. `EP`와 혼동 금지 |
|
||||
| 10 | 조치결과 작성 | 기초 | `RB→RC` | `submit-actn-result` |
|
||||
| 11 | 조치 제출·의뢰 | 광역·환경청 | `RC→RA→AV` | `request-actn-review` |
|
||||
| 12 | AI 금액검증 | AI·공단 | `AV→RK` | 결재선 복사(서버) |
|
||||
| 13 | 2차 검토·결재 | 공단 | `RK→RR` / `SL` | 2차 결재선 |
|
||||
| 14 | 최종 회신 | 환경청·광역·기초 | `RR→EP→TA` | 종결 |
|
||||
|
||||
---
|
||||
|
||||
## 4. `FNRSSTTS` 상세
|
||||
|
||||
관리: `tb_cs_lwpo_cd` / `tb_as_fnrs_cnsltn_mng.prgrs_stts_com_cd`
|
||||
|
||||
| 코드 | 국문 | 영문 | 화면 주체 | 전이 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `SV` | 저장 | Save | 기초 작성 | 신규/`SV` → `SV` |
|
||||
| `ST` | 신청완료 | Submit to Province | 기초→광역 | `SV→ST` |
|
||||
| `LE` | 환경청 검토 | Local Environment Review | 광역→환경청 | `ST→LE` |
|
||||
| `FV` | 파일검증 | File Verification | AI | `LE→FV` |
|
||||
| `ER` | 기술검토 의뢰 | Examine Request | 공단 접수자 | `FV→ER`, `SL→ER` |
|
||||
| `SL` | 보완요청 | Supplement Loop | 공단→기초 | `ER\|TE\|RK → SL` |
|
||||
| `AC` | 초안생성 | AI Auto Creation | AI | `ER→AC` |
|
||||
| `TE` | 기술검토 | Technical Examine | 공단 1차 결재 | `AC→TE`, `SL→TE` |
|
||||
| `TP` | 기술검토 회신 | Technical Pass to Basin | 공단→환경청 | `TE→TP` |
|
||||
| `RM` | 재원협의결과회신 | Result Message to Province | 환경청→광역 | `TP→RM` |
|
||||
| `RB` | 재원협의결과회신(지자체) | Result Back to Municipality | 광역→기초 | `RM→RB` |
|
||||
| `RC` | 조치결과서 제출 | Result Complete | 기초→광역 | `RB→RC` |
|
||||
| `RA` | 조치결과서 제출(환경청) | Result Ask to Basin | 광역→환경청 | `RC→RA` |
|
||||
| `AV` | 금액검증 | Amount Verification | AI | `RA→AV` |
|
||||
| `RK` | 조치결과서검토 | Result Check | 공단 2차 결재 | `AV→RK`, `SL→RK` |
|
||||
| `RR` | 조치결과서검토 회신 | Result Return to Basin | 공단→환경청 | `RK→RR` |
|
||||
| `EP` | 검토결과 회신 | Examine Pass to Province | 환경청→광역 | `LE→EP`(자체), `RR→EP`(최종) |
|
||||
| `TA` | 검토결과 회신(종료) | Technical Approval | 광역→기초 | `EP→TA` |
|
||||
|
||||
### 4-1. v2.1 → v3.0 대조 (프론트 폐기 대상)
|
||||
|
||||
| 구간 | v2.1 | v3.0 |
|
||||
| --- | --- | --- |
|
||||
| 의뢰 직후 파일검증 | `ER` | **`FV` → `ER`** |
|
||||
| 검토자지정 후 초안 | `EE` → `DV` | **`ER` → `AC` → `TE`** |
|
||||
| 1차 내부 결재 | `TR` → `TP` | **`TE` 유지 → `TP`** |
|
||||
| 환경청→광역 1차 회신 | `EP` | **`RM`** |
|
||||
| 금액검증 | `RA` | **`RA` → `AV` → `RK`** |
|
||||
| 공단 2차 회신 | `TP` | **`RR`** |
|
||||
| 최종 완료 | `TP` / 고시 `TB` | **`TA`** (고시 없음) |
|
||||
|
||||
가이드 v1.0 `PWRKSTTS`(`DV`, `AR`, `TR`, `AA` 등)도 동일하게 폐기.
|
||||
|
||||
---
|
||||
|
||||
## 5. 보완(`SL`) 3루프
|
||||
|
||||
복귀 상태는 `tb_as_doc_stts_chg_hstry` 직전 코드. 프론트는 복귀 코드를 요청에 넣지 않고, 라벨만 이력으로 고른다.
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
ER --> SL: 파일보완 팝업(파일리스트)
|
||||
SL --> ER: 파일보완완료
|
||||
TE --> SL: 보완 검토팝업()
|
||||
SL --> TE: 보완완료
|
||||
RK --> SL: 보완 검토팝업(금액)
|
||||
SL --> RK: 보완완료
|
||||
```
|
||||
|
||||
| 루프 | 팝업 | 요청 | 제출 버튼 |
|
||||
| --- | --- | --- | --- |
|
||||
| 파일 무결성 | 파일리스트 + 검증결과 | 공단 접수자 `ER` | [파일보완완료] |
|
||||
| 1차 기술 | 설계 미달·사유 | 공단 검토자 `TE` | [보완완료] |
|
||||
| 2차 금액 | 공종별 금액 체크 | 공단 검토자 `RK` | [보완완료] |
|
||||
|
||||
---
|
||||
|
||||
## 6. AI 3파이프라인·30분 Fallback
|
||||
|
||||
| ID | 단계 | 진입 → 완료 | 프론트 진입 | Fallback | 수동 재실행 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| AI-1 | 파일 무결성 | `FV→ER` | 환경청 [기술검토 의뢰] | 30분 → `ER` + [파일검증] | `POST /dcrag-to/documents/transfer` |
|
||||
| AI-2 | 초안 생성 | `AC→TE` | 공단 [검토자지정] | 30분 → `TE` + [실무협의서생성] | `POST /dcrag-to/documents/draft/generate` |
|
||||
| AI-3 | 금액 검증 | `AV→RK` | 환경청 [조치결과서 검토요청] | 30분 → `RK` + [금액검증] | `POST /dcrag-to/documents/cost/verify` |
|
||||
|
||||
- 예상 소요 안내는 **30분** (v1.0의 10분은 폐기).
|
||||
- 콜백 수신·스케줄러 전이는 서버. FE는 상세 refetch / draft status 폴링만.
|
||||
- `AC→TE` 성공 시에만 [실무협의서관리]·[검토결과조회] 활성. 실패·타임아웃이면 [실무협의서생성].
|
||||
|
||||
초안 폴링 이벤트 예:
|
||||
|
||||
| `eventType` | `eventMessage` |
|
||||
| --- | --- |
|
||||
| `REQUESTED` | 요청을 접수했습니다. |
|
||||
| `DOWNLOADING` | 신청서를 수신하는 중입니다. |
|
||||
| `ANALYZING` | 자료 준비가 완료되었습니다. |
|
||||
| `GENERATING` | 문서 저장이 완료되었습니다. |
|
||||
| `COMPLETED` | 문서 생성이 완료되었습니다. |
|
||||
| `FAILED` | `errorMessage` 표시 |
|
||||
|
||||
종료: `status === "COMPLETED" | "FAILED"` 또는 `events`에 해당 `eventType`.
|
||||
|
||||
---
|
||||
|
||||
## 7. 결재선 복사
|
||||
|
||||
규칙: `"금액검증후: 2차결재선 1차결재선 복사생성"`
|
||||
|
||||
1. `TE`에서 1차 결재선(`elaprCycl = 1`) 완료.
|
||||
2. `AV→RK` 콜백 시 서버가 `POST /elapr-apln/copy-line` (`source=1`, `target=2`).
|
||||
3. `RK` 화면은 2차 결재선을 조회해 검토자/승인자/최종승인자를 표시.
|
||||
4. 프론트는 copy-line을 **호출하지 않는다.**
|
||||
|
||||
결재 `process`:
|
||||
|
||||
- 상신·중간 승인: 문서 상태 `TE` 또는 `RK` 유지.
|
||||
- 1차 최종: 서버가 `TE→TP`.
|
||||
- 2차 최종: 서버가 `RK→RR`.
|
||||
- 반려: 직전 결재자 복귀. 문서 상태 유지.
|
||||
|
||||
---
|
||||
|
||||
## 8. 웹기안기
|
||||
|
||||
- `TE`/`RK` [실무협의서관리] → `webhwp/open` Blob 조회 후 팝업에서 `HwpCtrl.Open(File)`.
|
||||
- [검토완료] 시 HWP 파싱·DB 등재는 **서버**. FE는 결재 `process`만.
|
||||
- 편집본 저장: `POST /pwrk-cnsltn-doc-crt-rslt` multipart. `verSn`은 클라이언트가 보내지 않음.
|
||||
@@ -0,0 +1,173 @@
|
||||
# 레퍼런스 — 화면·버튼 매트릭스
|
||||
|
||||
> 상위: [개발가이드.md](./개발가이드.md)
|
||||
> SoT: `03` §4 화면 및 버튼별 상세 상태전이 명세서 (Row 4~42, 38행)
|
||||
|
||||
구현 위치: `pages/fnrs-cnsltn/lib/button-config.ts` + `ui/components/floating-action-buttons.tsx`
|
||||
|
||||
---
|
||||
|
||||
## 1. 화면 구분
|
||||
|
||||
| 화면 | 소유 | 라우트 (현행) | 비고 |
|
||||
| --- | --- | --- | --- |
|
||||
| 작성화면 | 기초 `ROLE_USER05` | `/fnrs-cnsltn/new`, `/fnrs-cnsltn/{id}/edit` | `SV`, `RB` |
|
||||
| 수정화면 | 기초 | `/fnrs-cnsltn/{id}/edit` | `SL` 보완 |
|
||||
| 상세화면 | 광역·환경청·공단·기초(TA) | `/fnrs-cnsltn/{id}` | 조회 + 액션 버튼 |
|
||||
| 자동화 | AI | 버튼 없음 | `FV`/`AC`/`AV` 진행 패널 |
|
||||
|
||||
공단 상세는 결재선 구분으로 같은 상태라도 버튼이 다르다.
|
||||
|
||||
| `atrzSeComCd` | 역할 |
|
||||
| --- | --- |
|
||||
| `ATRZ01` | 접수자 |
|
||||
| `ATRZ02` | 검토자 |
|
||||
| `ATRZ03` | 승인자 |
|
||||
| `ATRZ99` | 최종승인자 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 38행 매트릭스
|
||||
|
||||
API 컬럼은 [ref-endpoint.md](./ref-endpoint.md)와 동일.
|
||||
|
||||
| 순번 | 화면 | 권한 | 결재선 | 상태 | 버튼 | 전이 | 다음 담당 | FE 동작 |
|
||||
| :---: | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| 1 | 작성 | USER05 | - | 빈값, `SV` | 저장 | `SV` | 본인 | `save-apply` `SV` |
|
||||
| 2 | 작성 | USER05 | - | `SV` | 신청완료 | `ST` | 광역 | `save-apply` `ST` |
|
||||
| 3 | 작성 | USER05 | - | `SV` | 삭제 | 삭제 | — | `DELETE` |
|
||||
| 4 | 상세 | USER04 | - | `ST` | 환경청제출 | `LE` | 환경청 | `updatePrgrsStts` |
|
||||
| 5 | 상세 | USER03 | - | `LE` | 결과회신 | `EP` | 광역 | `tech-review` 자체종결 |
|
||||
| 6 | 상세 | USER03 | - | `LE` | 기술검토 의뢰 | `FV` | 공단 접수 | **`request-examine`** |
|
||||
| 7 | 자동화 | AI | - | `FV` | (파일검증) | `ER` | 공단 접수 | FE 호출 없음. 진행 패널 |
|
||||
| 8 | 상세 | USER01 | 접수자 | `ER` | 파일보완 | `SL` | 기초 | 팝업(파일리스트) → `reqsupplement` |
|
||||
| 9 | 상세 | USER01 | 접수자 | `ER` | 파일검증 | `ER` | 본인 | `dcrag-to/transfer` 재실행 |
|
||||
| 10 | 상세 | USER01 | 접수자 | `ER` | 검토자지정 | `AC` | 다음 결재자 | `POST /elapr-dmnd` |
|
||||
| 11 | 자동화 | AI | - | `AC` | (초안생성) | `TE` | 공단 검토 | FE 호출 없음. 폴링 |
|
||||
| 12 | 상세 | USER01 | 검토자 | `TE` | 실무협의서관리 | 유지 | — | 웹기안기 Open. **초안 성공 시만** |
|
||||
| 13 | 상세 | USER01 | 검토자 | `TE` | 검토결과조회 | 유지 | — | 실무협의 상세 |
|
||||
| 14 | 상세 | USER01 | 검토자 | `TE` | 실무협의서생성 | 유지 | — | **실패/타임아웃/초안 없음** 시 |
|
||||
| 15 | 상세 | USER01 | 검토자 | `TE` | 저장 | `TE` | — | `pwrk save-apply` |
|
||||
| 16 | 상세 | USER01 | 검토자 | `TE` | 검토완료 | `TE` | 다음 결재자 | `elapr process` 상신. 반려 시 직전 결재자 |
|
||||
| 17 | 상세 | USER01 | 검토자 | `TE` | 보완요청 | `SL` | 기초 | 보완 팝업() → `reqsupplement` |
|
||||
| 18 | 상세 | USER01 | 승인자 | `TE` | 결재(승인) | `TE` | 다음 결재자 | `process` |
|
||||
| 19 | 상세 | USER01 | 최종승인자 | `TE` | 결재(회신) | `TP` | 환경청 | `process` 최종 |
|
||||
| 20 | 수정 | USER05 | - | `SL` | 파일보완완료 | `ER` | 공단 접수 | `submitsupplement` (직전=`ER`) |
|
||||
| 21 | 수정 | USER05 | - | `SL` | 보완완료 | `TE` | 공단 검토 | `submitsupplement` (직전=`TE`) |
|
||||
| 22 | 수정 | USER05 | - | `SL` | 보완완료 | `RK` | 공단 검토 | `submitsupplement` (직전=`RK`) |
|
||||
| 23 | 상세 | USER03 | - | `TP` | 재원협의 결과 회신 | `RM` | 광역 | `tech-review` |
|
||||
| 24 | 상세 | USER04 | - | `RM` | 지자체회신 | `RB` | 기초 | `updatePrgrsStts` |
|
||||
| 25 | 작성 | USER05 | - | `RB` | 저장 | `RB` | 본인 | **`submit-actn-result` `isSubmit:false`** |
|
||||
| 26 | 작성 | USER05 | - | `RB` | 협의회신 | `RC` | 광역 | **`submit-actn-result` `isSubmit:true`** |
|
||||
| 27 | 상세 | USER04 | - | `RC` | 조치결과서제출 | `RA` | 환경청 | `updatePrgrsStts` |
|
||||
| 28 | 상세 | USER03 | - | `RA` | 조치결과서 검토요청 | `AV` | 공단 검토 | **`request-actn-review`** |
|
||||
| 29 | 자동화 | AI | - | `AV` | (금액검증) | `RK` | 공단 검토 | FE 호출 없음. 진행 패널 |
|
||||
| 30 | 상세 | USER01 | 검토자 | `RK` | 실무협의서관리 | 유지 | — | 웹기안기 |
|
||||
| 31 | 상세 | USER01 | 검토자 | `RK` | 금액검증 | `RK` | — | `dcrag-to/cost/verify` |
|
||||
| 32 | 상세 | USER01 | 검토자 | `RK` | 저장 | `RK` | — | `pwrk save-apply` |
|
||||
| 33 | 상세 | USER01 | 검토자 | `RK` | 검토완료 | `RK` | 다음 결재자 | `process` |
|
||||
| 34 | 상세 | USER01 | 승인자 | `RK` | 결재(승인) | `RK` | 다음 결재자 | `process` |
|
||||
| 35 | 상세 | USER01 | 최종승인자 | `RK` | 결재(회신) | `RR` | 환경청 | `process` 최종 |
|
||||
| 36 | 상세 | USER03 | - | `RR` | 결과회신 | `EP` | 광역 | `updatePrgrsStts` |
|
||||
| 37 | 작성/상세 | USER04 | - | `EP` | 결과회신 | `TA` | 기초 | `updatePrgrsStts` |
|
||||
| 38 | 상세 | USER05 | - | `TA` | 결과회신 | — | — | **API 없음. 조회** |
|
||||
|
||||
`SL` 20~22행: 버튼은 직전 상태에 따라 **하나만** 노출. 파일 루프면 [파일보완완료], 그 외 [보완완료].
|
||||
|
||||
---
|
||||
|
||||
## 3. `button-config` 권장 구조
|
||||
|
||||
액션 키는 현행 `FNRS_CNSLTN_VIEW_BUTTON_ACTION`을 확장한다. 신규·이름 변경:
|
||||
|
||||
| 신규/변경 액션 | 라벨 | 비고 |
|
||||
| --- | --- | --- |
|
||||
| `fileSplmnt` | 파일보완 | `ER` 접수자. 현행에 없음 |
|
||||
| `fileVrfc` | 파일검증 | 유지. `ER` 재실행 |
|
||||
| `dsgntRvwr` | 검토자지정 | 유지. 전이를 `AC`로 |
|
||||
| `pwrkCnsltnDocMng` | 실무협의서관리 | 유지. `TE`·`RK` |
|
||||
| `pwrkCnsltnDocCrt` | 실무협의서생성 | 유지. 실패/타임아웃만 |
|
||||
| `rvwRsltInq` | 검토결과조회 | 신규 |
|
||||
| `actnStrg` | 저장(조치결과) | `RB` |
|
||||
| `cnsltnRply` | 협의회신 | `RB→RC` |
|
||||
| `actnSbmsn` | 조치결과서제출 | 광역 `RC→RA` |
|
||||
| `rqstActnRvw` | 조치결과서 검토요청 | 환경청 `RA→AV` |
|
||||
| `costVrfc` | 금액검증 | `RK` |
|
||||
| `lclRply` | 지자체회신 | 광역 `RM→RB` |
|
||||
| `fnrsRsltRply` | 재원협의 결과 회신 | 환경청 `TP→RM` |
|
||||
| `rsltRply` | 결과회신 | `LE→EP`, `RR→EP`, `EP→TA`, `TA` 조회 |
|
||||
| `fileSplmntCmptn` | 파일보완완료 | `SL←ER` |
|
||||
| `splmntCmptn` | 보완완료 | `SL←TE\|RK` |
|
||||
|
||||
**삭제**: `fnrsCnsltnAncmnt`(고시→`TB`), `sncnAprv`(인가승인). `EP`/`TA`는 `rsltRply`로 통일.
|
||||
|
||||
### 3-1. 역할 × 상태 (공단 제외)
|
||||
|
||||
```
|
||||
ROLE_USER05
|
||||
"" | SV → 저장, 신청완료, 삭제
|
||||
SL → (직전 ER) 파일보완완료 | (직전 TE|RK) 보완완료
|
||||
RB → 저장(조치), 협의회신
|
||||
TA → 결과회신 (조회)
|
||||
|
||||
ROLE_USER04
|
||||
ST → 환경청제출 // 현행 반려(rjct→SV)는 SoT 38행에 없음. 유지 시 별도 합의
|
||||
RM → 지자체회신
|
||||
RC → 조치결과서제출
|
||||
EP → 결과회신
|
||||
|
||||
ROLE_USER03
|
||||
LE → 결과회신(자체종결), 기술검토 의뢰
|
||||
TP → 재원협의 결과 회신
|
||||
RA → 조치결과서 검토요청
|
||||
RR → 결과회신
|
||||
```
|
||||
|
||||
### 3-2. 공단 결재선 × 상태
|
||||
|
||||
```
|
||||
ER / ATRZ01 접수자
|
||||
→ 파일보완, 파일검증, 검토자지정
|
||||
|
||||
TE / ATRZ02 검토자
|
||||
→ (초안 성공) 실무협의서관리, 검토결과조회, 저장, 검토완료, 보완요청
|
||||
→ (초안 실패·타임아웃) 실무협의서생성 (+ 위 항목 중 관리/조회는 초안 생긴 뒤)
|
||||
|
||||
TE / ATRZ03 승인자 → 결재(승인)
|
||||
TE / ATRZ99 최종 → 결재(회신)
|
||||
|
||||
RK / ATRZ02 검토자
|
||||
→ 실무협의서관리, 금액검증, 저장, 검토완료, 보완요청
|
||||
RK / ATRZ03 → 결재(승인)
|
||||
RK / ATRZ99 → 결재(회신)
|
||||
```
|
||||
|
||||
`FV`, `AC`, `AV`: 액션 빈 배열 + 진행 패널.
|
||||
|
||||
현행 `TE`에서 `hasPendingApprovalRequest === false`이면 버튼을 숨긴다. v3.0에서는 검토자 버튼(저장·웹기안기·보완)은 결재 대기와 무관하게 두고, **검토완료/결재**만 결재건 존재 여부에 묶는다.
|
||||
|
||||
---
|
||||
|
||||
## 4. 초안 성공 vs 실패 버튼
|
||||
|
||||
`TE`에서 `crtDmndList[].crtSttsComCd` / 폴링 `status`로 분기.
|
||||
|
||||
| 조건 | 노출 |
|
||||
| --- | --- |
|
||||
| `C` / `COMPLETED` | 실무협의서관리, 검토결과조회, 저장, 검토완료, 보완요청 |
|
||||
| `F` / `FAILED` 또는 30분 Fallback 후 초안 없음 | 실무협의서생성 |
|
||||
| `R`/`I` 생성 중 | 진행 패널. 위 버튼 비활성 (`AC`와 동일 UX, 상태가 이미 `TE`면 패널+생성 버튼) |
|
||||
|
||||
`RK`는 금액검증 성공이 기본. Fallback 후 [금액검증] 재실행만 추가 노출.
|
||||
|
||||
---
|
||||
|
||||
## 5. 현행 `button-config.ts`와의 차이
|
||||
|
||||
| 항목 | 현재 | To-Be |
|
||||
| --- | --- | --- |
|
||||
| USER05 | `""/SV/TA(고시)` | `SL`, `RB` 추가. `TA`는 조회 |
|
||||
| USER04 | `ST`(제출+반려), `EP`(인가승인) | `RM/RC/EP` 추가. 인가승인 삭제 |
|
||||
| USER03 | `LE`, `TP`(→EP로 회신) | `RA`, `RR` 추가. `TP`는 `RM` |
|
||||
| USER01 | `ER`(파일검증, 검토자지정), `TE` | `ER`에 파일보완. `RK` 전면 추가 |
|
||||
| USER01 ATRZ | `ER`, `TE`만 `needsRoleUser01AtrzResolve` | `RK` 포함 |
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,498 @@
|
||||
# 화면설계서: 전국 행정지원 지도 (대시보드 메인)
|
||||
|
||||
## 1. 기본 정보
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ---------- | -------------------------------------------- |
|
||||
| 버전 | 1.2 |
|
||||
| 프로세스명 | UI설계 |
|
||||
| 화면명 | 전국 행정지원 지도 (대시보드 메인) |
|
||||
| 화면 ID | TBD |
|
||||
| 화면 유형 | [v] 조회 [ ] 입력 [ ] 수정 [ ] 삭제 [ ] 출력 |
|
||||
| 메뉴 경로 | 메인 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 화면 목적
|
||||
|
||||
로그인 사용자의 **기관 권한·당해 연도** 기준으로 문서별 진행상태 통계와 행정지원 알림을 한 화면에서 조회한다. 좌측 지도에서 전국 행정지원 현황을 보고, 우측에서 진행상황 카드·알림이력·공지사항을 확인한 뒤 알림 [처리하기]로 연계 업무 화면으로 이동한다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 화면 구성
|
||||
|
||||
### 3-1. 전체 레이아웃
|
||||
|
||||
브레드크럼·페이지 타이틀은 스크린샷에 없음. 홈(`/`) 메인 콘텐츠로 구성한다.
|
||||
|
||||
```
|
||||
┌────────────────────────────────┬─────────────────────────────────────────────────┐
|
||||
│ [영역 A] 전국 행정지원 지도 │ [영역 B] 문서별 진행상황 카드 (2×2) │
|
||||
│ 타이틀: 전국 행정지원 지도 │ ┌────────────────┐ ┌────────────────┐ │
|
||||
│ │ │ 재원협의 │ │ 설치인가 │ │
|
||||
│ (대한민국 시도·시군구 경계) │ │ 신청/진행/완료 │ │ 신청/진행/완료 │ │
|
||||
│ 마커: 서울 강남구, │ ├────────────────┤ ├────────────────┤ │
|
||||
│ 대전 유성구, │ │ 설계자문 │ │ 기본계획 │ │
|
||||
│ 부산 해운대구 │ │ 신청/진행/완료 │ │ 신청/진행/완료 │ │
|
||||
│ │ └────────────────┘ └────────────────┘ │
|
||||
│ ├─────────────────────────────────────────────────┤
|
||||
│ │ [영역 C] 알림이력 │
|
||||
│ │ 알림이력 업데이트 HH:mm [전체보기] │
|
||||
│ │ [오늘 N건] [지연 N건] [완료대기 N건] │
|
||||
│ │ [뱃지] [일자] 제목 [처리하기] │
|
||||
│ │ … (최대 5건) │
|
||||
│ ├─────────────────────────────────────────────────┤
|
||||
│ │ [영역 D] 공지사항 [더보기] │
|
||||
│ │ ┌────┬────────┬────────┬────────┬──────┐ │
|
||||
│ │ │번호│ 제목 │ 작성자 │ 작성일 │조회수│ │
|
||||
│ │ └────┴────────┴────────┴────────┴──────┘ │
|
||||
└────────────────────────────────┴─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3-2. 화면 데이터 흐름
|
||||
|
||||
```
|
||||
BEGIN
|
||||
|
||||
// 최초 진입
|
||||
// searchYear 미전달 → 서버가 당해 연도로 고정. 알림 목록은 최신 5건
|
||||
CALL getMainDashboard(DashboardMainSearchDto) // Authorization은 client 인터셉터
|
||||
DISPLAY 진행상황 카드 (영역 B) // docPrgrsSttsList, TOTAL 행 제외
|
||||
DISPLAY 알림 통계 칩 · 알림 목록 (영역 C) // notiStats, notiList
|
||||
|
||||
CALL getUserPostList(UserPostSearchParams) // board-service, AS_DASHBOARD_BBS_ID
|
||||
DISPLAY 공지사항 그리드 (영역 D)
|
||||
|
||||
CALL 지도 마커/음영 조회(…) // TBD — 대시보드 API에 없음
|
||||
DISPLAY 전국 지도 (영역 A)
|
||||
|
||||
ON 영역 C [전체보기] 버튼 CLICK DO
|
||||
NAVIGATE /nti-hstry
|
||||
END ON
|
||||
|
||||
ON 영역 C [처리하기] 버튼 CLICK DO
|
||||
CALL readNoti(notiId) // entities/noti
|
||||
NAVIGATE notiUrl // 비어 있으면 이동하지 않음
|
||||
CALL getMainDashboard(…) // → 영역 C 갱신 (invalidate)
|
||||
END ON
|
||||
|
||||
ON 영역 D [더보기] 버튼 CLICK DO
|
||||
NAVIGATE /cms/bbsnotice/{AS_DASHBOARD_BBS_ID} // as 앱 공지 목록 라우트 미구현. st/cm 관례
|
||||
END ON
|
||||
|
||||
ON 영역 D 행 CLICK DO
|
||||
NAVIGATE /cms/bbsnotice/{AS_DASHBOARD_BBS_ID}/{pstNo}
|
||||
END ON
|
||||
|
||||
ON 영역 B 카드 CLICK DO
|
||||
NAVIGATE 해당 문서 목록 // §5-3
|
||||
END ON
|
||||
|
||||
ON 영역 A 마커 CLICK DO
|
||||
// TBD — 팝업/필터/이동 여부 미확정
|
||||
END ON
|
||||
|
||||
END
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 영역 A — 전국 행정지원 지도
|
||||
|
||||
스크린샷 타이틀: **전국 행정지원 지도**.
|
||||
|
||||
`getMainDashboard` 응답에 지도·좌표·행정구역 음영 데이터는 **없다**. 마커·음영 소스 API는 **TBD**.
|
||||
|
||||
### 4-1. 표시 요소
|
||||
|
||||
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
|
||||
| ----------- | --------- | ---- | ------ | ----------------- | ----------------------------------------------------------------- |
|
||||
| 섹션 타이틀 | Text | — | — | — | 고정 문구 `전국 행정지원 지도` |
|
||||
| 전국 지도 | Map | — | — | TBD | 시도·시군구 경계. 북·동부 음영(청) / 남서부 비음영(백) — 의미 TBD |
|
||||
| 마커 라벨 | Marker | — | — | TBD | 스크린샷 샘플: `서울 강남구`, `대전 유성구`, `부산 해운대구` |
|
||||
|
||||
### 4-2. 상호작용
|
||||
|
||||
| 동작 | 설명 |
|
||||
| ---------- | ----------------------------------------- |
|
||||
| 마커 클릭 | TBD (상세 팝업 / 해당 지자체 필터 / 이동) |
|
||||
| 지도 줌·팬 | TBD (스크린샷에 컨트롤 없음) |
|
||||
|
||||
`entities/adstdstr`는 시도·구군 **콤보**만 제공하며 좌표를 주지 않는다. 지도 라이브러리도 as 앱에 없음 — 선정 TBD.
|
||||
|
||||
---
|
||||
|
||||
## 5. 영역 B — 문서별 진행상황 카드
|
||||
|
||||
`DashboardMainResponseDto.docPrgrsSttsList`를 **`docSeComCd`로 매핑**해 4장 카드를 그린다. 배열 순서에 의존하지 않는다.
|
||||
|
||||
스크린샷 배치(좌→우, 상→하):
|
||||
|
||||
| 화면 카드 타이틀 | `docSeComCd` | API `docSeNm` |
|
||||
| ----------------- | ------------ | -------------- |
|
||||
| 재원협의 진행상황 | `FNRS` | 재원협의 |
|
||||
| 설치인가 진행상황 | `INSTL` | 설치인가 |
|
||||
| 설계자문 진행상황 | `CNFM` | 설계자문 |
|
||||
| 기본계획 진행상황 | `SEWER` | 하수도기본계획 |
|
||||
|
||||
서버 SQL `ORDER BY`는 `INSTL → FNRS → SEWER → CNFM` 이며, `GROUP BY ROLLUP`으로 **`docSeComCd = TOTAL`** 합계 행이 추가된다. 카드에는 TOTAL 행을 **표시하지 않는다**.
|
||||
|
||||
카드 타이틀은 스크린샷 문구(`… 진행상황`)를 쓴다. `SEWER`의 API명 `하수도기본계획`은 화면에서 `기본계획`으로 축약한다.
|
||||
|
||||
### 5-1. 카드 내부 필드
|
||||
|
||||
각 카드 공통 구조. `totCnt`는 화면에 없음.
|
||||
|
||||
| 표시명 | Field | 비고 |
|
||||
| ------ | ---------- | ------------------ |
|
||||
| 신청 | `reqCnt` | 파란 아이콘 + 건수 |
|
||||
| 진행 | `prgrsCnt` | 초록 아이콘 + 건수 |
|
||||
| 완료 | `cmptnCnt` | 주황 아이콘 + 건수 |
|
||||
|
||||
상태 그룹(서버): 임시저장(`SV`/`CV`) 제외. 문서별 `prgrs_stts_com_cd` → `REQ`/`PRG`/`CMP`.
|
||||
|
||||
### 5-2. 검색 파라미터 (화면 미표시)
|
||||
|
||||
영역 B에 연도·필터 UI는 없다. 서버 기본값을 따른다.
|
||||
|
||||
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
|
||||
| ------ | --------- | ---- | --------- | ----------------- | --------------------------------------------------------------------- |
|
||||
| (없음) | — | — | 당해 연도 | `searchYear` | 미전달 시 서버 `LocalDate.now().getYear()`. 연도 셀렉터 추가 여부 TBD |
|
||||
|
||||
`docSeComCd` 쿼리는 **알림 목록 필터**에만 쓰이며, 진행상황 SQL에는 전달되지 않는다. 카드 4종은 항상 전원 집계한다.
|
||||
|
||||
### 5-3. 상호작용
|
||||
|
||||
카드 클릭 시 **해당 문서 목록 페이지**로 이동한다. `docSeComCd` → 라우트 고정 매핑.
|
||||
|
||||
| 동작 | `docSeComCd` | 이동 경로 |
|
||||
| --------- | ------------ | ----------------- |
|
||||
| 카드 클릭 | `FNRS` | `/fnrs-cnsltn` |
|
||||
| 카드 클릭 | `INSTL` | `/instl-sncn` |
|
||||
| 카드 클릭 | `CNFM` | `/design-cnstn` |
|
||||
| 카드 클릭 | `SEWER` | `/sewer-bsc-plan` |
|
||||
|
||||
---
|
||||
|
||||
## 6. 영역 C — 알림이력
|
||||
|
||||
서버가 cms-service 알림을 대시보드 응답에 합친다. 목록은 **page=0, size=5** (최신 5건). 페이지네이션 없음.
|
||||
|
||||
### 6-1. 헤더
|
||||
|
||||
| 표시명 | Field / 소스 | 비고 |
|
||||
| ------------- | ------------ | --------------------------------------------------------------------------------------- |
|
||||
| 섹션 타이틀 | — | 고정 문구 `알림이력` |
|
||||
| 업데이트 시각 | (client) | 스크린샷 `업데이트 10:32`. API 필드 없음. `getMainDashboard` 마지막 fetch 시각(`HH:mm`) |
|
||||
| 전체보기 | Button | `/nti-hstry`로 이동 |
|
||||
|
||||
### 6-2. 통계 칩
|
||||
|
||||
`NotiAsStatResponseDto`.
|
||||
|
||||
| 표시명 | Field | 비고 |
|
||||
| ------------ | -------------- | -------------------------------------- |
|
||||
| 오늘 N건 | `todayCount` | 당일 00:00 이후 수신 |
|
||||
| 지연 N건 | `delayCount` | `notReadYn=N` 이고 수신 후 3일 이상 |
|
||||
| 완료대기 N건 | `pendingCount` | `notReadYn=Y` 이고 `notiPrcsCmptnYn=N` |
|
||||
|
||||
### 6-3. 목록 행
|
||||
|
||||
| 표시명 | Field | 비고 |
|
||||
| ------ | --------- | --------------------------------------------- |
|
||||
| 일자 | `regDt` | 표시 `YYYY-MM-DD`. 응답 `yyyy-MM-dd HH:mm:ss` |
|
||||
| 제목 | `notiTtl` | 알림 제목 |
|
||||
| 처리 | (action) | 스크린샷 라벨 `처리하기`. 클릭 §6-4 |
|
||||
|
||||
응답에 있으나 이 목록에 **표시하지 않음**:
|
||||
|
||||
| Field | 비고 |
|
||||
| ----------------- | ----------------------------------------- |
|
||||
| `notiId` | 행 키 · `readNoti` 파라미터 |
|
||||
| `recvUnqId` | 수신 회원 UUID (서버 로그인 사용자 고정) |
|
||||
| `senderUnqId` | 발신 회원 UUID |
|
||||
| `notiType` | `ADSS` |
|
||||
| `notiCn` | 본문 |
|
||||
| `notiUrl` | 처리 후 이동 URL. 컬럼 미표시 |
|
||||
| `notiLnkgUnqNo` | 알림연결고유번호 |
|
||||
| `docSeComCd` | 문서구분 코드 |
|
||||
| `docSeComNm` | 문서구분명 (알림이력 화면에서는 연계업무) |
|
||||
| `notReadYn` | 뱃지 판정 TBD에 사용 가능 |
|
||||
| `notiIdntyYn` | 확인 여부 |
|
||||
| `notiIdntyDt` | 확인 일시 |
|
||||
| `notiPrcsCmptnYn` | 처리 완료 여부 |
|
||||
| `notiPrcsCmptnDt` | 처리 완료 일시 |
|
||||
|
||||
### 6-4. 행 상호작용
|
||||
|
||||
| 동작 | 설명 |
|
||||
| --------------- | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| [전체보기] 클릭 | `/nti-hstry` (알림이력 목록)로 이동 |
|
||||
| [처리하기] 클릭 | `readNoti(notiId)` 후 `notiUrl`로 이동. URL이 비면 이동하지 않음(읽음 처리는 수행). `dashboardBaseKey` invalidate |
|
||||
|
||||
`DashboardController`에 읽음 API는 없다. **`entities/noti`의 `readNoti`** 를 쓴다. 알림이력 화면과 동일 (`PUT /cms-service/api/v1/noti/{notiId}/read`).
|
||||
|
||||
### 6-5. 검색 파라미터 (화면 미표시)
|
||||
|
||||
서버 `toNotiSearchDto()` 기본: `notiType = ADSS`. 화면 필터 없음.
|
||||
|
||||
| DashboardMainSearchDto | 화면 | 비고 |
|
||||
| ---------------------- | ---- | --------------------------------- |
|
||||
| `notiType` | 없음 | 서버 기본 `ADSS` |
|
||||
| `notReadYn` | 없음 | 통계 칩 클릭 필터 시 후보 (TBD) |
|
||||
| `startDt` / `endDt` | 없음 | 대시보드 기본 미전달 |
|
||||
| `docSeComCd` | 없음 | 알림만 필터. 진행상황 카드와 무관 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 영역 D — 공지사항
|
||||
|
||||
`getMainDashboard`에 공지 필드는 없다. **board-service** `getUserPostList`로 별도 조회한다.
|
||||
|
||||
- Base: `GET /board-service/api/v1/posts/user/{AS_DASHBOARD_BBS_ID}`
|
||||
- `AS_DASHBOARD_BBS_ID` = `NEXT_PUBLIC_AS_DASHBOARD_BBS_ID` (로컬 예: `7`)
|
||||
- 클라이언트: `@/entities/board` — `postApi.getUserPostList` / `useUserPostList`
|
||||
|
||||
페이지네이션 컨트롤은 없다. 요약 목록만 표시한다 (`page=0`, `size=3`).
|
||||
|
||||
### 7-1. 헤더·버튼
|
||||
|
||||
| 버튼명 | 동작 |
|
||||
| ------ | ------------------------------------------------------------------------------------------- |
|
||||
| 더보기 | `/cms/bbsnotice/{AS_DASHBOARD_BBS_ID}` 로 이동. as 앱에 공지 목록 라우트는 없음(st/cm 관례) |
|
||||
|
||||
### 7-2. 그리드 컬럼
|
||||
|
||||
스크린샷 헤더 순서. `UserPost`.
|
||||
|
||||
| 표시명 | Field | 비고 |
|
||||
| ------ | --------- | ---------------------------------------------------- |
|
||||
| 번호 | `rowNo` | `upndFixYn` = `Y` 이면 `[공지]` Badge, 그 외 `rowNo` |
|
||||
| 제목 | `pstTtl` | truncate |
|
||||
| 작성자 | `wrtrNm` | |
|
||||
| 작성일 | `regDt` | 표시 `YYYY-MM-DD` (응답 datetime) |
|
||||
| 조회수 | `inqNocs` | 숫자 |
|
||||
|
||||
조회 파라미터 (`UserPostSearchParams`):
|
||||
|
||||
| 필드 | 값 | 비고 |
|
||||
| ------------- | ----------- | ------------------------------------------ |
|
||||
| `page` | `0` | Spring 0-based |
|
||||
| `size` | `3` | 대시보드 요약. 스크린샷 3행 |
|
||||
| `keywordType` | `bbsTypeCd` | st 대시보드 공지 패널과 동일 |
|
||||
| `keyword` | `BBSNOTICE` | 게시판 유형. path의 bbsId와 함께 공지 한정 |
|
||||
|
||||
응답에 있으나 그리드에 **표시하지 않음**: `bbsId`, `pstNo`(행 키·상세 이동), `pstCn`, `upndFixYn`(번호 열 판정만), `rgtrNm`, `atflCd` 등.
|
||||
|
||||
### 7-3. 행 상호작용
|
||||
|
||||
| 동작 | 설명 |
|
||||
| ------- | ------------------------------------------------------------------------------ |
|
||||
| 행 클릭 | `/cms/bbsnotice/{AS_DASHBOARD_BBS_ID}/{pstNo}` (as 앱 공지 상세 라우트 미구현) |
|
||||
|
||||
---
|
||||
|
||||
## 8. API/DTO 명세
|
||||
|
||||
### 8-1. 공통
|
||||
|
||||
| 항목 | 값 |
|
||||
| --------------- | ---------------------------------------------------------------------------------------------- |
|
||||
| Base prefix | `/as-service/api/v1/dashboard` (`ENDPOINT.AS_SERVICE.DASHBOARD`) |
|
||||
| 인증 | `Authorization` 헤더. 프론트는 `client` 인터셉터. 로그인 기관 권한은 서버가 사용자 캐시로 적용 |
|
||||
| 클라이언트 모듈 | `@/entities/dashboard` — `dashboardService` / `useGetMainDashboard` |
|
||||
| 알림 읽음 | `@/entities/noti` — `notiService.readNoti` / `useReadNoti` |
|
||||
| 공지사항 | `@/entities/board` — `postApi.getUserPostList` / `useUserPostList` |
|
||||
|
||||
영역 A(지도)는 본 컨트롤러·board-service 범위 밖 → **TBD**.
|
||||
|
||||
### 8-2. 대시보드 메인 조회 — `getMainDashboard`
|
||||
|
||||
| 기능 | Method | URL | Request DTO | Response DTO | API |
|
||||
| -------------------------------------------- | ------ | ------------- | ------------------------------- | -------------------------- | ----------------------------------------------------------- |
|
||||
| 대시보드 메인 통합진행상태 및 알림 정보 조회 | GET | `{BASE}/main` | Query: `DashboardMainSearchDto` | `DashboardMainResponseDto` | `dashboardService.getMainDashboard` / `useGetMainDashboard` |
|
||||
|
||||
**Request (`DashboardMainSearchDto`)**
|
||||
|
||||
| 필드 | 필수 | 설명 |
|
||||
| ------------ | ---- | ---------------------------------------------------------------------- |
|
||||
| `searchYear` | N | 검색 연도. 미전달 시 서버 당해 연도. 영역 B 집계 기간 |
|
||||
| `notiType` | N | 알림 종류. 미전달 시 서버 `ADSS` |
|
||||
| `notReadYn` | N | 알림 확인 여부 `Y`/`N`. 본 화면 미사용 |
|
||||
| `startDt` | N | 알림 시작일 `YYYY-MM-DD`. 본 화면 미사용 |
|
||||
| `endDt` | N | 알림 종료일 `YYYY-MM-DD`. 본 화면 미사용 |
|
||||
| `docSeComCd` | N | 알림 문서구분 (`INSTL`/`FNRS`/`SEWER`/`CNFM` 등). 진행상황 집계와 무관 |
|
||||
|
||||
`Authorization`은 service 시그니처에 넣지 않는다.
|
||||
|
||||
**Response (`DashboardMainResponseDto`)**
|
||||
|
||||
| 필드 | 타입 | 설명 |
|
||||
| ------------------ | --------------------------- | --------------------------- |
|
||||
| `docPrgrsSttsList` | `DocPrgrsSttsResponseDto[]` | 문서별 진행상태 + TOTAL 행 |
|
||||
| `notiList` | `NotiAsUserResponseDto[]` | 최신 최대 5건 |
|
||||
| `notiStats` | `NotiAsStatResponseDto` | 오늘 / 지연 / 완료대기 건수 |
|
||||
|
||||
**`DocPrgrsSttsResponseDto`**
|
||||
|
||||
| 필드 | 타입 | 설명 |
|
||||
| ------------ | ------ | --------------------------------------------- |
|
||||
| `docSeNm` | string | 문서 구분 명칭 |
|
||||
| `docSeComCd` | string | `INSTL` / `FNRS` / `SEWER` / `CNFM` / `TOTAL` |
|
||||
| `reqCnt` | number | 신청 건수 |
|
||||
| `prgrsCnt` | number | 진행 건수 |
|
||||
| `cmptnCnt` | number | 완료 건수 |
|
||||
| `totCnt` | number | 총 건수 (화면 미표시) |
|
||||
|
||||
**`NotiAsStatResponseDto`**
|
||||
|
||||
| 필드 | 타입 | 설명 |
|
||||
| -------------- | ------ | --------- |
|
||||
| `todayCount` | number | 오늘 알림 |
|
||||
| `delayCount` | number | 지연 알림 |
|
||||
| `pendingCount` | number | 완료대기 |
|
||||
|
||||
**`NotiAsUserResponseDto`** — 필드 상세는 `docs/spec/nti-hstry` §6-2와 동일 (`notiId`, `notiTtl`, `regDt`, `notiUrl`, `notReadYn`, `notiPrcsCmptnYn` 등).
|
||||
|
||||
### 8-3. 알림 읽음 처리 — `readNoti`
|
||||
|
||||
대시보드 컨트롤러가 아니라 cms-service. 영역 C [처리하기]에서 호출.
|
||||
|
||||
| 기능 | Method | URL | Request DTO | Response DTO | API |
|
||||
| -------------- | ------ | ---------------------------------------- | ----------------------- | ---------------- | -------------------------------------- |
|
||||
| 알림 읽음 처리 | PUT | `/cms-service/api/v1/noti/{notiId}/read` | Path: `notiId` (`Long`) | `Long` (알림 ID) | `notiService.readNoti` / `useReadNoti` |
|
||||
|
||||
Body 없음. 성공 시 `dashboardBaseKey`(및 필요 시 `notiBaseKey`) invalidate.
|
||||
|
||||
### 8-4. 전국 행정지원 지도 — TBD
|
||||
|
||||
| 기능 | Method | URL | Request DTO | Response DTO | API |
|
||||
| ------------------- | ------ | --- | ----------- | ------------ | --- |
|
||||
| 지도 마커·음영 조회 | TBD | TBD | TBD | TBD | TBD |
|
||||
|
||||
### 8-5. 공지사항 — `getUserPostList`
|
||||
|
||||
board-service. bbsId는 환경변수 `AS_DASHBOARD_BBS_ID`로 path에 고정한다.
|
||||
|
||||
| 기능 | Method | URL | Request DTO | Response DTO | API |
|
||||
| ----------------------- | ------ | ------------------------------------------ | ----------------------------- | ------------------------ | --------------------------------------------- |
|
||||
| 대시보드 공지 요약 목록 | GET | `/board-service/api/v1/posts/user/{bbsId}` | Query: `UserPostSearchParams` | `PageResponse<UserPost>` | `postApi.getUserPostList` / `useUserPostList` |
|
||||
|
||||
**Request (`UserPostSearchParams`)** — 본 화면 고정값
|
||||
|
||||
| 필드 | 필수 | 설명 |
|
||||
| ------------- | ---- | -------------- |
|
||||
| `page` | N | `0` |
|
||||
| `size` | N | `3` |
|
||||
| `keywordType` | N | `bbsTypeCd` |
|
||||
| `keyword` | N | `BBSNOTICE` |
|
||||
| `popupYn` 등 | N | 본 화면 미사용 |
|
||||
|
||||
**Response (`UserPost`) — 화면 매핑 필드**
|
||||
|
||||
| 필드 | 타입 | 설명 |
|
||||
| ----------- | ------ | --------------------------- |
|
||||
| `rowNo` | number | 번호. 고정글이 아니면 표시 |
|
||||
| `pstNo` | number | 게시 번호. 상세 이동 키 |
|
||||
| `bbsId` | number | 게시판 ID |
|
||||
| `pstTtl` | string | 제목 |
|
||||
| `wrtrNm` | string | 작성자 |
|
||||
| `regDt` | string | 작성일 |
|
||||
| `inqNocs` | number | 조회수 |
|
||||
| `upndFixYn` | string | 상단고정 `Y`/`N` → `[공지]` |
|
||||
|
||||
---
|
||||
|
||||
## 9. UI 구현 가이드
|
||||
|
||||
### 9-1. 기술 스택·원칙
|
||||
|
||||
- 라우트: 기존 홈 `app/(main)/(with-base-layout)/page.tsx` → `pages/home` (현재 `MainPage` 플레이스홀더)
|
||||
- UI: `@common/components` — Card, Badge, Button, Table(공지). 지도 라이브러리 TBD
|
||||
- 데이터: React Query `useGetMainDashboard()`. 처리하기는 `useReadNoti()`. 공지는 `useUserPostList()`
|
||||
- 진행상황 카드: `docSeComCd`로 4슬롯 매핑. `TOTAL` 제외
|
||||
- 알림 목록: 테이블이 아닌 행 리스트(뱃지 + 일자 + 제목 + 버튼)
|
||||
- 공지: `@tanstack/react-table` + `useDataTable`, 페이지네이션 없음
|
||||
- 검색 조건 URL 동기화: 본 화면 필터 없음. `searchYear` UI가 생기면 그때 검토
|
||||
|
||||
### 9-2. 모듈·파일 배치
|
||||
|
||||
| 구분 | 경로 |
|
||||
| ------------- | ------------------------------------------------------------ |
|
||||
| 라우트 | `app/(main)/(with-base-layout)/page.tsx` |
|
||||
| 페이지 | `pages/dashboard/ui/dashboard-page.tsx` |
|
||||
| 지도 | `pages/dashboard/ui/national-as-map.tsx` (API TBD, Skeleton) |
|
||||
| 진행상황 카드 | `pages/dashboard/ui/doc-prgrs-stts-cards.tsx` |
|
||||
| 알림이력 | `pages/dashboard/ui/dashboard-noti-list.tsx` |
|
||||
| 공지사항 | `pages/dashboard/ui/dashboard-notice-table.tsx` |
|
||||
| 공지 컬럼 | `pages/dashboard/model/dashboard-notice-table-columns.tsx` |
|
||||
| Entity API | `entities/dashboard/api/dashboard-service.ts` |
|
||||
| Entity Hook | `entities/dashboard/api/queries.ts` → `useGetMainDashboard` |
|
||||
| Entity Model | `entities/dashboard/model/dashboard-interface.ts` |
|
||||
| 알림 읽음 | `entities/noti` → `useReadNoti` |
|
||||
| 공지 Entity | `entities/board` → `useUserPostList` |
|
||||
|
||||
Entity(`getMainDashboard`, `getUserPostList`) 구현됨. 영역 B·C·D 화면 구현. 영역 A는 Skeleton.
|
||||
|
||||
### 9-3. 영역별 UI 매핑
|
||||
|
||||
| 영역 | 컴포넌트 | 데이터 |
|
||||
| ---- | ------------------------ | ---------------------------------------------------------------------------- |
|
||||
| A | `national-as-map` | Skeleton (지도 API TBD) |
|
||||
| B | `doc-prgrs-stts-cards` | `useGetMainDashboard` → `docPrgrsSttsList` (`TOTAL` 제외, 코드 매핑) |
|
||||
| C | `dashboard-noti-list` | 동 훅 → `notiStats`, `notiList`. 처리 → `useReadNoti`. 전체보기 `/nti-hstry` |
|
||||
| D | `dashboard-notice-table` | `useUserPostList` → `content[]`. [더보기]·행 클릭 → `/cms/bbsnotice/...` |
|
||||
|
||||
### 9-4. 다이얼로그·라우팅
|
||||
|
||||
| 항목 | 내용 |
|
||||
| -------------- | -------------------------------------------------------------------------------------- |
|
||||
| 다이얼로그 | 없음 |
|
||||
| 본 화면 라우트 | `/` (홈) |
|
||||
| 알림 처리 이동 | [처리하기] → `readNoti` 후 `notiUrl` (알림이력과 동일, SEWER URL은 상세 세그먼트 제거) |
|
||||
| 알림 전체보기 | `/nti-hstry` |
|
||||
| 공지 더보기 | `/cms/bbsnotice/{AS_DASHBOARD_BBS_ID}` (as 앱 공지 라우트 미구현, st/cm 관례) |
|
||||
| 공지 행 클릭 | `/cms/bbsnotice/{AS_DASHBOARD_BBS_ID}/{pstNo}` |
|
||||
| 진행상황 카드 | §5-3 목록 라우트 (`/fnrs-cnsltn`, `/instl-sncn`, `/design-cnstn`, `/sewer-bsc-plan`) |
|
||||
|
||||
### 9-5. 데이터 흐름
|
||||
|
||||
```
|
||||
BEGIN
|
||||
|
||||
// 최초 진입 (홈)
|
||||
CALL getMainDashboard({})
|
||||
DISPLAY 영역 B // docPrgrsSttsList — FNRS/INSTL/CNFM/SEWER 슬롯
|
||||
DISPLAY 영역 C // notiStats + notiList(≤5)
|
||||
|
||||
CALL getUserPostList({ page: 0, size: 3, keywordType: "bbsTypeCd", keyword: "BBSNOTICE" })
|
||||
DISPLAY 영역 D
|
||||
|
||||
CALL 지도 데이터 // TBD → 영역 A
|
||||
|
||||
ON 영역 C [전체보기] CLICK DO
|
||||
NAVIGATE /nti-hstry
|
||||
END ON
|
||||
|
||||
ON 영역 C [처리하기] CLICK DO
|
||||
CALL readNoti(notiId)
|
||||
IF notiUrl THEN
|
||||
NAVIGATE notiUrl
|
||||
END IF
|
||||
CALL getMainDashboard // invalidate dashboardBaseKey
|
||||
END ON
|
||||
|
||||
ON 영역 B 카드 CLICK DO
|
||||
NAVIGATE 문서 목록 // §5-3
|
||||
END ON
|
||||
|
||||
ON 영역 D [더보기] CLICK DO
|
||||
NAVIGATE /cms/bbsnotice/{AS_DASHBOARD_BBS_ID}
|
||||
END ON
|
||||
|
||||
ON 영역 D 행 CLICK DO
|
||||
NAVIGATE /cms/bbsnotice/{AS_DASHBOARD_BBS_ID}/{pstNo}
|
||||
END ON
|
||||
|
||||
END
|
||||
```
|
||||
@@ -0,0 +1,285 @@
|
||||
# 화면설계서: 내 행정지원
|
||||
|
||||
## 1. 기본 정보
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ---------- | -------------------------------------------- |
|
||||
| 버전 | 1.0 |
|
||||
| 프로세스명 | UI설계 |
|
||||
| 화면명 | 내 행정지원 |
|
||||
| 화면 ID | AS_SC0204011 |
|
||||
| 화면 유형 | [v] 조회 [ ] 입력 [ ] 수정 [ ] 삭제 [ ] 출력 |
|
||||
| 메뉴 경로 | 메인 > 행정지원서비스 > 내 행정지원 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 화면 목적
|
||||
|
||||
로그인한 사용자가 **본인이 신청한 행정지원 문서**를 기간·구분·진행상태로 조회한다. 목록에서 행을 선택하면 해당 문서 상세 화면으로 이동한다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 화면 구성
|
||||
|
||||
### 3-1. 전체 레이아웃
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 브레드크럼: 메인 > 행정지원서비스 > 내 행정지원 │
|
||||
│ 페이지 타이틀: 내 행정지원 │
|
||||
├─────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ [영역 A] 검색 필터 │
|
||||
│ 기간 [시작일] ~ [종료일] 구분 ▼ 진행상태 ▼ [검색] │
|
||||
├─────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ [영역 B] 내 신청 목록 │
|
||||
│ 타이틀: 내 신청 목록 N개 │
|
||||
│ ┌────┬──────┬────────┬──────────────┬────────────┬────────┬──────────────┐ │
|
||||
│ │ No │ 구분 │ 신청일 │ 진행상태 │ 완료(예정)일│ 담당자 │ 담당자연락처 │ │
|
||||
│ ├────┼──────┼────────┼──────────────┼────────────┼────────┼──────────────┤ │
|
||||
│ │ … │ … │ … │ prgrsSttsComNm│ … │ … │ … │ │
|
||||
│ └────┴──────┴────────┴──────────────┴────────────┴────────┴──────────────┘ │
|
||||
│ [페이지네이션] │
|
||||
└─────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3-2. 화면 데이터 흐름
|
||||
|
||||
```
|
||||
BEGIN
|
||||
|
||||
// 최초 진입
|
||||
// 기간 기본값: 오늘 기준 최근 1개월 (startDate = today-1M, endDate = today)
|
||||
CALL findMyElaperDocList(ElaprMyDocSearchDto + pageable)
|
||||
DISPLAY 내 신청 목록 그리드 (영역 B)
|
||||
|
||||
ON [검색] 버튼 CLICK DO
|
||||
CALL findMyElaperDocList(영역 A 조건 + page=0)
|
||||
DISPLAY 내 신청 목록 그리드 (영역 B)
|
||||
END ON
|
||||
|
||||
ON 페이지네이션 CHANGE DO
|
||||
CALL findMyElaperDocList(영역 A 조건 + page/size)
|
||||
DISPLAY 내 신청 목록 그리드 (영역 B)
|
||||
END ON
|
||||
|
||||
ON 영역 B 행 CLICK DO
|
||||
// docSeComCd·prgrsSttsComCd·rgtr → view/edit
|
||||
NAVIGATE 문서 상세 또는 수정 화면
|
||||
END ON
|
||||
|
||||
END
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 영역 A — 검색 필터
|
||||
|
||||
구성: **기간 · 구분 · 진행상태** (+ 검색).
|
||||
|
||||
> UI설계 상세설명의 `docSecs` / `docStatusCd`는 레거시·설계 문서 명칭이다.
|
||||
> **API SoT**는 `ElaprMyDocSearchDto`의 `docSeComCd` / `prgrsSttsComCd`를 사용한다.
|
||||
|
||||
### 4-1. 필드 정의
|
||||
|
||||
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
|
||||
| ---------- | ---------------- | ---- | --------------------------- | ----------------- | --------------------------------------------------------------------- |
|
||||
| 기간(시작) | DatePicker | N | 오늘 − 1개월 (`YYYY-MM-DD`) | `startDate` | 신청일(`reqDt`) 기준 검색으로 가정 |
|
||||
| 기간(종료) | DatePicker | N | 오늘 (`YYYY-MM-DD`) | `endDate` | |
|
||||
| 구분 | Select(공통코드) | N | 전체(미선택) | `docSeComCd` | 상위코드 `ASDOCSE`. UI설계 `docSecs` → API `docSeComCd` |
|
||||
| 진행상태 | Select(공통코드) | N | 전체(미선택) | `prgrsSttsComCd` | 상위코드 = `{docSeComCd}STTS` (예: `INSTL` → `INSTLSTTS`). UI설계 `docStatusCd` → API 필드명 |
|
||||
|
||||
### 4-2. 버튼
|
||||
|
||||
| 버튼명 | 동작 |
|
||||
| ------ | ----------------------------------------------------------------- |
|
||||
| 검색 | 영역 A 조건으로 `findMyElaperDocList` 호출 → 영역 B 갱신 (page=0) |
|
||||
|
||||
### 4-3. 콤보 의존
|
||||
|
||||
| 트리거 | 동작 |
|
||||
| ----------- | ------------------------------------------------------------------- |
|
||||
| 구분 변경 | 진행상태 옵션을 `upCd = docSeComCd + "STTS"`로 재조회. 진행상태 선택값 RESET |
|
||||
| 구분 미선택 | 진행상태 비활성(옵션 비움). `upCd` 조합 불가이므로 조회하지 않음 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 영역 B — 내 신청 목록
|
||||
|
||||
### 5-1. 그리드 컬럼
|
||||
|
||||
| 표시명 | Field | 비고 |
|
||||
| ------------ | ---------------- | ------------------------------------------------- |
|
||||
| No | (row index) | 페이지 오프셋 반영 |
|
||||
| 구분 | `docSeComNm` | 코드 `docSeComCd` (`ASDOCSE`) |
|
||||
| 신청일 | `reqDt` | 표시 `YYYY-MM-DD` (응답은 `yyyy-MM-dd HH:mm:ss`) |
|
||||
| 진행상태 | `prgrsSttsComNm` | 텍스트 렌더링 (단계형 프로그레스 미사용) |
|
||||
| 완료(예정)일 | `cmptnExpctDt` | 표시 `YYYY-MM-DD` |
|
||||
| 담당자 | `picNm` | |
|
||||
| 담당자연락처 | `picTelno` | |
|
||||
|
||||
응답에 있으나 그리드에 **표시하지 않음**:
|
||||
|
||||
| Field | 비고 |
|
||||
| ---------------- | ----------------------------------------------------------------- |
|
||||
| `docNo` | 문서번호 — 행 클릭 라우팅 키 |
|
||||
| `docTtl` | 문서명/사업명 — 미표시 |
|
||||
| `chgCycl` | 변경차수 — 그리드 미표시. SEWER 이동 시 query 파라미터로 사용 |
|
||||
| `prgrsSttsComCd` | 진행상태 코드 — 컬럼 표시는 `prgrsSttsComNm` |
|
||||
| `rgtr` | 등록자 UUID — edit 판정용 (`rgtr === userUnqId`) |
|
||||
|
||||
### 5-2. 목록 메타·페이지네이션
|
||||
|
||||
| 항목 | 매핑 |
|
||||
| ------------- | ------------------------------------------- |
|
||||
| 건수 `N개` | `PageResponse.totalElements` |
|
||||
| page / size | `ElaprMyDocSearchDto`의 `PageableParams` |
|
||||
| 페이지 컨트롤 | Spring `Page` 표준 (`number`, `totalPages`) |
|
||||
|
||||
### 5-3. 행 상호작용
|
||||
|
||||
| 동작 | 설명 |
|
||||
| ------- | -------------------------------------------------------------------------------------------- |
|
||||
| 행 클릭 | `docSeComCd`·`prgrsSttsComCd`·`rgtr`로 view/edit 분기 후 이동 (`resolveMyPbadmsSprtDetailPath`) |
|
||||
|
||||
**edit 조건** (기존 도메인 목록과 동일):
|
||||
|
||||
| `docSeComCd` | edit 조건 |
|
||||
| ------------------------ | ---------------------------------------------------------------------- |
|
||||
| `INSTL` / `FNRS` / `SEWER` | `(prgrsSttsComCd === "SV" \|\| "SL") && rgtr === loggedInUserUnqId` |
|
||||
| `CNFM` | `prgrsSttsComCd === "CV" && rgtr === loggedInUserUnqId` |
|
||||
|
||||
| `docSeComCd` | edit 경로 | view 경로 |
|
||||
| ------------ | -------------------------------------------- | --------------------------------------- |
|
||||
| `FNRS` | `/fnrs-cnsltn/{docNo}/edit` | `/fnrs-cnsltn/{docNo}` |
|
||||
| `INSTL` | `/instl-sncn/{docNo}/edit` | `/instl-sncn/{docNo}` |
|
||||
| `CNFM` | `/design-cnstn/{docNo}/edit` | `/design-cnstn/{docNo}` |
|
||||
| `SEWER` | `/sewer-bsc-plan/{docNo}/edit?chgCycl={n}` | `/sewer-bsc-plan/{docNo}?chgCycl={n}` |
|
||||
| 그 외 | 이동하지 않음 | |
|
||||
|
||||
- SEWER `chgCycl`: `row.chgCycl ?? 1`
|
||||
- 조건 불충족 시 항상 view
|
||||
|
||||
---
|
||||
|
||||
## 6. API/DTO 명세
|
||||
|
||||
### 6-1. 공통
|
||||
|
||||
| 항목 | 값 |
|
||||
| --------------- | ------------------------------------------------------------------------------------------ |
|
||||
| Base prefix | `/as-elapr-service/api/v1/elapr-prgrs-stts` (`ENDPOINT.AS_ELAPR_SERVICE.ELAPR_PRGRS_STTS`) |
|
||||
| 인증 | 로그인 사용자 UUID — 서버 `SecurityUtils.getLoggedInUserUnqIdUUID()` (클라이언트 미전달) |
|
||||
| 클라이언트 모듈 | `@/entities/elapr` — `elaprPrgrsSttsService` / `useElaprPrgrsSttsMyDocPage` |
|
||||
|
||||
### 6-2. 내 행정지원 목록 — `findMyElaperDocList`
|
||||
|
||||
| 기능 | Method | URL | Request DTO | Response DTO | API |
|
||||
| ----------------- | ------ | --------------- | --------------------------------------- | ------------------------------------ | -------------------------------------------------------------------------- |
|
||||
| 내 신청 목록 조회 | GET | `{BASE}/my/doc` | Query: `ElaprMyDocSearchDto` + Pageable | `PageResponse<MyAdssDocResponseDto>` | `elaprPrgrsSttsService.findMyElaperDocList` / `useElaprPrgrsSttsMyDocPage` |
|
||||
|
||||
**Request (`ElaprMyDocSearchDto` + Pageable)**
|
||||
|
||||
| 필드 | 필수 | 설명 |
|
||||
| ------------------------ | ---- | ------------------------------------------------------------ |
|
||||
| `startDate` | N | 검색 시작일 `YYYY-MM-DD` |
|
||||
| `endDate` | N | 검색 종료일 `YYYY-MM-DD` |
|
||||
| `docSeComCd` | N | 문서구분 (`INSTL` / `FNRS` / `DESIGN` / `SEWER` / `PWRK` 등) |
|
||||
| `prgrsSttsComCd` | N | 문서진행상태공통코드 |
|
||||
| `page` / `size` / `sort` | N | Spring Pageable |
|
||||
|
||||
**Response (`MyAdssDocResponseDto`)**
|
||||
|
||||
| 필드 | 타입 | 설명 |
|
||||
| ---------------- | ------- | ------------------------------------------- |
|
||||
| `docNo` | string | 문서번호 |
|
||||
| `docTtl` | string? | 문서명/사업명 |
|
||||
| `docSeComCd` | string? | 문서구분 코드 |
|
||||
| `docSeComNm` | string? | 문서구분 코드명 (`ASDOCSE`) |
|
||||
| `reqDt` | string? | 신청일시 `yyyy-MM-dd HH:mm:ss` |
|
||||
| `prgrsSttsComCd` | string? | 문서진행상태 코드 |
|
||||
| `prgrsSttsComNm` | string? | 문서진행상태 코드명 |
|
||||
| `cmptnExpctDt` | string? | 완료(예정)일 `yyyy-MM-dd` |
|
||||
| `picNm` | string? | 담당자 성명 |
|
||||
| `picTelno` | string? | 담당자 연락처 |
|
||||
| `chgCycl` | number? | 변경차수 |
|
||||
| `rgtr` | string? | 등록자 UUID |
|
||||
|
||||
### 6-3. 공통코드 (콤보)
|
||||
|
||||
| 용도 | 상위코드 | API | 비고 |
|
||||
| -------- | ------------ | --------------------------- | ----------------------------- |
|
||||
| 구분 | `ASDOCSE` | `useCodeList("ASDOCSE")` 등 | 기존 관리 화면과 동일 |
|
||||
| 진행상태 | `{docSeComCd}STTS` | `useCodeList(docSeComCd + "STTS")` | 예: `INSTLSTTS`, `FNRSSTTS`, `SEWERSTTS`. 구분 미선택 시 미호출 |
|
||||
|
||||
---
|
||||
|
||||
## 7. UI 구현 가이드
|
||||
|
||||
### 7-1. 기술 스택·원칙
|
||||
|
||||
- UI: `@common/components` — DatePicker, Select/`CodeSelect`, Button, DataTable, Pagination
|
||||
- 목록: `@tanstack/react-table` + `useDataTable` (기존 `instl-list` / `fnrs-list` 패턴)
|
||||
- 데이터: React Query `useElaprPrgrsSttsMyDocPage(params)`
|
||||
- 검색 조건·페이지: URL `searchParams` 동기화 권장 (`useSearchParamsState` 스킬)
|
||||
- 진행상태 컬럼: `prgrsSttsComNm` 텍스트 렌더링 (단계형 프로그레스 미사용)
|
||||
- 행 클릭: `docSeComCd` + `prgrsSttsComCd` + `rgtr`로 view/edit 분기 (`SEWER`는 `chgCycl` query 포함)
|
||||
|
||||
### 7-2. 앱·파일 배치
|
||||
|
||||
| 구분 | 경로 |
|
||||
| ------------ | -------------------------------------------------------------- |
|
||||
| 라우트 | `app/(main)/(with-base-layout)/my-pbadms-sprt/page.tsx` |
|
||||
| 페이지 | `pages/my-pbadms-sprt/ui/my-pbadms-sprt-list-page.tsx` |
|
||||
| 목록 그리드 | `pages/my-pbadms-sprt/ui/my-pbadms-sprt-list.tsx` |
|
||||
| 경로 해석 | `pages/my-pbadms-sprt/lib/resolve-detail-path.ts` |
|
||||
| 테이블 컬럼 | `pages/my-pbadms-sprt/model/my-pbadms-sprt-table-columns.tsx` |
|
||||
| Entity API | `entities/elapr/api/elapr-prgrs-stts-service.ts` |
|
||||
| Entity Hook | `entities/elapr/api/queries.ts` → `useElaprPrgrsSttsMyDocPage` |
|
||||
| Entity Model | `entities/elapr/model/elapr-prgrs-stts-interface.ts` |
|
||||
|
||||
### 7-3. 영역별 UI 매핑
|
||||
|
||||
| 영역 | 컴포넌트 | 데이터 |
|
||||
| ---- | -------------------------- | ------------------------------------------ |
|
||||
| A | `my-pbadms-sprt-list-page` | 로컬 state → `ElaprMyDocSearchDto` |
|
||||
| B | DataTable + columns | `useElaprPrgrsSttsMyDocPage` → `content[]` |
|
||||
|
||||
### 7-4. 다이얼로그·라우팅
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ----------- | -------------------------------------------------------------------- |
|
||||
| 다이얼로그 | 없음 (목록 전용) |
|
||||
| 목록 라우트 | `/my-pbadms-sprt` |
|
||||
| 상세 이동 | §5-3 표와 동일 (view/edit + `rgtr` 소유자 검사, SEWER `chgCycl`) |
|
||||
|
||||
### 7-5. 데이터 흐름
|
||||
|
||||
```
|
||||
BEGIN
|
||||
|
||||
// 최초 진입
|
||||
SET 기간 = 최근 1개월
|
||||
CALL findMyElaperDocList
|
||||
DISPLAY 영역 B
|
||||
|
||||
ON [검색] CLICK DO
|
||||
CALL findMyElaperDocList // → 영역 B 갱신
|
||||
END ON
|
||||
|
||||
ON 페이지 CHANGE DO
|
||||
CALL findMyElaperDocList
|
||||
END ON
|
||||
|
||||
ON 영역 B 행 CLICK DO
|
||||
IF NOT docNo OR unsupported docSeComCd THEN
|
||||
// no-op
|
||||
ELSE IF canEdit(prgrsSttsComCd, rgtr === userUnqId) THEN
|
||||
NAVIGATE edit 경로 // SEWER면 ?chgCycl=
|
||||
ELSE
|
||||
NAVIGATE view 경로
|
||||
END IF
|
||||
END ON
|
||||
|
||||
END
|
||||
```
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -71,10 +71,10 @@ export function middleware(req: NextRequest) {
|
||||
return addSecurityHeaders(response);
|
||||
}
|
||||
|
||||
// if (pathname === "/") {
|
||||
// const response = NextResponse.redirect(new URL("/dashboard", req.url));
|
||||
// return addSecurityHeaders(response);
|
||||
// }
|
||||
if (pathname === "/") {
|
||||
const response = NextResponse.redirect(new URL("/dashboard", req.url));
|
||||
return response;
|
||||
}
|
||||
|
||||
logMessages.push("\n--------------------------------\n");
|
||||
console.log(logMessages.join("\n"));
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 483 KiB |
@@ -0,0 +1,63 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 538 755">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #fff;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<!-- Generator: Adobe Illustrator 28.7.9, SVG Export Plug-In . SVG Version: 1.2.0 Build 218) -->
|
||||
<g>
|
||||
<g id="Layer_1">
|
||||
<g id="han" data-label-shift="dx,dy">
|
||||
<path class="cls-1" d="M159.2,197l-5,5.8,5.1,3.7,13.2,9.7-7.3,3.5-8.2-1.2-9.4,1.4,1.4,4.9-3.5,7.3,3.8,2.6-.9,4.7,8.9-2.3,4.2-3.7,5.2,4.2-6.1,2.3.9,12,3,7-.9,3.3-.5,1.6,5.4,6.8,5.2,13.4h-3.6c0-.1,6.4,3.8,6.4,3.8l4.8,6h1.2l3.6-5.4,3.6-.6,13.8,5.9,19.7-1.1,4.6-6.5,3-5.2,8.2-1.7,11.7-17.8,7.1,1.9,4.9,10,13.2,9.6,3.8,13-1.4,12.2-.5,9.5,1.6,1.1-.6,1.3v1.1c.1,0-2.2,2.4-2.2,2.4l-17.5,7.4,7.6,19.2,16.3,1.1,8.4,10.8,12.6.6,4.2-5.2-4.6-7.9-9.2-6.2,12.1-7.1,6.4,5.2,1.3-5.6-6.6-6.5,17.5-5.8,4.8,4.4,4.8-2.1-4.7-6.5,4.5-8.8,6.3-.4,4.7-3,8,3,24.1,4.5,11.5-7.8-2.8-6.4,6.5-11.7,10.5-5.5,7.6-7.9,10.2-2.3,13.4,4.6,4.4-8,12.4,8.2,3.4-3.1.2-15.2.4-11.3,13.4-3.2,4.6-.4-1.9,9.9,9.8,4.6,1.6,12.8,12.2,9.2,9.8-8.4,15-8.9-3.7-19-21.8-40.6-5.7-10.6,1.2-6.8-4.9-6.1-4-4.9,1.2-5.2-26.3-33.1-5.8-12.1-7.1-14.9-11.9-20.9-2.9-9.4-2.1-6.7-1.8-5.2-8.7-25.5-8.6-14.4-.9-11-5.2-11-3-6.3-6,6.5-5.2,22.8-9.8,11-5.5,2.8-7.4,3.7-17.1.5-12.7-1.6-8.2,2.3-5.2-3.4-3.4-2.2-18.8-2.3-21.8,4.5-19-3.7-3.5,4.7-11.2-7.3-5.2,4.2-6.1-.7-11.5,3.5-14.1,10.8-3.7,10.1-12.9,9.8-4,7.3-8.5,6.8-2.3,1.9-9.1,4.5-4,18.3-9.2,11.5-14.3,1.2-2.1,30,5.6,1.4,3.3,9.2-1.4,2.7.9,2.6,2.9-.7,1.8,3.2-4.5,2.2,1.7,8.6,3.8,6.9,8.9,3.4Z"/>
|
||||
</g>
|
||||
<g id="yeongsan" data-label-shift="dx,dy">
|
||||
<path class="cls-1" d="M57.4,578.4s.2,1.4.2,2.3.7,1.6.7,1.9.5,1.4.7,2.3c.2.9,1.2,1.2,1.2,1.2h0s.5,1.4.5,1.4c0,0-.5.7-.9,1.4-.5.7,1.6.9,1.6.9l2.8-.7s1.4-.7,1.4-1.2,0,0-.2-.7c-.2-.7-.5-.7-.9-1.2-.5-.5.5-.2.5-.2l.5.2,2.6-.5s.9-.5,1.4-.7c.5-.2-.2-.5-.2-.9s-.2-1.2-.2-1.2l.2-.9v-1.2c0-.5.7-.7.9-1.2.2-.5.5-.7.9-1.2.5-.5,0-.7,0-1.6s-.5-.5-.5-.9-.5,0-.5,0l-.9.5-.9.7s-.2.9-.5,1.2c-.2.2-1.4.2-1.4.2,0,0-.2-.5-1.2-.9-.9-.5,0,0-.5,0s-1.2-.2-1.2-.2c0,0-1.2-.7-1.6-.9-.5-.2-.5-.9-.5-.9h-1.2s-1.9,1.4-2.8,1.9c-.9.5,0,1.2,0,1.2Z"/>
|
||||
<path class="cls-1" d="M45.2,585.7c.2.2,0,.2.7.9.7.7.5,1.6.9,1.6s.2-.5,1.2,0c.9.5,1.4.5,1.9.7.5.2,1.6.9,1.9.9s1.4.7,1.4.9-.2.2,0,.5c.2.2,0,.5.5,1.4.5.9,1.9,2.6,1.9,2.6,0,0-.7.7.5.5,1.2-.2,1.9-.2,1.9-.9s.2-1.2.2-1.4-.2-.2-.5-1.2c-.2-.9-.7-1.6-.7-1.6,0,0,1.2-.2,0-1.2-1.2-.9-.2-1.2-1.2-.9-.9.2-1.9.2-1.9.2-.2-1.2-.2-1.9-.2-1.9l-.5-.5.7-1.4h0c.5,0,.9-.2,1.2-.2s.9-.7.9-.7c0,0,.2-.2,0-.7-.2-.5-.2-.2-.9-.7s-1.6-.7-1.6-.7l-2.1-1.2-1.4-.9c-.7-.2-2.6.9-2.6.9,0,0,.9-.5,0,.7-.9,1.2-2.3,1.9-2.3,1.9,0,0-.2.5,0,.9.2.5,0,1.2.2,1.4Z"/>
|
||||
<path class="cls-1" d="M28.6,585.5s0,.7,1.2.7h2.1c.5,0,.9.7.9.7v1.9l1.2.7s1.4,0,1.9-.5c.5-.5.5-.7.5-.7l-.5-1.4,2.3-.7s.5-.5.7-1.2c.2-.7-.2-1.2,0-2.3.2-1.2,0-2.8,0-2.8,0,0,.9-1.4.9-2.1s1.9-2.6,1.9-2.8h0v-.7h-1.4c-.2,0-1.7.5-2.1.7-.5.2,0,0-.9,1.2s-.2,1.6-.2,1.6l-1.9.5-1.4.7-.9.2s-1.2-.2-2.1-.2-.5.9-.5.9c0,0-1.2,1.4-1.9,1.6-.7.2-.7.7-.7.7,0,0,.9,2.3.9,2.8v.5Z"/>
|
||||
<path class="cls-1" d="M41.7,591.4c-.5.2-.7-1.2-.5,0,.2,1.2,1.2,2.1,1.2,2.1l1.4.9s.9-.2,1.4,0c.5.2.9,0,.9,0l1.9.7v.2s.7-.2.5-1.6c-.2-1.4,0-1.4,0-1.4l-.7-.9-2.6-1.4h0s-2.8-.7-3-.5c-.2.2-.5-.2-.5.2s.5,1.4,0,1.6Z"/>
|
||||
<path class="cls-1" d="M34.2,597.7s0,0,0,0c0,0,0,0,0,0Z"/>
|
||||
<path class="cls-1" d="M45,598.9l-.7-.7-.5-.9s.2-1.4-.2-1.4-1.9-.5-1.9-.5l-1.4-.7h0s-1.4,0-1.4,0c0,0-.9,0-1.2.7-.2.7-.9.5-.9.9s-2.1,1.2-2.5,1.4c0,0,.2.2.4.5.5.7.5,1.4,1.2,1.4s2.1.2,2.3,0c.2-.2.9-.7,1.2-.5.2.2.5.5.5.9s-.5,1.2-.5,1.6.5,0,0,.5c-.5.5.2,1.4.5,1.6.2.2-.7,2.1-.7,2.1,0,0-.5.5-.5.7s1.4.7,1.6,0c.2-.7.7-1.2.9-1.4.2-.2.9-.5.9-.5,0,0-.5.2.7.2s2.1.2,2.1-.5v-1.9s-.2-1.2,0-1.4c.2-.2,0-2.3,0-2.3Z"/>
|
||||
<path class="cls-1" d="M18.5,619.7c.7,0,1.9-.9,2.3,0,.5.9,1.9,2.6,1.9,2.6,0,0,.2-.2,1.4,0,1.2.2,1.2.5,2.1,0,.9-.5,1.4-1.4,1.4-1.4l1.2-1.2,3.3-1.4s-.2-.7-.7-2.1c-.5-1.4-.7-2.1-.7-2.1,0,0,1.2.2.5-.9-.7-1.2-.9-1.9-.9-1.9l-.5-1.6h0s-.7-.9-.7-.9l-2.1.2s-.2,1.2,0,1.9c.2.7.2.9,0,1.4-.2.5-.7-.5-1.2,0-.5.5-2.3.2-2.3.2h-1.2l-.9,1.4-1.2.9-2.3.9-2.1,1.4s-.5,0,0,.9c.5.9,2.1,1.6,2.8,1.6Z"/>
|
||||
<path class="cls-1" d="M33.1,624h-.9l-1.4-.7-2.1.7s-.9,1.2-.9,1.6.7,1.9,1.4,2.3c.7.5,1.6.7,1.9,1.6.2.9.2,1.9.2,1.9,0,0,1.9.7,2.3-.7.5-1.4.2-2.3.7-2.6.5-.2,1.9-1.6,1.9-1.6,0,0-.5-.5.7-.5s1.6-.5,1.6-.7v-1.9l-.9-.9h.9l.7-.9s.2-.7,0-1.2c-.2-.5-.5-1.9-.5-1.9l-2.6.5h0l-1.4,1.4-.5.9s.2.9,0,1.2c-.2.2-1.2,1.4-1.2,1.4Z"/>
|
||||
<path class="cls-1" d="M65.6,620.2l1.9-.7-2.1-2.6-3.5-1.9h0s-3.3.7-3.3.7l1.6,2.1.9,1.4s-.5.5-.9.9c-.5.5-1.6.7-1.6.7,0,0-.5.5-.5.9s-.9,0-2.1.7c-1.2.7-4.5,1.9-4.5,1.9h-.9c.9,0,1.6,2.6,2.3,2.6h3.1s-.2-.5.9-.7c1.2-.2,2.6-1.2,2.6-1.2,1.4.2,2.3-.7,3,.5.7,1.2,1.4.9,1.6,1.4.2.5,1.2,2.1,1.2,2.1l-.7,2.6s1.2.9,1.6.7c.5-.2,1.9-1.4,1.9-1.4l.9-1.9s-.5-1.4,0-1.9c.5-.5-1.6-1.4-1.6-1.4l-2.8-.7-1.9-2.6v-1.2l2.8-1.2Z"/>
|
||||
<path class="cls-1" d="M64.2,635.5v-1.6s-3.3,1.4-3.3,1.6.9,1.6.9,1.6h1.6l.7-1.6Z"/>
|
||||
<path class="cls-1" d="M59.8,639.2c0,1.2.5,2.8,1.2,3,.7.2,1.6,0,1.6,0v-3l-1.6-.9s-1.2-.2-1.2.9Z"/>
|
||||
<path class="cls-1" d="M38.4,638.3c1.2,0,2.8-4.5,2.8-4.5v-1.4h0s-3.5,0-3.5,0l-2.1,1.4-3.5.7s-.2,1.9,1.4,1.9.7-.1.6-.2c0,0,.3,0,.6.2,1.6.5,3.3.7,3.3,1.2s-.7.7.5.7Z"/>
|
||||
<path class="cls-1" d="M15.4,630.1h0s0,0,0,0h0Z"/>
|
||||
<path class="cls-1" d="M7,642.7c.2-.5,1.2-.7,1.2-1.4s-1.6-2.6-.5-3.1c1.2-.5,2.6-.9,2.8-1.2.2-.2,1.2-.7,1.6-.5s.9.9.9.9l1.4-.5.5-.9s.2-.9.5-1.4c.2-.5.7-1.9.7-1.9v-1.2l-.7-1.6c-.3.2-2.6,2.1-2.8,2.3-.2.2-3,1.2-3.3,1.2s-3.1.2-3.1.2l-3.8.5-2.6,1.9,1.9,2.1s-.2,1.9,0,2.1c.2.2-.5,1.2.5,1.6.9.5,1.6.9,1.6.9,0,0,2.8.2,3-.2Z"/>
|
||||
<path class="cls-1" d="M16.6,649.1c0-.7-1.2-1.6-1.2-1.6l-.7-.7-.9-.9s-.2-1.2-.2-1.4-.7-.9-.7-.9c0,0-2.3.2-2.3.5s-.7.7-.7.7c0,0-.2.7-1.6.7s-2.8-.5-2.6,0c.2.5-.2.9-.2,1.6s-.5,1.9-.5,1.9v1.2s.2,1.2.7,1.4c.5.2,1.2.5,1.4.7.2.2,1.4.9,1.2,1.4-.2.5,2.3.2,2.3.2l.5-.5c.2-.2,1.9-1.2,1.9-1.2,0,0,0-.9.5-.9s3.3-.5,3.3-.5v-1.6Z"/>
|
||||
<polygon class="cls-1" points="12.9 643.4 12.9 643.4 12.9 643.4 12.9 643.4"/>
|
||||
<path class="cls-1" d="M27.7,639.9c.5.5,1.4,1.6,2.1,1.9.7.2,1.2.5,1.4,1.2.2.7.2,1.4.5,2.1.2.7,1.9.7,1.9,1.2s.5-.2,1.4,0c.9.2.2-.2,1.6,1.2,1.4,1.4,2.3,2.1,2.6,2.6.2.5.9,0,.9,0l1.2-.9s-.2-1.6,0-1.6.2-2.1.2-2.3-.5-2.3-.5-2.3c0,0-1.2-1.4-1.4-1.4s-2.1-.7-2.1-.7l-1.2-1.2-.9-1.2-1.4-.5h0l-3.7-.7h-2.1l-1.2,1.6s.2.7.7,1.2Z"/>
|
||||
<path class="cls-1" d="M32.6,663.4c.5.2,2.3.5,2.8.5s1.6-.2,1.9-.5c.2-.2.9-.2,1.6-.2s3.5.2,3.5.2c0,0,.5-.7.2-1.4-.2-.7-.9-1.4-.9-1.4l-.2-.7-.7-.5.7-1.2v-.9s-.7-.5-1.4-.5-1.4.2-1.4-.2-.9-1.9-.9-1.9l-1.4.7h0c-.9.5-1.9.7-2.1.9-.2.2-3.3.2-3.1.7.2.5.7,1.2.9,1.4.2.2.9,1.6.9,1.6,0,0-.5,1.6-.5,1.9s-.5,1.2,0,1.4Z"/>
|
||||
<path class="cls-1" d="M21.3,670.4c0-.2,0-1.4.5-1.6.5-.2.7-.5.7-.7s-.7-1.2-.7-1.4-1.2-1.2-1.2-1.2l-.5-1.2-.2-1.2-.2-1.9h0s-.9-1.9-1.2-1.9-1.4-.2-1.4,0-.5.9-.5.9v.7c0,.2-.2.7,0,1.2.2.5-.2,1.4,0,1.6.2.2.2.7.5,1.4.2.7.7,1.2.7,1.4s-.2,1.2-.2,1.2c0,0-.5-.7-.5.5s-1.9,1.9-.5,2.1c1.4.2,2.8.5,3,.5s1.6-.2,1.6-.5Z"/>
|
||||
<path class="cls-1" d="M27.4,673s-.5-.9,0-1.6c.3-.5.5-.9.7-1.2.2-.5.9-.5.9-.9s.5-2.3,0-2.8c-.5-.5-.9-1.6-1.4-2.3-.5-.7-.2-.9-1.2-1.6-.9-.7-1.9-1.2-1.9-1.2l-1.2.2h0s-.7,2.1-.7,2.3.2,2.3.7,2.6c.5.2,1.6,1.4,1.9,1.9.2.5.2,1.2.2,1.6s-.7,2.1-.9,2.1,0,0-1.2.5c-1.2.5-2.6.2-3,.9-.5.7-1.2,1.4-1.2,1.4v1.4c0,.2-.2.7.5.7s2.3-.2,2.8-.5c.5-.2,1.4-.7,1.4-.7l2.3-2.1,1.2-.7Z"/>
|
||||
<path class="cls-1" d="M63.7,683.1c-.2-.5-1.4-2.1-1.4-2.1l-.5-.9s0-1.6-.9-1.9c-.9-.2-3-.2-3-.2l-1.6-2.3s-1.4-2.6-1.6-2.6-1.4,0-2.1-.2c-.7-.2-2.6-1.2-2.6-1.2l-.9.7c.2.7,1.6,2.8,1.6,2.8l1.2,2.3-.5,1.9s-1.9.5-2.3.5h-3s-.5-.7-.7.7c-.2,1.4-2.6,2.6-2.6,2.6h-3s-.9,1.6-.9,2.1-6.1,3.1-6.1,3.1l-1.9,2.6-1.9.5s-.5.7-.5.9-1.2,1.6-1.2,1.6h-.7s-2.3.5-2.3.9.5,1.6.5,2.1v1.6s0,1.6.2,1.6,1.4.7,1.4.7l.7,1.2s.5.2.5.5.7,1.2.7,1.2l1.2,1.2s-.5,1.6,0,2.1c.5.5.9.5,1.4.7.5.2,4.2.7,4.2.7,0,0,1.9.2,2.1.5.2.2,1.4.5,2.1,0,.7-.5,7-1.6,7-1.6,0,0-.7-.9,0-1.2.7-.2,1.9-.5,3-1.4,1.2-.9,3.7-1.2,3.7-1.2,0,0-.5-1.2.2-1.6.7-.5,2.6-2.1,2.8-2.1s2.1,1.4,2.1,1.4c0,0-.7.7.5.7s1.4-.5,1.9-2.1c.5-1.6,1.9-2.3,1.9-2.3l.5-.7s-.5-.5,0-1.6c.5-1.2.9-2.6.9-2.6v-3.5s-.5,0,.5-.5c.9-.5,2.1-2.3,1.4-3.1-.7-.7-1.6-1.9-1.9-2.3Z"/>
|
||||
<polygon class="cls-1" points="49 672.3 49 672.3 49 672.3 49 672.3"/>
|
||||
<path class="cls-1" d="M93.5,737.5c-2.3-1.2-4.2-2.3-4.2-2.3h-1.9l-1.2,3.5s1.4,0,1.2.9c-.2.9-1.6.5-1.2,1.6.5,1.2-.5.7,1.6,1.6,2.1.9,2.8,1.9,3.7.9.9-.9,1.9-2.6,1.9-2.6h0l.7-1.2s1.6-1.4-.7-2.6Z"/>
|
||||
<path class="cls-1" d="M86.2,745l-2.8-1.9s-2.8,2.6-2.8,3.1-2.1,3.3-1.6,4,1.2,1.6,2.8,1.6,3.3-1.6,5.4-2.6c2.1-.9,2.8-2.3,3.7-1.9.9.5,2.1-.5,2.1-.5h0l-1.9-1.2-4.9-.7Z"/>
|
||||
<path class="cls-1" d="M101.7,742h0l-3.5,1.2s-.9,1.2-.9,1.6.5,2.6.5,2.6c0,0-.5.5-.5,1.6v2.3s1.2,3.1,2.1,3.5c.9.5,2.6-.5,3.1-1.9.5-1.4-.9-5.6-.9-5.6,0,0-2.8-1.2-2.1-1.4.7-.2,2.8-.5,3.1-.5s.9-.7.9-.7l-.7-2.8h-.9Z"/>
|
||||
<path class="cls-1" d="M115.1,720.4l-1.2-1.6-.5-1.6h0v-2.6l-2.1-3.3s-1.6-1.2-1.9-1.6c-.2-.5-3-2.3-3-2.1s-2.3,2.3-2.3,2.6-3.7,2.8-3.7,2.8c0,0,1.2,4,1.2,4.2s-1.9.2-.5,1.9c1.4,1.6,5.9,2.1,5.9,2.1,0,0,.2-1.4.5.5.2,1.9-.5,2.8-.5,3.1s1.2,1.2,2.6,1.2,3.3-.2,3.5,0c.2.2,3.5.5,3.5.5,0,0,.2,0,0-2.1-.2-2.1-1.4-3.8-1.4-3.8Z"/>
|
||||
<path class="cls-1" d="M125.6,711.9c.5,0,1.4-1.9,1.4-1.9,0,0-.5-.7.9-.5,1.4.2.9,0,1.6.5.7.5,1.2.5,1.4.5s1.2-.7.5-1.4c-.7-.7-1.4-.9-1.9-1.6-.5-.7-.5-2.6-.5-2.6,0,0-.5-1.4-.7-1.4h-2.8l-.9.9h0c-.7,0-3.5-.7-3.5-.7l-.5,2.8-2.1,1.4s-.9.5-.5,1.2c.5.7.9,1.2.7,1.6-.2.5-1.6,0-1.4.7.2.7,1.6,1.9,1.6,1.9,0,0-.7,1.2-.7,1.6s.9,1.4,1.4,1.4,1.9-2.1,1.9-2.1c0,0,.5-.9.9-1.2.5-.2,2.1-1.2,2.1-1.2h.9Z"/>
|
||||
<path class="cls-1" d="M141.5,712.4c0-.2-.9-.5-1.2-.7-.2-.2-1.6-.7-1.6-.7l-3-.9h-1.7s-.7,2.1-.7,2.3-.5,1.2,0,1.4c.5.2,2.1.7,2.1.7v1.2c0,.5-.7.7.5,1.2,1.2.5,4.2-.5,4.2-.5h2.6s.9-1.4.5-1.9c-.5-.5-1.6-1.9-1.6-2.1Z"/>
|
||||
<path class="cls-1" d="M128.6,725.1s.2-.2.5,0c.2.2,1.4-.5,1.4-.5,0,0-.2-.5.5-.5s1.9.5,1.9.5c0,0,.7-.2.9-.7.2-.5-.2-.9,0-1.4.2-.5,0-1.6,0-1.6l-.7-1.6s-.9-.2-1.4-.2-1.2-.2-1.2-.2l-.5-.2-.9-.7h0s-2.3-.2-2.6.5c-.2.7-.7,1.6-.9,1.9-.2.2-1.9.9-1.9.9l1.4,1.4s.2.7,1.2,1.2c.9.5,2.3,1.4,2.3,1.4Z"/>
|
||||
<path class="cls-1" d="M121.1,719.9c-.3-.3-.7-.5-1.2-.5h0c-.6,0-1.1.6-1.2,1.2,0,.4.2.8.5,1.2.1.2.3.5.7.7.5.3,1.4.6,1.6.2.2-.2,0-.5-.2-1.6,0-.7,0-.9-.2-1.2Z"/>
|
||||
<path class="cls-1" d="M137.3,747.6c-.9-.5-1.9-.9-1.9-.9l-.5-1.2.9-1.2.2-2.3-4.2-.5-2.1.5h0s-1.2,2.1-1.4,2.6c-.2.5-2.1,1.9-2.1,2.1s0,.7-.2,1.4c-.2.7-.9,1.9,0,2.3.9.5,2.1.7,2.1.7,0,0,2.6-1.4,2.8-.7.2.7.2,1.2,1.2,1.9.9.7,2.1.9,2.3.9s.2-1.2.9-1.4c.7-.2,2.6-1.9,2.6-1.9,0,0,.2-1.9-.7-2.3Z"/>
|
||||
<path class="cls-1" d="M267.1,679.6s.5-.5,0-1.6c-.5-1.2-.7-2.1-.7-2.1l-.7-1.2-.5-1.4s-.5-.5-.2-1.2c.2-.7,0-2.6,0-2.6l-.2-.7.7-.9h0c.2-.2.5-2.1.5-2.1l-3.3-2.3s-1.4.9-1.2,1.9c.2.9.9,2.1.9,2.6s.2,1.6,0,2.1c-.2.5-.9,1.2-.9,1.4s-.7,1.4-.9,1.9c-.2.5-2.8,1.9-2.8,1.9,0,0-1.9,1.9-1.9,2.1s-1.2.2-.7,1.6c.5,1.4.5,3.3.5,3.3,0,0-1.2.9-.5,1.6.7.7,1.4,1.6,2.8,2.1,1.4.5,4.2,1.6,4.2,1.6l3.1.5s2.3-.9,1.9-1.4c-.5-.5-1.6-1.4-1.6-1.4l-.5-1.2s0-1.2.5-1.9,1.6-2.6,1.6-2.6Z"/>
|
||||
<polygon class="cls-1" points="279.4 600.4 279.4 586.1 272 579.9 262.7 571.1 261.3 561.5 254.5 559.9 251.7 557.3 244.7 562.9 243.5 566.9 235.1 563.3 234.4 553 235.1 535.7 240.9 533.6 245.9 532.1 247.1 523.8 256.9 510.3 248.3 507 239.9 515.4 232.7 509.4 238.7 493.8 231.6 486.6 229.2 475.8 217.2 472.2 206.4 484.2 205.2 493.8 195.7 486.6 186.1 495 186.1 502.2 176.5 510.6 159.8 515.4 157.4 523.8 140.6 521.4 127.4 517.8 124.5 512.7 119.4 508.3 115.3 512.5 110.2 512.4 96.4 517.8 89.3 530 84.4 535 85.6 538.3 88.3 540.1 87.2 541.1 82.3 539.9 76.8 551.3 72.3 553.6 67.3 564.8 76.6 566.5 73.6 571.4 75.3 574.4 80.6 583.9 85.4 585.9 85.4 589.7 80 596 78.4 589.4 69.9 588.5 68.8 590 71.3 593.4 75.4 595.9 74.3 598.4 69.7 600 68.7 603.3 63 605.4 61.9 611.5 67.8 615.5 70.4 613.1 73.2 605.9 74.3 613.6 75.3 626.5 69.7 634.5 69.7 639.2 68.1 643.4 67.8 644.3 80.4 650.4 92.7 661.4 89.3 661.9 86.5 659.8 85.6 664 79 654.2 69.4 650 67.8 656.3 70.6 661.9 74.3 663.3 77.1 670.4 72.7 669.9 66.1 661.5 62.9 648.1 58.5 642.1 51.9 654.2 55.1 668.3 59.6 674.6 61.9 673.9 65.8 676.6 67.1 680.5 69.9 680.2 72.7 677.9 79 686.6 77.7 698.2 82.8 699.7 79.7 706.7 76.6 709.9 77.4 712.1 82.5 712.8 82.7 721.2 93.1 719.9 97.3 713.3 97.1 709.8 112.1 701.6 110.7 695.9 117.2 694.3 120.8 688.2 120.2 673.4 122.8 671.1 124.7 675.1 124.5 684.5 122.4 694.3 125.7 699.9 130.4 702.1 134.8 704.2 136.9 700.4 143.5 702.3 145.8 697.6 149.1 698.3 148.3 688.6 152.1 687 152.9 684.2 151.7 682 155 673 162 671.1 167.4 666.2 174.9 662.4 179.6 664 183.8 662.6 187.1 655.8 192.4 654.7 194.6 657.2 202.1 657.2 202.7 663.8 200.6 669.2 196 669.8 194.8 663.8 189.6 664 187.5 671.6 189.1 675.6 185.9 681.9 181.9 681.2 183.1 676 181.4 674.3 176 692.2 189.2 694.5 191.7 701.4 195.5 706.3 203.9 704.4 207.4 697.8 213.3 694.1 214.2 689.9 209.3 687.1 210 684 218 684.7 221 686.6 224.2 683.9 222.7 674.6 209.2 661.4 209.5 657 212.8 653.7 210.5 650.9 215.4 646.2 211.9 643.7 213.5 642.5 216.5 642.5 222 642.5 227.6 640.4 228.1 635.9 230.9 638 231.2 641.7 231.8 647.4 236.2 650.7 238.4 657 234.6 663.8 232.6 672.3 234.9 676.9 238.7 676.9 243.5 679.8 244.5 675.1 243.3 671.1 245.9 662.9 249.9 659.4 255 664.1 261.6 661 262.3 655.8 265.1 651.9 267 645.5 264.6 642 261.1 642.5 257.9 642 248 646 246.3 642 239.9 634.6 244.2 626.8 246.3 631 251.6 632.8 262.5 623.5 278.8 628.6 285.4 617.2 285.4 607.6 279.4 600.4"/>
|
||||
</g>
|
||||
<g id="nakdong" data-label-shift="dx,dy">
|
||||
<path class="cls-1" d="M405.4,649.3l-2.1-.2-1.6,1.2-.9,1.6-1.2.2-.9-.5v-1.2l.2-.9,1.4-.9.9-.7v-.9l.9-.9.7-1.2-.2-1.6-.2-1.4.5-3v-1.9l-1.6-1.6-.9-.9.2-1.9.7-2.8h0c0,0,1.6-.5,1.6-.5,0,0,1.2-.9,1.2-1.2s.2-.7.2-.7l-.2-.9-1.4-.9-1.9-.7-1.6.2-1.2.5-.9,1.2-.7,1.2-.2,1.2.5,1.9v.9l-.5.7-.7.2-1.2.2-.9.5-.5.5-.2,1.2-.2.5h-1.4l-1.4.5s-.9.2-1.2.2-.7.7-.7.7l-1.2.5h-1.4c-.2,0-.7.2-.7.2l-.5.5v.5l.2.7s.7.7.9.7,0,.7,0,.7l.2.9v.7l.2.9.5.5.7.5s.5.5.7.5.2.7.2.7c0,0,.5.7.5.9v2.1h-.2s-.7-.2-.9-.2-.5-.5-.5-.5l-.9-.7h-.9l-.9-.5-.5-.7-.7-1.2-.7-.5-.5-.2-.5.2-.7.7-.7.2h-.9l-1.4-.9-1.4-.9-1.4-.7h-1.2l-.5.2-.7.9-.5.7-1.2.7s-.2,0-.5.2c-.2.2-.7.5-.7.5l-.5.7-.7,1.2-.5,1.2-.9.9v3.3l1.2,1.4s.7,1.4.7,1.6.2,1.4.2,1.4l.7,1.2.7,1.6,1.6.2,1.4-.9.7-1.2.7-.7h2.6l1.4-1.2.9-1.6.7-.9.7-.5.5.5.5.7v2.6s.5.5.7.5,0,.7,0,.7l-.7,1.2-1.2,1.9-.5.7-.2.5-.2.5-.9.5-1.2.2-1.4.2-.7.5-.5.5-.2.7,1.4.5.5.5v.7l.5.5.7.2h1.2l2.1.2.7.5.2.7-.2.5-.7.5s-.9.7-1.2.7-.5.9-.5.9l-.2.7.2.9v.7l.9.2.9.7,1.2.2v.9l-.2.2-.9.5-1.4.5-.7.2-.5.7v.9l.5.7s.5.5.7.5,1.4.2,1.4.2c0,0,1.4.2,1.6.2s.9-.5.9-.5l1.2-.5,1.2-.7.5-.7v-.7l.2-1.2s.5-.9.5-1.2.9-.5.9-.5l1.4-.2.2-.7-.9-.9-.7-1.4.5-1.6v-1.2l.7-.7,1.6-.2,1.6-.2.7-.9.2-1.2v-2.1l.7-.7,1.9.2,1.6-.2.5,2.1,1.2,1.4,2.1.9,1.6-.7.2-1.2-.2-1.2-1.6-1.2-.7-1.4.2-1.2-1.2-.7-.9-.9.5-1.4h1.9c.2,0,.7-.5.7-.5l.7-.9v-1.6h.9c.2,0,.7-.5.7-.5l.2-1.4s.2-.5.7-.7c.5-.2,0-1.4,0-1.4v-1.4Z"/>
|
||||
<path class="cls-1" d="M330.3,650.8v-1.2l-.9-.5h0s-4.2-1.2-4.2-.9-2.8,0-2.8,0l-1.4.9s-.2.7-.2.9.2,1.2.2,1.2l.5.2.9.7,2.8.5h1.9l.9-.7.9-.7.9-.2.5-.2Z"/>
|
||||
<polygon class="cls-1" points="333.8 654.8 332.6 654.1 332.2 652.9 331.7 652.7 331 652.9 329.8 653.9 329.6 654.8 329.1 656 329.1 657.6 329.4 659 329.8 659.7 330.8 659.7 331.9 659.5 332.4 659 333.8 659 335 658.8 335.2 657.8 335.2 656.9 334.7 655.7 333.8 654.8"/>
|
||||
<polygon class="cls-1" points="308 649.2 307.6 648.4 307.6 647.5 308 646.3 308.7 645.2 308.7 644.2 308.3 644 307.3 643.5 306.4 642.8 305.5 641.9 304.7 641.2 303.1 641.4 302.2 642.8 302.2 644.7 301.2 645.2 300.8 644.2 300.5 642.6 301 641.4 301.7 639.8 302.4 638.4 303.6 637.2 303.6 636.5 303.3 635.8 302.6 635.8 301 636 299.8 637 298.4 638.1 298.2 639.5 297.3 640 295.6 640.9 294.7 642.4 293.5 643.5 293.3 644.9 293.3 645.9 293.5 647 294.2 648.2 295.4 648.4 297 649.2 299.4 649.2 301.2 648.7 303.1 649.2 304.1 649.9 306.2 651 307.8 652 308.3 651.7 309.4 651.7 309.7 651 309.2 650.3 308 649.2"/>
|
||||
<path class="cls-1" d="M305.2,653.4l-1.2-.5-.9-.7-.7-.2h-2.1l-1.6-.2-.9-.7h-1.6l-1.2,1.2-1.6,1.2-.9.7v.7l-1.4-.2-.5-1.2-.9-1.4-.2-.7-.5-.9-1.2-.5-1.2-.5-.5-.5s-.2-.5-.2-.7-.2-1.2-.2-1.2v-.5c0-.2-.7-.7-.7-.7l-.2-.7-.5-1.2-.2-1.4.7-.9.2-.9,1.6-.2.7-.5.5-.2.2-.7.2-1.2.5-.5.7-.9v-.7l-.2-1.2-.5-1.4.2-1.4-2.1-1.4h-3.1l-1.9.2-1.4,1.2-.9,1.4-.5,1.2-.5,1.4v.7l-1.6.5-.9.5-.7.9s.2.5,0,.7c-.2.2-1.2.9-1.2.9v1.4l-.5,1.2-.7.5-.9.5-.2.9v1.4l.2,1.2-.2,1.4.5,1.6.5,1.9,1.2,1.9h0v1.6l.2,1.4.7.9.9.7.2.7-.2.7-.9.9-.9.7-.5.7-.2.7,1.2.7.7.5,1.4.7v.7l.2,1.2.5.9.9.5.9-.2h1.9l1.2-.2.9-.5.9-.9v-1.4l-.5-.9-.2-1.4.2-.7.7-.5,1.9-.2,1.2-.9h2.3l.5.9v.7s-.5.7-.5.9-.2,1.2-.2,1.2l-.5,1.4v.5l-.2.9.2.5.2.5.2,1.2v1.2l.5.9,2.1.2,2.3.7,1.6-.2,1.2.2.9-.7s.5.5.7.5.5.9.5.9l1.6.7,1.6.2h.7l.2-1.2-.2-1.4-.7-.7-.2-.9v-1.2l.5-.5.9-.9.7-.2.5-.5v-2.3s-.5-2.1-.5-2.3v-.9l.2-1.2.5-1.4s-.2-.9,0-.9.7-.7.7-.7l.7-.5.2-.9-.2-.9-1.2-.5Z"/>
|
||||
<polygon class="cls-1" points="533.7 451.9 529.9 457.3 516.8 465.7 509.4 457.5 516.6 447.7 513.8 417.3 513.1 408.8 521.3 392.4 522.9 355.1 523.6 353.3 531.1 333.9 526.2 315.4 526.2 282.1 519.8 263.8 504.8 272.7 495 281.2 482.8 272 481.1 259.2 471.4 254.5 473.3 244.6 468.7 245 455.3 248.2 454.9 259.5 454.7 274.8 451.4 277.9 438.9 269.7 434.5 277.6 421.1 273 410.9 275.3 403.3 283.3 392.8 288.8 386.2 300.5 389 306.9 377.6 314.7 353.5 310.3 345.4 307.2 340.7 310.3 334.5 310.7 330 319.5 334.6 326 329.8 328.1 325 323.7 307.5 329.5 314.2 336 312.9 341.5 306.5 336.4 294.4 343.5 303.6 349.6 308.1 357.5 304 362.7 314.1 371.6 316.5 383.6 314.1 388.4 326.1 392 323.3 402.6 325.2 414.4 315.5 413.8 315 423.6 307.5 439.1 298 443 298.5 447.6 301.2 454.4 296.8 462.5 286.2 472.3 276.7 473.4 269.4 480.7 267.1 483.1 264.5 485.8 256.9 505.2 256.9 510.3 247.1 523.8 245.9 532.1 240.9 533.6 235.1 535.7 234.4 553 235.1 563.3 243.5 566.9 244.7 562.9 251.7 557.3 254.5 559.9 261.3 561.5 262.7 571.1 272 579.9 279.4 586.1 279.4 600.4 285.4 607.6 285.4 617.2 278.8 628.6 282.6 628.4 288.8 622.6 289.3 620.3 291.9 621.5 292.3 624.3 296.3 623 298.7 624.3 301.6 622.6 298.9 615.9 301.2 614.9 303.4 615.5 304.7 611.2 309.9 609.7 307.1 614.2 306.9 625.2 305.5 632.3 314.8 637.2 317 641.5 318.3 642.1 322.4 640.3 325.1 641.7 326.8 641.4 329.4 634.9 335.5 635.5 338.5 640.5 342.8 638.4 346.2 633 347.4 636.3 348.6 639.8 343.1 642.3 344.4 645.9 349.4 647.4 351.4 645.9 353.5 647.7 353.3 655.5 347.7 654.1 346.2 655.7 351 659.7 353.7 666.6 356.3 667.8 358.7 665.9 359.9 659.7 358.5 655.6 361.6 654.2 365.6 646.5 363.1 643.5 360.7 647.8 358.5 646.8 361.3 632.3 361.1 630.7 361 629.5 368.3 628 371.4 624.3 365.9 619.1 357.2 620.8 355.4 623.6 351.9 624.1 354.8 618.5 361.5 616.3 367.3 614.4 366.1 610.6 379 609.8 381.7 611.6 383.5 619.4 389.9 616.8 383.5 596.4 387.2 591.5 388.9 592.4 387.7 599.2 390.1 604.4 394.7 605 398 608.8 399.5 604.8 401.5 606.5 400.5 612.2 406.2 614.7 414 613.3 414.9 615.6 420.1 616.2 424.6 617.3 428.8 614.1 432.5 616.6 438.5 610.1 436.6 622.3 438 626.7 441.3 625.8 442.5 620.6 445.7 622.8 453.5 612.9 457 616.4 461.6 616.6 461.7 613.8 459.8 610.5 463.1 606.6 469.9 608.2 480.9 594.4 480.7 591.3 485.5 581.8 491 579.8 494.6 576.1 498.5 573.7 497.1 571.8 499.6 567.6 499.9 561.3 498 558.2 504.1 554 504 546.1 505.5 548.8 506.9 554.5 511.6 551.2 514.6 544.4 514.4 537.4 515.8 532.7 513.9 528 514 525.4 524.7 493.4 527.1 486.2 527.8 477.8 535.1 466.5 538 455.6 533.7 451.9"/>
|
||||
</g>
|
||||
<g id="geum" data-label-shift="dx,dy">
|
||||
<path class="cls-1" d="M298,443l9.5-3.9,7.5-15.6.5-9.8,9.7.6-1.9-11.8,2.8-10.6-12-3.6,2.4-4.8-2.4-12-10.1-8.9-12.6-.6-8.4-10.8-16.3-1.1-7.6-19.2,17.5-7.4,2.3-2.4v-1.1c-.1,0,.4-1.3.4-1.3l-1.6-1.1.5-9.5,1.4-12.2-3.8-13-13.2-9.6-4.9-10-7.1-1.9-11.7,17.8-8.2,1.7-3,5.2-4.6,6.5-19.7,1.1-13.8-5.9-3.6.6-3.6,5.4h-1.2l-4.8-6-6.4-3.9-5-.2-9.1-18.5-5.4,3-2.4-5.7-11.4-2.8-11.7-9.2h-3.9c0,.1-3.4,4.8-3.4,4.8l1,6.2,2.1,5.4-1.2.8-4.4-7.5-6.7-3.2-6.3-.3-5.2,3.3,5.9,3.4-3.6,2.2,5.3,4.4,2.5.8.4,3-6.5,6.1-8.7,11.2-4-1.9,6.4-13.5-1.6-1.9,1.1-7.9-2.1-3-4.7,14.5-3,1.8-3.8-5.7-4.4,2.7.5,8.4-1.5,1.7-3.3-.8-4.2,3.2-5.1,13.8.5,1.5,3.9-.2.2-6.5,5.6,2.5,2.8,5.3-7.4,2.4v3c0,0,5.9,1.2,5.9,1.2l4.3-1.5,2.1-3.3,4-.3-1.4,6,4.1,3.7-2.4,9.4,4.3,6.1,1.1,27,11.6,2.1-6.6-26.7,2.3-4.1-6.3-4.9,1.7-1.9,1-6.9,2-1.5,2.2,9,4.4,3.2,3.1-1.9,3.1-6.1,1.8,1.5-1.9,8.6,2,14.4,1.9,13.7-2.8,6.5,8.2,7.8v2.1l-6.9,2.9,2.9,21.8-1.6,1.9-5.9,6.7,1.3,2.7,3.3-2.7,8.2,2.8,6.2,7.7-1.2,2.8,5.1,13.1,5.6,3.1-19.1.5-1.8,3.3,6.4,3.1-.6,10.7,7.3,2.8,3-3.3,8.9,1.9,9.4-3.4.3,1.4.2.9-3.1,5.4-5.1-.9-9.4,3.5,6.8,6.4,4.3,1.4-.2,5.6-20.6-5.8-6.4,13.7-18.1,10.4-1.9,6.6,5.9,4.7h19.9s3.8,4.2,3.8,4.2l-.5,3.4,3,5.1,13.2,3.6,16.8,2.4,2.4-8.4,16.8-4.8,9.6-8.4v-7.2l9.6-8.4,9.6,7.2,1.2-9.6,10.8-12,12,3.6,2.4,10.8,7.2,7.2-6,15.6,7.2,6,8.4-8.4,8.6,3.3v-5.1s7.6-19.4,7.6-19.4l12.2-12.4,9.6-1.1,10.5-9.8,4.5-8.1-2.7-6.8-.6-4.6Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 219 KiB |
@@ -0,0 +1,41 @@
|
||||
<!--
|
||||
~ Copyright 2026 Hancom Inc. All rights reserved.
|
||||
~
|
||||
~ https://www.hancom.com/
|
||||
-->
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en-us">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title>Hwp SDK Wasm Sample</title>
|
||||
<script type="text/javascript" data-type="css/app" data-framework="aori|lab" src="js/loadVersion.js"></script>
|
||||
<script type="text/javascript" data-type="css/app" data-framework="aori|lab" src="js/loadHead.js"></script>
|
||||
<script data-main="js/main" data-bundle="dist/main_bundle" src="js/loadMain.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="status" style="display:none;">
|
||||
<div class="sppiner-content">
|
||||
<div class="spinner" id='spinner'></div>
|
||||
<div class="emscripten" id="statustext"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="status_downloading">
|
||||
<div class="status-content">
|
||||
<div class="emscriptenDownloading">
|
||||
<span id="statusDownloadingText">구성 중 ... 0% </span>
|
||||
<span id="statusDownloadingPercent" class="emscriptenDownloadingPercent"></span>
|
||||
</div>
|
||||
<div class="emscriptenDownloadingInfo"></div>
|
||||
<progress id="download_progress" value="0" max="100"></progress>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<iframe id="download_iframe" style="width:0px;height:0px;border:0;display:none;"></iframe>
|
||||
<script type="text/javascript" src="hwpsdk/loadHwpsdk.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,66 @@
|
||||
<!doctype html>
|
||||
<html lang="ko" style="height: 100%">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<title>웹한글 기안기 브리지</title>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="http://hwp.onasys.co.kr/webhwpctrl/js/hwpctrlapp/utils/util.js"
|
||||
></script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="http://hwp.onasys.co.kr/webhwpctrl/js/hwpctrlapp/hwpCtrlApp.js"
|
||||
></script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="http://hwp.onasys.co.kr/webhwpctrl/js/webhwpctrl.js"
|
||||
></script>
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
#hwpctrl {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
#hwpctrl_frame {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="hwpctrl"></div>
|
||||
<script type="text/javascript">
|
||||
(function () {
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
var baseUrl = params.get("baseurl") || "http://hwp.onasys.co.kr/webhwpctrl";
|
||||
if (baseUrl.charAt(baseUrl.length - 1) !== "/") {
|
||||
baseUrl += "/";
|
||||
}
|
||||
|
||||
var HwpCtrl = BuildWebHwpCtrl(
|
||||
"hwpctrl",
|
||||
baseUrl,
|
||||
function () {
|
||||
try {
|
||||
window.parent.postMessage({ type: "hangulgiangi-ready" }, "*");
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
},
|
||||
2,
|
||||
);
|
||||
|
||||
// 부모(HangulGiangi)가 contentWindow.HwpCtrl 로 접근
|
||||
window.HwpCtrl = HwpCtrl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -3,7 +3,13 @@ import { AxiosResponse } from "axios";
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type { AttachService } from "../model/attach-interface";
|
||||
import type { Attach, AttachService } from "../model/attach-interface";
|
||||
|
||||
async function findByAtflCd(atflCd: string): Promise<Attach[]> {
|
||||
if (!atflCd) return [];
|
||||
const response = await client.get<Attach[]>(`${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}/${atflCd}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function deleteAttachmentByAtflId(atflId: string): Promise<void> {
|
||||
if (!atflId) return;
|
||||
@@ -16,7 +22,15 @@ async function downloadAttachmentByAtflCd(atflCd: string): Promise<AxiosResponse
|
||||
});
|
||||
}
|
||||
|
||||
async function downloadAttachmentByAtflId(atflId: string): Promise<AxiosResponse> {
|
||||
return await client.get<Blob>(`${ENDPOINT.ATTACH_SERVICE.DOWNLOAD_BY_ATFL_ID}/${atflId}`, {
|
||||
responseType: "blob",
|
||||
});
|
||||
}
|
||||
|
||||
export const attachService = {
|
||||
findByAtflCd,
|
||||
deleteAttachmentByAtflId,
|
||||
downloadAttachmentByAtflCd,
|
||||
downloadAttachmentByAtflId,
|
||||
} satisfies AttachService;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import type { AxiosResponse } from "axios";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
@@ -8,6 +9,22 @@ import { attachService } from "./attach-service";
|
||||
|
||||
const attachBaseKey = [ENDPOINT.ATTACH_SERVICE.ATTACHMENTS] as const;
|
||||
const attachDownloadBaseKey = [ENDPOINT.ATTACH_SERVICE.DOWNLOAD] as const;
|
||||
const attachDownloadByAtflIdKey = [ENDPOINT.ATTACH_SERVICE.DOWNLOAD_BY_ATFL_ID] as const;
|
||||
|
||||
function triggerBlobDownload(response: AxiosResponse) {
|
||||
const filename = parseFilenameFromResponse(response);
|
||||
const blob = new Blob([response.data]);
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
export function useDeleteAttachment() {
|
||||
return useMutation({
|
||||
@@ -21,19 +38,17 @@ export function useDownloadAttachmentByAtflCd() {
|
||||
mutationKey: [...attachDownloadBaseKey, "download"],
|
||||
mutationFn: async (atflCd: string) => {
|
||||
const response = await attachService.downloadAttachmentByAtflCd(atflCd);
|
||||
const filename = parseFilenameFromResponse(response);
|
||||
|
||||
const blob = new Blob([response.data]);
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(link);
|
||||
triggerBlobDownload(response);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDownloadAttachmentByAtflId() {
|
||||
return useMutation({
|
||||
mutationKey: [...attachDownloadByAtflIdKey, "download"],
|
||||
mutationFn: async (atflId: string) => {
|
||||
const response = await attachService.downloadAttachmentByAtflId(atflId);
|
||||
triggerBlobDownload(response);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
export { attachService } from "./api/attach-service";
|
||||
export { useDeleteAttachment, useDownloadAttachmentByAtflCd } from "./api/queries";
|
||||
export {
|
||||
useDeleteAttachment,
|
||||
useDownloadAttachmentByAtflCd,
|
||||
useDownloadAttachmentByAtflId,
|
||||
} from "./api/queries";
|
||||
export { parseFilenameFromResponse } from "./lib/parse-filename-from-response";
|
||||
export type { Attach, AttachService } from "./model/attach-interface";
|
||||
|
||||
@@ -17,6 +17,8 @@ export interface Attach {
|
||||
}
|
||||
|
||||
export interface AttachService {
|
||||
findByAtflCd: (atflCd: string) => Promise<Attach[]>;
|
||||
deleteAttachmentByAtflId: (atflId: string) => Promise<void>;
|
||||
downloadAttachmentByAtflCd: (atflCd: string) => Promise<AxiosResponse>;
|
||||
downloadAttachmentByAtflId: (atflId: string) => Promise<AxiosResponse>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type { UserPostSearchParams } from "../model/post-interface";
|
||||
|
||||
import { postApi } from "./post-service";
|
||||
|
||||
export function useUserPostList(params?: UserPostSearchParams) {
|
||||
return useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, params],
|
||||
queryFn: () => postApi.getUserPostList(params),
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { AS_DASHBOARD_BBS_ID, ENDPOINT } from "@/shared/config";
|
||||
|
||||
import { PostService, UserPostListResponse, UserPostSearchParams } from "../model/post-interface";
|
||||
|
||||
async function getUserPostList(params?: UserPostSearchParams): Promise<UserPostListResponse> {
|
||||
const response = await client.get<UserPostListResponse>(
|
||||
`${ENDPOINT.BOARD_SERVICE.POSTS}/user/${AS_DASHBOARD_BBS_ID}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const postApi: PostService = {
|
||||
getUserPostList,
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
export { useUserPostList } from "./api/post-queries";
|
||||
export { postApi } from "./api/post-service";
|
||||
export type {
|
||||
PostService,
|
||||
UserPost,
|
||||
UserPostListResponse,
|
||||
UserPostSearchParams,
|
||||
} from "./model/post-interface";
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { PageableParams, PageResponse } from "@/shared/model";
|
||||
|
||||
export interface UserPostSearchParams extends PageableParams {
|
||||
keywordType?: string;
|
||||
keyword?: string;
|
||||
popupYn?: string;
|
||||
upndFixYn?: string;
|
||||
prdPstgYn?: string;
|
||||
siteId?: string;
|
||||
bbsTypeCd?: string;
|
||||
ansUseYn?: string;
|
||||
}
|
||||
|
||||
export interface UserPost {
|
||||
rowNo: number;
|
||||
bbsId: number;
|
||||
pstNo: number;
|
||||
pstTtl: string;
|
||||
pstCn: string;
|
||||
inqNocs: number;
|
||||
rfrncUrl: string;
|
||||
popupYn: string;
|
||||
upndFixYn: string;
|
||||
prdPstgYn: string;
|
||||
rootPath: string;
|
||||
pstPath: string;
|
||||
ansPstn: number;
|
||||
wrtrNm: string;
|
||||
wrtrCplc: string;
|
||||
atflCd: string;
|
||||
regDt: string;
|
||||
rgtrNm: string;
|
||||
cmntCnt: number;
|
||||
}
|
||||
|
||||
export type UserPostListResponse = PageResponse<UserPost>;
|
||||
|
||||
export interface PostService {
|
||||
getUserPostList(params?: UserPostSearchParams): Promise<UserPostListResponse>;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type {
|
||||
DashboardMainResponseDto,
|
||||
DashboardMainSearchDto,
|
||||
DashboardService,
|
||||
} from "../model/dashboard-interface";
|
||||
|
||||
const BASE = ENDPOINT.AS_SERVICE.DASHBOARD;
|
||||
|
||||
async function getMainDashboard(
|
||||
searchDto: DashboardMainSearchDto,
|
||||
): Promise<DashboardMainResponseDto> {
|
||||
const response = await client.get<DashboardMainResponseDto>(`${BASE}/main`, {
|
||||
params: searchDto,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const dashboardService = {
|
||||
getMainDashboard,
|
||||
} satisfies DashboardService;
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type { DashboardMainSearchDto } from "../model/dashboard-interface";
|
||||
|
||||
import { dashboardService } from "./dashboard-service";
|
||||
|
||||
export const dashboardBaseKey = [ENDPOINT.AS_SERVICE.DASHBOARD] as const;
|
||||
|
||||
const EMPTY_SEARCH: DashboardMainSearchDto = {};
|
||||
|
||||
export function useGetMainDashboard(params: DashboardMainSearchDto = EMPTY_SEARCH) {
|
||||
return useQuery({
|
||||
queryKey: [...dashboardBaseKey, "getMainDashboard", params],
|
||||
queryFn: () => dashboardService.getMainDashboard(params),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export { dashboardService } from "./api/dashboard-service";
|
||||
export { dashboardBaseKey, useGetMainDashboard } from "./api/queries";
|
||||
export type {
|
||||
DashboardMainResponseDto,
|
||||
DashboardMainSearchDto,
|
||||
DashboardService,
|
||||
DocPrgrsSttsResponseDto,
|
||||
NotiAsStatResponseDto,
|
||||
NotiAsUserResponseDto,
|
||||
} from "./model/dashboard-interface";
|
||||
@@ -0,0 +1,58 @@
|
||||
/** 백엔드 DashboardMainSearchDto */
|
||||
export interface DashboardMainSearchDto {
|
||||
searchYear?: number;
|
||||
notiType?: string;
|
||||
notReadYn?: "Y" | "N";
|
||||
startDt?: string;
|
||||
endDt?: string;
|
||||
docSeComCd?: string;
|
||||
}
|
||||
|
||||
/** 백엔드 DocPrgrsSttsResponseDto */
|
||||
export interface DocPrgrsSttsResponseDto {
|
||||
docSeNm: string;
|
||||
docSeComCd: string;
|
||||
reqCnt: number;
|
||||
prgrsCnt: number;
|
||||
cmptnCnt: number;
|
||||
totCnt: number;
|
||||
}
|
||||
|
||||
/** 백엔드 NotiAsUserResponseDto */
|
||||
export interface NotiAsUserResponseDto {
|
||||
notiId: number;
|
||||
recvUnqId: string;
|
||||
senderUnqId: string;
|
||||
notiType: string;
|
||||
notiTtl: string;
|
||||
notiCn?: string;
|
||||
notReadYn?: "Y" | "N";
|
||||
regDt?: string;
|
||||
notiUrl?: string;
|
||||
notiLnkgUnqNo?: string;
|
||||
docSeComCd?: string;
|
||||
docSeComNm?: string;
|
||||
notiIdntyDt?: string;
|
||||
notiIdntyYn?: "Y" | "N";
|
||||
notiPrcsCmptnYn?: "Y" | "N";
|
||||
notiPrcsCmptnDt?: string;
|
||||
}
|
||||
|
||||
/** 백엔드 NotiAsStatResponseDto */
|
||||
export interface NotiAsStatResponseDto {
|
||||
todayCount: number;
|
||||
delayCount: number;
|
||||
pendingCount: number;
|
||||
}
|
||||
|
||||
/** 백엔드 DashboardMainResponseDto */
|
||||
export interface DashboardMainResponseDto {
|
||||
docPrgrsSttsList: DocPrgrsSttsResponseDto[];
|
||||
notiList: NotiAsUserResponseDto[];
|
||||
notiStats: NotiAsStatResponseDto;
|
||||
}
|
||||
|
||||
/** Controller public 메서드명·시그니처와 동일 */
|
||||
export interface DashboardService {
|
||||
getMainDashboard(searchDto: DashboardMainSearchDto): Promise<DashboardMainResponseDto>;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type {
|
||||
DcRagApiToService,
|
||||
DcRagCommonResponseDto,
|
||||
DcRagDraftGenerateResponseDto,
|
||||
DcRagDraftStatusResponseDto,
|
||||
} from "../model/dcrag-to-interface";
|
||||
import type {
|
||||
DcRagCostVerifyRequestDto,
|
||||
DcRagDocumentTransferRequestDto,
|
||||
DcRagDraftGenerateRequestDto,
|
||||
} from "../model/dcrag-to-payload-schema";
|
||||
|
||||
const BASE = ENDPOINT.AS_SERVICE.DCRAG_TO;
|
||||
|
||||
async function requestDocumentTransfer(
|
||||
body: DcRagDocumentTransferRequestDto,
|
||||
): Promise<DcRagCommonResponseDto> {
|
||||
const response = await client.post<DcRagCommonResponseDto>(`${BASE}/documents/transfer`, body);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function requestDraftGeneration(
|
||||
body: DcRagDraftGenerateRequestDto,
|
||||
): Promise<DcRagDraftGenerateResponseDto> {
|
||||
const response = await client.post<DcRagDraftGenerateResponseDto>(
|
||||
`${BASE}/documents/draft/generate`,
|
||||
body,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function getDraftGenerationStatus(crtDmndUuid: string): Promise<DcRagDraftStatusResponseDto> {
|
||||
const response = await client.get<DcRagDraftStatusResponseDto>(
|
||||
`${BASE}/documents/draft/status/${crtDmndUuid}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function requestCostVerification(
|
||||
body: DcRagCostVerifyRequestDto,
|
||||
): Promise<DcRagCommonResponseDto> {
|
||||
const response = await client.post<DcRagCommonResponseDto>(`${BASE}/documents/cost/verify`, body);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const dcRagApiToService = {
|
||||
requestDocumentTransfer,
|
||||
requestDraftGeneration,
|
||||
getDraftGenerationStatus,
|
||||
requestCostVerification,
|
||||
} satisfies DcRagApiToService;
|
||||
@@ -0,0 +1,70 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type { DcRagDraftStatusResponseDto } from "../model/dcrag-to-interface";
|
||||
import type {
|
||||
DcRagCostVerifyRequestDto,
|
||||
DcRagDocumentTransferRequestDto,
|
||||
DcRagDraftGenerateRequestDto,
|
||||
} from "../model/dcrag-to-payload-schema";
|
||||
|
||||
import { dcRagApiToService } from "./dcrag-to-service";
|
||||
|
||||
export const dcRagApiToBaseKey = [ENDPOINT.AS_SERVICE.DCRAG_TO] as const;
|
||||
|
||||
const DRAFT_STATUS_POLL_INTERVAL_MS = 5_000;
|
||||
|
||||
function isDraftGenerationSettled(data: DcRagDraftStatusResponseDto | undefined): boolean {
|
||||
if (!data) return false;
|
||||
if (data.status === "COMPLETED" || data.status === "FAILED") return true;
|
||||
return data.events?.some((event) => event.eventType === "COMPLETED") ?? false;
|
||||
}
|
||||
|
||||
export function useDraftGenerationStatus(crtDmndUuid: string | null | undefined) {
|
||||
return useQuery({
|
||||
queryKey: [...dcRagApiToBaseKey, "getDraftGenerationStatus", crtDmndUuid],
|
||||
queryFn: () => dcRagApiToService.getDraftGenerationStatus(crtDmndUuid!),
|
||||
enabled: Boolean(crtDmndUuid),
|
||||
refetchInterval: (query) =>
|
||||
isDraftGenerationSettled(query.state.data) ? false : DRAFT_STATUS_POLL_INTERVAL_MS,
|
||||
});
|
||||
}
|
||||
|
||||
export function useRequestDocumentTransfer() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (body: DcRagDocumentTransferRequestDto) =>
|
||||
dcRagApiToService.requestDocumentTransfer(body),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: dcRagApiToBaseKey });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useRequestDraftGeneration() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (body: DcRagDraftGenerateRequestDto) =>
|
||||
dcRagApiToService.requestDraftGeneration(body),
|
||||
onSuccess: (_data, body) => {
|
||||
queryClient.invalidateQueries({ queryKey: dcRagApiToBaseKey });
|
||||
if (body.crtDmndUuid) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...dcRagApiToBaseKey, "getDraftGenerationStatus", body.crtDmndUuid],
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useRequestCostVerification() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (body: DcRagCostVerifyRequestDto) =>
|
||||
dcRagApiToService.requestCostVerification(body),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: dcRagApiToBaseKey });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
export { dcRagApiToService } from "./api/dcrag-to-service";
|
||||
export { dcRagApiToBaseKey } from "./api/queries";
|
||||
export {
|
||||
useDraftGenerationStatus,
|
||||
useRequestCostVerification,
|
||||
useRequestDocumentTransfer,
|
||||
useRequestDraftGeneration,
|
||||
} from "./api/queries";
|
||||
export type {
|
||||
DcRagApiToService,
|
||||
DcRagCommonResponseDto,
|
||||
DcRagDraftGenerateResponseDto,
|
||||
DcRagDraftStatusResponseDto,
|
||||
EventInfoDto,
|
||||
} from "./model/dcrag-to-interface";
|
||||
export type {
|
||||
DcRagCostVerifyRequestDto,
|
||||
DcRagDocumentTransferRequestDto,
|
||||
DcRagDraftGenerateRequestDto,
|
||||
DcRagFileInfoDto,
|
||||
} from "./model/dcrag-to-payload-schema";
|
||||
export {
|
||||
dcRagCostVerifyRequestSchema,
|
||||
dcRagDocumentTransferRequestSchema,
|
||||
dcRagDraftGenerateRequestSchema,
|
||||
dcRagFileInfoSchema,
|
||||
} from "./model/dcrag-to-payload-schema";
|
||||
@@ -0,0 +1,52 @@
|
||||
import type {
|
||||
DcRagCostVerifyRequestDto,
|
||||
DcRagDocumentTransferRequestDto,
|
||||
DcRagDraftGenerateRequestDto,
|
||||
} from "./dcrag-to-payload-schema";
|
||||
|
||||
/** 백엔드 DcRagCommonResponseDto */
|
||||
export interface DcRagCommonResponseDto {
|
||||
resultCode?: string;
|
||||
message?: string;
|
||||
requestId?: string;
|
||||
}
|
||||
|
||||
/** 백엔드 DcRagDraftGenerateResponseDto */
|
||||
export interface DcRagDraftGenerateResponseDto {
|
||||
resultCode?: string;
|
||||
message?: string;
|
||||
crtDmndUuid?: string;
|
||||
/** R:생성요청, I:생성중, C:생성완료, F:생성실패 */
|
||||
crtSttsComCd?: "R" | "I" | "C" | "F";
|
||||
requestId?: string;
|
||||
estimatedMinutes?: number;
|
||||
}
|
||||
|
||||
/** 백엔드 DcRagDraftStatusResponseDto.EventInfoDto */
|
||||
export interface EventInfoDto {
|
||||
eventType?: string;
|
||||
eventMessage?: string;
|
||||
occurredDt?: string;
|
||||
}
|
||||
|
||||
/** 백엔드 DcRagDraftStatusResponseDto */
|
||||
export interface DcRagDraftStatusResponseDto {
|
||||
crtDmndUuid?: string;
|
||||
status?: "REQUESTED" | "DOWNLOADING" | "ANALYZING" | "GENERATING" | "COMPLETED" | "FAILED";
|
||||
statusName?: string;
|
||||
progressPercent?: number;
|
||||
events?: EventInfoDto[];
|
||||
startedDt?: string;
|
||||
estimatedCompletionDt?: string;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
/** Controller public 메서드명·시그니처와 동일 */
|
||||
export interface DcRagApiToService {
|
||||
requestDocumentTransfer(body: DcRagDocumentTransferRequestDto): Promise<DcRagCommonResponseDto>;
|
||||
requestDraftGeneration(
|
||||
body: DcRagDraftGenerateRequestDto,
|
||||
): Promise<DcRagDraftGenerateResponseDto>;
|
||||
getDraftGenerationStatus(crtDmndUuid: string): Promise<DcRagDraftStatusResponseDto>;
|
||||
requestCostVerification(body: DcRagCostVerifyRequestDto): Promise<DcRagCommonResponseDto>;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { z } from "zod";
|
||||
|
||||
/** 백엔드 DcRagFileInfoDto */
|
||||
export const dcRagFileInfoSchema = z.object({
|
||||
fileId: z.string().optional(),
|
||||
fileGroupId: z.string().optional(),
|
||||
fileName: z.string().optional(),
|
||||
physicalPath: z.string().optional(),
|
||||
downloadUrl: z.string().optional(),
|
||||
fileSize: z.number().optional(),
|
||||
esntlAtchFileId: z.string().optional(),
|
||||
esntlAtchFileNm: z.string().optional(),
|
||||
});
|
||||
|
||||
/** 백엔드 DcRagDocumentTransferRequestDto */
|
||||
export const dcRagDocumentTransferRequestSchema = z.object({
|
||||
fnrsCnsltnDocNo: z.string(),
|
||||
taskSeComCd: z.string().optional(),
|
||||
taskSeCdNm: z.string().optional(),
|
||||
essentialFiles: z.array(dcRagFileInfoSchema).optional(),
|
||||
etcFiles: z.array(dcRagFileInfoSchema).optional(),
|
||||
callbackUrl: z.string().optional(),
|
||||
});
|
||||
|
||||
/** 백엔드 DcRagDraftGenerateRequestDto */
|
||||
export const dcRagDraftGenerateRequestSchema = z.object({
|
||||
fnrsCnsltnDocNo: z.string(),
|
||||
pwrkCnsltnDocNo: z.string(),
|
||||
crtDmndUuid: z.string().optional(),
|
||||
essentialFiles: z.array(dcRagFileInfoSchema).optional(),
|
||||
etcFiles: z.array(dcRagFileInfoSchema).optional(),
|
||||
rqstrUuid: z.string().optional(),
|
||||
callbackUrl: z.string().optional(),
|
||||
});
|
||||
|
||||
/** 백엔드 DcRagCostVerifyRequestDto */
|
||||
export const dcRagCostVerifyRequestSchema = z.object({
|
||||
fnrsCnsltnDocNo: z.string(),
|
||||
pwrkCnsltnDocNo: z.string(),
|
||||
});
|
||||
|
||||
export type DcRagFileInfoDto = z.infer<typeof dcRagFileInfoSchema>;
|
||||
export type DcRagDocumentTransferRequestDto = z.infer<typeof dcRagDocumentTransferRequestSchema>;
|
||||
export type DcRagDraftGenerateRequestDto = z.infer<typeof dcRagDraftGenerateRequestSchema>;
|
||||
export type DcRagCostVerifyRequestDto = z.infer<typeof dcRagCostVerifyRequestSchema>;
|
||||
@@ -1,7 +1,5 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type {
|
||||
DesignCnstnBizCstSaveRequestDto,
|
||||
DesignCnstnBizCstSearchDto,
|
||||
@@ -35,26 +33,52 @@ import { designCnstnEsntlAtchFileService } from "./design-cnstn-esntl-atch-file-
|
||||
import { designCnstnMngService } from "./design-cnstn-mng-service";
|
||||
import { designCnstnRvwOpnnService } from "./design-cnstn-rvw-opnn-service";
|
||||
|
||||
export const designCnstnMngBaseKey = [ENDPOINT.AS_SERVICE.DESIGN_CNSTN] as const;
|
||||
export const designCnstnBizCstBaseKey = [ENDPOINT.AS_SERVICE.DESIGN_CNSTN_BIZ_CST] as const;
|
||||
export const designCnstnBizSclBaseKey = [ENDPOINT.AS_SERVICE.DESIGN_CNSTN_BIZ_SCL] as const;
|
||||
export const designCnstnEsntlAtchFileBaseKey = [
|
||||
ENDPOINT.AS_SERVICE.DESIGN_CNSTN_ESNTL_ATCH_FILE,
|
||||
] as const;
|
||||
export const designCnstnRvwOpnnBaseKey = [ENDPOINT.AS_SERVICE.DESIGN_CNSTN_RVW_OPNN] as const;
|
||||
export const designCnstnKeys = {
|
||||
all: ["design-cnstn"] as const,
|
||||
list: () => [...designCnstnKeys.all, "list"] as const,
|
||||
detail: (id: string) => [...designCnstnKeys.all, "detail", id] as const,
|
||||
};
|
||||
|
||||
export const designCnstnBizCstKeys = {
|
||||
all: ["design-cnstn-biz-cst"] as const,
|
||||
list: () => [...designCnstnBizCstKeys.all, "list"] as const,
|
||||
detail: (docNo: string, acntgSeCd: string) =>
|
||||
[...designCnstnBizCstKeys.all, "detail", docNo, acntgSeCd] as const,
|
||||
};
|
||||
|
||||
export const designCnstnBizSclKeys = {
|
||||
all: ["design-cnstn-biz-scl"] as const,
|
||||
list: () => [...designCnstnBizSclKeys.all, "list"] as const,
|
||||
detail: (docNo: string, cnstpSeCd: string) =>
|
||||
[...designCnstnBizSclKeys.all, "detail", docNo, cnstpSeCd] as const,
|
||||
};
|
||||
|
||||
export const designCnstnEsntlAtchFileKeys = {
|
||||
all: ["design-cnstn-esntl-atch-file"] as const,
|
||||
list: () => [...designCnstnEsntlAtchFileKeys.all, "list"] as const,
|
||||
detail: (docNo: string, esntlAtchFileId: string) =>
|
||||
[...designCnstnEsntlAtchFileKeys.all, "detail", docNo, esntlAtchFileId] as const,
|
||||
};
|
||||
|
||||
export const designCnstnRvwOpnnKeys = {
|
||||
all: ["design-cnstn-rvw-opnn"] as const,
|
||||
list: () => [...designCnstnRvwOpnnKeys.all, "list"] as const,
|
||||
detail: (docNo: string, rvwOpnnSn: number) =>
|
||||
[...designCnstnRvwOpnnKeys.all, "detail", docNo, rvwOpnnSn] as const,
|
||||
};
|
||||
|
||||
/* ─── DesignCnstnMng ─── */
|
||||
|
||||
export function useDesignCnstnMngPage(params: DesignCnstnMngSearchDto) {
|
||||
return useQuery({
|
||||
queryKey: [...designCnstnMngBaseKey, "findPage", params],
|
||||
queryKey: [...designCnstnKeys.list(), params],
|
||||
queryFn: () => designCnstnMngService.findPage(params),
|
||||
});
|
||||
}
|
||||
|
||||
export function useDesignCnstnMng(id: string | null | undefined) {
|
||||
return useQuery({
|
||||
queryKey: [...designCnstnMngBaseKey, "findById", id],
|
||||
queryKey: designCnstnKeys.detail(id!),
|
||||
queryFn: () => designCnstnMngService.findById(id!),
|
||||
enabled: Boolean(id),
|
||||
});
|
||||
@@ -65,7 +89,7 @@ export function useSaveDesignCnstnMng() {
|
||||
return useMutation({
|
||||
mutationFn: (body: DesignCnstnMngSaveRequestDto) => designCnstnMngService.save(body),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnKeys.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -75,7 +99,7 @@ export function useSaveApplyDesignCnstnMng() {
|
||||
return useMutation({
|
||||
mutationFn: (body: DesignCnstnApplySaveRequestDto) => designCnstnMngService.saveApply(body),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnKeys.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -86,9 +110,9 @@ export function useUpdateDesignCnstnMng() {
|
||||
mutationFn: ({ id, body }: { id: string; body: DesignCnstnMngUpdateRequestDto }) =>
|
||||
designCnstnMngService.update(id, body),
|
||||
onSuccess: (_data, { id }) => {
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnKeys.all });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...designCnstnMngBaseKey, "findById", id],
|
||||
queryKey: designCnstnKeys.detail(id),
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -100,9 +124,9 @@ export function useTechReviewDesignCnstnMng() {
|
||||
mutationFn: ({ id, body }: { id: string; body: DesignCnstnApplySaveRequestDto }) =>
|
||||
designCnstnMngService.techReview(id, body),
|
||||
onSuccess: (_data, { id }) => {
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnKeys.all });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...designCnstnMngBaseKey, "findById", id],
|
||||
queryKey: designCnstnKeys.detail(id),
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -113,7 +137,7 @@ export function useDeleteDesignCnstnMng() {
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => designCnstnMngService.delete(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnKeys.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -122,7 +146,7 @@ export function useDeleteDesignCnstnMng() {
|
||||
|
||||
export function useDesignCnstnBizCstPage(params: DesignCnstnBizCstSearchDto) {
|
||||
return useQuery({
|
||||
queryKey: [...designCnstnBizCstBaseKey, "findPage", params],
|
||||
queryKey: [...designCnstnBizCstKeys.list(), params],
|
||||
queryFn: () => designCnstnBizCstService.findPage(params),
|
||||
});
|
||||
}
|
||||
@@ -132,7 +156,7 @@ export function useDesignCnstnBizCst(
|
||||
acntgSeCd: string | null | undefined,
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: [...designCnstnBizCstBaseKey, "findById", designCnstnDocNo, acntgSeCd],
|
||||
queryKey: designCnstnBizCstKeys.detail(designCnstnDocNo!, acntgSeCd!),
|
||||
queryFn: () => designCnstnBizCstService.findById(designCnstnDocNo!, acntgSeCd!),
|
||||
enabled: Boolean(designCnstnDocNo && acntgSeCd),
|
||||
});
|
||||
@@ -143,8 +167,8 @@ export function useSaveDesignCnstnBizCst() {
|
||||
return useMutation({
|
||||
mutationFn: (body: DesignCnstnBizCstSaveRequestDto) => designCnstnBizCstService.save(body),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnBizCstBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnBizCstKeys.all });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnKeys.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -162,11 +186,11 @@ export function useUpdateDesignCnstnBizCst() {
|
||||
body: DesignCnstnBizCstUpdateRequestDto;
|
||||
}) => designCnstnBizCstService.update(designCnstnDocNo, acntgSeCd, body),
|
||||
onSuccess: (_data, { designCnstnDocNo, acntgSeCd }) => {
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnBizCstBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnBizCstKeys.all });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...designCnstnBizCstBaseKey, "findById", designCnstnDocNo, acntgSeCd],
|
||||
queryKey: designCnstnBizCstKeys.detail(designCnstnDocNo, acntgSeCd),
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnKeys.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -182,8 +206,8 @@ export function useDeleteDesignCnstnBizCst() {
|
||||
acntgSeCd: string;
|
||||
}) => designCnstnBizCstService.delete(designCnstnDocNo, acntgSeCd),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnBizCstBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnBizCstKeys.all });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnKeys.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -192,7 +216,7 @@ export function useDeleteDesignCnstnBizCst() {
|
||||
|
||||
export function useDesignCnstnBizSclPage(params: DesignCnstnBizSclSearchDto) {
|
||||
return useQuery({
|
||||
queryKey: [...designCnstnBizSclBaseKey, "findPage", params],
|
||||
queryKey: [...designCnstnBizSclKeys.list(), params],
|
||||
queryFn: () => designCnstnBizSclService.findPage(params),
|
||||
});
|
||||
}
|
||||
@@ -202,7 +226,7 @@ export function useDesignCnstnBizScl(
|
||||
cnstpSeCd: string | null | undefined,
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: [...designCnstnBizSclBaseKey, "findById", designCnstnDocNo, cnstpSeCd],
|
||||
queryKey: designCnstnBizSclKeys.detail(designCnstnDocNo!, cnstpSeCd!),
|
||||
queryFn: () => designCnstnBizSclService.findById(designCnstnDocNo!, cnstpSeCd!),
|
||||
enabled: Boolean(designCnstnDocNo && cnstpSeCd),
|
||||
});
|
||||
@@ -213,8 +237,8 @@ export function useSaveDesignCnstnBizScl() {
|
||||
return useMutation({
|
||||
mutationFn: (body: DesignCnstnBizSclSaveRequestDto) => designCnstnBizSclService.save(body),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnBizSclBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnBizSclKeys.all });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnKeys.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -232,11 +256,11 @@ export function useUpdateDesignCnstnBizScl() {
|
||||
body: DesignCnstnBizSclUpdateRequestDto;
|
||||
}) => designCnstnBizSclService.update(designCnstnDocNo, cnstpSeCd, body),
|
||||
onSuccess: (_data, { designCnstnDocNo, cnstpSeCd }) => {
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnBizSclBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnBizSclKeys.all });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...designCnstnBizSclBaseKey, "findById", designCnstnDocNo, cnstpSeCd],
|
||||
queryKey: designCnstnBizSclKeys.detail(designCnstnDocNo, cnstpSeCd),
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnKeys.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -252,8 +276,8 @@ export function useDeleteDesignCnstnBizScl() {
|
||||
cnstpSeCd: string;
|
||||
}) => designCnstnBizSclService.delete(designCnstnDocNo, cnstpSeCd),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnBizSclBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnBizSclKeys.all });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnKeys.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -262,7 +286,7 @@ export function useDeleteDesignCnstnBizScl() {
|
||||
|
||||
export function useDesignCnstnEsntlAtchFilePage(params: DesignCnstnEsntlAtchFileSearchDto) {
|
||||
return useQuery({
|
||||
queryKey: [...designCnstnEsntlAtchFileBaseKey, "findPage", params],
|
||||
queryKey: [...designCnstnEsntlAtchFileKeys.list(), params],
|
||||
queryFn: () => designCnstnEsntlAtchFileService.findPage(params),
|
||||
});
|
||||
}
|
||||
@@ -272,14 +296,8 @@ export function useDesignCnstnEsntlAtchFile(
|
||||
esntlAtchFileId: string | null | undefined,
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: [
|
||||
...designCnstnEsntlAtchFileBaseKey,
|
||||
"findById",
|
||||
designCnstnDocNo,
|
||||
esntlAtchFileId,
|
||||
],
|
||||
queryFn: () =>
|
||||
designCnstnEsntlAtchFileService.findById(designCnstnDocNo!, esntlAtchFileId!),
|
||||
queryKey: designCnstnEsntlAtchFileKeys.detail(designCnstnDocNo!, esntlAtchFileId!),
|
||||
queryFn: () => designCnstnEsntlAtchFileService.findById(designCnstnDocNo!, esntlAtchFileId!),
|
||||
enabled: Boolean(designCnstnDocNo && esntlAtchFileId),
|
||||
});
|
||||
}
|
||||
@@ -290,8 +308,8 @@ export function useSaveDesignCnstnEsntlAtchFile() {
|
||||
mutationFn: (body: DesignCnstnEsntlAtchFileSaveRequestDto) =>
|
||||
designCnstnEsntlAtchFileService.save(body),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnEsntlAtchFileBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnEsntlAtchFileKeys.all });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnKeys.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -309,16 +327,11 @@ export function useUpdateDesignCnstnEsntlAtchFile() {
|
||||
body: DesignCnstnEsntlAtchFileUpdateRequestDto;
|
||||
}) => designCnstnEsntlAtchFileService.update(designCnstnDocNo, esntlAtchFileId, body),
|
||||
onSuccess: (_data, { designCnstnDocNo, esntlAtchFileId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnEsntlAtchFileBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnEsntlAtchFileKeys.all });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [
|
||||
...designCnstnEsntlAtchFileBaseKey,
|
||||
"findById",
|
||||
designCnstnDocNo,
|
||||
esntlAtchFileId,
|
||||
],
|
||||
queryKey: designCnstnEsntlAtchFileKeys.detail(designCnstnDocNo, esntlAtchFileId),
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnKeys.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -334,8 +347,8 @@ export function useDeleteDesignCnstnEsntlAtchFile() {
|
||||
esntlAtchFileId: string;
|
||||
}) => designCnstnEsntlAtchFileService.delete(designCnstnDocNo, esntlAtchFileId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnEsntlAtchFileBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnEsntlAtchFileKeys.all });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnKeys.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -344,7 +357,7 @@ export function useDeleteDesignCnstnEsntlAtchFile() {
|
||||
|
||||
export function useDesignCnstnRvwOpnnPage(params: DesignCnstnRvwOpnnSearchDto) {
|
||||
return useQuery({
|
||||
queryKey: [...designCnstnRvwOpnnBaseKey, "findPage", params],
|
||||
queryKey: [...designCnstnRvwOpnnKeys.list(), params],
|
||||
queryFn: () => designCnstnRvwOpnnService.findPage(params),
|
||||
});
|
||||
}
|
||||
@@ -354,7 +367,7 @@ export function useDesignCnstnRvwOpnn(
|
||||
rvwOpnnSn: number | null | undefined,
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: [...designCnstnRvwOpnnBaseKey, "findById", designCnstnDocNo, rvwOpnnSn],
|
||||
queryKey: designCnstnRvwOpnnKeys.detail(designCnstnDocNo!, rvwOpnnSn!),
|
||||
queryFn: () => designCnstnRvwOpnnService.findById(designCnstnDocNo!, rvwOpnnSn!),
|
||||
enabled: Boolean(designCnstnDocNo && rvwOpnnSn != null),
|
||||
});
|
||||
@@ -365,8 +378,8 @@ export function useSaveDesignCnstnRvwOpnn() {
|
||||
return useMutation({
|
||||
mutationFn: (body: DesignCnstnRvwOpnnSaveRequestDto) => designCnstnRvwOpnnService.save(body),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnRvwOpnnBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnRvwOpnnKeys.all });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnKeys.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -384,11 +397,11 @@ export function useUpdateDesignCnstnRvwOpnn() {
|
||||
body: DesignCnstnRvwOpnnUpdateRequestDto;
|
||||
}) => designCnstnRvwOpnnService.update(designCnstnDocNo, rvwOpnnSn, body),
|
||||
onSuccess: (_data, { designCnstnDocNo, rvwOpnnSn }) => {
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnRvwOpnnBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnRvwOpnnKeys.all });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...designCnstnRvwOpnnBaseKey, "findById", designCnstnDocNo, rvwOpnnSn],
|
||||
queryKey: designCnstnRvwOpnnKeys.detail(designCnstnDocNo, rvwOpnnSn),
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnKeys.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -404,8 +417,8 @@ export function useDeleteDesignCnstnRvwOpnn() {
|
||||
rvwOpnnSn: number;
|
||||
}) => designCnstnRvwOpnnService.delete(designCnstnDocNo, rvwOpnnSn),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnRvwOpnnBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnMngBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnRvwOpnnKeys.all });
|
||||
queryClient.invalidateQueries({ queryKey: designCnstnKeys.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ export { designCnstnEsntlAtchFileService } from "./api/design-cnstn-esntl-atch-f
|
||||
export { designCnstnMngService } from "./api/design-cnstn-mng-service";
|
||||
export { designCnstnRvwOpnnService } from "./api/design-cnstn-rvw-opnn-service";
|
||||
export {
|
||||
designCnstnBizCstBaseKey,
|
||||
designCnstnBizSclBaseKey,
|
||||
designCnstnEsntlAtchFileBaseKey,
|
||||
designCnstnMngBaseKey,
|
||||
designCnstnRvwOpnnBaseKey,
|
||||
designCnstnBizCstKeys,
|
||||
designCnstnBizSclKeys,
|
||||
designCnstnEsntlAtchFileKeys,
|
||||
designCnstnKeys,
|
||||
designCnstnRvwOpnnKeys,
|
||||
useDeleteDesignCnstnBizCst,
|
||||
useDeleteDesignCnstnBizScl,
|
||||
useDeleteDesignCnstnEsntlAtchFile,
|
||||
|
||||
@@ -5,11 +5,12 @@ import type { PageResponse } from "@/shared/model";
|
||||
import type {
|
||||
ElaprApproverChangeRequestDto,
|
||||
ElaprApproverInsertRequestDto,
|
||||
ElaprPrgrsSttsMyDocSearchParams,
|
||||
ElaprMyDocSearchDto,
|
||||
ElaprPrgrsSttsResponseDto,
|
||||
ElaprPrgrsSttsSaveRequestDto,
|
||||
ElaprPrgrsSttsService,
|
||||
ElaprProcessRequestDto,
|
||||
MyAdssDocResponseDto,
|
||||
} from "../model/elapr-prgrs-stts-interface";
|
||||
|
||||
const BASE = ENDPOINT.AS_ELAPR_SERVICE.ELAPR_PRGRS_STTS;
|
||||
@@ -83,9 +84,9 @@ async function insertApprover(
|
||||
}
|
||||
|
||||
async function findMyElaperDocList(
|
||||
params: ElaprPrgrsSttsMyDocSearchParams,
|
||||
): Promise<PageResponse<ElaprPrgrsSttsResponseDto>> {
|
||||
const response = await client.get<PageResponse<ElaprPrgrsSttsResponseDto>>(`${BASE}/my/doc`, {
|
||||
params: ElaprMyDocSearchDto,
|
||||
): Promise<PageResponse<MyAdssDocResponseDto>> {
|
||||
const response = await client.get<PageResponse<MyAdssDocResponseDto>>(`${BASE}/my/doc`, {
|
||||
params,
|
||||
});
|
||||
return response.data;
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { ElaprDmndSaveRequestDto, ElaprDmndSearchDto } from "../model/elapr
|
||||
import type {
|
||||
ElaprApproverChangeRequestDto,
|
||||
ElaprApproverInsertRequestDto,
|
||||
ElaprPrgrsSttsMyDocSearchParams,
|
||||
ElaprMyDocSearchDto,
|
||||
ElaprPrgrsSttsSaveRequestDto,
|
||||
ElaprProcessRequestDto,
|
||||
} from "../model/elapr-prgrs-stts-interface";
|
||||
@@ -261,7 +261,7 @@ export function useElaprPrgrsStts(elaprPrgrsId: string | null | undefined) {
|
||||
});
|
||||
}
|
||||
|
||||
export function useElaprPrgrsSttsMyDocPage(params: ElaprPrgrsSttsMyDocSearchParams) {
|
||||
export function useElaprPrgrsSttsMyDocPage(params: ElaprMyDocSearchDto) {
|
||||
return useQuery({
|
||||
queryKey: [...elaprPrgrsSttsBaseKey, "findMyElaperDocList", params],
|
||||
queryFn: () => elaprPrgrsSttsService.findMyElaperDocList(params),
|
||||
|
||||
@@ -92,13 +92,14 @@ export type {
|
||||
export type {
|
||||
ElaprApproverChangeRequestDto,
|
||||
ElaprApproverInsertRequestDto,
|
||||
ElaprPrgrsSttsMyDocSearchParams,
|
||||
ElaprMyDocSearchDto,
|
||||
ElaprPrgrsSttsResponseDto,
|
||||
ElaprPrgrsSttsSaveMode,
|
||||
ElaprPrgrsSttsSaveRequestDto,
|
||||
ElaprPrgrsSttsSearchDto,
|
||||
ElaprPrgrsSttsService,
|
||||
ElaprProcessRequestDto,
|
||||
MyAdssDocResponseDto,
|
||||
} from "./model/elapr-prgrs-stts-interface";
|
||||
export { ELAPR_PRGRS_STTS_SAVE_MODE } from "./model/elapr-prgrs-stts-interface";
|
||||
export type {
|
||||
|
||||
@@ -16,7 +16,7 @@ export function validateAtrzOrder(rows: AtrzOrderValidateRow[]): string | null {
|
||||
const visibleRows = rows.filter((row) => !row.isDeleted);
|
||||
|
||||
const finalCount = visibleRows.filter((row) => row.atrzSeComCd === ATRZ_FINAL_CD).length;
|
||||
if (finalCount === 0) {
|
||||
if (visibleRows.length > 0 && finalCount === 0) {
|
||||
return "최종승인자는 반드시 등록해야 합니다.";
|
||||
}
|
||||
if (finalCount > 1) {
|
||||
@@ -52,7 +52,8 @@ export function validateAllRowsForSave(
|
||||
}
|
||||
|
||||
const visibleRows = getVisibleRows(rows);
|
||||
if (visibleRows.length === 0) {
|
||||
const hasDeletedDirtyRows = rows.some((row) => row.isDeleted && row.isDirty);
|
||||
if (visibleRows.length === 0 && !hasDeletedDirtyRows) {
|
||||
return "저장할 결재선이 없습니다.";
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,26 @@ export interface ApproverStatusConfig {
|
||||
}
|
||||
|
||||
export const APPROVER_STATUS_MAP: Record<string, ApproverStatusConfig> = {
|
||||
ATRZ_PRGRS_001: { label: "대기", badgeClass: "bg-gray-100 text-gray-700" },
|
||||
ATRZ_PRGRS_002: { label: "요청", badgeClass: "bg-yellow-100 text-yellow-700" },
|
||||
ATRZ_PRGRS_003: { label: "승인", badgeClass: "bg-green-100 text-green-700" },
|
||||
ATRZ_PRGRS_004: { label: "반려", badgeClass: "bg-red-100 text-red-700" },
|
||||
ATRZ_PRGRS_999: { label: "결재안함", badgeClass: "bg-blue-100 text-blue-700" },
|
||||
ATRZ_PRGRS_001: {
|
||||
label: "대기",
|
||||
badgeClass: "bg-gray-100 text-gray-700",
|
||||
},
|
||||
ATRZ_PRGRS_002: {
|
||||
label: "결재요청",
|
||||
badgeClass: "bg-yellow-100 text-yellow-700",
|
||||
},
|
||||
ATRZ_PRGRS_003: {
|
||||
label: "결재승인",
|
||||
badgeClass: "bg-green-100 text-green-700",
|
||||
},
|
||||
ATRZ_PRGRS_004: {
|
||||
label: "반려",
|
||||
badgeClass: "bg-red-100 text-red-700",
|
||||
},
|
||||
ATRZ_PRGRS_999: {
|
||||
label: "결재안함",
|
||||
badgeClass: "bg-blue-100 text-blue-700",
|
||||
},
|
||||
};
|
||||
|
||||
export function sortByAtrzSeqNo(
|
||||
@@ -19,10 +34,19 @@ export function sortByAtrzSeqNo(
|
||||
return list.slice().sort((a, b) => Number(a.atrzSeqNo ?? 0) - Number(b.atrzSeqNo ?? 0));
|
||||
}
|
||||
|
||||
export function getApproverStatus(status: string | undefined): ApproverStatusConfig {
|
||||
export function getApproverStatus(
|
||||
atrzPrgrsSttsComCd?: string,
|
||||
atrzSeComCd?: string,
|
||||
): ApproverStatusConfig {
|
||||
if (atrzPrgrsSttsComCd === "ATRZ_PRGRS_003" && atrzSeComCd === "ATRZ01")
|
||||
return {
|
||||
label: "기술검토 접수",
|
||||
badgeClass: "bg-green-100 text-green-700",
|
||||
};
|
||||
|
||||
return (
|
||||
APPROVER_STATUS_MAP[status ?? ""] ?? {
|
||||
label: status || "-",
|
||||
APPROVER_STATUS_MAP[atrzPrgrsSttsComCd ?? ""] ?? {
|
||||
label: "-",
|
||||
badgeClass: "bg-gray-100 text-gray-700",
|
||||
}
|
||||
);
|
||||
|
||||
@@ -7,6 +7,8 @@ export interface ElaprDmndSaveRequestDto {
|
||||
elaprDocNo: string;
|
||||
dmndTtl: string;
|
||||
dmndCn: string;
|
||||
elaprCycl?: number;
|
||||
docPrgrsSttsComCd?: string;
|
||||
elaprPrgrsSttsList?: ElaprPrgrsSttsSaveRequestDto[];
|
||||
}
|
||||
|
||||
|
||||
@@ -44,14 +44,35 @@ export interface ElaprPrgrsSttsSearchDto {
|
||||
atrzPrgrsSttsComCd?: string;
|
||||
}
|
||||
|
||||
/** 백엔드 ElaprPrgrsSttsSearchDto + Pageable */
|
||||
export interface ElaprPrgrsSttsMyDocSearchParams extends ElaprPrgrsSttsSearchDto, PageableParams {}
|
||||
/** 백엔드 MyAdssDocResponseDto */
|
||||
export interface MyAdssDocResponseDto {
|
||||
docNo: string;
|
||||
docTtl?: string;
|
||||
docSeComCd?: string;
|
||||
docSeComNm?: string;
|
||||
reqDt?: string;
|
||||
prgrsSttsComCd?: string;
|
||||
prgrsSttsComNm?: string;
|
||||
cmptnExpctDt?: string;
|
||||
picNm?: string;
|
||||
picTelno?: string;
|
||||
chgCycl?: number;
|
||||
/** 등록자 UUID */
|
||||
rgtr?: string;
|
||||
}
|
||||
|
||||
/** 백엔드 ElaprMyDocSearchDto + Pageable */
|
||||
export interface ElaprMyDocSearchDto extends PageableParams {
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
docSeComCd?: string;
|
||||
prgrsSttsComCd?: string;
|
||||
}
|
||||
|
||||
/** 백엔드 ElaprProcessRequestDto */
|
||||
export interface ElaprProcessRequestDto {
|
||||
atrzPrgrsSttsComCd: string;
|
||||
rjctRsn?: string;
|
||||
finalApprovalYn?: boolean;
|
||||
}
|
||||
|
||||
/** 백엔드 ElaprApproverChangeRequestDto */
|
||||
@@ -87,6 +108,6 @@ export interface ElaprPrgrsSttsService {
|
||||
body: ElaprApproverInsertRequestDto,
|
||||
): Promise<ElaprPrgrsSttsResponseDto>;
|
||||
findMyElaperDocList(
|
||||
params: ElaprPrgrsSttsMyDocSearchParams,
|
||||
): Promise<PageResponse<ElaprPrgrsSttsResponseDto>>;
|
||||
params: ElaprMyDocSearchDto,
|
||||
): Promise<PageResponse<MyAdssDocResponseDto>>;
|
||||
}
|
||||
|
||||
@@ -314,8 +314,8 @@ function ElaprApprovalDialog({
|
||||
<col width="10%" />
|
||||
<col width="30%" />
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
<col width="24%" />
|
||||
<col width="14%" />
|
||||
<col width="20%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
|
||||
@@ -4,7 +4,9 @@ import { Fragment } from "react";
|
||||
|
||||
import { ChevronRight } from "lucide-react";
|
||||
|
||||
import { dayjs, useUserStore } from "@/shared/lib";
|
||||
import type { ElaprDocPrgrsClientResponseDto } from "@/shared/model";
|
||||
import { SectionCollapsible } from "@/shared/ui";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
|
||||
import { cn, Skeleton } from "@common/components";
|
||||
|
||||
@@ -18,7 +20,7 @@ interface ElaprDocPrgrsStatusProps {
|
||||
}
|
||||
|
||||
function ElaprDocPrgrsStep({ item }: { item: ElaprDocPrgrsClientResponseDto }) {
|
||||
const status = getApproverStatus(item.atrzPrgrsSttsComCd);
|
||||
const status = getApproverStatus(item.atrzPrgrsSttsComCd, item.atrzSeComCd);
|
||||
const isRejected = item.atrzPrgrsSttsComCd === REJECT_STATUS_CD;
|
||||
const rjctRsn = item.rjctRsn?.trim();
|
||||
const metaParts = [item.ognzNm, item.jbpsSeNm].filter(Boolean);
|
||||
@@ -26,7 +28,7 @@ function ElaprDocPrgrsStep({ item }: { item: ElaprDocPrgrsClientResponseDto }) {
|
||||
const statusBadge = (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center rounded-sm px-2 py-1 text-lg font-medium",
|
||||
"inline-flex items-center rounded-sm px-4 py-1.5 text-2xl font-semibold",
|
||||
status.badgeClass,
|
||||
isRejected && rjctRsn && "cursor-help underline decoration-dotted underline-offset-4",
|
||||
)}
|
||||
@@ -36,7 +38,7 @@ function ElaprDocPrgrsStep({ item }: { item: ElaprDocPrgrsClientResponseDto }) {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex min-w-[12rem] flex-1 flex-col items-center gap-3 p-4 text-center">
|
||||
<div className="card card-border flex min-w-[12rem] flex-1 flex-col items-center gap-4 border p-6 text-center">
|
||||
{isRejected && rjctRsn ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{statusBadge}</TooltipTrigger>
|
||||
@@ -49,11 +51,16 @@ function ElaprDocPrgrsStep({ item }: { item: ElaprDocPrgrsClientResponseDto }) {
|
||||
)}
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xl text-muted-foreground">[{item.atrzSeComNm || "-"}]</span>
|
||||
<span className="text-xl font-semibold">{item.aprvrNm || item.aprvrUuid || "-"}</span>
|
||||
<p className="text-2xl font-semibold text-foreground">
|
||||
<span className="text-muted-foreground">[{item.atrzSeComNm || "-"}]</span>{" "}
|
||||
{item.aprvrNm || item.aprvrUuid || "-"}
|
||||
</p>
|
||||
</div>
|
||||
{metaParts.length > 0 && (
|
||||
<span className="text-base text-gray-500">{metaParts.join(" · ")}</span>
|
||||
<span className="text-lg text-gray-500">{metaParts.join(" · ")}</span>
|
||||
)}
|
||||
{item.atrzDt && (
|
||||
<span className="text-lg text-gray-500">{dayjs(item.atrzDt).format("YYYY-MM-DD")}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -61,34 +68,45 @@ function ElaprDocPrgrsStep({ item }: { item: ElaprDocPrgrsClientResponseDto }) {
|
||||
}
|
||||
|
||||
function ElaprDocPrgrsStatus({ elaprDocNo }: ElaprDocPrgrsStatusProps) {
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const { data, isLoading } = useElaprDocPrgrsByDocNo(elaprDocNo);
|
||||
const steps = sortByAtrzSeqNo(data ?? []);
|
||||
|
||||
if (userInfo?.dtlInstCd !== "ROLE_USER01") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center gap-3 px-4 py-6">
|
||||
<Skeleton className="h-24 flex-1 rounded-sm" />
|
||||
<Skeleton className="h-24 flex-1 rounded-sm" />
|
||||
<Skeleton className="h-24 flex-1 rounded-sm" />
|
||||
</div>
|
||||
<SectionCollapsible title="결재현황">
|
||||
<div className="flex items-center gap-3 px-4 py-6">
|
||||
<Skeleton className="h-24 flex-1 rounded-sm" />
|
||||
<Skeleton className="h-24 flex-1 rounded-sm" />
|
||||
<Skeleton className="h-24 flex-1 rounded-sm" />
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
);
|
||||
}
|
||||
|
||||
if (steps.length === 0) {
|
||||
return <div className="py-8 text-center text-xl text-gray-500">결재 정보가 없습니다.</div>;
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-stretch justify-start gap-4 overflow-x-auto">
|
||||
{steps.map((item, index) => (
|
||||
<Fragment key={item.elaprPrgrsId ?? `${item.atrzSeqNo}-${item.aprvrUuid ?? index}`}>
|
||||
<div className="flex items-center">
|
||||
{index > 0 && <ChevronRight className="size-8 text-muted-foreground" />}
|
||||
</div>
|
||||
<ElaprDocPrgrsStep item={item} />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
<SectionCollapsible title="결재현황">
|
||||
<div className="flex flex-wrap items-stretch justify-start gap-4 overflow-x-auto p-4">
|
||||
{steps.map((item, index) => (
|
||||
<Fragment key={item.elaprPrgrsId ?? `${item.atrzSeqNo}-${item.aprvrUuid ?? index}`}>
|
||||
{index > 0 && (
|
||||
<div className="flex items-center">
|
||||
{<ChevronRight className="size-8 text-muted-foreground" />}
|
||||
</div>
|
||||
)}
|
||||
<ElaprDocPrgrsStep item={item} />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -73,14 +73,14 @@ function SortablePrgrsSttsRow({
|
||||
const isDcarbable = isPrgrsSttsRowDcarbable(row);
|
||||
const rowDisabled = isLocked && !isEditing;
|
||||
const atrzSeLabel = row.atrzSeComNm || findCodeLabel(atrzSeCodes, row.atrzSeComCd);
|
||||
const status = getApproverStatus(row.atrzPrgrsSttsComCd);
|
||||
const status = getApproverStatus(row.atrzPrgrsSttsComCd, row.atrzSeComCd);
|
||||
const isRejectedRow = row.atrzPrgrsSttsComCd === "ATRZ_PRGRS_004";
|
||||
const rjctRsn = row.rjctRsn?.trim();
|
||||
|
||||
const statusBadge = (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center rounded-sm px-3 py-1.5 text-lg font-semibold",
|
||||
"inline-flex items-center whitespace-nowrap rounded-sm px-3 py-1.5 text-lg font-semibold",
|
||||
status.badgeClass,
|
||||
isRejectedRow && rjctRsn && "cursor-help underline decoration-dotted underline-offset-4",
|
||||
)}
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
import type { PageableParams, PageResponse } from "@/shared/model";
|
||||
import type {
|
||||
AtflMngClientResponseDto,
|
||||
EsntlAtchFileMngTreeResponseDto,
|
||||
PageableParams,
|
||||
PageResponse,
|
||||
} from "@/shared/model";
|
||||
|
||||
/** 필수첨부파일관리 트리 조회 응답 DTO */
|
||||
export interface EsntlAtchFileMngTreeResponseDto {
|
||||
/** 필수첨부파일아이디 */
|
||||
esntlAtchFileId: string;
|
||||
/** 문서구분공통코드 */
|
||||
docSeComCd: string;
|
||||
/** 필수첨부파일명 */
|
||||
esntlAtchFileNm: string;
|
||||
/** 필수첨부여부 ("Y" | "N") */
|
||||
esntlAtchYn: "Y" | "N";
|
||||
/** 필수첨부파일비고 (파일형식 등) */
|
||||
esntlAtchFileRmrk: string;
|
||||
}
|
||||
export type { EsntlAtchFileMngTreeResponseDto };
|
||||
|
||||
/** 필수첨부파일관리 엔티티 (목록/상세 공용 응답 DTO) */
|
||||
export interface EsntlAtchFileMng {
|
||||
@@ -26,6 +19,10 @@ export interface EsntlAtchFileMng {
|
||||
esntlAtchYn: "Y" | "N";
|
||||
/** 필수첨부파일비고 */
|
||||
esntlAtchFileRmrk?: string;
|
||||
/** 첨부파일그룹아이디 (양식파일 그룹) */
|
||||
atchFileGroupId?: string;
|
||||
/** 첨부파일 목록 (양식파일) */
|
||||
atchFileList?: AtflMngClientResponseDto[];
|
||||
/** 등록자UUID */
|
||||
rgtr?: string;
|
||||
/** 등록자명 */
|
||||
@@ -80,4 +77,6 @@ export interface EsntlAtchFileMngSaveRequest {
|
||||
esntlAtchYn?: "Y" | "N";
|
||||
/** 필수첨부파일비고 */
|
||||
esntlAtchFileRmrk?: string;
|
||||
/** 첨부파일그룹아이디 (양식파일 그룹) */
|
||||
atchFileGroupId?: string;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { PageResponse } from "@/shared/model";
|
||||
import type {
|
||||
DocPrgrsUpdateRequestDto,
|
||||
FnrsCnsltnApplySaveRequestDto,
|
||||
FnrsCnsltnFileVrfcRsltResponseDto,
|
||||
FnrsCnsltnMngDetResponseDto,
|
||||
FnrsCnsltnMngResponseDto,
|
||||
FnrsCnsltnMngSaveRequestDto,
|
||||
@@ -12,6 +13,10 @@ import type {
|
||||
FnrsCnsltnMngService,
|
||||
FnrsCnsltnMngUpdateRequestDto,
|
||||
} from "../model/fnrs-cnsltn-interface";
|
||||
import type {
|
||||
EnvoActnReviewReqDto,
|
||||
FnrsExamineRequestDto,
|
||||
} from "../model/fnrs-cnsltn-payload-schema";
|
||||
|
||||
const BASE = ENDPOINT.AS_SERVICE.FNRS_CNSLTN_MNG;
|
||||
|
||||
@@ -51,6 +56,16 @@ async function techReview(id: string, body: FnrsCnsltnApplySaveRequestDto): Prom
|
||||
await client.post<void>(`${BASE}/${id}/tech-review`, body);
|
||||
}
|
||||
|
||||
async function requestExamine(docNo: string, body: FnrsExamineRequestDto): Promise<string> {
|
||||
const response = await client.post<string>(`${BASE}/${docNo}/request-examine`, body);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function requestActnReview(docNo: string, body: EnvoActnReviewReqDto): Promise<string> {
|
||||
const response = await client.post<string>(`${BASE}/${docNo}/request-actn-review`, body);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function submitSupplement(id: string, body: FnrsCnsltnApplySaveRequestDto): Promise<string> {
|
||||
const response = await client.post<string>(`${BASE}/${id}/submitsupplement`, body);
|
||||
return response.data;
|
||||
@@ -68,6 +83,15 @@ async function remove(fnrsCnsltnDocNo: string): Promise<void> {
|
||||
await client.delete<void>(`${BASE}/${fnrsCnsltnDocNo}`);
|
||||
}
|
||||
|
||||
async function findFileVrfcResults(
|
||||
fnrsCnsltnDocNo: string,
|
||||
): Promise<FnrsCnsltnFileVrfcRsltResponseDto[]> {
|
||||
const response = await client.get<FnrsCnsltnFileVrfcRsltResponseDto[]>(
|
||||
`${BASE}/${fnrsCnsltnDocNo}/file-vrfc-results`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function changePrgrsSttsComCd(body: DocPrgrsUpdateRequestDto): Promise<void> {
|
||||
await client.post<void>(`${BASE}/doc/updatePrgrsStts`, body);
|
||||
}
|
||||
@@ -79,8 +103,11 @@ export const fnrsCnsltnMngService = {
|
||||
saveApply,
|
||||
update,
|
||||
techReview,
|
||||
requestExamine,
|
||||
requestActnReview,
|
||||
submitSupplement,
|
||||
submitReqSupplement,
|
||||
remove,
|
||||
findFileVrfcResults,
|
||||
changePrgrsSttsComCd,
|
||||
} satisfies FnrsCnsltnMngService;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type {
|
||||
DocPrgrsUpdateRequestDto,
|
||||
FnrsCnsltnApplySaveRequestDto,
|
||||
@@ -9,32 +7,49 @@ import type {
|
||||
FnrsCnsltnMngSearchDto,
|
||||
FnrsCnsltnMngUpdateRequestDto,
|
||||
} from "../model/fnrs-cnsltn-interface";
|
||||
import type {
|
||||
EnvoActnReviewReqDto,
|
||||
FnrsExamineRequestDto,
|
||||
} from "../model/fnrs-cnsltn-payload-schema";
|
||||
|
||||
import { fnrsCnsltnMngService } from "./fnrs-cnsltn-service";
|
||||
|
||||
export const fnrsCnsltnMngBaseKey = [ENDPOINT.AS_SERVICE.FNRS_CNSLTN_MNG] as const;
|
||||
export const fnrsCnsltnKeys = {
|
||||
all: ["fnrs-cnsltn"] as const,
|
||||
list: () => [...fnrsCnsltnKeys.all, "list"] as const,
|
||||
detail: (id: string) => [...fnrsCnsltnKeys.all, "detail", id] as const,
|
||||
fileVrfcResults: (id: string) => [...fnrsCnsltnKeys.all, "fileVrfcResults", id] as const,
|
||||
};
|
||||
|
||||
export function useFnrsCnsltnMngPage(params: FnrsCnsltnMngSearchDto) {
|
||||
return useQuery({
|
||||
queryKey: [...fnrsCnsltnMngBaseKey, "findPage", params],
|
||||
queryKey: [...fnrsCnsltnKeys.list(), params],
|
||||
queryFn: () => fnrsCnsltnMngService.findPage(params),
|
||||
});
|
||||
}
|
||||
|
||||
export function useFnrsCnsltnMng(fnrsCnsltnDocNo: string | null | undefined) {
|
||||
return useQuery({
|
||||
queryKey: [...fnrsCnsltnMngBaseKey, "findById", fnrsCnsltnDocNo],
|
||||
queryKey: fnrsCnsltnKeys.detail(fnrsCnsltnDocNo!),
|
||||
queryFn: () => fnrsCnsltnMngService.findById(fnrsCnsltnDocNo!),
|
||||
enabled: Boolean(fnrsCnsltnDocNo),
|
||||
});
|
||||
}
|
||||
|
||||
export function useFnrsCnsltnFileVrfcResults(fnrsCnsltnDocNo: string | null | undefined) {
|
||||
return useQuery({
|
||||
queryKey: fnrsCnsltnKeys.fileVrfcResults(fnrsCnsltnDocNo!),
|
||||
queryFn: () => fnrsCnsltnMngService.findFileVrfcResults(fnrsCnsltnDocNo!),
|
||||
enabled: Boolean(fnrsCnsltnDocNo),
|
||||
});
|
||||
}
|
||||
|
||||
export function useSaveFnrsCnsltnMng() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (body: FnrsCnsltnMngSaveRequestDto) => fnrsCnsltnMngService.save(body),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: fnrsCnsltnMngBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -44,7 +59,7 @@ export function useSaveApplyFnrsCnsltnMng() {
|
||||
return useMutation({
|
||||
mutationFn: (body: FnrsCnsltnApplySaveRequestDto) => fnrsCnsltnMngService.saveApply(body),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: fnrsCnsltnMngBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -60,9 +75,9 @@ export function useUpdateFnrsCnsltnMng() {
|
||||
body: FnrsCnsltnMngUpdateRequestDto;
|
||||
}) => fnrsCnsltnMngService.update(fnrsCnsltnDocNo, body),
|
||||
onSuccess: (_data, { fnrsCnsltnDocNo }) => {
|
||||
queryClient.invalidateQueries({ queryKey: fnrsCnsltnMngBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...fnrsCnsltnMngBaseKey, "findById", fnrsCnsltnDocNo],
|
||||
queryKey: fnrsCnsltnKeys.detail(fnrsCnsltnDocNo),
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -74,9 +89,37 @@ export function useTechReviewFnrsCnsltnMng() {
|
||||
mutationFn: ({ id, body }: { id: string; body: FnrsCnsltnApplySaveRequestDto }) =>
|
||||
fnrsCnsltnMngService.techReview(id, body),
|
||||
onSuccess: (_data, { id }) => {
|
||||
queryClient.invalidateQueries({ queryKey: fnrsCnsltnMngBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...fnrsCnsltnMngBaseKey, "findById", id],
|
||||
queryKey: fnrsCnsltnKeys.detail(id),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useRequestExamineFnrsCnsltnMng() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ docNo, body }: { docNo: string; body: FnrsExamineRequestDto }) =>
|
||||
fnrsCnsltnMngService.requestExamine(docNo, body),
|
||||
onSuccess: (_data, { docNo }) => {
|
||||
queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: fnrsCnsltnKeys.detail(docNo),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useRequestActnReviewFnrsCnsltnMng() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ docNo, body }: { docNo: string; body: EnvoActnReviewReqDto }) =>
|
||||
fnrsCnsltnMngService.requestActnReview(docNo, body),
|
||||
onSuccess: (_data, { docNo }) => {
|
||||
queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: fnrsCnsltnKeys.detail(docNo),
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -88,9 +131,9 @@ export function useSubmitSupplementFnrsCnsltnMng() {
|
||||
mutationFn: ({ id, body }: { id: string; body: FnrsCnsltnApplySaveRequestDto }) =>
|
||||
fnrsCnsltnMngService.submitSupplement(id, body),
|
||||
onSuccess: (_data, { id }) => {
|
||||
queryClient.invalidateQueries({ queryKey: fnrsCnsltnMngBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...fnrsCnsltnMngBaseKey, "findById", id],
|
||||
queryKey: fnrsCnsltnKeys.detail(id),
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -102,9 +145,9 @@ export function useSubmitReqSupplementFnrsCnsltnMng() {
|
||||
mutationFn: ({ id, body }: { id: string; body: FnrsCnsltnApplySaveRequestDto }) =>
|
||||
fnrsCnsltnMngService.submitReqSupplement(id, body),
|
||||
onSuccess: (_data, { id }) => {
|
||||
queryClient.invalidateQueries({ queryKey: fnrsCnsltnMngBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...fnrsCnsltnMngBaseKey, "findById", id],
|
||||
queryKey: fnrsCnsltnKeys.detail(id),
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -115,7 +158,7 @@ export function useDeleteFnrsCnsltnMng() {
|
||||
return useMutation({
|
||||
mutationFn: (fnrsCnsltnDocNo: string) => fnrsCnsltnMngService.remove(fnrsCnsltnDocNo),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: fnrsCnsltnMngBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -125,7 +168,7 @@ export function useChangePrgrsSttsComCd() {
|
||||
return useMutation({
|
||||
mutationFn: (body: DocPrgrsUpdateRequestDto) => fnrsCnsltnMngService.changePrgrsSttsComCd(body),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: fnrsCnsltnMngBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
export { fnrsCnsltnMngService } from "./api/fnrs-cnsltn-service";
|
||||
export { fnrsCnsltnMngBaseKey } from "./api/queries";
|
||||
export { fnrsCnsltnKeys } from "./api/queries";
|
||||
export {
|
||||
useChangePrgrsSttsComCd,
|
||||
useDeleteFnrsCnsltnMng,
|
||||
useFnrsCnsltnFileVrfcResults,
|
||||
useFnrsCnsltnMng,
|
||||
useFnrsCnsltnMngPage,
|
||||
useRequestActnReviewFnrsCnsltnMng,
|
||||
useRequestExamineFnrsCnsltnMng,
|
||||
useSaveApplyFnrsCnsltnMng,
|
||||
useSaveFnrsCnsltnMng,
|
||||
useSubmitReqSupplementFnrsCnsltnMng,
|
||||
@@ -25,6 +28,7 @@ export type {
|
||||
EsntlAtchFileItem,
|
||||
EsntlAtchFileMngTreeResponseDto,
|
||||
FnrsCnsltnApplySaveRequestDto,
|
||||
FnrsCnsltnFileVrfcRsltResponseDto,
|
||||
FnrsCnsltnMngDetResponseDto,
|
||||
FnrsCnsltnMngResponseDto,
|
||||
FnrsCnsltnMngSaveRequestDto,
|
||||
@@ -37,3 +41,11 @@ export type {
|
||||
FnrsCnsltnSplmntDmndResponseDto,
|
||||
FnrsSharItem,
|
||||
} from "./model/fnrs-cnsltn-interface";
|
||||
export type {
|
||||
EnvoActnReviewReqDto,
|
||||
FnrsExamineRequestDto,
|
||||
} from "./model/fnrs-cnsltn-payload-schema";
|
||||
export {
|
||||
envoActnReviewReqSchema,
|
||||
fnrsExamineRequestSchema,
|
||||
} from "./model/fnrs-cnsltn-payload-schema";
|
||||
|
||||
@@ -16,6 +16,11 @@ import type {
|
||||
} from "@/shared/model";
|
||||
import type { FnrsSharBizCstTreeDto } from "@/entities/fnrs-shar-se";
|
||||
|
||||
import type {
|
||||
EnvoActnReviewReqDto,
|
||||
FnrsExamineRequestDto,
|
||||
} from "./fnrs-cnsltn-payload-schema";
|
||||
|
||||
export type {
|
||||
AtflMngClientResponseDto,
|
||||
BizCstTreeDto,
|
||||
@@ -79,6 +84,8 @@ export interface FnrsCnsltnMngSearchDto extends PageableParams {
|
||||
atchFileGroupId?: string;
|
||||
sharAtchFileGroupId?: string;
|
||||
prgrsSttsComCd?: string;
|
||||
rtlztnFcltSeComCd?: string;
|
||||
rtlztnUsgSeComCd?: string;
|
||||
}
|
||||
|
||||
interface FnrsCnsltnMngResponseExtension {
|
||||
@@ -86,12 +93,17 @@ interface FnrsCnsltnMngResponseExtension {
|
||||
envoSeCdNm?: string;
|
||||
taskSeCdNm?: string;
|
||||
prgrsSttsCdNm?: string;
|
||||
beforePrgrsSttsComCd?: string;
|
||||
beforePrgrsSttsCdNm?: string;
|
||||
rtlztnFcltSeComNm?: string;
|
||||
rtlztnUsgSeComNm?: string;
|
||||
regDt?: string;
|
||||
rgtr?: string;
|
||||
rgtrNm?: string;
|
||||
mdfcnDt?: string;
|
||||
sido?: string;
|
||||
gugun?: string;
|
||||
techRplyHstryYn?: "Y" | "N";
|
||||
}
|
||||
|
||||
/** 백엔드 FnrsCnsltnMngResponseDto */
|
||||
@@ -162,6 +174,22 @@ export interface FnrsCnsltnApplySaveRequestDto extends PbadmsApplySaveHeader {
|
||||
fnrsSharList?: FnrsSharItem[];
|
||||
}
|
||||
|
||||
/** 백엔드 FnrsCnsltnFileVrfcRsltResponseDto */
|
||||
export interface FnrsCnsltnFileVrfcRsltResponseDto {
|
||||
fileSeComCd?: string;
|
||||
fileSeNm?: string;
|
||||
esntlAtchFileId?: string;
|
||||
atchFileGroupId?: string;
|
||||
atflId?: string;
|
||||
orgnlFileNm?: string;
|
||||
atflFlieSz?: number;
|
||||
vrfcSttsComCd?: string;
|
||||
vrfcSttsComNm?: string;
|
||||
vrfcRsltCn?: string;
|
||||
vrfcDt?: string;
|
||||
regDt?: string;
|
||||
}
|
||||
|
||||
/** Controller public 메서드명·시그니처와 동일 */
|
||||
export interface FnrsCnsltnMngService {
|
||||
findPage(searchDto: FnrsCnsltnMngSearchDto): Promise<PageResponse<FnrsCnsltnMngResponseDto>>;
|
||||
@@ -170,8 +198,11 @@ export interface FnrsCnsltnMngService {
|
||||
saveApply(body: FnrsCnsltnApplySaveRequestDto): Promise<string>;
|
||||
update(fnrsCnsltnDocNo: string, body: FnrsCnsltnMngUpdateRequestDto): Promise<string>;
|
||||
techReview(id: string, body: FnrsCnsltnApplySaveRequestDto): Promise<void>;
|
||||
requestExamine(docNo: string, body: FnrsExamineRequestDto): Promise<string>;
|
||||
requestActnReview(docNo: string, body: EnvoActnReviewReqDto): Promise<string>;
|
||||
submitSupplement(id: string, body: FnrsCnsltnApplySaveRequestDto): Promise<string>;
|
||||
submitReqSupplement(id: string, body: FnrsCnsltnApplySaveRequestDto): Promise<string>;
|
||||
remove(fnrsCnsltnDocNo: string): Promise<void>;
|
||||
findFileVrfcResults(fnrsCnsltnDocNo: string): Promise<FnrsCnsltnFileVrfcRsltResponseDto[]>;
|
||||
changePrgrsSttsComCd(body: DocPrgrsUpdateRequestDto): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { z } from "zod";
|
||||
|
||||
/** 백엔드 FnrsExamineRequestDto */
|
||||
export const fnrsExamineRequestSchema = z.object({
|
||||
prgrsSttsComCd: z.string().optional(),
|
||||
});
|
||||
|
||||
/** 백엔드 EnvoActnReviewReqDto */
|
||||
export const envoActnReviewReqSchema = z.object({
|
||||
envoDocNo: z.string().min(1),
|
||||
specialRqstCn: z.string().optional(),
|
||||
});
|
||||
|
||||
export type FnrsExamineRequestDto = z.infer<typeof fnrsExamineRequestSchema>;
|
||||
export type EnvoActnReviewReqDto = z.infer<typeof envoActnReviewReqSchema>;
|
||||
@@ -6,7 +6,8 @@ export const FNRS_SHAR_SE_SAVE_MODE = {
|
||||
DELETE: "D",
|
||||
} as const;
|
||||
|
||||
export type FnrsSharSeSaveMode = (typeof FNRS_SHAR_SE_SAVE_MODE)[keyof typeof FNRS_SHAR_SE_SAVE_MODE];
|
||||
export type FnrsSharSeSaveMode =
|
||||
(typeof FNRS_SHAR_SE_SAVE_MODE)[keyof typeof FNRS_SHAR_SE_SAVE_MODE];
|
||||
|
||||
/** 백엔드 FnrsSharSeMngResponseDto */
|
||||
export interface FnrsSharSeMngResponseDto {
|
||||
@@ -69,6 +70,8 @@ export interface FnrsSharBizCstTreeDto {
|
||||
ajmtAmt?: number;
|
||||
rmrkCn?: string;
|
||||
envoRmrkCn?: string;
|
||||
chgAmt: number;
|
||||
chgRt: number;
|
||||
}
|
||||
|
||||
/** Controller public 메서드명·시그니처와 동일 */
|
||||
|
||||
@@ -148,6 +148,8 @@ export interface InstlSncnMngResponseDto extends InstlSncnMngSaveRequestDto {
|
||||
taskSeCdNm: string;
|
||||
/** 진행상태공통코드명 */
|
||||
prgrsSttsCdNm: string;
|
||||
/** 기술검토회신이력여부 */
|
||||
techRplyHstryYn: "Y" | "N";
|
||||
/** 재이용시설구분공통코드명 */
|
||||
rtlztnFcltSeComNm: string;
|
||||
/** 재이용용도구분공통코드명 */
|
||||
|
||||
@@ -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,27 @@
|
||||
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 });
|
||||
queryClient.invalidateQueries({ queryKey: [ENDPOINT.AS_SERVICE.DASHBOARD] });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type {
|
||||
PwrkCnsltnDocCrtRsltResponseDto,
|
||||
PwrkCnsltnDocCrtRsltService,
|
||||
} from "../model/pwrk-cnsltn-doc-crt-rslt-interface";
|
||||
import type { PwrkCnsltnDocCrtRsltSaveRequestDto } from "../model/pwrk-cnsltn-doc-crt-rslt-payload-schema";
|
||||
|
||||
const BASE = ENDPOINT.AS_SERVICE.PWRK_CNSLTN_DOC_CRT_RSLT;
|
||||
|
||||
async function findLatestByCrtDmndUuid(
|
||||
crtDmndUuid: string,
|
||||
): Promise<PwrkCnsltnDocCrtRsltResponseDto> {
|
||||
const response = await client.get<PwrkCnsltnDocCrtRsltResponseDto>(
|
||||
`${BASE}/${crtDmndUuid}/latest`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function openWebHwpLatest(crtDmndUuid: string) {
|
||||
return client.get<Blob>(`${BASE}/${crtDmndUuid}/webhwp/open`, {
|
||||
responseType: "blob",
|
||||
});
|
||||
}
|
||||
|
||||
async function download(crtDmndUuid: string, verSn: number) {
|
||||
return client.get<Blob>(`${BASE}/${crtDmndUuid}/${verSn}/download`, {
|
||||
responseType: "blob",
|
||||
});
|
||||
}
|
||||
|
||||
async function downloadLatest(crtDmndUuid: string) {
|
||||
return client.get<Blob>(`${BASE}/${crtDmndUuid}/download`, {
|
||||
responseType: "blob",
|
||||
});
|
||||
}
|
||||
|
||||
async function saveMultipart(
|
||||
saveDto: PwrkCnsltnDocCrtRsltSaveRequestDto,
|
||||
file?: Blob | File,
|
||||
fileName?: string,
|
||||
): Promise<PwrkCnsltnDocCrtRsltResponseDto> {
|
||||
const formData = new FormData();
|
||||
formData.append("crtDmndUuid", saveDto.crtDmndUuid);
|
||||
formData.append("pwrkCnsltnDocNo", saveDto.pwrkCnsltnDocNo);
|
||||
if (saveDto.atchFileGroupId) {
|
||||
formData.append("atchFileGroupId", saveDto.atchFileGroupId);
|
||||
}
|
||||
if (file) {
|
||||
const resolvedFileName = fileName ?? (file instanceof File ? file.name : "document.hwpx");
|
||||
formData.append("file", file, resolvedFileName);
|
||||
}
|
||||
|
||||
const response = await client.post<PwrkCnsltnDocCrtRsltResponseDto>(BASE, formData, {
|
||||
headers: { "Content-Type": undefined },
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function updateMultipart(
|
||||
crtDmndUuid: string,
|
||||
verSn: number,
|
||||
formData: FormData,
|
||||
): Promise<PwrkCnsltnDocCrtRsltResponseDto> {
|
||||
const response = await client.put<PwrkCnsltnDocCrtRsltResponseDto>(
|
||||
`${BASE}/${crtDmndUuid}/${verSn}`,
|
||||
formData,
|
||||
{ headers: { "Content-Type": undefined } },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const pwrkCnsltnDocCrtRsltService = {
|
||||
findLatestByCrtDmndUuid,
|
||||
openWebHwpLatest,
|
||||
download,
|
||||
downloadLatest,
|
||||
saveMultipart,
|
||||
updateMultipart,
|
||||
} satisfies PwrkCnsltnDocCrtRsltService;
|
||||
@@ -0,0 +1,10 @@
|
||||
export { pwrkCnsltnDocCrtRsltService } from "./api/pwrk-cnsltn-doc-crt-rslt-service";
|
||||
export type {
|
||||
PwrkCnsltnDocCrtRsltResponseDto,
|
||||
PwrkCnsltnDocCrtRsltService,
|
||||
PwrkCnsltnDocCrtRsltUpdateRequestDto,
|
||||
} from "./model/pwrk-cnsltn-doc-crt-rslt-interface";
|
||||
export {
|
||||
type PwrkCnsltnDocCrtRsltSaveRequestDto,
|
||||
pwrkCnsltnDocCrtRsltSaveRequestSchema,
|
||||
} from "./model/pwrk-cnsltn-doc-crt-rslt-payload-schema";
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import type { AxiosResponse } from "axios";
|
||||
|
||||
import type { PwrkCnsltnDocCrtRsltSaveRequestDto } from "./pwrk-cnsltn-doc-crt-rslt-payload-schema";
|
||||
|
||||
export interface PwrkCnsltnDocCrtRsltResponseDto {
|
||||
crtDmndUuid: string;
|
||||
verSn: number;
|
||||
rgtrUuid?: string;
|
||||
rgtrNm?: string;
|
||||
orgnlOtptDocCn: null;
|
||||
crtCmptnDt?: string;
|
||||
}
|
||||
|
||||
/** 백엔드 PwrkCnsltnDocCrtRsltUpdateRequestDto (모두 optional) */
|
||||
export interface PwrkCnsltnDocCrtRsltUpdateRequestDto {
|
||||
rgtrUuid?: string;
|
||||
orgnlOtptDocCn?: string;
|
||||
crtCmptnDt?: string;
|
||||
}
|
||||
|
||||
export interface PwrkCnsltnDocCrtRsltService {
|
||||
findLatestByCrtDmndUuid(crtDmndUuid: string): Promise<PwrkCnsltnDocCrtRsltResponseDto>;
|
||||
openWebHwpLatest(crtDmndUuid: string): Promise<AxiosResponse<Blob>>;
|
||||
download(crtDmndUuid: string, verSn: number): Promise<AxiosResponse<Blob>>;
|
||||
downloadLatest(crtDmndUuid: string): Promise<AxiosResponse<Blob>>;
|
||||
saveMultipart(
|
||||
saveDto: PwrkCnsltnDocCrtRsltSaveRequestDto,
|
||||
file?: Blob | File,
|
||||
fileName?: string,
|
||||
): Promise<PwrkCnsltnDocCrtRsltResponseDto>;
|
||||
updateMultipart(
|
||||
crtDmndUuid: string,
|
||||
verSn: number,
|
||||
formData: FormData,
|
||||
): Promise<PwrkCnsltnDocCrtRsltResponseDto>;
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { z } from "zod";
|
||||
|
||||
/** 백엔드 PwrkCnsltnDocCrtRsltSaveRequestDto (orgnlOtptDocCn은 multipart file로 전송) */
|
||||
export const pwrkCnsltnDocCrtRsltSaveRequestSchema = z.object({
|
||||
crtDmndUuid: z.string().uuid(),
|
||||
pwrkCnsltnDocNo: z.string().min(1).max(100),
|
||||
atchFileGroupId: z.string().max(30).optional(),
|
||||
});
|
||||
|
||||
export type PwrkCnsltnDocCrtRsltSaveRequestDto = z.infer<
|
||||
typeof pwrkCnsltnDocCrtRsltSaveRequestSchema
|
||||
>;
|
||||
@@ -0,0 +1,18 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type {
|
||||
PwrkCnsltnMngDetResponseDto,
|
||||
PwrkCnsltnMngService,
|
||||
} from "../model/pwrk-cnsltn-mng-interface";
|
||||
|
||||
const BASE = ENDPOINT.AS_SERVICE.PWRK_CNSLTN_MNG;
|
||||
|
||||
async function findByUpDocNo(upDocNo: string): Promise<PwrkCnsltnMngDetResponseDto> {
|
||||
const response = await client.get<PwrkCnsltnMngDetResponseDto>(`${BASE}/updocno/${upDocNo}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const pwrkCnsltnMngService = {
|
||||
findByUpDocNo,
|
||||
} satisfies PwrkCnsltnMngService;
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { pwrkCnsltnMngService } from "./pwrk-cnsltn-mng-service";
|
||||
|
||||
export const pwrkCnsltnMngKeys = {
|
||||
all: ["pwrk-cnsltn-mng"] as const,
|
||||
byUpDocNo: (upDocNo: string) => [...pwrkCnsltnMngKeys.all, "by-up-doc-no", upDocNo] as const,
|
||||
};
|
||||
|
||||
export function usePwrkCnsltnMngByUpDocNo(upDocNo: string | null | undefined, isEnabled = true) {
|
||||
return useQuery({
|
||||
queryKey: pwrkCnsltnMngKeys.byUpDocNo(upDocNo!),
|
||||
queryFn: () => pwrkCnsltnMngService.findByUpDocNo(upDocNo!),
|
||||
enabled: Boolean(upDocNo) && isEnabled,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export { pwrkCnsltnMngService } from "./api/pwrk-cnsltn-mng-service";
|
||||
export { pwrkCnsltnMngKeys, usePwrkCnsltnMngByUpDocNo } from "./api/queries";
|
||||
export { findCompletedPwrkDraft } from "./lib/find-completed-draft";
|
||||
export type {
|
||||
PwrkCnsltnDocCrtDmndResponseDto,
|
||||
PwrkCnsltnDocCrtRsltResponseDto,
|
||||
PwrkCnsltnMngDetResponseDto,
|
||||
PwrkCnsltnMngService,
|
||||
} from "./model/pwrk-cnsltn-mng-interface";
|
||||
@@ -0,0 +1,12 @@
|
||||
import type {
|
||||
PwrkCnsltnDocCrtDmndResponseDto,
|
||||
PwrkCnsltnMngDetResponseDto,
|
||||
} from "../model/pwrk-cnsltn-mng-interface";
|
||||
|
||||
export function findCompletedPwrkDraft(
|
||||
data: PwrkCnsltnMngDetResponseDto | null | undefined,
|
||||
): PwrkCnsltnDocCrtDmndResponseDto | undefined {
|
||||
return data?.crtDmndList?.find(
|
||||
(item) => item.crtSttsComCd === "C" && Boolean(item.crtDmndUuid) && Boolean(item.crtRslt),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
export interface PwrkCnsltnDocCrtRsltResponseDto {
|
||||
crtDmndUuid: string;
|
||||
verSn: number;
|
||||
rgtrUuid?: string;
|
||||
rgtrNm?: string;
|
||||
orgnlOtptDocCn: null;
|
||||
crtCmptnDt?: string;
|
||||
}
|
||||
|
||||
export interface PwrkCnsltnDocCrtDmndResponseDto {
|
||||
crtDmndUuid: string;
|
||||
pwrkCnsltnDocNo: string;
|
||||
crtSttsComCd?: string;
|
||||
crtSttsComCdNm?: string;
|
||||
failRsn?: string;
|
||||
dmndDt?: string;
|
||||
cmptnDt?: string;
|
||||
crtRslt?: PwrkCnsltnDocCrtRsltResponseDto | null;
|
||||
}
|
||||
|
||||
export interface PwrkCnsltnMngDetResponseDto {
|
||||
pwrkCnsltnDocNo: string;
|
||||
upDocNo: string;
|
||||
upDocSeComCd?: string;
|
||||
prgrsSttsComCd?: string;
|
||||
bizNm?: string;
|
||||
atchFileGroupId?: string;
|
||||
crtDmndList?: PwrkCnsltnDocCrtDmndResponseDto[];
|
||||
crtRsltList?: PwrkCnsltnDocCrtRsltResponseDto[];
|
||||
}
|
||||
|
||||
export interface PwrkCnsltnMngService {
|
||||
findByUpDocNo(upDocNo: string): Promise<PwrkCnsltnMngDetResponseDto>;
|
||||
}
|
||||
@@ -41,11 +41,7 @@ async function findById(
|
||||
}
|
||||
|
||||
async function saveRvwMnpwMng(body: RvwMnpwMngSaveRequestDto): Promise<RvwMnpwMngResponseDto> {
|
||||
const { atrzSeComCd, ognzSn, userUuid } = body;
|
||||
const response = await client.post<RvwMnpwMngResponseDto>(
|
||||
`${BASE}/${atrzSeComCd}/${ognzSn}/${userUuid}`,
|
||||
body,
|
||||
);
|
||||
const response = await client.post<RvwMnpwMngResponseDto>(BASE, body);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,28 +1,36 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type {
|
||||
SewerBscPlanApplySaveRequestDto,
|
||||
SewerBscPlanSaveRequestDto,
|
||||
} from "../model/sewer-bsc-plan-apply-payload-schema";
|
||||
import type {
|
||||
SewerBscPlanSearchRequestDto,
|
||||
SewerBscPlanUpdateRequestDto,
|
||||
} from "../model/sewer-bsc-plan-interface";
|
||||
|
||||
import { sewerBscPlanService } from "./sewer-bsc-plan-service";
|
||||
|
||||
export const sewerBscPlanBaseKey = [ENDPOINT.AS_SERVICE.SEWER_BSC_PLAN] as const;
|
||||
export const sewerBscPlanKeys = {
|
||||
all: ["sewer-bsc-plan"] as const,
|
||||
list: () => [...sewerBscPlanKeys.all, "list"] as const,
|
||||
detail: (id: string) => [...sewerBscPlanKeys.all, "detail", id] as const,
|
||||
details: (id: string, chgCycl?: number) =>
|
||||
chgCycl == null
|
||||
? ([...sewerBscPlanKeys.all, "details", id] as const)
|
||||
: ([...sewerBscPlanKeys.all, "details", id, chgCycl] as const),
|
||||
};
|
||||
|
||||
export function useSewerBscPlanPage(params: SewerBscPlanSearchRequestDto) {
|
||||
return useQuery({
|
||||
queryKey: [...sewerBscPlanBaseKey, "findPage", params],
|
||||
queryKey: [...sewerBscPlanKeys.list(), params],
|
||||
queryFn: () => sewerBscPlanService.findPage(params),
|
||||
});
|
||||
}
|
||||
|
||||
export function useSewerBscPlan(sewerBscPlanDocNo: string | null | undefined) {
|
||||
return useQuery({
|
||||
queryKey: [...sewerBscPlanBaseKey, "findById", sewerBscPlanDocNo],
|
||||
queryKey: sewerBscPlanKeys.detail(sewerBscPlanDocNo!),
|
||||
queryFn: () => sewerBscPlanService.findById(sewerBscPlanDocNo!),
|
||||
enabled: Boolean(sewerBscPlanDocNo),
|
||||
});
|
||||
@@ -33,7 +41,7 @@ export function useSewerBscPlanDetails(
|
||||
chgCycl: number | null | undefined,
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: [...sewerBscPlanBaseKey, "findDetails", sewerBscPlanDocNo, chgCycl],
|
||||
queryKey: sewerBscPlanKeys.details(sewerBscPlanDocNo!, chgCycl ?? undefined),
|
||||
queryFn: () => sewerBscPlanService.findDetails(sewerBscPlanDocNo!, chgCycl!),
|
||||
enabled: Boolean(sewerBscPlanDocNo) && chgCycl != null,
|
||||
});
|
||||
@@ -44,7 +52,7 @@ export function useSaveSewerBscPlan() {
|
||||
return useMutation({
|
||||
mutationFn: (body: SewerBscPlanSaveRequestDto) => sewerBscPlanService.save(body),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: sewerBscPlanBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: sewerBscPlanKeys.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -60,9 +68,9 @@ export function useUpdateSewerBscPlan() {
|
||||
body: SewerBscPlanUpdateRequestDto;
|
||||
}) => sewerBscPlanService.update(sewerBscPlanDocNo, body),
|
||||
onSuccess: (_data, { sewerBscPlanDocNo }) => {
|
||||
queryClient.invalidateQueries({ queryKey: sewerBscPlanBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: sewerBscPlanKeys.all });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...sewerBscPlanBaseKey, "findById", sewerBscPlanDocNo],
|
||||
queryKey: sewerBscPlanKeys.detail(sewerBscPlanDocNo),
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -73,7 +81,7 @@ export function useDeleteSewerBscPlan() {
|
||||
return useMutation({
|
||||
mutationFn: (sewerBscPlanDocNo: string) => sewerBscPlanService.delete(sewerBscPlanDocNo),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: sewerBscPlanBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: sewerBscPlanKeys.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -83,7 +91,7 @@ export function useSaveApplySewerBscPlan() {
|
||||
return useMutation({
|
||||
mutationFn: (body: SewerBscPlanApplySaveRequestDto) => sewerBscPlanService.saveApply(body),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: sewerBscPlanBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: sewerBscPlanKeys.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -94,9 +102,9 @@ export function useSubmitSupplementSewerBscPlan() {
|
||||
mutationFn: ({ id, body }: { id: string; body: SewerBscPlanApplySaveRequestDto }) =>
|
||||
sewerBscPlanService.submitSupplement(id, body),
|
||||
onSuccess: (_data, { id }) => {
|
||||
queryClient.invalidateQueries({ queryKey: sewerBscPlanBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: sewerBscPlanKeys.all });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...sewerBscPlanBaseKey, "findById", id],
|
||||
queryKey: sewerBscPlanKeys.detail(id),
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -115,9 +123,9 @@ export function useSubmitReqSupplementSewerBscPlan() {
|
||||
body: SewerBscPlanApplySaveRequestDto;
|
||||
}) => sewerBscPlanService.submitReqSupplement(id, chgCycl, body),
|
||||
onSuccess: (_data, { id }) => {
|
||||
queryClient.invalidateQueries({ queryKey: sewerBscPlanBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: sewerBscPlanKeys.all });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...sewerBscPlanBaseKey, "findById", id],
|
||||
queryKey: sewerBscPlanKeys.detail(id),
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -129,9 +137,9 @@ export function useTechReviewSewerBscPlan() {
|
||||
mutationFn: ({ id, body }: { id: string; body: SewerBscPlanApplySaveRequestDto }) =>
|
||||
sewerBscPlanService.techReview(id, body),
|
||||
onSuccess: (_data, { id }) => {
|
||||
queryClient.invalidateQueries({ queryKey: sewerBscPlanBaseKey });
|
||||
queryClient.invalidateQueries({ queryKey: sewerBscPlanKeys.all });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...sewerBscPlanBaseKey, "findById", id],
|
||||
queryKey: sewerBscPlanKeys.detail(id),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,10 +4,12 @@ import type { PageResponse } from "@/shared/model";
|
||||
|
||||
import type {
|
||||
SewerBscPlanApplySaveRequestDto,
|
||||
SewerBscPlanSaveRequestDto,
|
||||
} from "../model/sewer-bsc-plan-apply-payload-schema";
|
||||
import type {
|
||||
SewerBscPlanApplySaveResponseDto,
|
||||
SewerBscPlanDetResponseDto,
|
||||
SewerBscPlanResponseDto,
|
||||
SewerBscPlanSaveRequestDto,
|
||||
SewerBscPlanSearchRequestDto,
|
||||
SewerBscPlanService,
|
||||
SewerBscPlanUpdateRequestDto,
|
||||
@@ -24,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;
|
||||
}
|
||||
|
||||
@@ -75,8 +77,11 @@ async function submitReqSupplement(
|
||||
id: string,
|
||||
chgCycl: number,
|
||||
body: SewerBscPlanApplySaveRequestDto,
|
||||
): Promise<string> {
|
||||
const response = await client.post<string>(`${BASE}/${id}/${chgCycl}/reqsupplement`, body);
|
||||
): Promise<SewerBscPlanApplySaveResponseDto> {
|
||||
const response = await client.post<SewerBscPlanApplySaveResponseDto>(
|
||||
`${BASE}/${id}/${chgCycl}/reqsupplement`,
|
||||
body,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export { sewerBscPlanBaseKey } from "./api/queries";
|
||||
export { sewerBscPlanKeys } from "./api/queries";
|
||||
export {
|
||||
useDeleteSewerBscPlan,
|
||||
useSaveApplySewerBscPlan,
|
||||
@@ -13,35 +13,52 @@ export {
|
||||
} from "./api/queries";
|
||||
export { sewerBscPlanService } from "./api/sewer-bsc-plan-service";
|
||||
export type {
|
||||
AtflMngClientResponseDto,
|
||||
CklstResultItem,
|
||||
CklstRvwRsltTreeDto,
|
||||
EsntlAtchFileItem,
|
||||
EsntlAtchFileMngTreeResponseDto,
|
||||
SewerBscPlanApplySaveRequestDto,
|
||||
SewerBscPlanApplySaveResponseDto,
|
||||
SewerBscPlanBizCstItem,
|
||||
SewerBscPlanDtlSaveRequestDto,
|
||||
SewerBscPlanFnrsCstItem,
|
||||
SewerBscPlanOtlnItem,
|
||||
SewerBscPlanRvwOpnnItem,
|
||||
SewerBscPlanSaveRequestDto,
|
||||
SewerBscPlanSplmntDmndItem,
|
||||
SewerBscPlanTmplntDtlItem,
|
||||
SewerBscPlanTmplntItem,
|
||||
SewerBscPlanTmplntTmprStrgItem,
|
||||
} from "./model/sewer-bsc-plan-apply-payload-schema";
|
||||
export {
|
||||
sewerBscPlanApplySaveSchema,
|
||||
sewerBscPlanBizCstItemSchema,
|
||||
sewerBscPlanCklstResultItemSchema,
|
||||
sewerBscPlanDtlSaveSchema,
|
||||
sewerBscPlanEsntlAtchFileItemSchema,
|
||||
sewerBscPlanFnrsCstItemSchema,
|
||||
sewerBscPlanOtlnItemSchema,
|
||||
sewerBscPlanRvwOpnnItemSchema,
|
||||
sewerBscPlanSaveSchema,
|
||||
sewerBscPlanSplmntDmndItemSchema,
|
||||
sewerBscPlanTmplntDtlItemSchema,
|
||||
sewerBscPlanTmplntItemSchema,
|
||||
sewerBscPlanTmplntTmprStrgItemSchema,
|
||||
} from "./model/sewer-bsc-plan-apply-payload-schema";
|
||||
export type {
|
||||
AtflMngClientResponseDto,
|
||||
CklstRvwRsltTreeDto,
|
||||
EsntlAtchFileMngTreeResponseDto,
|
||||
SewerBscPlanApplySaveResponseDto,
|
||||
SewerBscPlanBizCstResponseDto,
|
||||
SewerBscPlanDetResponseDto,
|
||||
SewerBscPlanDtlResponseDto,
|
||||
SewerBscPlanDtlSaveRequestDto,
|
||||
SewerBscPlanFnrsCstItem,
|
||||
SewerBscPlanFnrsCstResponseDto,
|
||||
SewerBscPlanOtlnItem,
|
||||
SewerBscPlanOtlnResponseDto,
|
||||
SewerBscPlanResponseDto,
|
||||
SewerBscPlanRvwOpnnItem,
|
||||
SewerBscPlanRvwOpnnResponseDto,
|
||||
SewerBscPlanSaveRequestDto,
|
||||
SewerBscPlanSearchRequestDto,
|
||||
SewerBscPlanService,
|
||||
SewerBscPlanSplmntDmndItem,
|
||||
SewerBscPlanSplmntDmndResponseDto,
|
||||
SewerBscPlanTmplntDtlItem,
|
||||
SewerBscPlanTmplntDtlResponseDto,
|
||||
SewerBscPlanTmplntItem,
|
||||
SewerBscPlanTmplntResponseDto,
|
||||
SewerBscPlanTmplntTmprStrgItem,
|
||||
SewerBscPlanTmplntTmprStrgResponseDto,
|
||||
SewerBscPlanUpdateRequestDto,
|
||||
} from "./model/sewer-bsc-plan-interface";
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
import { z } from "zod";
|
||||
|
||||
/** 백엔드 SewerBscPlanSaveRequestDto */
|
||||
export const sewerBscPlanSaveSchema = z.object({
|
||||
sewerBscPlanDocNo: z.string().optional(),
|
||||
admdstCd: z.string().optional(),
|
||||
envoSeComCd: z.string().optional(),
|
||||
sewerBscPlanSeComCd: z.string().optional(),
|
||||
sewerBscPlanNm: z.string().optional(),
|
||||
taskSeComCd: z.string().optional(),
|
||||
prgrsSttsComCd: z.string().optional(),
|
||||
});
|
||||
|
||||
/** 백엔드 SewerBscPlanDtlSaveRequestDto */
|
||||
export const sewerBscPlanDtlSaveSchema = z.object({
|
||||
sewerBscPlanDocNo: z.string().optional(),
|
||||
chgCycl: z.number().optional(),
|
||||
aplcntNm: z.string().optional(),
|
||||
aplcntMblTelno: z.string().optional(),
|
||||
aplcntOgdpNm: z.string().optional(),
|
||||
aplcntEmlAddr: z.string().optional(),
|
||||
dsgnrNm: z.string().optional(),
|
||||
dsgnrTelno: z.string().optional(),
|
||||
dsgnrOgdpNm: z.string().optional(),
|
||||
dsgnrEmlAddr: z.string().optional(),
|
||||
planYr: z.string().optional(),
|
||||
crtrYr: z.string().optional(),
|
||||
bizOtlnCn: z.string().optional(),
|
||||
frlvlGoalYr: z.string().optional(),
|
||||
sclvlGoalYr: z.string().optional(),
|
||||
thlvlGoalYr: z.string().optional(),
|
||||
folvlGoalYr: z.string().optional(),
|
||||
atchFileGroupId: z.string().optional(),
|
||||
});
|
||||
|
||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanOtlnItem */
|
||||
export const sewerBscPlanOtlnItemSchema = z.object({
|
||||
planStpNo: z.number().optional(),
|
||||
sewerPlanPpltnCnt: z.number().optional(),
|
||||
sewerPrcsPpltnCnt: z.number().optional(),
|
||||
sewerPsplyRt: z.number().optional(),
|
||||
});
|
||||
|
||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.CklstResultItem */
|
||||
export const sewerBscPlanCklstResultItemSchema = z.object({
|
||||
cklstRvwSn: z.number().optional(),
|
||||
cklstArtclSn: z.number().optional(),
|
||||
cklstSn: z.number().optional(),
|
||||
rvwStpComCd: z.string(),
|
||||
selected: z.string().optional(),
|
||||
picRvwCn: z.string().nullish(),
|
||||
});
|
||||
|
||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanFnrsCstItem */
|
||||
export const sewerBscPlanFnrsCstItemSchema = z.object({
|
||||
planStpNo: z.number().optional(),
|
||||
gvexpAmt: z.number().optional(),
|
||||
lcexpAmt: z.number().optional(),
|
||||
orgprBdchgAmt: z.number().optional(),
|
||||
etcAmt: z.number().optional(),
|
||||
sumAmt: z.number().optional(),
|
||||
});
|
||||
|
||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanSplmntDmndItem */
|
||||
export const sewerBscPlanSplmntDmndItemSchema = z.object({
|
||||
splmntDmndSn: z.number().nullish(),
|
||||
splmntArtclComCd: z.string().optional(),
|
||||
splmntDmndCn: z.string().optional(),
|
||||
splmntDmndDt: z.string().optional(),
|
||||
splmntDmndAtchFileGroupId: z.string().optional(),
|
||||
splmntCn: z.string().optional(),
|
||||
splmntPicNm: z.string().optional(),
|
||||
splmntCmptnDt: z.string().optional(),
|
||||
splmntAtchFileGroupId: z.string().optional(),
|
||||
cmptnYn: z.string().optional(),
|
||||
});
|
||||
|
||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanRvwOpnnItem */
|
||||
export const sewerBscPlanRvwOpnnItemSchema = z.object({
|
||||
rvwOpnnSn: z.number().optional(),
|
||||
opnrSeComCd: z.string().optional(),
|
||||
opnrNm: z.string().optional(),
|
||||
rvwRsltCn: z.string().optional(),
|
||||
rvwRsltAnmlyYn: z.string().optional(),
|
||||
atchFileGroupId: z.string().optional(),
|
||||
});
|
||||
|
||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.EsntlAtchFileItem */
|
||||
export const sewerBscPlanEsntlAtchFileItemSchema = z.object({
|
||||
esntlAtchFileId: z.string().optional(),
|
||||
atchFileGroupId: z.string().optional(),
|
||||
});
|
||||
|
||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanBizCstItem */
|
||||
export const sewerBscPlanBizCstItemSchema = z.object({
|
||||
planStpNo: z.number().optional(),
|
||||
acntgSeCd: z.string().optional(),
|
||||
cstrnAmt: z.number().optional(),
|
||||
});
|
||||
|
||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanTmplntDtlItem */
|
||||
export const sewerBscPlanTmplntDtlItemSchema = z.object({
|
||||
planStpNo: z.number().optional(),
|
||||
swtrmtFcltCd: z.string().optional(),
|
||||
prcsZoneArea: z.number().optional(),
|
||||
swtrmtPpltnCnt: z.number().optional(),
|
||||
planSwqty: z.number().optional(),
|
||||
fcltCpct: z.number().optional(),
|
||||
sprsysDrtwPplLen: z.number().optional(),
|
||||
sprsysExlnPplLen: z.number().optional(),
|
||||
cbsysPplLen: z.number().optional(),
|
||||
icptgPplLen: z.number().optional(),
|
||||
drgCnt: z.number().optional(),
|
||||
drtwRlyPumpCnt: z.number().optional(),
|
||||
drtwPumpCnt: z.number().optional(),
|
||||
exlnPumpCnt: z.number().optional(),
|
||||
});
|
||||
|
||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanTmplntItem */
|
||||
export const sewerBscPlanTmplntItemSchema = z.object({
|
||||
swtrmtFcltCd: z.string().optional(),
|
||||
atchFileGroupId: z.string().optional(),
|
||||
sewerBscPlanTmplntDtlList: z.array(sewerBscPlanTmplntDtlItemSchema).optional(),
|
||||
});
|
||||
|
||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanTmplntTmprStrgItem */
|
||||
export const sewerBscPlanTmplntTmprStrgItemSchema = z.object({
|
||||
dtStrgDt: z.string().optional(),
|
||||
swtrmtFcltCd: z.string().optional(),
|
||||
prcsZoneArea: z.number().optional(),
|
||||
swtrmtPpltnCnt: z.number().optional(),
|
||||
planSwqty: z.number().optional(),
|
||||
fcltCpct: z.number().optional(),
|
||||
sprsysDrtwPplLen: z.number().optional(),
|
||||
sprsysExlnPplLen: z.number().optional(),
|
||||
cbsysPplLen: z.number().optional(),
|
||||
icptgPplLen: z.number().optional(),
|
||||
drgCnt: z.number().optional(),
|
||||
drtwRlyPumpCnt: z.number().optional(),
|
||||
drtwPumpCnt: z.number().optional(),
|
||||
exlnPumpCnt: z.number().optional(),
|
||||
});
|
||||
|
||||
/** 백엔드 SewerBscPlanApplySaveRequestDto */
|
||||
export const sewerBscPlanApplySaveSchema = z.object({
|
||||
techReViewSaveCd: z.string().optional(),
|
||||
rvwStpCd: z.string().optional(),
|
||||
sewerBscPlan: sewerBscPlanSaveSchema,
|
||||
sewerBscPlanDtl: sewerBscPlanDtlSaveSchema,
|
||||
sewerBscPlanOtlnList: z.array(sewerBscPlanOtlnItemSchema).optional(),
|
||||
cklstResultList: z.array(sewerBscPlanCklstResultItemSchema).optional(),
|
||||
fnrsCstList: z.array(sewerBscPlanFnrsCstItemSchema).optional(),
|
||||
splmntDmndList: z.array(sewerBscPlanSplmntDmndItemSchema).optional(),
|
||||
rvwOpnnList: z.array(sewerBscPlanRvwOpnnItemSchema).optional(),
|
||||
esntlAtchFileList: z.array(sewerBscPlanEsntlAtchFileItemSchema).optional(),
|
||||
bizCstList: z.array(sewerBscPlanBizCstItemSchema).optional(),
|
||||
sewerBscPlanTmplntList: z.array(sewerBscPlanTmplntItemSchema).optional(),
|
||||
tmplntTmprStrgList: z.array(sewerBscPlanTmplntTmprStrgItemSchema).optional(),
|
||||
});
|
||||
|
||||
export type SewerBscPlanSaveRequestDto = z.infer<typeof sewerBscPlanSaveSchema>;
|
||||
export type SewerBscPlanDtlSaveRequestDto = z.infer<typeof sewerBscPlanDtlSaveSchema>;
|
||||
export type SewerBscPlanOtlnItem = z.infer<typeof sewerBscPlanOtlnItemSchema>;
|
||||
export type CklstResultItem = z.infer<typeof sewerBscPlanCklstResultItemSchema>;
|
||||
export type SewerBscPlanFnrsCstItem = z.infer<typeof sewerBscPlanFnrsCstItemSchema>;
|
||||
export type SewerBscPlanSplmntDmndItem = z.infer<typeof sewerBscPlanSplmntDmndItemSchema>;
|
||||
export type SewerBscPlanRvwOpnnItem = z.infer<typeof sewerBscPlanRvwOpnnItemSchema>;
|
||||
export type EsntlAtchFileItem = z.infer<typeof sewerBscPlanEsntlAtchFileItemSchema>;
|
||||
export type SewerBscPlanBizCstItem = z.infer<typeof sewerBscPlanBizCstItemSchema>;
|
||||
export type SewerBscPlanTmplntDtlItem = z.infer<typeof sewerBscPlanTmplntDtlItemSchema>;
|
||||
export type SewerBscPlanTmplntItem = z.infer<typeof sewerBscPlanTmplntItemSchema>;
|
||||
export type SewerBscPlanTmplntTmprStrgItem = z.infer<typeof sewerBscPlanTmplntTmprStrgItemSchema>;
|
||||
export type SewerBscPlanApplySaveRequestDto = z.infer<typeof sewerBscPlanApplySaveSchema>;
|
||||
@@ -1,35 +1,27 @@
|
||||
import type {
|
||||
AtflMngClientResponseDto,
|
||||
CklstResultSaveBase,
|
||||
CklstRvwRsltTreeDto,
|
||||
DocSttsChgHstryResponseDto,
|
||||
EsntlAtchFileMngTreeResponseDto,
|
||||
EsntlAtchFileSaveBase,
|
||||
PageableParams,
|
||||
PageResponse,
|
||||
PbadmsApplySaveHeader,
|
||||
RvwOpnnResponseExtension,
|
||||
RvwOpnnSaveBase,
|
||||
SplmntDmndResponseExtension,
|
||||
SplmntDmndSaveBase,
|
||||
} from "@/shared/model";
|
||||
|
||||
import type {
|
||||
SewerBscPlanApplySaveRequestDto,
|
||||
SewerBscPlanSaveRequestDto,
|
||||
} from "./sewer-bsc-plan-apply-payload-schema";
|
||||
|
||||
export type {
|
||||
AtflMngClientResponseDto,
|
||||
CklstRvwRsltTreeDto,
|
||||
EsntlAtchFileMngTreeResponseDto,
|
||||
} from "@/shared/model";
|
||||
|
||||
/** 백엔드 SewerBscPlanSaveRequestDto */
|
||||
export interface SewerBscPlanSaveRequestDto {
|
||||
sewerBscPlanDocNo?: string;
|
||||
admdstCd?: string;
|
||||
envoSeComCd?: string;
|
||||
sewerBscPlanSeComCd?: string;
|
||||
sewerBscPlanNm?: string;
|
||||
taskSeComCd?: string;
|
||||
prgrsSttsComCd?: string;
|
||||
}
|
||||
|
||||
/** 백엔드 SewerBscPlanUpdateRequestDto */
|
||||
export interface SewerBscPlanUpdateRequestDto {
|
||||
admdstCd?: string;
|
||||
@@ -70,28 +62,7 @@ export interface SewerBscPlanResponseDto {
|
||||
rgtr?: string;
|
||||
rgtrNm?: string;
|
||||
mdfcnDt?: string;
|
||||
}
|
||||
|
||||
/** 백엔드 SewerBscPlanDtlSaveRequestDto */
|
||||
export interface SewerBscPlanDtlSaveRequestDto {
|
||||
sewerBscPlanDocNo?: string;
|
||||
chgCycl?: number;
|
||||
aplcntNm?: string;
|
||||
aplcntMblTelno?: string;
|
||||
aplcntOgdpNm?: string;
|
||||
aplcntEmlAddr?: string;
|
||||
dsgnrNm?: string;
|
||||
dsgnrTelno?: string;
|
||||
dsgnrOgdpNm?: string;
|
||||
dsgnrEmlAddr?: string;
|
||||
planYr?: string;
|
||||
crtrYr?: string;
|
||||
bizOtlnCn?: string;
|
||||
frlvlGoalYr?: string;
|
||||
sclvlGoalYr?: string;
|
||||
thlvlGoalYr?: string;
|
||||
folvlGoalYr?: string;
|
||||
atchFileGroupId?: string;
|
||||
techRplyHstryYn?: "Y" | "N";
|
||||
}
|
||||
|
||||
/** 백엔드 SewerBscPlanDtlResponseDto */
|
||||
@@ -210,103 +181,6 @@ export interface SewerBscPlanTmplntTmprStrgResponseDto {
|
||||
exlnPumpCnt?: number;
|
||||
}
|
||||
|
||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanOtlnItem */
|
||||
export interface SewerBscPlanOtlnItem {
|
||||
planStpNo?: number;
|
||||
sewerPlanPpltnCnt?: number;
|
||||
sewerPrcsPpltnCnt?: number;
|
||||
sewerPsplyRt?: number;
|
||||
}
|
||||
|
||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.CklstResultItem */
|
||||
export interface CklstResultItem extends CklstResultSaveBase {
|
||||
selected?: string;
|
||||
}
|
||||
|
||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanFnrsCstItem */
|
||||
export interface SewerBscPlanFnrsCstItem {
|
||||
planStpNo?: number;
|
||||
gvexpAmt?: number;
|
||||
lcexpAmt?: number;
|
||||
orgprBdchgAmt?: number;
|
||||
etcAmt?: number;
|
||||
sumAmt?: number;
|
||||
}
|
||||
|
||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanSplmntDmndItem */
|
||||
export type SewerBscPlanSplmntDmndItem = SplmntDmndSaveBase;
|
||||
|
||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanRvwOpnnItem */
|
||||
export type SewerBscPlanRvwOpnnItem = RvwOpnnSaveBase;
|
||||
|
||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.EsntlAtchFileItem */
|
||||
export type EsntlAtchFileItem = EsntlAtchFileSaveBase;
|
||||
|
||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanBizCstItem */
|
||||
export interface SewerBscPlanBizCstItem {
|
||||
planStpNo?: number;
|
||||
acntgSeCd?: string;
|
||||
cstrnAmt?: number;
|
||||
}
|
||||
|
||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanTmplntDtlItem */
|
||||
export interface SewerBscPlanTmplntDtlItem {
|
||||
planStpNo?: number;
|
||||
swtrmtFcltCd?: string;
|
||||
prcsZoneArea?: number;
|
||||
swtrmtPpltnCnt?: number;
|
||||
planSwqty?: number;
|
||||
fcltCpct?: number;
|
||||
sprsysDrtwPplLen?: number;
|
||||
sprsysExlnPplLen?: number;
|
||||
cbsysPplLen?: number;
|
||||
icptgPplLen?: number;
|
||||
drgCnt?: number;
|
||||
drtwRlyPumpCnt?: number;
|
||||
drtwPumpCnt?: number;
|
||||
exlnPumpCnt?: number;
|
||||
}
|
||||
|
||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanTmplntItem */
|
||||
export interface SewerBscPlanTmplntItem {
|
||||
swtrmtFcltCd?: string;
|
||||
atchFileGroupId?: string;
|
||||
sewerBscPlanTmplntDtlList?: SewerBscPlanTmplntDtlItem[];
|
||||
}
|
||||
|
||||
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanTmplntTmprStrgItem */
|
||||
export interface SewerBscPlanTmplntTmprStrgItem {
|
||||
dtStrgDt?: string;
|
||||
swtrmtFcltCd?: string;
|
||||
prcsZoneArea?: number;
|
||||
swtrmtPpltnCnt?: number;
|
||||
planSwqty?: number;
|
||||
fcltCpct?: number;
|
||||
sprsysDrtwPplLen?: number;
|
||||
sprsysExlnPplLen?: number;
|
||||
cbsysPplLen?: number;
|
||||
icptgPplLen?: number;
|
||||
drgCnt?: number;
|
||||
drtwRlyPumpCnt?: number;
|
||||
drtwPumpCnt?: number;
|
||||
exlnPumpCnt?: number;
|
||||
}
|
||||
|
||||
/** 백엔드 SewerBscPlanApplySaveRequestDto */
|
||||
export interface SewerBscPlanApplySaveRequestDto extends PbadmsApplySaveHeader {
|
||||
sewerBscPlan: SewerBscPlanSaveRequestDto;
|
||||
sewerBscPlanDtl: SewerBscPlanDtlSaveRequestDto;
|
||||
sewerBscPlanOtlnList?: SewerBscPlanOtlnItem[];
|
||||
cklstResultList?: CklstResultItem[];
|
||||
fnrsCstList?: SewerBscPlanFnrsCstItem[];
|
||||
splmntDmndList?: SewerBscPlanSplmntDmndItem[];
|
||||
rvwOpnnList?: SewerBscPlanRvwOpnnItem[];
|
||||
esntlAtchFileList?: EsntlAtchFileItem[];
|
||||
bizCstList?: SewerBscPlanBizCstItem[];
|
||||
sewerBscPlanTmplntList?: SewerBscPlanTmplntItem[];
|
||||
tmplntTmprStrgList?: SewerBscPlanTmplntTmprStrgItem[];
|
||||
}
|
||||
|
||||
/** 백엔드 SewerBscPlanApplySaveResponseDto */
|
||||
export interface SewerBscPlanApplySaveResponseDto {
|
||||
sewerBscPlanDocNo: string;
|
||||
@@ -327,12 +201,13 @@ export interface SewerBscPlanDetResponseDto {
|
||||
bizCstList?: SewerBscPlanBizCstResponseDto[];
|
||||
tmplntList?: SewerBscPlanTmplntResponseDto[];
|
||||
tmplntTmprStrgList?: SewerBscPlanTmplntTmprStrgResponseDto[];
|
||||
sttsChgHstryList?: DocSttsChgHstryResponseDto[];
|
||||
}
|
||||
|
||||
/** 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,
|
||||
@@ -346,6 +221,6 @@ export interface SewerBscPlanService {
|
||||
id: string,
|
||||
chgCycl: number,
|
||||
body: SewerBscPlanApplySaveRequestDto,
|
||||
): Promise<string>;
|
||||
): Promise<SewerBscPlanApplySaveResponseDto>;
|
||||
techReview(id: string, body: SewerBscPlanApplySaveRequestDto): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -3,13 +3,15 @@
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, type RowSelectionState } from "@tanstack/react-table";
|
||||
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable, useMutationWrapper } from "@/shared/lib";
|
||||
import {
|
||||
type ElaprTmplt,
|
||||
elaprTmpltService,
|
||||
useCreateElaprTmplt,
|
||||
useDeleteElaprTmplt,
|
||||
useElaprTmpltPage,
|
||||
useUpdateElaprTmplt,
|
||||
} from "@/entities/elapr";
|
||||
@@ -26,6 +28,7 @@ interface ElaprTmpltListProps {
|
||||
|
||||
function ElaprTmpltList({ selectedTmpltId, onSelectTmplt }: ElaprTmpltListProps) {
|
||||
const { confirm } = useConfirm();
|
||||
const queryClient = useQueryClient();
|
||||
const [dialogMode, setDialogMode] = useState<"create" | "edit" | null>(null);
|
||||
const [editingTmplt, setEditingTmplt] = useState<ElaprTmplt | null>(null);
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
@@ -38,7 +41,14 @@ function ElaprTmpltList({ selectedTmpltId, onSelectTmplt }: ElaprTmpltListProps)
|
||||
|
||||
const createMutation = useCreateElaprTmplt();
|
||||
const updateMutation = useUpdateElaprTmplt();
|
||||
const deleteMutation = useDeleteElaprTmplt();
|
||||
const { mutateAsync: deleteTmplt } = useMutationWrapper({
|
||||
mutationFn: (elaprTmpltId: string) => elaprTmpltService.remove(elaprTmpltId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_TMPLT],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
@@ -49,13 +59,13 @@ function ElaprTmpltList({ selectedTmpltId, onSelectTmplt }: ElaprTmpltListProps)
|
||||
},
|
||||
onDelete: async (row) => {
|
||||
if (!(await confirm("선택한 결재서식을 삭제하시겠습니까?"))) return;
|
||||
await deleteMutation.mutateAsync(row.elaprTmpltId);
|
||||
await deleteTmplt(row.elaprTmpltId);
|
||||
if (selectedTmpltId === row.elaprTmpltId) {
|
||||
onSelectTmplt(null);
|
||||
}
|
||||
},
|
||||
}),
|
||||
[confirm, deleteMutation, onSelectTmplt, selectedTmpltId],
|
||||
[confirm, deleteTmplt, onSelectTmplt, selectedTmpltId],
|
||||
);
|
||||
|
||||
const { tableComponent } = useDataTable({
|
||||
|
||||
@@ -82,7 +82,13 @@ function SortableAplnRow({
|
||||
<Select
|
||||
value={row.atrzSeComCd ?? ""}
|
||||
onValueChange={(v) =>
|
||||
onChange({ atrzSeComCd: v, atrzSeComNm: findCodeLabel(selectableAtrzSeCodes, v) })
|
||||
onChange({
|
||||
atrzSeComCd: v,
|
||||
atrzSeComNm: findCodeLabel(selectableAtrzSeCodes, v),
|
||||
aprvrUserId: "",
|
||||
aprvrUuid: "",
|
||||
aprvrNm: "",
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="min-w-[120px]">
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { DashboardPage as MainPage } from "./ui/dashboard-page";
|
||||
@@ -0,0 +1,153 @@
|
||||
.mapArea {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.bgLayer {
|
||||
position: absolute;
|
||||
inset: var(--map-inset);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
width: calc(100% - var(--map-inset) * 2);
|
||||
height: calc(100% - var(--map-inset) * 2);
|
||||
background-image: url("/images/region_map_bg.png");
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
background-position: center center;
|
||||
image-rendering: -webkit-optimize-contrast;
|
||||
image-rendering: crisp-edges;
|
||||
image-rendering: pixelated;
|
||||
opacity: 0.7;
|
||||
filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.02));
|
||||
pointer-events: none;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.svgLayer {
|
||||
position: absolute;
|
||||
inset: var(--map-inset);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
width: calc(100% - var(--map-inset) * 2);
|
||||
height: calc(100% - var(--map-inset) * 2);
|
||||
}
|
||||
|
||||
.svgLayer > svg {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
image-rendering: -webkit-optimize-contrast;
|
||||
image-rendering: crisp-edges;
|
||||
shape-rendering: geometricPrecision;
|
||||
}
|
||||
|
||||
.svgContainer :global(svg) :global(path) {
|
||||
fill: rgba(0, 150, 255, 0.2) !important;
|
||||
stroke: #0066cc;
|
||||
stroke-width: 1;
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
|
||||
.svgContainer :global(svg) :global(.cls-1) {
|
||||
fill: rgba(0, 150, 255, 0.2) !important;
|
||||
stroke: #0066cc;
|
||||
stroke-width: 1;
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
|
||||
.svgContainer :global(svg) :global(.basin) {
|
||||
opacity: 0;
|
||||
pointer-events: all;
|
||||
transition: opacity 0.15s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.svgContainer :global(svg) :global(.basin.active),
|
||||
.svgContainer :global(svg) :global(.basin:hover) {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.svgContainer :global(svg) :global(.basin) :global(path),
|
||||
.svgContainer :global(svg) :global(.basin) :global(polygon),
|
||||
.svgContainer :global(svg) :global(.basin) :global(polyline) {
|
||||
fill: rgba(0, 209, 209, 0.5);
|
||||
stroke: #399ac3;
|
||||
stroke-width: 1.3;
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
|
||||
.popup {
|
||||
position: absolute;
|
||||
width: 300px;
|
||||
height: 150px;
|
||||
min-width: 220px;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 10px;
|
||||
box-shadow:
|
||||
0 12px 24px rgba(0, 0, 0, 0.18),
|
||||
0 2px 0 rgba(0, 0, 0, 0.05);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translate(-50%, -8px) scale(0.98);
|
||||
transition:
|
||||
opacity 0.12s,
|
||||
visibility 0.12s,
|
||||
transform 0.12s;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.popup.show {
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
transform: translate(-50%, -12px) scale(1) !important;
|
||||
}
|
||||
|
||||
.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.labelsLayer {
|
||||
position: absolute;
|
||||
inset: var(--map-inset);
|
||||
pointer-events: none;
|
||||
z-index: 3;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.basinLabel {
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
background: #ffffffee;
|
||||
border: 2px solid #d0d0d0;
|
||||
white-space: nowrap;
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.basinLabel.active {
|
||||
background: #006bf8;
|
||||
color: #fff;
|
||||
border-color: #96c3ff;
|
||||
box-shadow: 0 4px 12px rgba(0, 107, 248, 0.3);
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
import { type CSSProperties, useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import {
|
||||
BASIN_DATA,
|
||||
BASIN_IDS,
|
||||
type BasinKey,
|
||||
MANUAL_LABEL_POS,
|
||||
type ManualLabelPos,
|
||||
} from "../model/map-data";
|
||||
|
||||
import styles from "./national-as-map.module.css";
|
||||
|
||||
const SVG_URL = "/images/region_map.svg";
|
||||
const POPUP_EDGE_PADDING_PX = 12;
|
||||
const LABEL_OFFSET_Y_PX = 18;
|
||||
const DEFAULT_VIEWBOX = "0 0 100 100";
|
||||
|
||||
export const PLACEHOLDER_POPUP_ITEMS = [{ header: "현황", data: "-" }] as const;
|
||||
|
||||
export interface MapPopupItem {
|
||||
header: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
export interface MapPopupContent {
|
||||
title: string;
|
||||
items: MapPopupItem[];
|
||||
}
|
||||
|
||||
const INITIAL_POPUP_CONTENT: MapPopupContent = {
|
||||
title: "",
|
||||
items: [...PLACEHOLDER_POPUP_ITEMS],
|
||||
};
|
||||
|
||||
function isBasinKey(key: string | undefined): key is BasinKey {
|
||||
return Boolean(key && key in BASIN_DATA);
|
||||
}
|
||||
|
||||
function resolveBasinName(group: SVGGElement) {
|
||||
const key = group.dataset.key;
|
||||
if (isBasinKey(key)) return BASIN_DATA[key].name;
|
||||
return group.dataset.label ?? "";
|
||||
}
|
||||
|
||||
function hidePopup(popup: HTMLDivElement | null) {
|
||||
if (!popup) return;
|
||||
if (styles.show) popup.classList.remove(styles.show);
|
||||
popup.style.opacity = "0";
|
||||
popup.style.visibility = "hidden";
|
||||
}
|
||||
|
||||
function showPopupAtGroup(stage: HTMLDivElement, popup: HTMLDivElement, group: SVGGElement) {
|
||||
const stageRect = stage.getBoundingClientRect();
|
||||
const groupRect = group.getBoundingClientRect();
|
||||
const centerX = (groupRect.left + groupRect.right) / 2 - stageRect.left;
|
||||
const topY = groupRect.top - stageRect.top;
|
||||
|
||||
popup.style.left = `${centerX}px`;
|
||||
popup.style.top = `${topY + 6}px`;
|
||||
popup.style.opacity = "1";
|
||||
popup.style.visibility = "visible";
|
||||
if (styles.show) popup.classList.add(styles.show);
|
||||
|
||||
const popupRect = popup.getBoundingClientRect();
|
||||
const clampedX = Math.max(
|
||||
POPUP_EDGE_PADDING_PX + popupRect.width / 2,
|
||||
Math.min(centerX, stageRect.width - POPUP_EDGE_PADDING_PX - popupRect.width / 2),
|
||||
);
|
||||
popup.style.left = `${clampedX}px`;
|
||||
}
|
||||
|
||||
function collectBasinGroups(svg: SVGSVGElement) {
|
||||
const groups: SVGGElement[] = [];
|
||||
|
||||
BASIN_IDS.forEach((item) => {
|
||||
let group: SVGGElement | null = null;
|
||||
for (const id of item.labels) {
|
||||
group = svg.getElementById(id) as SVGGElement | null;
|
||||
if (group) break;
|
||||
}
|
||||
if (!group) return;
|
||||
|
||||
group.classList.add("basin");
|
||||
group.dataset.key = item.key;
|
||||
group.dataset.label = item.labels[0] ?? "";
|
||||
|
||||
group.querySelectorAll("path").forEach((path) => {
|
||||
path.style.fill = "rgba(0, 150, 255, 0.2)";
|
||||
path.style.stroke = "#0066cc";
|
||||
path.style.strokeWidth = "1";
|
||||
path.style.vectorEffect = "non-scaling-stroke";
|
||||
});
|
||||
|
||||
groups.push(group);
|
||||
});
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
function createLabels(labelsLayer: HTMLDivElement, groups: SVGGElement[]) {
|
||||
labelsLayer.innerHTML = "";
|
||||
return groups.map((group) => {
|
||||
const label = document.createElement("div");
|
||||
label.className = styles.basinLabel ?? "";
|
||||
label.textContent = group.dataset.label ?? "";
|
||||
labelsLayer.appendChild(label);
|
||||
return label;
|
||||
});
|
||||
}
|
||||
|
||||
function applyViewBox(svg: SVGSVGElement, stage: HTMLDivElement) {
|
||||
if (!svg.hasAttribute("viewBox")) {
|
||||
const bbox = svg.getBBox();
|
||||
svg.setAttribute("viewBox", [bbox.x, bbox.y, bbox.width, bbox.height].join(" "));
|
||||
}
|
||||
|
||||
const viewBox = (svg.getAttribute("viewBox") || "").split(/\s+/).map(Number);
|
||||
if (viewBox.length === 4 && viewBox[3]! > 0)
|
||||
stage.style.aspectRatio = String(viewBox[2]! / viewBox[3]!);
|
||||
|
||||
svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
||||
svg.style.width = "100%";
|
||||
svg.style.height = "100%";
|
||||
}
|
||||
|
||||
function positionLabels(
|
||||
stage: HTMLDivElement,
|
||||
svg: SVGSVGElement,
|
||||
groups: SVGGElement[],
|
||||
labels: HTMLDivElement[],
|
||||
) {
|
||||
const stageRect = stage.getBoundingClientRect();
|
||||
const inset = parseFloat(getComputedStyle(stage).getPropertyValue("--map-inset") || "0") / 100;
|
||||
const left = stageRect.width * inset;
|
||||
const top = stageRect.height * inset;
|
||||
const width = stageRect.width * (1 - inset * 2);
|
||||
const height = stageRect.height * (1 - inset * 2);
|
||||
const [viewX, viewY, viewWidth, viewHeight] = (svg.getAttribute("viewBox") || DEFAULT_VIEWBOX)
|
||||
.split(/\s+/)
|
||||
.map(Number);
|
||||
|
||||
const svgRect = svg.getBoundingClientRect();
|
||||
const offsetX = svgRect.left - stageRect.left - left;
|
||||
const offsetY = svgRect.top - stageRect.top - top;
|
||||
|
||||
groups.forEach((group, index) => {
|
||||
const key = group.dataset.key;
|
||||
const manual = key ? (MANUAL_LABEL_POS as Record<string, ManualLabelPos>)[key] : undefined;
|
||||
let posX: number;
|
||||
let posY: number;
|
||||
|
||||
if (manual && (("x" in manual && "y" in manual) || ("px" in manual && "py" in manual))) {
|
||||
if ("px" in manual) {
|
||||
posX = left + (manual.px || 0) + offsetX;
|
||||
posY = top + (manual.py || 0) + offsetY;
|
||||
} else {
|
||||
posX = left + width * (manual.x || 0) + offsetX;
|
||||
posY = top + height * (manual.y || 0) + offsetY;
|
||||
}
|
||||
} else {
|
||||
const bbox = group.getBBox();
|
||||
const centerX = bbox.x + bbox.width / 2;
|
||||
const centerY = bbox.y + bbox.height / 2;
|
||||
posX = ((centerX - viewX!) / viewWidth!) * width + left + offsetX;
|
||||
posY = ((centerY - viewY!) / viewHeight!) * height + top + offsetY;
|
||||
}
|
||||
|
||||
const label = labels[index];
|
||||
if (!label) return;
|
||||
label.style.left = `${posX}px`;
|
||||
label.style.top = `${posY + LABEL_OFFSET_Y_PX}px`;
|
||||
});
|
||||
}
|
||||
|
||||
export function useNationalAsMap() {
|
||||
const stageRef = useRef<HTMLDivElement>(null);
|
||||
const svgLayerRef = useRef<HTMLDivElement>(null);
|
||||
const labelsLayerRef = useRef<HTMLDivElement>(null);
|
||||
const popupRef = useRef<HTMLDivElement>(null);
|
||||
const [popupContent, setPopupContent] = useState<MapPopupContent>(INITIAL_POPUP_CONTENT);
|
||||
|
||||
const showFor = useCallback((group: SVGGElement) => {
|
||||
const stage = stageRef.current;
|
||||
const popup = popupRef.current;
|
||||
if (!stage || !popup) return;
|
||||
|
||||
setPopupContent({
|
||||
title: resolveBasinName(group),
|
||||
items: [...PLACEHOLDER_POPUP_ITEMS],
|
||||
});
|
||||
showPopupAtGroup(stage, popup, group);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const abortController = new AbortController();
|
||||
let svg: SVGSVGElement | null = null;
|
||||
let groups: SVGGElement[] = [];
|
||||
let labels: HTMLDivElement[] = [];
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
let positionLabelsHandler: (() => void) | null = null;
|
||||
let clearActive: (() => void) | null = null;
|
||||
let handleStageClick: (() => void) | null = null;
|
||||
|
||||
const listeners: Array<{
|
||||
target: EventTarget;
|
||||
type: string;
|
||||
listener: EventListener;
|
||||
}> = [];
|
||||
|
||||
function addListener(target: EventTarget, type: string, listener: EventListener) {
|
||||
target.addEventListener(type, listener);
|
||||
listeners.push({ target, type, listener });
|
||||
}
|
||||
|
||||
fetch(SVG_URL, { signal: abortController.signal })
|
||||
.then((response) => {
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
return response.text();
|
||||
})
|
||||
.then((svgText) => {
|
||||
if (abortController.signal.aborted) return;
|
||||
if (!svgLayerRef.current || !stageRef.current) return;
|
||||
|
||||
svgLayerRef.current.innerHTML = svgText;
|
||||
svg = svgLayerRef.current.querySelector("svg");
|
||||
if (!svg) throw new Error("SVG element not found in loaded content");
|
||||
|
||||
applyViewBox(svg, stageRef.current);
|
||||
groups = collectBasinGroups(svg);
|
||||
labels = labelsLayerRef.current ? createLabels(labelsLayerRef.current, groups) : [];
|
||||
|
||||
clearActive = () => {
|
||||
groups.forEach((group) => group.classList.remove("active"));
|
||||
hidePopup(popupRef.current);
|
||||
labels.forEach((label) => label.classList.remove("active"));
|
||||
};
|
||||
|
||||
const revealGroup = (group: SVGGElement) => {
|
||||
showFor(group);
|
||||
const index = groups.indexOf(group);
|
||||
labels.forEach((label) => label.classList.remove("active"));
|
||||
if (index >= 0) labels[index]?.classList.add("active");
|
||||
};
|
||||
|
||||
groups.forEach((group) => {
|
||||
addListener(group, "mouseenter", () => {
|
||||
clearActive?.();
|
||||
group.classList.add("active");
|
||||
revealGroup(group);
|
||||
});
|
||||
addListener(group, "mousemove", () => {
|
||||
if (group.classList.contains("active")) revealGroup(group);
|
||||
});
|
||||
addListener(group, "mouseleave", () => clearActive?.());
|
||||
addListener(group, "click", (event) => {
|
||||
event.stopPropagation();
|
||||
clearActive?.();
|
||||
group.classList.add("active");
|
||||
revealGroup(group);
|
||||
});
|
||||
});
|
||||
|
||||
handleStageClick = () => clearActive?.();
|
||||
addListener(stageRef.current, "mouseleave", () => clearActive?.());
|
||||
addListener(stageRef.current, "click", () => handleStageClick?.());
|
||||
|
||||
positionLabelsHandler = () => {
|
||||
if (!stageRef.current || !svg) return;
|
||||
positionLabels(stageRef.current, svg, groups, labels);
|
||||
};
|
||||
positionLabelsHandler();
|
||||
window.addEventListener("resize", positionLabelsHandler);
|
||||
resizeObserver = new ResizeObserver(positionLabelsHandler);
|
||||
resizeObserver.observe(stageRef.current);
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
if (abortController.signal.aborted) return;
|
||||
const message = error instanceof Error ? error.message : "알 수 없는 오류";
|
||||
if (svgLayerRef.current) {
|
||||
svgLayerRef.current.innerHTML = `<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #666; font-size: 14px;">지도를 불러올 수 없습니다. (${message})</div>`;
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
abortController.abort();
|
||||
listeners.forEach(({ target, type, listener }) => {
|
||||
target.removeEventListener(type, listener);
|
||||
});
|
||||
if (positionLabelsHandler) window.removeEventListener("resize", positionLabelsHandler);
|
||||
resizeObserver?.disconnect();
|
||||
if (svgLayerRef.current) svgLayerRef.current.innerHTML = "";
|
||||
if (labelsLayerRef.current) labelsLayerRef.current.innerHTML = "";
|
||||
};
|
||||
}, [showFor]);
|
||||
|
||||
const popupGridStyle = {
|
||||
minWidth: 160,
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${Math.max(1, popupContent.items.length)}, 1fr)`,
|
||||
} satisfies CSSProperties;
|
||||
|
||||
return {
|
||||
stageRef,
|
||||
svgLayerRef,
|
||||
labelsLayerRef,
|
||||
popupRef,
|
||||
popupContent,
|
||||
popupGridStyle,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
|
||||
import { dayjs } from "@/shared/lib";
|
||||
import type { UserPost } from "@/entities/board";
|
||||
import { Badge } from "@common/components";
|
||||
|
||||
const columnHelper = createColumnHelper<UserPost>();
|
||||
|
||||
export const dashboardNoticeColumns = [
|
||||
columnHelper.display({
|
||||
id: "no",
|
||||
header: "번호",
|
||||
meta: { align: "center" },
|
||||
cell: ({ row }) => {
|
||||
if (row.original.upndFixYn === "Y") {
|
||||
return <Badge size="lg">공지</Badge>;
|
||||
}
|
||||
return row.original.rowNo;
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("pstTtl", {
|
||||
header: "제목",
|
||||
meta: { align: "left" },
|
||||
cell: ({ getValue }) => <div className="min-w-[12rem] truncate px-2 text-left">{getValue()}</div>,
|
||||
}),
|
||||
columnHelper.accessor("wrtrNm", {
|
||||
header: "작성자",
|
||||
meta: { align: "center" },
|
||||
}),
|
||||
columnHelper.accessor("regDt", {
|
||||
header: "작성일",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => (getValue() ? dayjs(getValue()).format("YYYY-MM-DD") : "-"),
|
||||
}),
|
||||
columnHelper.accessor("inqNocs", {
|
||||
header: "조회수",
|
||||
meta: { align: "center" },
|
||||
}),
|
||||
];
|
||||
@@ -0,0 +1,29 @@
|
||||
export const MANUAL_LABEL_POS = {
|
||||
han: { x: 0.52, y: 0.14 },
|
||||
geum: { x: 0.28, y: 0.47 },
|
||||
nakdong: { x: 0.64, y: 0.47 },
|
||||
yeongsan: { x: 0.18, y: 0.77 },
|
||||
} as const;
|
||||
|
||||
export const BASIN_DATA = {
|
||||
han: { name: "한강" },
|
||||
geum: { name: "금강" },
|
||||
nakdong: { name: "낙동강" },
|
||||
yeongsan: { name: "영산강" },
|
||||
} as const;
|
||||
|
||||
export const BASIN_IDS = [
|
||||
{ key: "han", labels: ["한강", "han"] },
|
||||
{ key: "geum", labels: ["금강", "geum"] },
|
||||
{ key: "nakdong", labels: ["낙동강", "nakdong"] },
|
||||
{ key: "yeongsan", labels: ["영산강", "yeongsan"] },
|
||||
] as const;
|
||||
|
||||
export type BasinKey = keyof typeof BASIN_DATA;
|
||||
|
||||
export interface ManualLabelPos {
|
||||
x?: number;
|
||||
y?: number;
|
||||
px?: number;
|
||||
py?: number;
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { dayjs } from "@/shared/lib";
|
||||
import { NoData } from "@/shared/ui/no-data";
|
||||
import type { NotiAsStatResponseDto, NotiAsUserResponseDto } from "@/entities/dashboard";
|
||||
import { useReadNoti } from "@/entities/noti";
|
||||
import { Badge, Button, Skeleton } from "@common/components";
|
||||
|
||||
interface DashboardNotiListProps {
|
||||
notiList: NotiAsUserResponseDto[] | undefined;
|
||||
notiStats: NotiAsStatResponseDto | undefined;
|
||||
isPending: boolean;
|
||||
isError: boolean;
|
||||
dataUpdatedAt: number;
|
||||
}
|
||||
|
||||
export function DashboardNotiList({
|
||||
notiList,
|
||||
notiStats,
|
||||
isPending,
|
||||
isError,
|
||||
dataUpdatedAt,
|
||||
}: DashboardNotiListProps) {
|
||||
const router = useRouter();
|
||||
const { mutateAsync: readNoti, isPending: isReadPending } = useReadNoti();
|
||||
|
||||
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],
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="card card-border m-0 bg-background">
|
||||
<div className="card-header flex items-center justify-between gap-4">
|
||||
<h3 className="text-2xl font-semibold">
|
||||
알림이력
|
||||
{dataUpdatedAt > 0 ? (
|
||||
<span className="pl-2 text-lg font-medium text-muted-foreground">
|
||||
(업데이트 일시: {dayjs(dataUpdatedAt).format("YYYY-MM-DD HH:mm")})
|
||||
</span>
|
||||
) : null}
|
||||
</h3>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button type="button" size="sm" color="white" onClick={() => router.push("/nti-hstry")}>
|
||||
전체보기
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 p-2">
|
||||
<div className="flex flex-wrap gap-2 pt-2">
|
||||
{NOTI_STAT_CHIPS.map((chip) => (
|
||||
<Badge key={chip.field} color={chip.color}>
|
||||
{chip.label} {isPending ? "-" : (notiStats?.[chip.field] ?? 0)}건
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{isPending ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-14 w-full" />
|
||||
<Skeleton className="h-14 w-full" />
|
||||
<Skeleton className="h-14 w-full" />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{isError ? (
|
||||
<p className="flex min-h-[12rem] items-center justify-center text-muted-foreground">
|
||||
알림이력을 불러오지 못했습니다.
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{!isPending && !isError && (notiList?.length ?? 0) === 0 ? <NoData /> : null}
|
||||
|
||||
<div>
|
||||
{!isPending && !isError && notiList && notiList.length > 0
|
||||
? notiList.map((row) => (
|
||||
<DashboardNotiRow
|
||||
key={row.notiId}
|
||||
row={row}
|
||||
isActionPending={isReadPending}
|
||||
onAction={() => {
|
||||
void handleAction(row);
|
||||
}}
|
||||
/>
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
interface DashboardNotiRowProps {
|
||||
row: NotiAsUserResponseDto;
|
||||
isActionPending: boolean;
|
||||
onAction: () => void;
|
||||
}
|
||||
|
||||
function DashboardNotiRow({ row, isActionPending, onAction }: DashboardNotiRowProps) {
|
||||
return (
|
||||
<div className="flex items-center gap-4 border-b px-2 py-4 last:border-b-0">
|
||||
<span className="shrink-0 text-lg text-muted-foreground">
|
||||
{row.regDt ? dayjs(row.regDt).format("YYYY-MM-DD") : "-"}
|
||||
</span>
|
||||
<p className="min-w-0 flex-1 truncate text-xl">{row.notiTtl}</p>
|
||||
<Button type="button" size="sm" color="white" disabled={isActionPending} onClick={onAction}>
|
||||
처리하기
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function 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 NOTI_STAT_CHIPS = [
|
||||
{ field: "todayCount", label: "오늘", color: "green" },
|
||||
{ field: "delayCount", label: "지연", color: "orange" },
|
||||
{ field: "pendingCount", label: "완료대기", color: "gray" },
|
||||
] as const;
|
||||
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { getCoreRowModel } from "@tanstack/react-table";
|
||||
|
||||
import { AS_DASHBOARD_BBS_ID } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { type UserPost, type UserPostSearchParams, useUserPostList } from "@/entities/board";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
import { dashboardNoticeColumns } from "../model/dashboard-notice-table-columns";
|
||||
|
||||
const DASHBOARD_NOTICE_LIST_PARAMS: UserPostSearchParams = {
|
||||
page: 0,
|
||||
size: 3,
|
||||
keywordType: "bbsTypeCd",
|
||||
keyword: "BBSNOTICE",
|
||||
};
|
||||
|
||||
export function DashboardNoticeTable() {
|
||||
const router = useRouter();
|
||||
const userPostListQuery = useUserPostList(DASHBOARD_NOTICE_LIST_PARAMS);
|
||||
|
||||
const handleRowClick = (post: UserPost) => {
|
||||
if (!AS_DASHBOARD_BBS_ID) return;
|
||||
router.push(`/cms/bbsnotice/${AS_DASHBOARD_BBS_ID}/${post.pstNo}`);
|
||||
};
|
||||
|
||||
const handleMoreClick = () => {
|
||||
if (!AS_DASHBOARD_BBS_ID) return;
|
||||
router.push(`/cms/bbsnotice/${AS_DASHBOARD_BBS_ID}`);
|
||||
};
|
||||
|
||||
const { tableComponent } = useDataTable({
|
||||
query: userPostListQuery,
|
||||
tableOptions: {
|
||||
columns: dashboardNoticeColumns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
},
|
||||
dataTableOptions: {
|
||||
variant: "line",
|
||||
onRowClick: handleRowClick,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<section className="card card-border m-0 bg-background">
|
||||
<div className="card-header flex items-center justify-between">
|
||||
<h3 className="text-2xl font-semibold">공지사항</h3>
|
||||
<Button type="button" size="sm" color="white" onClick={handleMoreClick}>
|
||||
더보기
|
||||
</Button>
|
||||
</div>
|
||||
{tableComponent}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import { useGetMainDashboard } from "@/entities/dashboard";
|
||||
|
||||
import { DashboardNotiList } from "./dashboard-noti-list";
|
||||
import { DashboardNoticeTable } from "./dashboard-notice-table";
|
||||
import { DocPrgrsSttsCards } from "./doc-prgrs-stts-cards";
|
||||
import { NationalAsMap } from "./national-as-map";
|
||||
|
||||
export function DashboardPage() {
|
||||
const { data, isPending, isError, dataUpdatedAt } = useGetMainDashboard();
|
||||
|
||||
return (
|
||||
<div className="mt-4 flex gap-4 px-[1.8rem] pb-6">
|
||||
<div className="flex-[0.5]">
|
||||
<NationalAsMap />
|
||||
</div>
|
||||
<div className="flex min-h-0 flex-1 flex-col gap-4">
|
||||
<DocPrgrsSttsCards items={data?.docPrgrsSttsList} isPending={isPending} isError={isError} />
|
||||
<DashboardNotiList
|
||||
notiList={data?.notiList}
|
||||
notiStats={data?.notiStats}
|
||||
isPending={isPending}
|
||||
isError={isError}
|
||||
dataUpdatedAt={dataUpdatedAt}
|
||||
/>
|
||||
<DashboardNoticeTable />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { CircleCheck, FilePlus, type LucideIcon, RefreshCw } from "lucide-react";
|
||||
|
||||
import type { DocPrgrsSttsResponseDto } from "@/entities/dashboard";
|
||||
import { cn, Skeleton } from "@common/components";
|
||||
|
||||
interface DocPrgrsSttsCardsProps {
|
||||
items: DocPrgrsSttsResponseDto[] | undefined;
|
||||
isPending: boolean;
|
||||
isError: boolean;
|
||||
}
|
||||
|
||||
export function DocPrgrsSttsCards({ items, isPending, isError }: DocPrgrsSttsCardsProps) {
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{DOC_PRGRS_STTS_SLOTS.map((slot) => (
|
||||
<Skeleton key={slot.docSeComCd} className="h-[14rem] rounded-xl" />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="card card-border m-0 flex min-h-[14rem] items-center justify-center bg-background text-muted-foreground">
|
||||
진행상황을 불러오지 못했습니다.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const itemByCd = new Map(items?.map((item) => [item.docSeComCd, item]));
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{DOC_PRGRS_STTS_SLOTS.map((slot) => {
|
||||
const item = itemByCd.get(slot.docSeComCd);
|
||||
return (
|
||||
<div
|
||||
key={slot.docSeComCd}
|
||||
className="card card-border hover:bg-muted/40 m-0 block bg-background transition-colors"
|
||||
>
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-semibold">{slot.title}</h3>
|
||||
</div>
|
||||
<Link key={slot.docSeComCd} href={slot.href}>
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
{STAT_METRICS.map((metric) => (
|
||||
<DocPrgrsSttsMetric
|
||||
key={metric.field}
|
||||
label={metric.label}
|
||||
count={item?.[metric.field] ?? 0}
|
||||
iconClassName={metric.iconClassName}
|
||||
bgColor={metric.bgColor}
|
||||
Icon={metric.Icon}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface DocPrgrsSttsMetricProps {
|
||||
label: string;
|
||||
count: number;
|
||||
iconClassName: string;
|
||||
bgColor: string;
|
||||
Icon: LucideIcon;
|
||||
}
|
||||
|
||||
function DocPrgrsSttsMetric({
|
||||
label,
|
||||
count,
|
||||
iconClassName,
|
||||
bgColor,
|
||||
Icon,
|
||||
}: DocPrgrsSttsMetricProps) {
|
||||
return (
|
||||
<div className={cn("card flex flex-1 flex-col items-center gap-2 rounded-2xl p-4", bgColor)}>
|
||||
<span
|
||||
className={cn(
|
||||
"flex size-12 items-center justify-center rounded-full text-white",
|
||||
iconClassName,
|
||||
)}
|
||||
>
|
||||
<Icon className="size-6" />
|
||||
</span>
|
||||
<span className="text-xl text-muted-foreground">{label}</span>
|
||||
<span className="text-3xl font-semibold tabular-nums">{count}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const DOC_PRGRS_STTS_SLOTS = [
|
||||
{ docSeComCd: "FNRS", title: "재원협의 진행상황", href: "/fnrs-cnsltn" },
|
||||
{ docSeComCd: "INSTL", title: "설치인가 진행상황", href: "/instl-sncn" },
|
||||
{ docSeComCd: "CNFM", title: "설계자문 진행상황", href: "/design-cnstn" },
|
||||
{ docSeComCd: "SEWER", title: "기본계획 진행상황", href: "/sewer-bsc-plan" },
|
||||
] as const;
|
||||
|
||||
const STAT_METRICS = [
|
||||
{
|
||||
field: "reqCnt",
|
||||
label: "신청",
|
||||
iconClassName: "bg-blue-500",
|
||||
bgColor: "bg-blue-100",
|
||||
Icon: FilePlus,
|
||||
},
|
||||
{
|
||||
field: "prgrsCnt",
|
||||
label: "진행",
|
||||
iconClassName: "bg-emerald-500",
|
||||
bgColor: "bg-emerald-100",
|
||||
Icon: RefreshCw,
|
||||
},
|
||||
{
|
||||
field: "cmptnCnt",
|
||||
label: "완료",
|
||||
iconClassName: "bg-orange-500",
|
||||
bgColor: "bg-orange-100",
|
||||
Icon: CircleCheck,
|
||||
},
|
||||
] as const;
|
||||
@@ -0,0 +1,85 @@
|
||||
"use client";
|
||||
|
||||
import type { CSSProperties } from "react";
|
||||
|
||||
import { useNationalAsMap } from "../lib/use-national-as-map";
|
||||
|
||||
import styles from "../lib/national-as-map.module.css";
|
||||
|
||||
export function NationalAsMap() {
|
||||
const { stageRef, svgLayerRef, labelsLayerRef, popupRef, popupContent, popupGridStyle } =
|
||||
useNationalAsMap();
|
||||
const popupColumnSpan = Math.max(1, popupContent.items.length);
|
||||
|
||||
return (
|
||||
<section className="card card-border m-0 flex h-full min-h-[40rem] flex-col bg-background">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-semibold">전국 행정지원 지도</h3>
|
||||
</div>
|
||||
<div className="flex-1 p-4">
|
||||
<div className={styles.mapArea}>
|
||||
<div
|
||||
ref={stageRef}
|
||||
style={
|
||||
{
|
||||
aspectRatio: 0.72 / 1,
|
||||
width: "100%",
|
||||
maxHeight: "100%",
|
||||
position: "relative",
|
||||
margin: "auto",
|
||||
"--map-inset": "0%",
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
<div className={styles.bgLayer} />
|
||||
<div
|
||||
ref={svgLayerRef}
|
||||
className={`${styles.svgLayer} ${styles.svgContainer}`}
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
/>
|
||||
<div
|
||||
ref={labelsLayerRef}
|
||||
className={styles.labelsLayer}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
ref={popupRef}
|
||||
className={`${styles.popup} invisible absolute z-20 overflow-hidden rounded-lg border border-blue-200 bg-blue-50 font-sans opacity-0 shadow-md`}
|
||||
style={popupGridStyle}
|
||||
>
|
||||
<div
|
||||
className="flex items-center justify-center border-b-2 border-blue-200 bg-blue-100 text-center font-bold"
|
||||
style={{ gridColumn: `span ${popupColumnSpan}` }}
|
||||
>
|
||||
{popupContent.title}
|
||||
</div>
|
||||
{popupContent.items.map((item, index) => (
|
||||
<div
|
||||
key={`header-${item.header}`}
|
||||
className={`flex items-center justify-center border-b-2 border-blue-200 text-center ${index < popupContent.items.length - 1 ? "border-r-2" : ""}`}
|
||||
>
|
||||
{item.header}
|
||||
</div>
|
||||
))}
|
||||
{popupContent.items.map((item, index) => (
|
||||
<div
|
||||
key={`data-${item.header}`}
|
||||
className={`flex items-center justify-center text-center ${index < popupContent.items.length - 1 ? "border-r-2" : ""}`}
|
||||
>
|
||||
{item.data}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -1,23 +1,10 @@
|
||||
import type { IUserInfo } from "@/shared/store";
|
||||
import type { DesignCnstnMngDetResponseDto } from "@/entities/design-cnstn";
|
||||
import { toEsntlAtchFileSaveList } from "@/widgets/pbadms-sprt";
|
||||
|
||||
import type { DesignCnstnRegistFormData } from "./design-cnstn-regist-schema";
|
||||
import { parseTaskSeComCdFromApi } from "./design-cnstn-regist-schema";
|
||||
|
||||
function mapEsntlAtchFileList(esntlFileList: DesignCnstnMngDetResponseDto["esntlFileList"]) {
|
||||
const esntlMap = new Map<string, string>();
|
||||
|
||||
for (const item of esntlFileList ?? []) {
|
||||
if (!item.esntlAtchFileId || !item.atflCd) continue;
|
||||
esntlMap.set(item.esntlAtchFileId, item.atflCd);
|
||||
}
|
||||
|
||||
return Array.from(esntlMap.entries()).map(([esntlAtchFileId, atchFileGroupId]) => ({
|
||||
esntlAtchFileId,
|
||||
atchFileGroupId,
|
||||
}));
|
||||
}
|
||||
|
||||
export function mapDetailToFormData(data: DesignCnstnMngDetResponseDto): DesignCnstnRegistFormData {
|
||||
const detDto = data.detDto;
|
||||
if (!detDto) {
|
||||
@@ -63,7 +50,7 @@ export function mapDetailToFormData(data: DesignCnstnMngDetResponseDto): DesignC
|
||||
thtiDsgn: item.tstmDesignQty,
|
||||
diff: item.tstmDesignQty - item.bgtRfltQty,
|
||||
})),
|
||||
esntlAtchFileList: mapEsntlAtchFileList(data.esntlFileList),
|
||||
esntlAtchFileList: toEsntlAtchFileSaveList(data.esntlFileList),
|
||||
privacyConsent: "Y",
|
||||
};
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user