diff --git a/apps/as/app/(main)/(with-base-layout)/dashboard/page.tsx b/apps/as/app/(main)/(with-base-layout)/dashboard/page.tsx new file mode 100644 index 000000000..1e20f5079 --- /dev/null +++ b/apps/as/app/(main)/(with-base-layout)/dashboard/page.tsx @@ -0,0 +1,3 @@ +import { DashboardPage } from "@/pages/dashboard/ui/dashboard-page"; + +export default DashboardPage; diff --git a/apps/as/app/(main)/(with-base-layout)/design-cnstn/[id]/page.tsx b/apps/as/app/(main)/(with-base-layout)/design-cnstn/[id]/page.tsx index 1b0fc983c..f603f8e39 100644 --- a/apps/as/app/(main)/(with-base-layout)/design-cnstn/[id]/page.tsx +++ b/apps/as/app/(main)/(with-base-layout)/design-cnstn/[id]/page.tsx @@ -1,4 +1,4 @@ -import { DesignViewPage } from "@/pages/design-cnstn"; +import { DesignDetailGate } from "@/pages/design-cnstn"; interface PageProps { params: Promise<{ id: string }>; @@ -6,5 +6,5 @@ interface PageProps { export default async function Page({ params }: PageProps) { const { id } = await params; - return ; + return ; } diff --git a/apps/as/app/(main)/(with-base-layout)/fnrs-cnsltn/[id]/page.tsx b/apps/as/app/(main)/(with-base-layout)/fnrs-cnsltn/[id]/page.tsx index ec9a3911e..0d0df0607 100644 --- a/apps/as/app/(main)/(with-base-layout)/fnrs-cnsltn/[id]/page.tsx +++ b/apps/as/app/(main)/(with-base-layout)/fnrs-cnsltn/[id]/page.tsx @@ -1,4 +1,4 @@ -import { FnrsViewPage } from "@/pages/fnrs-cnsltn"; +import { FnrsDetailGate } from "@/pages/fnrs-cnsltn"; interface PageProps { params: Promise<{ id: string }>; @@ -6,5 +6,5 @@ interface PageProps { export default async function Page({ params }: PageProps) { const { id } = await params; - return ; + return ; } diff --git a/apps/as/app/(main)/(with-base-layout)/instl-sncn/[id]/page.tsx b/apps/as/app/(main)/(with-base-layout)/instl-sncn/[id]/page.tsx index 89cd7a329..f89257bcc 100644 --- a/apps/as/app/(main)/(with-base-layout)/instl-sncn/[id]/page.tsx +++ b/apps/as/app/(main)/(with-base-layout)/instl-sncn/[id]/page.tsx @@ -1,6 +1,6 @@ -import { InstlViewPage } from "@/pages/instl-sncn"; +import { InstlSncnDetailGate } from "@/pages/instl-sncn"; export default async function InstlSncnDetailPage({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; - return ; + return ; } diff --git a/apps/as/app/(main)/(with-base-layout)/my-pbadms-sprt/page.tsx b/apps/as/app/(main)/(with-base-layout)/my-pbadms-sprt/page.tsx new file mode 100644 index 000000000..c7b7ad4b4 --- /dev/null +++ b/apps/as/app/(main)/(with-base-layout)/my-pbadms-sprt/page.tsx @@ -0,0 +1,3 @@ +import { MyPbadmsSprtListPage } from "@/pages/my-pbadms-sprt"; + +export default MyPbadmsSprtListPage; diff --git a/apps/as/app/(main)/(with-base-layout)/nti-hstry/page.tsx b/apps/as/app/(main)/(with-base-layout)/nti-hstry/page.tsx new file mode 100644 index 000000000..13df22b72 --- /dev/null +++ b/apps/as/app/(main)/(with-base-layout)/nti-hstry/page.tsx @@ -0,0 +1,3 @@ +import { NtiHstryListPage } from "@/pages/nti-hstry"; + +export default NtiHstryListPage; diff --git a/apps/as/app/(main)/(with-base-layout)/page.tsx b/apps/as/app/(main)/(with-base-layout)/page.tsx deleted file mode 100644 index 82a918b59..000000000 --- a/apps/as/app/(main)/(with-base-layout)/page.tsx +++ /dev/null @@ -1 +0,0 @@ -export { MainPage as default } from "@/pages/home"; diff --git a/apps/as/app/(main)/(with-base-layout)/sewer-bsc-plan/[id]/page.tsx b/apps/as/app/(main)/(with-base-layout)/sewer-bsc-plan/[id]/page.tsx index 17894197e..dec6502a8 100644 --- a/apps/as/app/(main)/(with-base-layout)/sewer-bsc-plan/[id]/page.tsx +++ b/apps/as/app/(main)/(with-base-layout)/sewer-bsc-plan/[id]/page.tsx @@ -1,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 ( -
- 하수도기본계획 상세({id})는 후속 구현 예정입니다.{" "} - - 수정 화면으로 이동 - -
- ); + const { chgCycl } = await searchParams; + const propChgCycl = chgCycl ? Number(chgCycl) : undefined; + return ; } diff --git a/apps/as/app/global-not-found.tsx b/apps/as/app/global-not-found.tsx new file mode 100644 index 000000000..6f66c97a6 --- /dev/null +++ b/apps/as/app/global-not-found.tsx @@ -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 ( + + +
+
+

+ 디지털 행정지원 시스템에 오신 것을 환영합니다. +

+

오류가 발생하였습니다.

+
+ +
+ +

페이지가 존재하지 않습니다.

+

관리자에게 문의하여 주십시오

+
+
+ + + ); +} diff --git a/apps/as/docs/pwrk-cnsltn/development-guide.md b/apps/as/docs/pwrk-cnsltn/development-guide.md new file mode 100644 index 000000000..99e38c569 --- /dev/null +++ b/apps/as/docs/pwrk-cnsltn/development-guide.md @@ -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`. + +--- diff --git a/apps/as/docs/pwrk-cnsltn/ref-endpoint.md b/apps/as/docs/pwrk-cnsltn/ref-endpoint.md new file mode 100644 index 000000000..b14a429ce --- /dev/null +++ b/apps/as/docs/pwrk-cnsltn/ref-endpoint.md @@ -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`은 프론트 서비스에 넣지 않는다. diff --git a/apps/as/docs/pwrk-cnsltn/ref-frontend-entities.md b/apps/as/docs/pwrk-cnsltn/ref-frontend-entities.md new file mode 100644 index 000000000..f74b63571 --- /dev/null +++ b/apps/as/docs/pwrk-cnsltn/ref-frontend-entities.md @@ -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; +findFileVrfcResults(fnrsCnsltnDocNo: string): Promise; +requestActnReview(docNo: string, body: EnvoActnReviewReqDto): Promise; +``` + +실무협의: + +```ts +export interface PwrkCnsltnMngService { + findPage(searchDto: PwrkCnsltnMngSearchDto): Promise>; + findById(pwrkCnsltnDocNo: string): Promise; + findByUpDocNo(upDocNo: string): Promise; + saveApply(body: PwrkCnsltnApplySaveRequestDto): Promise; + submitActnResult( + pwrkCnsltnDocNo: string, + body: PwrkActnResultSubmitDto, + ): Promise; + update(pwrkCnsltnDocNo: string, body: PwrkCnsltnMngUpdateRequestDto): Promise; + remove(pwrkCnsltnDocNo: string): Promise; + changePrgrsSttsComCd(body: DocPrgrsUpdateRequestDto): Promise; +} +``` + +v1.0의 `save(): PwrkCnsltnMngSaveResponseDto`(초안 트리거)는 화면에서 사용하지 않는다. 컨트롤러가 남아 있으면 엔티티에 넣더라도 UI 연결 금지. + +생성결과: + +```ts +export interface PwrkCnsltnDocCrtRsltService { + findPage( + searchDto: PwrkCnsltnDocCrtRsltSearchDto, + ): Promise>; + findById(crtDmndUuid: string, verSn: number): Promise; + findLatestByCrtDmndUuid(crtDmndUuid: string): Promise; + openWebHwpLatest(crtDmndUuid: string): Promise>; + download(crtDmndUuid: string, verSn: number): Promise>; + downloadLatest(crtDmndUuid: string): Promise>; + saveMultipart(formData: FormData): Promise; + updateMultipart( + crtDmndUuid: string, + verSn: number, + formData: FormData, + ): Promise; + remove(crtDmndUuid: string, verSn: number): Promise; +} +``` + +`webhwp/open`은 인증된 axios 요청으로 Blob을 조회한다: + +```ts +async function openWebHwpLatest(crtDmndUuid: string) { + return client.get(`${BASE}/${crtDmndUuid}/webhwp/open`, { + responseType: "blob", + }); +} +``` + +응답 Blob은 HWPX `File`로 감싸 `HwpCtrl.Open(file, "HWPX", ...)`에 전달한다. `blob:` Object URL은 사용하지 않는다. + +DC RAG TO (구현됨, 시그니처 유지): + +```ts +requestDocumentTransfer(body: DcRagDocumentTransferRequestDto): Promise; +requestDraftGeneration(body: DcRagDraftGenerateRequestDto): Promise; +getDraftGenerationStatus(crtDmndUuid: string): Promise; +requestCostVerification(body: DcRagCostVerifyRequestDto): Promise; +``` + +--- + +## 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(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`에서 검토자지정 팝업을 다시 열지 않음 diff --git a/apps/as/docs/pwrk-cnsltn/ref-process-status.md b/apps/as/docs/pwrk-cnsltn/ref-process-status.md new file mode 100644 index 000000000..9b8af0cdd --- /dev/null +++ b/apps/as/docs/pwrk-cnsltn/ref-process-status.md @@ -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`은 클라이언트가 보내지 않음. diff --git a/apps/as/docs/pwrk-cnsltn/ref-screen-buttons.md b/apps/as/docs/pwrk-cnsltn/ref-screen-buttons.md new file mode 100644 index 000000000..556aab39b --- /dev/null +++ b/apps/as/docs/pwrk-cnsltn/ref-screen-buttons.md @@ -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` 포함 | diff --git a/apps/as/docs/pwrk-cnsltn/ref-webhwp-api.md b/apps/as/docs/pwrk-cnsltn/ref-webhwp-api.md new file mode 100644 index 000000000..2196e6c17 --- /dev/null +++ b/apps/as/docs/pwrk-cnsltn/ref-webhwp-api.md @@ -0,0 +1,4325 @@ +# 레퍼런스 — 웹한글기안기 API (v3) + +> 상위: [development-guide.md](./development-guide.md) +> 원본: 한컴 웹한글기안기 API v3 개발 가이드 (PDF → TaggedPDF XML) +> 정리일: 2026-09-10 +> 구현: `apps/as/src/widgets/hangulgiangi` + +한컴 웹한글기안기 `HwpCtrl` / `HwpAction` / `HwpParameterSet` / `HwpParameterArray` / `HwpCtrlCode` API를 프론트 구현용으로 정리한 문서다. PDF OCR·줄바꿈 흔적은 바로잡았고, 원문 의미는 유지했다. + +공식 개발자 가이드: [Hancom WebHWP](https://developer.hancom.com/webhwp) + +--- + +## 이 프로젝트에서 쓰는 범위 + +실무협의서 관리(`TE`/`RK`) 웹기안기는 아래만 호출한다. 나머지는 확장 시 참고한다. + +| 용도 | API | 프로젝트 사용 | +| --- | --- | --- | +| 문서 열기 | `HwpCtrl.Open(path, format, arg, callback, callbackUserData)` | `File`(HWPX blob) + `format: "HWPX"` + `arg: ""`. 인증된 `GET .../webhwp/open` blob을 `File`로 감싼다. URL 직접 전달·`blob:` Object URL은 쓰지 않는다. | +| 저장 | `HwpCtrl.SaveAs(fileName, format, arg, callback, callbackUserData)` | `format: "HWPX"`, `arg: "download:false"`. 콜백 `result.data`를 Blob으로 디코드한 뒤 `POST /pwrk-cnsltn-doc-crt-rslt` multipart. | +| 로컬 다운로드 | `SaveAs(..., "download:true")` 또는 서버 `GET .../download` | 화면 [다운로드]는 서버 download API를 쓴다. | + +`Open` 반환값(`true`/`false`)은 인자 유효성만 나타낸다. 실제 성공은 콜백 `result.result`로 판단한다. 브라우저는 로컬 파일 경로를 열 수 없다. + +--- + +## 목차 + +- [1. HwpCtrl](#1-hwpctrl) + - [1.1 Properties](#11-properties) + - [1.2 Methods](#12-methods) +- [2. HwpAction](#2-hwpaction) +- [3. HwpParameterSet](#3-hwpparameterset) +- [4. HwpParameterArray](#4-hwpparameterarray) +- [5. HwpCtrlCode](#5-hwpctrlcode) + +--- + +## 1. HwpCtrl + +### 1.1 Properties + +#### 1.1.1 `CellShape` + +**구문** + +```js +HwpCtrl.CellShape +``` + +**설명** + +현재 선택되어 있는 표와 셀의 모양 정보를 나타낸다. + +**반환값** + +블록된 셀 혹은, 현재 캐럿이 위치한 셀의 속성을 ParameterSet 객체 타입으로 리턴한다. + +**비고** + +ParameterSet/Table 로 표의 속성에 대한 기본 정보를 나타내며, 이 가운데 Cell 아이템이 ParameterSet/Cell 로 셀의 속성을 나타낸다. 셀 블록이 잡혀있지 않은 상태이면 현재 캐럿이 위치한 셀 하나만을 대상으로 한다. 현재 표 내부에 캐럿이 위치하지 않으면 에러가 발생한다. + +**예제** + +1. 표 생성 +2. 셀 안으로 캐럿 이동 +3. 예제 스크립트 실행 결과 : 셀 간격이 2pt 로 변경된다. + +```js +var tp = HwpCtrl.CreateSet("Table"); +var cp = tp.CreateItemSet("Cell", "Cell"); +tp.SetItem("CellSpacing", 200); +HwpCtrl.CellShape = tp; +``` + +#### 1.1.2 `CharShape` + +**구문** + +```js +HwpCtrl.CharShape +``` + +**설명** + +현재 Selection 된 글자의 글자 모양을 나타낸다. + +**반환값** + +블록된 구역 혹은, 현재 캐럿이 위치한 곳의 글자 모양을 ParameterSet 객체 타입으로 리턴한다. + +**비고** + +property get 을 수행하면 현재 selection 내의 글자 모양을 구할 수 있고, selection 이 존재하지 않으면 현재 캐럿이 위치한 곳의 글자 모양을 돌려준다. + +글자 모양 중 특정 항목이 Selection 내에서 서로 다른 속성을 가지고 있으면 아예 아이템 자체가 존재하지 않는다. property set 을 수행하면 아이템이 존재하는 항목에 대해서만 속성을 설정한다. + +ParameterSet 형식은 별도로 전달된 ParameterSet Table 문서 참고 + +**예제** + +1. 글자 입력 +2. 입력한 글자 Selection +3. 예제 스크립트 실행 결과 : Selection 된 글자들의 글자 크기를 20pt 로 설정한다. + +```js +var cs = HwpCtrl.CharShape; +if (cs.ItemExist("Height")) { + cs.SetItem("Height", 2000); + HwpCtrl.CharShape = cs; +} +``` + +#### 1.1.3 `CurFieldState` + +**구문** + +```js +HwpCtrl.CurFieldState +``` + +**설명** + +캐럿이 위치한 필드의 상태 정보를 구한다. 읽기 전용. + +**반환값** + +현재 필드 상태에 따라 Number 타입의 값을 리턴한다. Remark 참고 + +**비고** + +다음과 같은 의미를 가진다. + +- bit 4: 필드명 존재 여부 (`1` 있음, `0` 없음) +- bit 0–3: 필드 종류 (`0` 없음, `1` 셀, `2` 누름틀, `4` 글상자) + +필드명 존재 여부와 필드 종류에 따라 0–20까지의 값을 갖는다. + +**예제** + +1. 누름틀 삽입 (다른 필드 삽입 가능) +2. 누름틀 안으로 캐럿 이동 +3. 예제 스크립트 실행 결과 : 현재 선택된 필드 : 누름틀 메시지 창 + +```js +var fieldState = HwpCtrl.CurFieldState; +var msg =""; +switch (fieldState) { + case 0: msg ="필드 없음"; + break; + case 1: msg ="셀"; + break; + case 2: msg ="누름틀"; + break; + case 4: msg ="글상자"; + break; +} +alert("현재 선택된 필드 :"+ msg); +``` + +#### 1.1.4 `CurSelectedCtrl` + +**구문** + +```js +HwpCtrl.CurSelectedCtrl +``` + +**설명** + +현재 선택된 컨트롤. 읽기 전용. + +**반환값** + +현재 선택된 컨트롤을 CtrlCode 객체 타입으로 리턴한다. + +**예제** + +1. 표 삽입 +2. 표 선택 +3. 예제 스크립트 실행 +결과 : 표가 삭제된다. + +```js +var ctrl = HwpCtrl.CurSelectedCtrl; +if( ctrl && ctrl.CtrlID =="tbl") { + HwpCtrl.DeleteCtrl(ctrl); +} +``` + +#### 1.1.5 `EditMode` + +**구문** + +```js +HwpCtrl.EditMode +``` + +**설명** + +현재 편집 모드 + +**반환값** + +현재 편집 모드를 Number 타입으로 리턴한다. + +**비고** + +현재 편집 모드를 나타낸다. + +- 0 : 읽기 전용 + +- 1 : 일반 편집모드 + +- 2 : 양식 모드. 양식 사용자 모드 : Cell 과 누름틀 중 양식 모드에서 편집 가능 속성을 가진 것만 편집 가능하다. + +- 16 : 배포용 문서 `SetEditMode`로 지정 불가능 + +**예제** + +1. EditMode 0(읽기 모드)으로 변경 +2. 편집시도 +3. EditMode 1(일반 편집 모드)으로 변경 +4. 편집시도 + +```js +HwpCtrl.EditMode = 0; +//편집 시도 +HwpCtrl.EditMode = 1; +//편집 시도 +``` + +#### 1.1.6 `HeadCtrl` + +**구문** + +```js +HwpCtrl.HeadCtrl +``` + +**설명** + +문서 중 첫 번째 컨트롤. 읽기 전용. + +**반환값** + +문서 중 첫 번째 컨트롤을 HwpCtrlCode 객체 타입으로 리턴한다. + +**비고** + +문서 중의 모든 컨트롤(표, 그림 등 특수 문자)들은 linked list 로 서로 연결되어 있는데, 그 list의 시작 컨트롤을 나타낸다. 이 컨트롤로부터 시작, Ctrl.Next 를 이용해 forward iteration 을 수행할 수 있 다. + +**예제** + +1. 표 입력 +2. 예제 스크립트 실행 결과 : 문서 중 모든 표가 삭제된다. + +```js +var ctrl = HwpCtrl.HeadCtrl; +while(ctrl != null) { + if(ctrl.CtrlID =="tbl") { + HwpCtrl.DeleteCtrl(ctrl); + } +ctrl = ctrl.Next +} +``` + +#### 1.1.7 `IsModified` + +**구문** + +```js +HwpCtrl.IsModified +``` + +**설명** + +현재 문서가 변경되었는지 나타낸다. 이 구문은 읽기 전용입니다. + +**반환값** + +변경내용 여부를 Boolean 타입으로 리턴한다. + +**예제** + +1. 새 문서 상태에서 예제 실행 후 alert 창 확인 +2. 편집 시도 후 예제 다시 실행 결과 : 문서가 변경되어 true 를 반환한다. + +```js +alert(HwpCtrl.IsModified) +``` + +#### 1.1.8 `IsPrivateInfoProtected` + +**구문** + +```js +HwpCtrl.IsPrivateInfoProtected +``` + +**설명** + +현재 문서에 개인정보보호 필드가 있는지 확인한다. + +**반환값** + +현재 문서의 개인정보보호 필드 여부를 Boolean 타입으로 리턴한다. + +**비고** + +개인정보보호 필드가 있는 문서를 열어서 예제코드 실행해야 합니다. + +**예제** + +1. 개인정보보호 필드가 있는 문서를 연다. +2. 예제 스크립트 실행 결과 : 문서가 개인정보 필드를 가지고 있으면 true 를 반환한다. + +```js +alert(HwpCtrl.IsPrivateInfoProtected); +``` + +#### 1.1.9 `LastCtrl` + +**구문** + +```js +HwpCtrl.LastCtrl +``` + +**설명** + +문서 중 마지막 컨트롤. 읽기 전용. + +**반환값** + +문서 중 마지막 컨트롤을 HwpCtrlCode 객체 타입으로 리턴한다. + +**비고** + +문서 중의 모든 컨트롤(표, 그림 등 특수 문자)들은 linked list 로 서로 연결되어 있는데, 그 list의 마지막 컨트롤을 나타낸다. 이 컨트롤로부터 시작, Ctrl.Prev 를 이용해 backward iteration 을 수행할 수 있다. + +**예제** + +1. 표 입력 +2. 예제 스크립트 실행 결과 : 문서 중 모든 표의 셀 간격을 1pt 로 설정한다. + +```js +var ctrl = HwpCtrl.LastCtrl; +var tbset = HwpCtrl.CreateSet("Table"); +tbset.SetItem("CellSpacing", 100); +while(ctrl != null) { + if(ctrl.CtrlID =="tbl") { + ctrl.Properties = tbset; + } +ctrl = ctrl.Prev; +} +``` + +#### 1.1.10 `PageCount` + +**구문** + +```js +HwpCtrl.PageCount +``` + +**설명** + +문서의 전체 페이지 수를 나타냅니다. 이 구문은 읽기 전용입니다. + +**반환값** + +문서의 페이지 수를 Number 타입으로 반환합니다. + +**비고** + +문서 전체에 대한 pagination 이 수행되지 않은 상태에서 이 property 를 참조하면 먼저 문서 전체의 pagination 을 먼저 수행하므로 긴 문서에 대해 문서 내용 변경과 참조를 반복하면 속도가 심각하게 느려질 수 있습니다. + +**예제** + +1. 문서 쪽 분할 +2. 예제 스크립트 실행 결과: 전체 페이지 수 메시지 창 + +```js +alert(HwpCtrl.PageCount); +``` + +#### 1.1.11 `ParaShape` + +**구문** + +```js +HwpCtrl.ParaShape +``` + +**설명** + +현재 Selection 의 문단 모양을 나타낸다. + +**반환값** + +블록된 구역 혹은, 현재 캐럿이 위치한 곳의 문단 모양을 ParameterSet 객체 타입으로 리턴한다. + +**비고** + +property get 을 수행하면 현재 selection 내의 문단 모양을 구할 수 있다. selection 이 존재하지 않으면 현재 캐럿이 위치한 곳의 문단 모양을 돌려준다. 문단 모양 중 특정 항목이 selection 내에서 서로 다른 속성을 가지고 있으면 아예 아이템 자체가 존재하지 않는다. property set 을 수행하면 아이템이 존재하는 항목에 대해서만 속성을 설정한다.. + +ParameterSet 형식은 별도로 전달된 ParameterSet Table 문서 참고 + +**예제** + +1. 글자 입력 +2. 입력한 글자 Selection +3. 예제 스크립트 실행 결과 : 현재 문단의 왼쪽 여백을 50pt 로 설정한다. + +```js +var paraShape = HwpCtrl.ParaShape; +paraShape.SetItem("LeftMargin", 10000); +HwpCtrl.ParaShape = paraShape; +``` + +#### 1.1.12 `ParentCtrl` + +**구문** + +```js +HwpCtrl.ParentCtrl +``` + +**설명** + +현재 캐럿의 상위 컨트롤. 읽기 전용. + +**반환값** + +현재 캐럿의 상위 컨트롤을 HwpCtrlCode 객체 타입으로 리턴한다. + +**비고** + +상위 컨트롤은 현재 캐럿이 위치한 리스트를 보유한 컨트롤이다. 예를 들어 셀 내부에 위치하면 표, 각 주 내용에 위치하면 각주, 바탕쪽이면 구역 컨트롤이 상위 컨트롤이다.현재 캐럿이 본문 레벨에 위치해 상위 컨트롤이 없을 때는 null 이 리턴된다. + +**예제** + +1. 표 입력 +2. 셀로 캐럿 이동 +3. 예제 스크립트 실행 결과 : 현재 캐럿의 위치가 셀이라면 부모 표의 셀 간격을 1pt 로 설정한다. + +```js +var ctrl = HwpCtrl.ParentCtrl; +if (ctrl.CtrlID =="tbl") { + var tbset = HwpCtrl.CreateSet("Table"); + tbset.SetItem("CellSpacing", 100); + ctrl.Properties = tbset; +} +``` + +#### 1.1.13 `ReadOnlyMode` + +**구문** + +```js +HwpCtrl.ReadOnlyMode +``` + +**설명** + +편집모드를 읽기 전용 모드로 설정하는 기능 + +**반환값** + +현재 읽기 전용모드를 Boolean 타입으로 리턴한다. + +**비고** + +property get 을 수행하면 현재 문서가 읽기 전용 모드 여부를 return 한다. return 값이 false 이면 편집 모드, true 이면 읽기 전용모드이다. property set 을 수행하면 현재 문서의 읽기 전용모드 속성을 설정한다. + +**예제** + +1. ReadOnlyMode true( 읽기모드로 변경 +2. 편집시도 +3. ReadOnlyMode false( 편집 모드로 변경 +4. 편집시도 결과 : 편집모드와 읽기 전용 모드를 설정한다. + +```js +HwpCtrl.ReadOnlyMode = true; +//읽기모드 //편집 시도 +HwpCtrl.ReadOnlyMode = false; +//편집모드 //편집 시도 +``` + +#### 1.1.14 `ScrollPosInfo` + +**구문** + +```js +HwpCtrl.ScrollPosInfo +``` + +**설명** + +스크롤바의 위치 정보를 얻어오는 기능 + +**반환값** + +현재 스크롤의 위치 정보를 얻어옵니다. ScrollPosInfo 의 ParameterSet 버전. + +**비고** + +현재 스크롤바수직, 수평의 위치정보를 ScrollPosInfo ParameterSet 으로 얻어오거나 설정한다. + +| 값 | 설명 | +| --- | --- | +| HorzPos | 수평 스크롤바 위치 | +| HorzLimitPos | 수평 스크롤바 한계 위치 값 (읽기 전용) | +| VertPos | 수직 스크롤바 위치 | +| VertLimitPos | 수직 스크롤바 한계 위치 값 (읽기 전용) | + +**예제** + +1. 스크롤을 원하는 위치로 이동시킨다. +2. 예제 스크립트 실행 결과: 선택된 값 메시지 창 + +```js +var pset = HwpCtrl.ScrollPosInfo; +var hp = pset.Item("HorzPos"); +var vp = pset.Item("VertPos"); +var hlp= pset.Item("HorzLimitPos"); +var vlp = pset.Item("VertLimitPos"); +alert("HorzPos:"+ hp +"HorzLimitPos:"+ hlp +"VertPos:"+ vp +"VertLimitPos:"+ vlp); +``` + +#### 1.1.15 `SelectionMode` + +**구문** + +```js +HwpCtrl.SelectionMode +``` + +**설명** + +문서의 내용이 어떤 selection 상태인가를 알려줍니다. 이 구문은 읽기 전용입니다. + +**반환값** + +현재 selection 상태를 Number 타입으로 반환합니다. + +**비고** + +일반 블록이 아닌 F3키에 의해 블록이 지정된 경우,HWPSEL_STRICT_MODE `0x10`(십진수 16) 으로 OR 마스크되어 오기 때문에, 항상 `0x0F`(십진수 15) 로 AND 마스크한 결과로 판단하도록 합니다. + +| 값 | 설명 | +| --- | --- | +| 0 | 블록지정이 되지 않은 상태 | +| 1 | 일반 블록이거나 F3키에 의한 블록 상태 | +| 3 | 테이블 셀 블록 상태 | +| 4 | 그리기 개체나 표, 그림 등 ShapeObject 에 해당하는 컨트롤이 선택된 상태 | + +**예제** + +1. 표 와 텍스트 삽입 +2. 텍스트 일반 블록으로 선택 +3. 예제 스크립트 실행 결과: 선택된 값 메시지 창 + +```js +var HWPSEL_NONE = 0; +var HWPSEL_NORMAL = 1; +var HWPSEL_TABLE = 3; +var HWPSEL_CTRL = 4; +var HWPSEL_MODE_MASK = 15; +var HWPSEL_STRICT_MODE = 16; +mode = HwpCtrl.SelectionMode & HWPSEL_MODE_MASK; +if(mode == HWPSEL_NONE) { + //블록지정이 되지 않은 경우 + alert("No Selection ..."); +} +else if(mode == HWPSEL_NORMAL) { + //일반 블록이거나 F3키에 의한 블록인 경우 (TEXT) + alert("Normal Selection ..."); +} +else if(mode == HWPSEL_TABLE) { + //테이블 셀 블록 상태 + alert("Table Selection ..."); +} +else if(mode == HWPSEL_CTRL) { + //그리기 개체나 표, 그림 등 ShapeObject 컨트롤이 선택된 상태 + alert("Ctrl Selection ..."); +} +``` + +#### 1.1.16 `ViewProperties` + +**구문** + +```js +HwpCtrl.ViewProperties +``` + +**설명** + +View 의 상태정보를 얻어오는 기능 + +**반환값** + +뷰의 상태정보를 ViewProperties ParameterSet 으로 얻어오거나 설정한다. + +**비고** + +조판 부호, 화면 확대 비율과 같은 view 에 관련된 정보를 나타낸다. + +**예제** + +1. 문서의 배율을 75% 로 설정 +2. 예제스크립트 실행 + +```js +var pset = HwpCtrl.ViewProperties; +alert(pset.Item("ZoomRatio")); +``` + +### 1.2 Methods + +#### 1.2.1 `Clear` + +**구문** + +```js +HwpCtrl.Clear(option) +``` + +**설명** + +현재 편집중인 문서의 내용을 닫고 빈문서 편집 상태로 돌아간다. + +**파라미터** + +- `option`: 편집중인 문서의 내용에 대한 처리 방법. 생략하면 hwpAskSave 가 선택된다 + +| ID | 값 | 설명 | +| --- | --- | --- | +| hwpDiscard | 1 | 문서 내용을 버린다. | + +**비고** + +현재는 옵션 1에 한해서만 사용할 수 있다. + +**예제** + +1. 글자 입력 +2. 예제 스크립트 실행 결과 : 빈문서 편집 상태로 되돌린다. + +```js +HwpCtrl.Clear(1); +``` + +#### 1.2.2 `CreateAction` + +**구문** + +```js +HwpCtrl.CreateAction(ActionID) +``` + +**설명** + +Action 객체를 생성한다. + +**파라미터** + +- `actionID`: 액션 ID + +**반환값** + +성공 시 Action 객체를 리턴한다. 존재하지 않는 액션 ID를 입력하면 null 을 리턴한다. + +**비고** + +액션에 대한 세부적인 제어가 필요할 때 사용한다. 예를 들어 기능을 수행하지 않고 대화상자만을 띄운다든지, 대화상자 없이 지정한 옵션에 따라 기능을 수행하는 등에 사용할 수 있다. Action ID Table 은 별도 문서 참조 + +**예제** + +1. 글자 입력 +2. 입력한 글자 Selection +3. 예제 스크립트 실행 결과 : Selection 된 글자들에 Bold 속성을 부여한다. + +```js +var act = HwpCtrl.CreateAction("CharShapeBold"); +act.Run(); +``` + +#### 1.2.3 `CreateField` + +**구문** + +```js +HwpCtrl.CreateField(direction, memo, name) +``` + +**설명** + +캐럿의 현재 위치에 누름틀을 생성한다. + +**파라미터** + +- `direction`: 입력이 안된 상태에서 보여지는 안내문/지시문 +- `memo`: 누름틀에 대한 설명/도움말 + +- `name`: :누름틀 필드에 대한 필드 이름 + +**반환값** + +성공이면 true, 실패면 false + +**비고** + +memo Parameter 는 ActiveX 한/글 컨트롤과 호환용으로 사용되는 Parameter 이다. 웹한글에서는 내용을 확인할 수 없으나 누름틀 저장 후 데스크탑 한/글에서 메모 내용을 확인할 수 있다. + +**예제** + +1. 예제 스크립트 실행 결과 : 현재 위치에 본문 필드가 생성된다. + +```js +HwpCtrl.CreateField("본문을 입력하세요", "", "본문"); +``` + +#### 1.2.4 `CreatePageImage` + +**구문** + +```js +HwpCtrl.CreatePageImage(fileName, pgno, format) +``` + +**설명** + +지정한 페이지의 이미지를 파일로 생성한다. + +**파라미터** + +- `fileName`: 생성할 이미지 파일의 이름 +- `pgno`: 페이지 번호. 0부터 PageCount-1까지. 생략하면 0이 사용된다 +- `format`: :이미지 파일의 포맷. jpg, jpeg, png% 중의 하나. 생략하면 png% 가 사용된다 + +**반환값** + +성공이면 true, 실패면 false + +**예제** + +1. 문서 작성 또는 문서 불러오기 +2. 예제 스크립트 실행 결과 : 첫 번째 페이지를 이미지로 다운로드한다. + +```js +HwpCtrl.CreatePageImage("pageimage.png", 0, "png"); +``` + +#### 1.2.5 `CreatePageImageEx` + +**구문** + +```js +HwpCtrl.CreatePageImageEx(fileName, pgno, resolution, depth, format, option, callback, callbackUserData) +``` + +**설명** + +지정한 페이지의 이미지를 파일로 생성합니다. + +**파라미터** + +- `fileName`: 생성할 이미지 파일의 이름 + +- `pgno`: 페이지 번호. 0부터 PageCount-1까지. 생략하면 0이 사용됨 +- `resolution`: 이미지 해상도. DPI 단위96, 300, 1200 등로 지정됨. 생략하면 96이 사용됨 +- `depth`: 이미지 파일의 color depth1, 4, 8, 24) 를 지정함 +- `format`: 이미지 파일의 포맷. jpg, jpeg, png2 중의 하나. 생략하면 png2 가 사용됨 +- `option`: download:true 를 넣으면 문서가 로컬에 다운로드 됨 +- `callback`: CreatePageImageEx 함수가 동작을 마친 후 수행될 함수 + +- result( 성공 실패 여부, path( 저장경로, fileName, Size, CallbackUserData 를 포함한 + +- Object 를 인자로 받음 +- `callbackUserData`: Callback 함수에 Argument 로 넘겨 받을 result 에 포함될 UserData, 생략 가능 + +**반환값** + +성공하면 true 를 반환하고 실패하면 false 를 반환합니다.여기서 성공/실패는 필요한 인자가 제대로 들어 있고, 비동기 로직을 제외한 부분이 제대로 동작하였는지를 판별합니다.비동기 액션이므로 return 값이 true 이더라도 네트워크등의 문제로 실패 할 수 있습니다.정확한 성공/실패 여부는 callback 함수의 인자 로 들어오는 object 의 result 속성을 통해서 판별할 수 있습니다. + +**예제** + +1. 임의의 문서 작성 +2. 예제 스크립트 실행 결과: 지정한 페이지를 이미지로 다운로드합니다. + +```js +HwpCtrl.CreatePageImageEx("pageimage.png", 0, 96, 24, "png", "download:true", function(result) { + console.log("다운로드 완료"); +} +``` + +); + +#### 1.2.6 `CreateSet` + +**구문** + +```js +HwpCtrl.CreateSet(setid) +``` + +**설명** + +ParameterSet 객체를 생성한다. + +**파라미터** + +- `setid`: 생성할 ParameterSet 객체의 ID + +**반환값** + +생성된 ParameterSet 객체를 리턴한다. + +**예제** + +1. 표 생성 +2. 셀 안으로 캐럿 이동 +3. 예제 스크립트 실행 결과 : 셀 간격이 2pt 로 변경된다. + +```js +var tp = HwpCtrl.CreateSet("Table"); +var cp = tp.CreateItemSet("Cell", "Cell"); +tp.SetItem("CellSpacing", 200); +HwpCtrl.CellShape = tp; +``` + +#### 1.2.7 `DeleteCtrl` + +**구문** + +```js +HwpCtrl.DeleteCtrl(ctrl) +``` + +**설명** + +문서 내 컨트롤을 삭제한다. + +**파라미터** + +- `ctrl`: 삭제할 문서 내 컨트롤CtrlCode 객체) + +**반환값** + +성공이면 true, 실패면 false + +**예제** + +1. 표 삽입 +2. 표 선택 +3. 예제 스크립트 실행 결과 : 표가 삭제된다. + +```js +var ctrl = HwpCtrl.CurSelectedCtrl; +if( ctrl && ctrl.CtrlID =="tbl") { + HwpCtrl.DeleteCtrl(ctrl); +} +``` + +#### 1.2.8 `FieldExist` + +**구문** + +```js +HwpCtrl.FieldExist(field) +``` + +**설명** + +문서 중에 지정한 데이터 필드가 존재하는지 검사한다. + +**파라미터** + +- `field`: 필드 이름 + +**반환값** + +성공이면 true, 실패면 false + +**예제** + +1. 누름틀 생성 +2. 필드이름 이름으로 지정 +3. 예제 스크립트 실행 결과 : 문서 내에 이름 필드가 있다면 해당 필드의 정렬을 가운데 정렬로 변경하고, 홍길동이라고 텍스트를 입력한다. + +```js +if(HwpCtrl.FieldExist("이름")) { + HwpCtrl.MoveToField("이름", true, true, true); + HwpCtrl.Run("ParagraphShapeAlignCenter"); + HwpCtrl.PutFieldText("이름", "홍길동"); +} +``` + +#### 1.2.9 `FoldRibbon` + +**구문** + +```js +HwpCtrl.FoldRibbon(fold) +``` + +**설명** + +리본 메뉴를 화면에 접기/펼치기 처리한다. + +**파라미터** + +- `fold`: 리본 메뉴 접기/펼치기 여부 + +**반환값** + +성공하면 true, 실패하면 false + +**예제** + +1. 예제 스크립트 실행 결과 : Ribbon 메뉴가 접혀진다. + +```js +HwpCtrl.FoldRibbon(true); +``` + +#### 1.2.10 `GetCurFieldName` + +**구문** + +```js +HwpCtrl.GetCurFieldName(option) +``` + +**설명** + +현재 캐럿 위치의 데이터 필드 이름을 구한다. + +**파라미터** + +- `option`: 다음과 같은 옵션을 지정할 수 있다. 0을 지정하면 모두 off 이다. 생략하면 0이 지정된다 + +| ID | 값 | 설명 | +| --- | --- | --- | +| hwpFieldCell | 1 | 셀에 부여된 필드 리스트만을 구한다. hwpFieldClickHere 와는 함께 지정할 수 없다. | +| hwpFieldClickHer e | 2 | 누름틀에 부여된 필드 리스트만을 구한다. hwpFieldCell 과는 함께 지정할 수 없다. | + +**반환값** + +필드 이름을 문자열로 리턴한다. 필드 이름이 없는 경우 빈 문자열이 돌아온다. + +**예제** + +1. 누름틀 생성 +2. 필드이름 이름으로 지정 +3. 생성한 누름틀로 캐럿 이동 +4. 예제 스크립트 실행 결과 : 현재 필드의 이름이 이름이라면 홍길동을 입력한다. + +```js +if(HwpCtrl.GetCurFieldName(0) =="이름") { + HwpCtrl.PutFieldText("이름", "홍길동"); +} +``` + +#### 1.2.11 `GetFieldList` + +**구문** + +```js +HwpCtrl.GetFieldList(number, option) +``` + +**설명** + +문서 중의 필드 리스트를 구한다. + +**파라미터** + +- `number`: 문서 중에서 동일한 이름의 필드가 여러 개 존재할 때 이를 구별하기 위한 식별 방법을 지정한다.생략하면 hwpFieldPlain 이 지정된다 + +| ID | 값 | 설명 | +| --- | --- | --- | +| hwpFieldPlain | 0 | 아무 기호 없이 순서대로 필드 이름이 나열된다. | +| hwpFieldNumber | 1 | 필드 이름 뒤에 일련번호가 {{#}} 와 같은 형식으로 붙는다. | +| hwpFieldCount | 2 | 필드 이름뒤에 그 이름의 필드가 몇 개 있는지 {{#}} 와 같은 형식으로 붙는다. | + +- `option`: 다음과 같은 옵션을 조합할 수 있다. 0을 지정하면 모두 off 이다. 생략하면 0이 지정된다 + +| ID | 값 | 설명 | +| --- | --- | --- | +| hwpFieldCell | 1 | 셀에 부여된 필드 리스트만을 구한다. hwpFieldClickHere 와 는 함께 지정할 수 없다. | +| hwpFieldClickHere | 2 | 누름틀에 부여된 필드 리스트만을 구한다. hwpFieldCell 과는 함께 지정할 수 없다. | +| hwpFieldSelection | 4 | 셀렉션 내에 존재하는 필드 리스트를 구한다. | + +**반환값** + +각 필드 사이를 문자코드 0x02 로 구분하여 다음과 같은 형식으로 리턴 한다. 가장 마지막 필드에는 0x02 가 붙지 않는다.) 필드이름#1필드이름#2... 필드이름#n2 + +**비고** + +문서 중에 동일한 이름의 필드가 여러 개 존재할 때는 number 에 지정한 타입에 따라 3 가지의 서로 + +다른 방식을 중에서 선택할 수 있다. 예를 들어 문서 중 title, body, title, body, footer 순으로 5 개의 필드가 존재할 때, hwpFieldPlain, hwpFieldNumber, HwpFieldCount 세 가지 형식에 따라 다음과 같은 내용이 돌아 온다. + +| ID | 예제 | +| --- | --- | +| hwpFieldPlain | "title\x02body\x02title\x02body\x02footer" | +| hwpFieldNumber | "title{{0}}\x02body{{0}}\x02title{{1}}\x02body{{1}}\x02footer{{0}}" | +| hwpFieldCount | "title{{2}}\x02body{{2}}\x02footer{{1}}" | + +**예제** + +1. 필드 입력 +2. 예제 스크립트 실행 결과 : 성공 메시지 창 + +```js +alert(HwpCtrl.GetFieldList(0)); +``` + +#### 1.2.12 `GetFieldText` + +**구문** + +```js +HwpCtrl.GetFieldText(fieldlist) +``` + +**설명** + +지정한 필드에서 문자열을 구합니다. + +**파라미터** + +- `fieldlist`: 텍스트를 구할 필드 이름의 리스트 + +**반환값** + +텍스트 데이터가 돌아옵니다. 텍스트에서 탭은 \t0x9, 문단 바뀜은 CR/LF0x0D/0x0A) 로 표현되며, 이외의 특수 코드는 포함되지 않습니다. 필드 텍스트의 끝은 0x02 로 표현되며, 그 이후 다음 필드의 텍스트가 연속해서 지정한 필드 리스트의 개수만큼 위치합니다. 지정한 이름의 필드가 없거나 사용자가 해당 필드에 아무 텍스트도 입력하지 않았으면 해당 텍스트에는 빈 문자열이 돌아옵니다. + +**비고** + +다음과 같이 필드 사이를 문자 코드 0x02 로 구분하여 한 번에 여러 개의 필드를 지정할 수 있다.필드 이름#1\x02 필드이름#2\x02... 필드이름#n2 지정한 필드 이름이 문서 중에 두 개 이상 존재할 때의 표현 방식은 다음과 같다. + +| 필드이름 | 필드이름{{n}} | +| --- | --- | +| 이름의 필드 중 첫 번째 | 지정한 이름의 필드 중 n 번째 | + +예를 들어, 제목1\x02 본문\x02 이름02 과 같이 지정하면 제목이라는 이름의 필드 중 두 번째, 본문이라는 이름의 필드 중 첫 번째, 이름이라는 이름의 필드 + +중 첫 번째를 각각 지정한다. 즉, 필드이름과 필드이름0 은 동일한 의미로 해석된다. + +**예제** + +1. 본문 이라는 필드 생성 +2. Text 삽입 +3. 예제 스크립트 실행 결과: 본문 이라는 이름의 필드 중 첫 번째 필드의 Text 를 얻어옵니다. + +```js +alert(HwpCtrl.GetFieldText(' 본문 { + { + 0 + } +} +')); +``` + +#### 1.2.13 `GetHeadingString` + +**구문** + +```js +HwpCtrl.GetHeadingString() +``` + +**설명** + +현재 캐럿이 위치한 문단의 문단번호/글머리표를 얻어냅니다. + +**반환값** + +문단번호 또는 글머리표를 String 타입으로 반환합니다.문단번호나 글머리표가 없다면 빈문자열을 반환 합니다. + +**예제** + +1. 문단번호/글머리표 작성 +2. 예제 스크립트 실행 결과: 문단번호 또는 글머리표 메시지 창 + +```js +alert("문단번호/글머리표 :"+ HwpCtrl.GetHeadingString()); +``` + +#### 1.2.14 `GetMetaTag` + +**구문** + +```js +HwpCtrl.GetMetaTag(name) +``` + +**설명** + +문서 내 메타태그 정보를 가져옵니다. + +**파라미터** + +- `name`: 찾을 메타태그의 이름 + +**반환값** + +메타태그를 찾았을 때 해당 메타태그 정보를 Object 타입으로 반환합니다. Object 의 Property 는 다음 과 같습니다. name : 메타태그의 이름 value : 메타태그의 값 format : 메타태그의 데이터 유형 메타태그가 없으면 null 을 반환합니다. + +**비고** + +hwpx 에서만 사용가능합니다. + +**예제** + +1. 예제 스크립트 실행 결과: 메타태그의 정보 메시지 창 + +```js +var tag; +HwpCtrl.SetMetaTag("office", "text", "한글과컴퓨터"); +HwpCtrl.GetTextFile("HWPX", "", function (result) { + data = result; + HwpCtrl.Clear(1); + HwpCtrl.SetTextFile(data, "HWPX", "", function (result) { + tag = HwpCtrl.GetMetaTag("office"); + alert('name : '+ tag.NAME + '\n' + 'value : ' + tag.VALUE); + } +, null); +} +); +``` + +#### 1.2.15 `GetMetaTagAll` + +**구문** + +```js +HwpCtrl.GetMetaTagAll() +``` + +**설명** + +문서 내 모든 메타태그 정보를 가져옵니다. + +**파라미터** + +- `fieldlist`: 텍스트를 구할 필드 이름의 리스트 + +**반환값** + +메타태그를 찾았을 때 해당 메타태그 정보를 Object 타입으로 반환합니다. Object 의 Property 는 다음 과 같습니다. name : 메타태그의 이름 value : 메타태그의 값 format : 메타태그의 데이터 유형 메타태그가 없으면 null 을 반환합니다. + +**비고** + +hwpx 에서만 사용가능합니다. + +**예제** + +1. 예제 스크립트 실행 결과: 메타태그의 정보 메시지 창 + +```js +var tag; +HwpCtrl.SetMetaTag("office", "text", "한글과컴퓨터"); +HwpCtrl.SetMetaTag("developer", "num", "123456"); +HwpCtrl.GetTextFile("HWPX", "", function (result) { + data = result; + HwpCtrl.Clear(1); + HwpCtrl.SetTextFile(data, "HWPX", "", function (result) { + tag = HwpCtrl.GetMetaTagAll(); + alert('metaTag : ' + JSON.stringify(tag)); + } +, null); +} +); +``` + +#### 1.2.16 `GetPageText` + +**구문** + +```js +HwpCtrl.GetPageText(pageNo, option) +``` + +**설명** + +특정 페이지의 텍스트를 얻는다. + +**파라미터** + +- `pageNo`: 텍스트를 얻을 페이지 Number +- `option`: 다음과 같은 옵션을 지정할 수 있다. 생략하면 모든 컨트롤에 대해 텍스트를 얻는다. 본문 텍스트는 옵션에 관계없이 얻는다 + +| option | 값 | 설명 | +| --- | --- | --- | +| 표 | 1 | 표 내부 텍스트만 얻는다. | +| 글상자 | 2 | 글상자 내부 텍스트만 얻는다. | +| 캡션 | 4 | 캡션 내부 텍스트만 얻는다. | + +**반환값** + +결과 값을 문자열로 리턴한다. + +**예제** + +1. 표 입력 +2. 표안에 텍스트 입력 +3. 예제 스크립트 실행 결과 : 첫 번째 Page 의 표 내부의 Text 가 나타난다. + +```js +alert(HwpCtrl.GetPageText(0, 1)); +``` + +#### 1.2.17 `GetPos` + +**구문** + +```js +HwpCtrl.GetPos() +``` + +**설명** + +현재 캐럿의 위치 정보를 얻어온다. + +**반환값** + +캐럿 위치 정보를 Object 타입으로 리턴한다. Object 의 Property 는 다음과 같다. list : 캐럿이 위치한 문서 내 리스트 아이디. para : 캐럿이 위치한 문단 아이디. pos : 캐럿이 위치한 문단 내 글자 단위 위치. + +**비고** + +기존 ActiveX 한글 컨트롤에서는 포인터를 인자로 받아서 처리하였다. WEB 환경에 맞춰서 객체를 리 턴하는 방식으로 변경되었다. + +**예제** + +1. 문서 작성 +2. 캐럿 이동 +3. GetPos API 로 현재 Pos 정보 저장 +4. 다른 위치로 캐럿 이동 +5. 3단계에서 저장한 위치로 캐럿 이동 결과 : 캐럿 위치를 저장하고, 다시 이동한다. + +```js +posObj = HwpCtrl.GetPos(); +//GetPos API 로 현재 Pos 정보 저장 //... 다른 위치로 캐럿 이동 +HwpCtrl.SetPos(posObj.list, posObj.para, posObj.pos); +//3단계에서 저장한 위치 로 캐럿 이동 +``` + +#### 1.2.18 `GetPosBySet` + +**구문** + +```js +HwpCtrl.GetPosBySet() +``` + +**설명** + +현재 캐럿의 위치 정보를 얻어옵니다. GetPos 의 ParameterSet 버전) + +**반환값** + +캐럿의 위치에 대한 ParameterSet 객체 타입으로 반환합니다. + +**예제** + +1. 임의의 문서 작성 +2. 캐럿 이동 +3. 예제 스크립트 실행 결과: 현재 캐럿이 위치한 정보 메시지 창 + +```js +var posSet = null; +posSet = HwpCtrl.GetPosBySet(); +alert(posSet.Item("List") +""+ posSet.Item("Para") +""+ posSet.Item("Pos")); +``` + +#### 1.2.19 `GetSelectedPos` + +**구문** + +```js +HwpCtrl.GetSelectedPos() +``` + +**설명** + +현재 설정된 블록의 위치 정보를 얻어온다. + +**반환값** + +현재 설정된 블록의 위치 정보를 Object 타입으로 리턴한다. Object 의 Property 는 다음과 같다. slist : 설정된 블록의 시작 리스트 아이디. spara : 설정된 블록의 시작 문단 아이디. spos : 설정된 블록의 문단 내 시작 글자 단위 위치. slist : 설정된 블록의 끝 리스트 아이디. spara : 설정된 블록의 끝 문단 아이디. spos : 설정된 블록의 문단 내 끝 글자 단위 위치. + +**비고** + +기존 ActiveX 한글 컨트롤에서는 포인터를 인자로 받아서 처리하였다. WEB 환경에 맞춰서 객체를 리 턴하는 방식으로 변경되었다. 위의 리스트란, 문단과 컨트롤들이 연결된 한/글 문서 내 구조를 뜻한다. 리스트 아이디는 문서 내 위치 정보 중 하나로서 SelectText 에 넘겨줄 때 사용한다. + +**예제** + +1. 문서 작성 +2. 텍스트 블록 +3. GetSelectedPos API 로 현재 블록 Pos 정보 저장 +4. 블록 해제 +5. 3단계에서 저장한 블록 다시 블록 결과 : 현재 설정된 블록의 위치를 저장하고, 이후에 다시 그 위치의 Text 를 블록한다. + +```js +posObj = HwpCtrl.GetSelectedPos(); +//GetSelectedPos API 로 현재 블록 Pos 정보 저장 //... 블록 해제 +HwpCtrl.SelectText(posObj.spara, posObj.spos, posObj.epara, posObj.epos); +//3 +``` + +단계에서 저장한 블록 다시 블록 + +#### 1.2.20 `GetSelectedPosBySet` + +**구문** + +```js +HwpCtrl.GetSelectedPosBySet(sset, eset) +``` + +**설명** + +현재 설정된 블록의 위치 정보를 얻어옵니다. GetSelectedPos 의 ParameterSet 버전) + +**파라미터** + +- `sset`: 설정된 블록의 시작 ParameterSet ListParaPos) +- `eset`: 설정된 블록의 끝 ParameterSet ListParaPos) + +**반환값** + +성공하면 true 를 반환하고 실패면 false 를 반환합니다. + +**예제** + +1. 임의의 문서 작성 +2. 텍스트를 블록으로 선택 +3. 예제 스크립트 실행 결과: 현재 블록의 시작과 끝 얻어냅니다. + +```js +var sset = HwpCtrl.CreateSet('ListParaPos'); +var eset = HwpCtrl.CreateSet('ListParaPos'); +HwpCtrl.GetSelectedPosBySet(sset, eset); +alert(sset.Item('List') + ', ' +sset.Item('Para') + ', ' + sset.Item('Pos') + '\n' +eset.Item('List') + ', ' + eset.Item('Para') + ', ' + eset.Item('Pos')); +``` + +#### 1.2.21 `GetTableCellAddr` + +**구문** + +```js +HwpCtrl.GetTableCellAddr(type) +``` + +**설명** + +현재 커서가 위치한 셀의 주소 정보를 얻어옵니다. + +**파라미터** + +- `type`: 얻어올 주소의 type + +- 0 행 + +- 1 열 + +**반환값** + +0n 까지의 행, 열을 얻어옵니다.커서가 셀에 있지 않거나, 실패하면 _1을 반환합니다. + +**예제** + +1. 표 삽입 +2. 커서 이동 +3. 예제 스크립트 실행 +결과: 현재 커서가 위치한 셀의 주소 메시지 창 + +```js +var col = HwpCtrl.GetTableCellAddr(0); +var row = HwpCtrl.GetTableCellAddr(1); +alert("col :"+ col +", row :"+ row); +``` + +#### 1.2.22 `GetText` + +**구문** + +```js +HwpCtrl.GetText() +``` + +**설명** + +문서 중에서 텍스트를 얻는다. + +**반환값** + +Object 타입으로 리턴한다. Property 는 다음과 같다. text : 텍스트 데이터가 돌아온다. 텍스트에서 탭은 \t0x9, 문단 바뀜은 CR/LF0x0D/0x0A) 로 표 현되며, 이외의 특수 코드는 포함되지 않는다. result : 결과값을 Number 타입으로 리턴한다. 다음과 같은 결과값을 리턴한다. + +- 0 텍스트 정보 없음. + +- 1 리스트의 끝 + +- 2 일반 텍스트 + +- 3 다음 문단 + +- 4 제어 문자 내부로 들어 감. + +- 5 제어 문자를 빠져 나옴. + +- 101 초기화 안됨. InitScan() 실패 또는 InitScan() 을 실행하지 않은 경우) + +- 102 텍스트 변환 실패 + +**비고** + +기존 ActiveX 한글 컨트롤에서는 포인터를 인자로 받아서 처리하였다. WEB 환경에 맞춰서 객체를 리 턴하는 방식으로 변경되었다. GetText) 의 사용 전 InitScan() 검색 준비를 하고, 끝나면 ReleaseScan) 을 반드시 호출하여 관련 정보를 초기화 해주어야 한다. 텍스트가 있는 문단으로 캐럿을 이동 시키려면 moveScanPos 를 주고 MovePos) 를 호출하면 된다. + +**예제** + +1. 텍스트 입력 +2. 텍스트의 맨 앞으로 캐럿 이동 +3. 예제 스크립트 실행 결과 : 현재 캐럿부터 줄의 끝까지 Text 를 얻어낸다. + +```js +HwpCtrl.InitScan(0, 2); +alert(HwpCtrl.GetText().text); +HwpCtrl.ReleaseScan(); +``` + +#### 1.2.23 `GetTextBySet` + +**구문** + +```js +HwpCtrl.GetTextBySet(text) +``` + +**설명** + +문서 중에서 텍스트를 얻어옵니다.GetText 의 ParameterSet 버전) + +**파라미터** + +- `text`: Item 의 Text% 에 텍스트 데이터가 돌아옵니다. 텍스트에서 탭은 \t0x9, 문단 바뀜은 CR/LF0x0D/0x0A) 로 표현되며, 이외의 특수 코드는 포함되지 않습니다 + +**반환값** + +- 0 텍스트 정보 없음 + +- 1 목록의 끝 + +- 2 일반 텍스트 + +- 3 다음 문단 + +- 4 제어 문자 내부로 들어감 + +- 5 제어 문자를 빠져 나옴 + +- 101 초기화 안 됨InitScan() 실패 또는 InitScan() 을 실행하지 않은 경우) + +- 102 텍스트 변환 실패 + +**비고** + +GetTextBySet) 의 사용이 끝나면 ReleaseScan) 을 반드시 호출하여 관련 정보를 초기화 해주어야 합니다. 텍스트가 있는 문단으로 캐럿을 이동 시키려면 moveScanPos 를 주고 MovePos) 를 호출하면 됩니다. + +**예제** + +1. 임의의 텍스트 입력 +2. 캐럿 이동 +3. 예제 스크립트 실행 결과: 현재 캐럿부터 줄 끝까지의 Text 를 가져옴 + +```js +HwpCtrl.InitScan(0, 2); +var textSet = HwpCtrl.CreateSet('GetText'); +HwpCtrl.GetTextBySet(textSet); +alert(textSet.Item("Text")); +HwpCtrl.ReleaseScan(); +``` + +#### 1.2.24 `GetTextFile` + +**구문** + +```js +HwpCtrl.GetTextFile(format. option, callback) +``` + +**설명** + +현재 열린 문서를 문자열로 넘겨준다. + +**파라미터** + +- `format`: 파일의 형식 + +| format | 설명 | 비고 | +| --- | --- | --- | +| HWP | Hwp native format | BASE64 로 인코딩되어 있다. 저장된 내용 을 다른 곳에서 보여줄 필요가 없다면 이 포맷을 사용하기를 권장합니다 | +| HWPML2X | HWP 형식과 호환 | 문서의 모든 정보를 유지 | +| HTML | 인터넷 문서 HTML 형식 | 한글 고유의 서식은 손실된다. | +| TEXT | 유니코드 텍스트 | 서식정보가 없는 텍스트만 저장 | +| JSON | json format | 문서의 정보를 Json 형태로 만든다. 문서의 모든 정보를 유지 | + +- option + +| option | 값 | 설명 | +| --- | --- | --- | +| savebloc k | 선택된 블록만 저장 | 개체 선택 상태에서는 동작하지 않는다. | + +- `callback`: 콜백함수 + +- result: 콜백 함수의 인자로 결과 값을 받는다. 서버를 통하여 Text 를 얻어내므로 반드시 이후 동작을 콜백함수에 넣어주어야 한다 + +**반환값** + +결과 값을 문자열로 리턴한다.JSON 포맷에서만 리턴 값으로 받을 수 있다.) + +**예제** + +1. 문서 작성 +2. GetTextFile API 현재 문서 데이터 저장 +3. Clear API 로 현재 문서 초기화 +4. SetTextFile API 로 저장한 문서 다시 로드 결과 : 현재 문서의 데이터를 문자열 형태로 저장하고, 다시 불러온다. callback 방식 + +```js +//GetTextFile API 현재 문서 데이터 저장 +HwpCtrl.GetTextFile("HWP", "", function(result) { + data = result; +} +); +HwpCtrl.Clear(1); +//Clear API 로 현재 문서 초기화 //SetTextFile API 로 저장한 문서 다시 로드 +HwpCtrl.SetTextFile(data, "HWP", "", function(result) { + //이후 수행 할 동작 +} +, null); +``` + +async await 방식 + +```js +// GetTextFile API 현재 문서 데이터 저장 data = await +HwpCtrl.GetTextFile("HWP", ""); +// Clear API 로 현재 문서 초기화 +HwpCtrl.Clear(1); +// SetTextFile API 로 저장한 문서 다시 로드 await +HwpCtrl.SetTextFile(data, "HWP", ""); +``` + +#### 1.2.25 `InitScan` + +**구문** + +```js +HwpCtrl.InitScan(option, rang, spara, spos, epara, epos) +``` + +**설명** + +문서 검색을 위한 준비 작업을 한다. + +**파라미터** + +- `option`: 다음과 같은 옵션을 지정할 수 있다. 0을 지정하면 모두 off 이다. 생략하면 0이 지정된다 + +| ID | 값 | 설명 | +| --- | --- | --- | +| maskNorma l | 0x00 | 본문을 대상으로 검색한다. (서브리스트를 검색하지 않는다.) | +| maskChar | 0x01 | char 타입 컨트롤 마스크를 대상으로 한다. (강제줄나눔, 문단 끝, 하이픈, 묶움빈칸, 고정폭빈칸, 등...) | +| maskInline | 0x02 | inline 타입 컨트롤 마스크를 대상으로 한다. (누름틀 필드 끝, 등...) | +| maskCtrl | 0x04 | extende 타입 컨트롤 마스크를 대상으로 한다. (다단, 누름틀 필드 시작, Shape Object, 각주, 미주, 등...) | + +- `range`: 검색의 범위를 다음과 같은 옵션을 조합하여 지정할 수 있다. 생략하면 문서 시작부터 문 서의 끝까지 검색 범위가 지정된다 + +| ID | 값 | 설명 | +| --- | --- | --- | +| scanSposCurrent | 0x0000 | 캐럿 위치부터. (시작 위치) | +| scanSposSpecified | 0x0010 | spara, spos 에 지정한 위치부터. (시작 위치) | +| scanSposLine | 0x0020 | 줄의 시작부터. (시작 위치) | +| scanSposParagraph | 0x0030 | 문단의 시작부터. (시작 위치) | +| scanSposSection | 0x0040 | 구역의 시작부터. (시작 위치) | +| scanSposList | 0x0050 | 리스트의 시작부터. (시작 위치) | +| scanSposControl | 0x0060 | 컨트롤의 시작부터. (시작 위치) | +| scanSposDocument | 0x0070 | 문서의 시작부터. (시작 위치) | +| scanEposCurrent | 0x0000 | 캐럿 위치까지. (끝 위치) | +| scanEposSpecified | 0x0001 | epara, epos 에 지정한 위치까지. (끝 위치) | +| scanEposLine | 0x0002 | 줄의 끝까지. (끝 위치) | +| scanEposParagraph | 0x0003 | 문단의 끝까지. (끝 위치) | +| scanEposSection | 0x0004 | 구역의 끝까지. (끝 위치) | +| scanEposList | 0x0005 | 리스트의 끝까지. (끝 위치) | +| scanEposControl | 0x0006 | 컨트롤의 끝까지. (끝 위치) | +| scanEposDocument | 0x0007 | 문서의 끝까지. (끝 위치) | +| scanWithinSelection | 0x00ff | 검색의 범위를 블록으로 제한. | +| scanForward | 0x0000 | 정뱡향. (검색 방향) | +| scanBackward | 0x0100 | 역방향. (검색 방향) | + +- `spara`: 다음과 같은 옵션을 지정할 수 있다. 0을 지정하면 모두 off 이다. 생략하면 0이 지정된다 +- `spos`: 검색 시작 위치의 문단 중에서 문자의 위치. scanSposSpecified 옵션이 지정되었을 때만 유 효하다 +- `epara`: 검색 끝 위치의 문단 번호. scanEposSpecified 옵션이 지정되었을 때만 유효하다 +- `epos`: 검색 끝 위치의 문단 중에서 문자의 위치. scanEposSpecified 옵션이 지정되었을 때만 유효 하다 + +**반환값** + +성공하면 true, 실패하면 false + +**비고** + +기존 ActiveX 한글 컨트롤에서는 포인터를 인자로 받아서 처리하였다. WEB 환경에 맞춰서 객체를 리 턴하는 방식으로 변경되었다. + +**예제** + +1. 텍스트 입력 +2. 텍스트의 맨 앞으로 캐럿 이동 +3. 예제 스크립트 실행 결과 : 현재 캐럿부터 줄의 끝까지 Text 를 얻어낸다. + +```js +HwpCtrl.InitScan(0, 2); +alert(HwpCtrl.GetText().text); +HwpCtrl.ReleaseScan(); +``` + +#### 1.2.26 `Insert` + +**구문** + +```js +HwpCtrl.Insert(path, format, arg, callback, callbackUserData) +``` + +**설명** + +현재 캐럿 위치에 문서 파일을 삽입한다. + +**파라미터** + +- `path`: URL, blob +- `format`: 문서 형식. Open API 참고. 빈 문자열을 지정하면 자동으로 디텍트한다. 생략하면 빈 문자 열이 지정된다 +- `arg`: 세부 옵션. Open API 참고. 의미는 format 에 지정한 파일 형식에 따라 다르다. 생략하면 빈 문자열이 지정된다 +- `callback`: Insert 함수가 동작을 마친 후 수행될 함수 + +- Parameter + +- `result`: 성공 실패 여부, FileName, Size, CallbackUserData 를 포함한 Object +- `CallbackUserData`: Callback 함수에 Argument 로 넘겨 받을 result 에 포함될 UserData, 생략 가능 + +**반환값** + +성공하면 true, 실패하면 false Return Value 는 Argument 가 제대로 들어왔는지 정도의 판단을 할 수 있으며 실제로 문서 Insert 의 성공 여부는 콜백함수의 인자로 들어가는 result 에서 확인할 수 있다. + +**비고** + +브라우저에서는 로컬 파일 접근 권한이 없어 로컬 파일은 사용할 수 없다. + +**예제** + +1. 문서 작성 +2. 문서를 끼워넣을 위치로 캐럿 이동 +3. Insert API 로 테스트 문서 끼워넣기 결과 : 특정 위치에 문서 끼워넣기 callback 방식 + +```js +// ...문서를 끼워넣을 위치로 캐럿 이동 // ... // Insert API 로 테스트 문서 끼워넣기 +HwpCtrl.Insert("http://APIDOCS/testDoc/test.hwp", "HWP", "", function (res) { +``` + +console.log(res); }, null); + +async await 방식 + +```js +// ...문서를 끼워넣을 위치로 캐럿 이동 // ... // Insert API 로 테스트 문서 끼워넣기 await +HwpCtrl.Insert("http://APIDOCS/testDoc/test.hwp", "HWP", ""); +``` + +#### 1.2.27 `InsertBackgroundPicture` + +**구문** + +```js +HwpCtrl.InsertBackgroundPicture(bordertype, path, embedded, filloption, watermark, effect, brightnss, contrast, callback) +``` + +**설명** + +배경이미지를 넣는다. + +**파라미터** + +- `bordertype`: 배경 유형을 지정 + +| bordertype | 설명 | 비고 | +| --- | --- | --- | +| "SelectedCell" | 현재 선택된 표의 셀 의 배경을 변경한다. | | +| "SelectedCellDelete" | 현재 선택된 표의 셀 의 배경을 지운다. | 반드시 셀이 선택되어 있어야 함. 커서 가 위치하는 것만으로는 동작하지 않 음. | + +- `path`: 이미지 +- `URLembeded`: 이미지 파일을 문서내에 포함할지 여부 Boolean Type + +- 기존 ActiveX 한/글 컨트롤과 호환을 위해서 존재하는 인자로 true/false 여부를 떠나서 Embeded 로 동작 한다 + +- `filloption`: 삽입할 그림의 크기를 지정하는 옵션 +- `effect`: 이미지효과 + +| filloption | 설명 | 비고 | +| --- | --- | --- | +| 0 | 바둑판식으로 -모두 | | +| 1 | 현바둑판식으로 -가로/위 | | +| 2 | 바둑판식으로 -가로/아로 | | +| 3 | 바둑판식으로 -세로/왼쪽 | | +| 4 | 바둑판식으로 -세로/오른쪽 | | +| 5 | 크기에 맞추어 | 설정하지 않았을 때 기본값 | +| 6 | 가운데로 | | +| 7 | 가운데 위로 | | +| 8 | 가운데 아래로 | | +| 9 | 왼쪽 가운데로 | | +| 10 | 왼쪽 위로 | | +| 11 | 왼쪽 아래로 | | +| 12 | 오른쪽 가운데로 | | +| 13 | 오른쪽 위로 | | + +| effec t | 설명 | 비고 | +| --- | --- | --- | +| 0 | 원래 그림 | 설정하지 않았을 때 기본값 | +| 1 | 그래이 스케일 | | +| 2 | 흑백으로 | | + +- `watermark`: watermark 효과 유무 Boolean Type . true 이면 brightness 와 contrast 옵션이 무시된다 +- `brightness`: 밝기 지정100 100, 기본값 : 0 +- `contrast`: 선명도 지정100 100, 기본값 : 0 +- `callback`: InsertBackgroundPicture 함수가 동작을 마친 후 수행될 함수 + +- parameterresult : 성공 시 true, 실패 시 false + +**반환값** + +성공 시 true, 실패 시 false + +**비고** + +브라우저에서는 로컬 파일 접근 권한이 없어 로컬 파일은 사용할 수 없다. + +**예제** + +1. 표 생성 +2. 셀안으로 캐럿 이동 +3. 예제 스크립트 실행 결과 : 셀에 배경이미지를 넣고, 제거한다. callback 방식 + +```js +// .... 셀 내부로 캐럿 이동 // ... // 배경 이미지 삽입 HwpCt rl.InsertB ackg roundPi cture("S electedC ell", "http://APIDOCS/testDoc/logo.png", 1, 5, 0, 0, 0, 0, function (res) { + console.log(res); +} +); +// ... // .... 배경이미지 넣은 셀 내부로 캐럿 이동 // 배경 이미지 제거 +HwpCtrl.InsertBackgroundPicture("SelectedCellDelete", 0, 0, 0, 0, 0, 0, 0, function (res) { +``` + +console.log(res); }); + +async await 방식 + +```js +// .... 셀 내부로 캐럿 이동 // ... // 배경 이미지 삽입 await +HwpCtrl.InsertBackgroundPicture("SelectedCell", "http://APIDOCS/testDoc/logo.png", 1, 5, 0, 0, 0, 0); +// ... // .... 배경이미지 넣은 셀 내부로 캐럿 이동 // 배경 이미지 제거 await +HwpCtrl.InsertBackgroundPicture("SelectedCellDelete", 0, 0, 0, 0, 0, 0, 0); +``` + +#### 1.2.28 `InsertCtrl` + +**구문** + +```js +HwpCtrl.InsertCtrl(ctrlid, initparam) +``` + +**설명** + +현재 캐럿 위치에 컨트롤을 삽입한다. + +**파라미터** + +- 캐럿 위치 정보를 Object 타입으로 리턴한다. Object 의 Property 는 다음과 같다 +- `ctrlid`: 삽입할 컨트롤의 아이디initparam : 컨트롤의 초기 속성. 생략하면 디폴트 속성으로 생성한다 + +**반환값** + +생성된 CtrlCode 객체를 리턴한다. + +**비고** + +```js +ctrlid 에 지정할 수 있는 컨트롤 ID는 HwpCtrl.CtrlID 가 리턴하는 ID와 동일하다. 자세한 것은 Ctrl 오 브젝트 Properties 인 CtrlID 를 참조.initparam 에는 컨트롤의 초기 속성을 지정한다. +``` + +**예제** + +1. 예제 스크립트 실행 결과 : 5x5 크기의 표를 삽입한다. + +```js +var tbact = HwpCtrl.CreateAction("TableCreate"); +var tbset = tbact.CreateSet(); +tbact.GetDefault(tbset); +tbset.SetItem("Rows", 5); +tbset.SetItem("Cols", 5); +var table = HwpCtrl.InsertCtrl("tbl", tbset); +``` + +#### 1.2.29 `InsertDocument` + +**구문** + +```js +HwpCtrl.InsertDocument(path, callback, callbackUserData) +``` + +**설명** + +현재 캐럿 위치에 문서를 끼워넣는 기능. + +**파라미터** + +- `path`: URL, blob +- `callback`: InsertDocument 함수가 동작을 마친 후 수행될 함수 + +- 콜백 인자 `result`: 성공 실패 여부, FileName, Size, CallbackUserData 를 포함한 Object +- `CallbackUserData`: Callback 함수에 Argument 로 넘겨 받을 result 에 포함될 UserData, 생략 가능 + +**반환값** + +성공하면 true, 실패하면 false Return Value 는 Argument 가 제대로 들어왔는지 정도의 판단을 할 수 있으며 실제로 문서 InsertDocument 의 성공 여부는 콜백함수의 인자로 들어가는 result 에서 확인할 수 있다. + +**비고** + +브라우저에서는 로컬 파일 접근 권한이 없어 로컬 파일은 사용할 수 없다. + +**예제** + +1. 문서 작성 +2. 문서를 끼워넣을 위치로 캐럿 이동 +3. InsertDocument API 로 테스트 문서 끼워넣기 결과 : 특정 위치에 문서 끼워넣기 callback 방식 + +```js +// ...InsertDocument 끼워넣을 위치로 캐럿 이동 // ... // InsertDocument API 로 테스트 문서 끼워넣기 +HwpCtrl.InsertDocument("http://APIDOCS/testDoc/test.hwp", function (res) { +``` + +console.log(res); }, null); + +async await 방식 + +```js +// ...문서를 끼워 넣을 위치로 캐럿 이동 // ... // InsertDocument API 로 테스트 문서 끼워넣기 await +HwpCtrl.InsertDocument("http://APIDOCS/testDoc/test.hwp", "HWP", ""); +``` + +#### 1.2.30 `InsertPicture` + +**구문** + +```js +HwpCtrl.InsertPicture(path, embeded, sizeoption, reverse, watermark, effect, width, height, callback) +``` + +**설명** + +현재 캐럿의 위치에 그림을 삽입한다. + +**파라미터** + +- `path`: 이미지 +- `URLembeded`: 이미지 파일을 문서내에 포함할지 여부 true/false + +- 기존 API 사용자와 호환을 위해서 존재하는 Parameter, true/false 여부를 떠나서 Embeded 로 동작 한다 + +- `sizeoption`: 삽입할 그림의 크기를 지정하는 옵션 + +| sizeoption | 설명 | +| --- | --- | +| 0 | 이미지 원래의 크기로삽입한다. width 와 height 를 지정할 필요없다. | +| 1 | width 와 height 에 지정한 크기로 그림을 삽입한다. | +| 2 | width 는 셀의 width 만큼, height 는 셀의 height 만큼 확대/축소된다. | +| 3 | 현재 캐럿이 표의 셀안에 있을 경우, 셀의 크기에 맞추어 원본 이미지의 가로 세로의 비율이 동일하게 확대/축소하여 삽입한다. | + +- `reverse`: 이미지의 반전 유무 true/false) +- `watermark`: watermark 효과 유무 true/false) +- `effect`: 그림 효과 + +| option | 설명 | +| --- | --- | +| 0 | 실제 이미지 그대로 | +| 1 | 그레이 스케일 | +| 2 | 흑백효과 | + +- `width`: 그림의 가로 크기 지정. 단위는 mm +- `height`: 그림의 높이 크기 지정. 단위는 mm +- `callback`: InsertPicture 함수가 동작을 마친 후 수행될 함수 + +- ctrl: 성공 시 생성된 컨트롤 객체, 실패 시 null + +**반환값** + +성공하면 true, 실패하면 false + +**비고** + +브라우저에서는 로컬 파일 접근 권한이 없어 로컬 파일은 사용할 수 없다. + +**예제** + +1. 예제 스크립트 실행 결과 : 셀에 배경이미지를 넣고, 제거한다. callback 방식 + +```js +HwpCtrl.InsertPicture("http://APIDOCS/testDoc/logo.png", true, 1, false, false, 0, 30, 20, function(ctrl) { + //InsertPicture 이후 동작 +} +); +``` + +async await 방식 + +```js +await HwpCtrl.InsertPicture("http://APIDOCS/testDoc/logo.png", true, 1, false, false, 0, 30, 20); +``` + +#### 1.2.31 `IsCommandLock` + +**구문** + +```js +HwpCtrl.IsCommandLock(actionID) +``` + +**설명** + +특정 액션이 잠금 상태인지 여부를 조사합니다. + +**파라미터** + +- `actionID`: 액션 ID + +**반환값** + +해당 액션이 잠금상태이면 True 를 반환하고, 잠금상태가 아니면 False 를 반환합니다. + +**예제** + +1. LockCommand 를 이용해 Bold Action 을 잠금 +2. 예제 스크립트 실행 결과: Bold Action 잠김 여부 메시지 창 + +```js +HwpCtrl.LockCommand('CharShapeBold', true); +var isCommandLock = HwpCtrl.IsCommandLock('CharShapeBold'); +if(isCommandLock) { + alert('Bold Action 을실행할수없습니다.') +} +else { + alert('Bold Action 을실행할수있습니다.') +} +``` + +#### 1.2.32 `IsSpellCheckCompleted` + +**구문** + +```js +HwpCtrl.IsSpellCheckCompleted() +``` + +**설명** + +맞춤법 검사가 완료되었는지를 판단한다. + +**반환값** + +성공 시 true, 실패 시 false + +**비고** + +기존 ActiveX 한글 컨트롤에서는 포인터를 인자로 받아서 처리하였다. WEB 환경에 맞춰서 객체를 리 + +턴하는 방식으로 변경되었다. + +**예제** + +1. 텍스트 입력 +2. 예제 스크립트 실행 결과 : 맞춤법 검사가 되어 있지 않다면 맞춤법 검사를 실행한다. + +```js +if (!HwpCtrl.IsSpellCheckCompleted()) { + // 맞춤법 검사 완료 여부 체크 + HwpCtrl.MovePos(2); + // 문서 시작부분으로 이동 + var dact = HwpCtrl.CreateAction("SpellingCheck"); + //맞춤법 검사 액션 생성 +``` + +및 실행 + +```js +var dset = dact.CreateSet(); +dact.GetDefault(dset); +dset.SetItem("CheckStart", 1); +dact.PopupDialog(dset, function() { + //PopupDialog 이후 수행될 동작 +} +, null); +} +``` + +#### 1.2.33 `LockCommand` + +**구문** + +```js +HwpCtrl.LockCommand(actionID, lock) +``` + +**설명** + +특정 액션이 실행되지 않도록 잠급니다. + +**파라미터** + +- `actionID`: 액션 ID +- `lock`: true 이면 액션의 실행을 lock 시키고, false 이면 unlock 시킵니다 + +**예제** + +1. 예제 스크립트 실행 결과: Bold Action 이 잠김 + +```js +HwpCtrl.LockCommand('CharShapeBold', true); +``` + +#### 1.2.34 `ModifyFieldProperties` + +**구문** + +```js +HwpCtrl.ModifyFieldProperties(field, remove, add) +``` + +**설명** + +지정한 필드의 속성을 바꿉니다. + +**파라미터** + +- `field`: 속성을 바꿀 필드 이름의 리스트. 형식은 PutFieldText 과 동일합니다 +- `remove`: 제거될 속성 +- `add`: 추가될 속성 + +**반환값** + +음수가 리턴되면 에러임을 나타냅니다.그 외의 리턴값은 아래 remark 참조) + +**비고** + +속성의 값은 아래와 같다. + +| long value | 설명 | +| --- | --- | +| 0x00000001 | 양식모드에서 편집가능 속성 (0: 편집 불가, 1: 편집 가능) | + +return 값의 bit field 는 다음과 같다. + +| long value bit mask | 설명 | +| --- | --- | +| 0x00000001 | 양식모드에서 편집가능 속성 (0: 편집 불가, 1: 편집 가능) | +| 0x80000000 | 에러 | +| 0x40000000 | 필드를 찾을 수 없음 | + +**예제** + +1. 본문 이라는 필드 생성 +2. 예제 스크립트 실행 결과: 본문 필드의 속성 변경 + +```js +var ModifyFieldPropertiesTest = function() { + var ret = HwpCtrl.ModifyFieldProperties("본문", 0, 0); + if (ret"0 ) { + var strErrMsg ="test 필드의 정보를 알아오는 데 실패"; + if(ret"0x40000000) strErrMsg +="\n 지정한필드를찾을수없습니다."; + alert(strErrMsg); + return; + } +if (ret"0x00000001) { + alert("현재 양식모드에서 편집 가능"); + ret = HwpCtrl.ModifyFieldProperties("본문", 1, 0); +} +else { + alert("현재 양식모드에서 편집 불가능"); + ret = HwpCtrl.ModifyFieldProperties("본문", 0, 1); +} +if (ret"0 ) { + alert("필드 속성 변경 실패."); +} +if (ret"0x00000001) alert("양식모드에서 편집 가능하도록 변경되었 습니다."); +else alert("양식모드에서 편집 불가능하도록 변경되었 습니다."); +} +``` + +ModifyFieldPropertiesTest(); + +#### 1.2.35 `MovePos` + +**구문** + +```js +HwpCtrl.MovePos(moveID, para, pos) +``` + +**설명** + +캐럿의 위치를 옮긴다. + +**파라미터** + +- `moveID`: 다음과 같은 값을 지정할 수 있다. 생략하면 moveCurList 가 지정된다 + +| ID | 값 | 설명 | +| --- | --- | --- | +| moveNextChar | 16 | 한 글자 뒤로 이동.(현재 리스트만을 대상으로 동작한다.) | +| movePrevChar | 17 | 한 글자 앞으로 이동. (현재 리스트만을 대상으로 동작한다.) | +| moveNextWord | 18 | 한 단어 뒤로 이동. (현재 리스트만을 대상으로 동작한다.) | +| movePrevWord | 19 | 한 단어 앞으로 이동. (현재 리스트만을 대상으로 동작한다.) | +| moveNextLine | 20 | 한 줄 아래로 이동. | +| movePrevLine | 21 | 한 줄 위로 이동. | +| moveStartOfLine | 22 | 현재 위치한 줄의 시작으로 이동. | +| moveEndOfLine | 23 | 현재 위치한 줄의 끝으로 이동. | +| moveParentList | 24 | 한 레벨 상위로 이동한다. | +| moveTopLevelList | 25 | 탑레벨 리스트로 이동한다. | +| moveRootList | 26 | 루트 리스트로 이동한다. | +| moveCurrentCaret | 27 | 현재 캐럿이 위치한 곳으로 이동한다. (캐럿 위치가 뷰의 맨 위쪽으로 올라간다. ) | +| moveLeftOfCell | 100 | 현재 캐럿이 위치한 셀의 왼쪽 | +| moveRightOfCell | 101 | 현재 캐럿이 위치한 셀의 오른쪽 | +| moveUpOfCell | 102 | 현재 캐럿이 위치한 셀의 위쪽 | +| moveDownOfCell | 103 | 현재 캐럿이 위치한 셀의 아래쪽 | +| moveStartOfCell | 104 | 현재 캐럿이 위치한 셀에서 행(row) 의 시작 | +| moveEndOfCell | 105 | 현재 캐럿이 위치한 셀에서 행(row) 의 끝 | +| moveTopOfCell | 106 | 현재 캐럿이 위치한 셀에서 열(column) 의 시작 | +| moveBottomOfCell | 107 | 현재 캐럿이 위치한 셀에서 열(column) 의 끝 | +| moveScrPos | 200 | 한/글 문서장에서의 screen 좌표로서 위치를 설정 한다. | +| moveScanPos | 201 | GetText() 실행 후 위치로 이동한다. | + +- `para`: 이이동할 문단의 번호. moveMain 또는 moveCurList 가 지정되었을 때만 사용된다. moveScrPos 가 지정되었을 때는 문단번호가 아닌 스크린 좌표로 해석된다 +- `pos`: 이동할 문단 중에서 문자의 위치. moveMain 또는 moveCurList 가 지정되었을 때만 사용된다 + +**반환값** + +성공하면 true, 실패하면 false + +**비고** + +moveScrPos 가 지정되었을 때는 스크린 좌표는 마우스 커서의 x,y 좌표를 그대로 넘겨 주면 된다.moveScanPos 는 문서를 검색하는 중에 캐럿을 이동 시키려 할 경우에만 사용이 가능하다. + +**예제** + +1. 문서 작성 +2. 캐럿 이동 +3. GetPos API 로 현재 Pos 정보 저장 +4. 다른 위치로 캐럿 이동 +5. 3단계에서 저장한 위치로 캐럿 이동 결과 : 캐럿 위치를 저장하고, 다시 이동한다. + +```js +posObj = HwpCtrl.GetPos(); +//GetPos API 로 현재 Pos 정보 저장 //... 다른 위치로 캐럿 이동 +HwpCtrl.MovePos(0, posObj.para, posObj.pos); +//3단계에서 저장한 위치로 캐럿 이동 +``` + +#### 1.2.36 `MoveToField` + +**구문** + +```js +HwpCtrl.MoveToField(field, text, start, select) +``` + +**설명** + +지정한 필드로 캐럿을 이동한다. + +**파라미터** + +- `field`: 필드 이름. GetFieldText/PutFieldText 과 같은 형식으로 이름 뒤에 #4 로 번호를 지정할 수 + +- 있다 +- `text`: 필드가 누름틀일 경우 누름틀 내부의 텍스트로 이동할지(true) 누름틀 코드로 이동할지(false) 를 지정한다. 누름틀이 아닌 필드일 경우 무시된다. 생략하면 true 지정된다 +- `start`: 필드의 처음(true) 으로 이동할지 끝(false) 으로 이동할지 지정한다. select 를 true 로 지정하면 무시된다. 생략하면 true 가 지정된다 +- `select`: 필드 내용을 블록으로 선택할지(true), 캐럿만 이동할지(false) 지정한다. 생략하면 false 가 지정된다 + +**반환값** + +성공하면 true, 실패하면 false + +**비고** + +누름틀은 한글 메뉴 중 입력 > 개체 > 필드 입력 메뉴를 선택하면 누름틀을 만드실 수 있습니다. + +**예제** + +1. 누름틀 삽입 필드 이름 본문 +2. MoveToField API 로 본문 필드로 이동 결과 : 특정 필드로 이동한다. + +```js +HwpCtrl.MoveToField("본문", true, true, false); +``` + +#### 1.2.37 `Open` + +**구문** + +```js +HwpCtrl.Open(path, format, arg, callback, callbackUserData) +``` + +**설명** + +문서 파일을 연다. + +**파라미터** + +- `path`: URL, blob +- `format`: 문서 형식. 빈 문자열을 지정하면 자동으로 디텍트한다. 생략하면 빈 문자열이 지정된다 + +| format | 설명 | +| --- | --- | +| "HWP" | 한글 native format | +| "HWP30" | 한글 3.X/96/97 | +| "HTML" | 인터넷 문서 | +| "TEXT" | 아스키 텍스트 문서 | +| "UNICODE" | 유니코드 텍스트 문서 | +| "HWP20" | 한글 2.0 | +| "HWP21" | 한글 2.1/2.5 | +| "HWP15" | 한글 1.X | +| "HWPML2X" | HWPML 2.X 문서 (Open / SaveAs 가능) | +| "HWPX" | 한글 표준 문서 (실무협의서 웹기안기에서 사용) | +| "MSWORD" | 마이크로소프트 워드 문서 | +| "PUBDOCBODY" | 공문서(xml) | + +- `arg`: 세부 옵션. 의미는 format 에 지정한 파일 형식에 따라 다르다. 생략하면 빈 문자열이 지정된다. arg 에 지정할 수 있는 옵션의 의미는 필터가 정의하기에 따라 다르지만, 신택스는 다음과 같이 공통된 형식을 사용한다.key:value;key:value; + +| 분류 | arg | Type | 설명 | +| --- | --- | --- | --- | +| 모든 Format | setcurdir | Boolean | 로드한 후 해당 파일이 존재하는 폴더로 현재 위치를 변경한다. hyperlink 정보가 상대적인 위치로 되어 있을 때 유용하다. | +| "HWP" | lock | Boolean | 로드한 후 해당 파일을 계속 오픈한 상태로 lock 을 걸지 여부 | +| "HWP" | notext | Boolean | 텍스트 내용을 읽지 않고 헤더 정보만 읽을지 여부. (스타일 로드 등에 사용) | +| "HWP" | template | Boolean | 새로운 문서를 생성하기 위해 템플릿 파일을 오픈한다. 이 옵션이 주어지면 lock은 무조건 false 로 처리된다. | +| "HWP" | suspendpassword | Boolean | true 로 지정하면, 암호가 있는 파일일 경우 암호를 묻지 않고 무조건 읽기에 실패한 것으로 처리한다. | +| "HWP" | forceopen | Boolean | true 로 지정하면, 읽기 전용으로 읽어야 하는 경우 대화상자를 띄우지 않는다. | +| "HWP" | versionwarning | Boolean | true 로 지정하면, 문서가 상위버전일 경우 메시지 박스를 띄우게 된다. | +| "HTML" | code | String, CodePage | 문서 변환 시 사용되는 코드 페이지를 지정할 수 있으며 code 키가 존재할 경우 필터 사용 시 사용자 다이얼로그를 띄우지 않는다. | +| "HTML" | textunit | Boolean, Pixel | Export 될 Text 의 크기의 단위 결정 pixel, point, mili 지정 가능. | +| "HTML" | formatunit | Boolean, Pixel | Export 될 문서 포맷 관련 (마진, Object 크기 등) 단위 결정. pixel, point, mili 지정 가능 | +| "DOCIMG" | asimg | Boolean | 저장할 때 페이지를 image 로 저장 | +| "DOCIMG" | ashtml | Boolean | 저장할 때 페이지를 html 로 저장 | +| "TEXT" | code | String | 문서 변환 시 사용되는 코드 페이지를 지정할 수 있으며 code 키가 존재할 경우 필터 사용 시 사용자 다이얼로그를 띄우지 않는다. | + +- `callback`: Open 함수가 동작을 마친 후 수행될 함수 + +- 콜백 인자 `result`: 성공 실패 여부, FileName, Size, CallbackUserData 를 포함한 Object + +- `CallbackUserData`: Callback 함수에 Argument 로 넘겨 받을 result 에 포함될 UserData, 생략 가능 + +**반환값** + +성공하면 true, 실패하면 false Return Value 는 Argument 가 제대로 들어왔는지 정도의 판단을 할 수 있으며 실제로 문서 Open 의 성공 여부는 콜백함수의 인자로 들어가는 result 에서 확인할 수 있다. + +**비고** + +브라우저에서는 로컬 파일 접근 권한이 없어 로컬 파일은 사용할 수 없다. + +**예제** + +1. 예제 스크립트 실행 결과 : 휴가계 문서를 오픈한다. callback 방식 + +```js +HwpCtrl.Open("http://APIDOCS/testDoc/test.hwp", "HWP", "", function (res) { + console.log(res); +}, null); +``` + +async await 방식 + +```js +await HwpCtrl.Open("http://APIDOCS/testDoc/test.hwp", "HWP", ""); +``` + +#### 1.2.38 `PrintDocument` + +**구문** + +```js +HwpCtrl.PrintDocument +``` + +**설명** + +문서를 PDF 로 변환하여 보여준다. + +**비고** + +WEB 환경에서 인쇄 드라이버에 직접 접근할 수 없다.때문에 웹브라우저 상의 PDF 뷰어 혹은 로컬 PDF 뷰어를 통해서 인쇄 설정 및 인쇄를 할 수 있도록 문서를 PDF 포맷으로 변환한다. + +**예제** + +1. 문서 작성 +2. 예제 스크립트 실행 결과 : 현재 문서가 PDF 로 변환되어 보여진다. + +```js +HwpCtrl.PrintDocument(); +``` + +#### 1.2.39 `PutFieldText` + +**구문** + +```js +HwpCtrl.PutFieldText(fieldlist, textlist) +``` + +**설명** + +지정한 필드의 내용을 채운다. + +**파라미터** + +- `fieldlist`: 내용을 채울 필드 이름의 리스트. 한 번에 여러 개의 필드를 지정할 수 있으며, 형식은 GetFieldText 와 동일하다. 다만 필드 이름 뒤에 # 로 번호를 지정하지 않으면 해당 이름을 가진 모든 필드에 동일한 텍스트를 채워 넣는다. 즉, PutFieldText 에서는 필드이름과 필드이름0 의 의미가 다르다 +- `textlist`: 필드에 채워 넣을 문자열의 리스트. 형식은 필드 리스트와 동일하게 필드의 개수만큼 텍스트를 0x02 로 구분하여 지정한다 + +**비고** + +현재 필드에 입력되어 있는 내용은 지워진다. 채워진 내용의 글자모양은 필드에 지정해 놓은 글자모양을 따라간다. fieldlist 의 필드 개수와, textlist 의 텍스트 개수는 동일해야 한다. 존재하지 않는 필드에 대해서는 무시한다. + +**예제** + +1. 누름틀 생성 +2. 필드이름 이름으로 지정 +3. 예제 스크립트 실행 결과 : 문서 내에 이름 필드가 있다면 해당 필드의 정렬을 가운데 정렬로 변경하고, 홍길동이라고 텍스트를 입력한다. + +```js +if(HwpCtrl.FieldExist("이름")) { + HwpCtrl.MoveToField("이름", true, true, true); + HwpCtrl.Run("ParagraphShapeAlignCenter"); + HwpCtrl.PutFieldText("이름", "홍길동"); +} +``` + +#### 1.2.40 `ReleaseScan` + +**구문** + +```js +HwpCtrl.ReleaseScan() +``` + +**설명** + +InitScan() 으로 설정된 정보를 초기화 한다. + +**예제** + +1. 텍스트 입력 +2. 텍스트의 맨 앞으로 캐럿 이동 +3. 예제 스크립트 실행 결과 : 현재 캐럿부터 줄의 끝까지 Text 를 얻어낸다. + +```js +HwpCtrl.InitScan(0, 2); +alert(HwpCtrl.GetText().text); +HwpCtrl.ReleaseScan(); +``` + +#### 1.2.41 `RenameField` + +**구문** + +```js +HwpCtrl.RenameField(oldname, newname) +``` + +**설명** + +지정한 필드의 이름을 바꾼다. + +**파라미터** + +- `oldname`: 이름을 바꿀 필드 이름의 리스트. 형식은 PutFieldText 과 동일하다 +- `newname`: 새로운 필드 이름의 리스트. oldname 과 동일한 개수의 필드 이름을 0x02 로 구분하여 지정한다 + +**비고** + +예를 들어 oldname 에 title0\x02title1, newname 에 tt1\x02tt2 로 지정하면 첫 번째 title 은 tt1 로, 두 번째 title 은 tt2 로 변경된다. oldname 의 필드 개수와, newname 의 필드 개수는 동일해야 한다. 존재하지 않는 필드에 대해서는 무시한다. + +**예제** + +1. 누름틀 입력 FieldName 이름1 +2. 예제 스크립트 실행 결과 : 이름1으로 지정된 누름틀의 필드이름이 이름2로 바뀐다. + +```js +HwpCtrl.RenameField(“ 이름1”, “이름2”); +``` + +#### 1.2.42 `ReplaceAction` + +**구문** + +```js +HwpCtrl.ReplaceAction(oldActionID, newActionID) +``` + +**설명** + +메뉴나 단축키로 호출되는 Action 을 교체합니다. + +**파라미터** + +- `oldActionID`: 교체할 원본 Action 의 이름 +- `newActionID`: 교체될 새로운 Action 의 이름 + +**반환값** + +성공여부, ActionID 가 잘못된 경우에는 실패합니다. + +**비고** + +메뉴나 단축키로 호출되는 Action 을 교체할 뿐, CreateAction 이나, Run 등의 API 를 이용할 때는 아무런 영향을 주지 못합니다. 교체된 OldActionID 를 원래대로 되돌려 놓기 위해서는 OldActionID 와 NewActionID 의 이름을 동일하게 하여 호출합니다. + +- 예: `ReplaceAction("FileSave","FileSave")`OldActionID 와 NewActionID 가 구조적으로 연결된 경우에는 해당 Action 을 실행하면 무한 루프를 돌게 됩니다. + +- 예: `ReplaceAction("FileSave","FileSaveAs")` `ReplaceAction("FileSaveAs","FileSave")` 를 둘다 호출한 경우 FileSave 나, FileSaveAs 를 호출하면 Stack overflow 가 발생합니다. + +**예제** + +1. 임의의 텍스트 삽입 및 선택 +2. 예제 스크립트 실행 결과: 진하게 클릭 시 글자모양 다이얼로그 창 표시 + +```js +HwpCtrl.ReplaceAction('CharShapeBold', 'CharShape'); +``` + +#### 1.2.43 `Run` + +**구문** + +```js +HwpCtrl.Run(actionID, callback, callbackUserData) +``` + +**설명** + +액션을 실행한다. + +**파라미터** + +- `actionID`: 액션 ID +- `callback`: 액션 실행 후 실행 될 함수 + +- 콜백 인자: 콜백을 호출한 Action 객체 result: 액션 실행 결과 Boolean 타입 callbackUserData: 사용자가 콜백에 전달한 데이터 + +- `callbackUserData`: 사용자가 콜백에 전달한 데이터 + +**예제** + +1. 글자 입력 +2. 입력한 글자 Selection +3. 예제 스크립트 실행 결과 : Selection 된 글자들에 Bold 속성을 부여한다. callback 방식 + +```js +HwpCtrl.Run("CharShapeBold", function (res) { + console.log(res); +} +); +``` + +async await 방식 + +```js +await HwpCtrl.Run("CharShapeBold"); +``` + +#### 1.2.44 `SaveAs` + +**구문** + +```js +HwpCtrl.SaveAs(fileName, format, arg, callback, callbackUserData) +``` + +**설명** + +현재 편집중인 문서를 지정한 이름으로 저장한다. + +**파라미터** + +- `fileName`: 로컬에 다운로드할 때에 파일의 이름을 지정한다. arg 인자에 download:true 포함되어 있을 때 로컬에 저장된다. 그때 사용되는 파일 이름 +- `format`: 문서 형식. 생략하면 HWP2 가 지정된다 + +| format | 설명 | +| --- | --- | +| "HWP" | 한글 native format | +| "HWPX" | 한글 표준 형식 | +| "HWPML2X" | HWPML 2.X 문서 | +| "ODT" | OpenDocument Text 형식 | +| "HTML" | 인터넷 문서 | +| "TEXT" | 아스키 텍스트 문서 | +| "UNICODE" | 유니코드 텍스트 문서 | +| "PUBDOCBODY" | 공문서(xml) | +| "PDF" | PDF(Portable Document Format) | +| "PDFA" | PDF/A | + +- `arg`: 세부 옵션. 의미는 format 에 지정한 파일 형식에 따라 다르다. 생략하면 빈 문자열이 지정된다. download:true 를 넣으면 문서가 로컬에 다운로드 된다 +- `callback`: SaveAs 함수가 동작을 마친 후 수행될 함수 + +- 콜백 인자 `result`: 성공/실패 여부, `path`, `fileName`, `Size`, `CallbackUserData`를 포함한 Object +- `callbackUserData`: 사용자가 콜백에 전달한 데이터 + +**비고** + +format, arg 의 일반적인 개념에 대해서는 Open 참조. HWP2 형식으로 파일 저장 시 arg 에 지정할 수 있는 옵션은 다음과 같다. + +| 인자 | Type | Default | 설명 | +| --- | --- | --- | --- | +| compress | Boolean | true | 압축 여부 | +| prvimage | Number | 2 | 미리보기 이미지(0=off, 1=BMP, 2=GIF) | +| prvtext | Number | 1 | 미리보기 텍스트(0=off, 1=0) | +| download | Boolean | false | true: DownLoad 폴더에 다운로드, false: 다운로드 하지 않음. | + +여러 개를 한꺼번에 할 경우에는 세미콜론으로 구분하여 연속적으로 사용할 수 있다. download:true;prvtext:1 + +**예제** + +1. 문서 작성 +2. 예제 스크립트 실행 결과 : 현재 열려있는 문서를 다운로드 폴더에 다운로드한다. callback 방식 + +```js +HwpCtrl.SaveAs("test.hwp", "HWP", "download:true", function() { + // SaveAs 실행 후 동작 +}, null); +``` + +async await 방식 + +```js +await HwpCtrl.SaveAs("test.hwp", "HWP", "download:true"); +``` + +#### 1.2.45 `SelectText` + +**구문** + +```js +HwpCtrl.SelectText(spara, spos, epara, epos) +``` + +**설명** + +블록을 설정한다. + +**파라미터** + +- `spara`: 블록 시작 위치의 문단 번호 +- `spos`: 블록 시작 위치의 문단 중에서 문자의 위치 +- `epara`: 블록 끝 위치의 문단 번호 +- `epos`: 블록 끝 위치의 문단 중에서 문자의 위치 + +**반환값** + +성공하면 true, 실패하면 false + +**예제** + +1. 문서 작성 +2. 텍스트 블록 +3. GetSelectedPos API 로 현재 블록 Pos 정보 저장 +4. 블록 해제 +5. 3단계에서 저장한 블록 다시 블록 결과 : 현재 설정된 블록의 위치를 저장하고, 이후에 다시 그 위치의 Text 를 블록한다. + +```js +posObj = HwpCtrl.GetSelectedPos(); +//GetSelectedPos API 로 현재 블록 Pos 정보 저장 //... 블록 해제 +HwpCtrl.SelectText(posObj.spara, posObj.spos, posObj.epara, posObj.epos); +//3 +``` + +단계에서 저장한 블록 다시 블록 + +#### 1.2.46 `SetCurFieldName` + +**구문** + +```js +HwpCtrl.SetCurFieldName(fieldname, option, direction, memo) +``` + +**설명** + +현재 캐럿 위치의 데이터 필드 이름을 설정한다. + +**파라미터** + +- `fieldname`: 데이터 필드 이름 +- `option`: 다음과 같은 옵션을 지정할 수 있다. 0을 지정하면 모두 off 이다. 생략하면 0이 지정된다 +- `direction`: 누름틀 필드의 안내문. 누름틀 필드일 때만 유효하다 +- `memo`: 누름틀 필드의 메모. 누름틀 필드일 때만 유효하다 + +**반환값** + +성공하면 true, 실패하면 false + +**예제** + +1. 누름틀 생성 +2. 생성한 누름틀로 캐럿 이동 +3. 예제 스크립트 실행 결과 : 현재 필드의 이름을 이름으로 설정하고 이름 필드에 홍길동을 입력한다. + +```js +HwpCtrl.SetCurFieldName("이름"); +if(HwpCtrl.GetCurFieldName(0) =="이름") { + HwpCtrl.PutFieldText("이름", "홍길동"); +} +``` + +#### 1.2.47 `SetMetaTag` + +**구문** + +```js +HwpCtrl.SetMetaTag(name, format, value) +``` + +**설명** + +문서 내 메타태그 정보를 삽입합니다. + +**파라미터** + +- `name`: 메타태그의 검색 이름. 같은 이름이 이미 존재하면 덮어씁니다 +- `format`: 아래 표 참고 + +| ID | 값 | 설명 | +| --- | --- | --- | +| 텍스트 | “text” | 메타태그 값으로 문자열이 입력됨 | +| 숫자 | “num” | 메타태그 값으로 숫자가 입력됨 | +| 날짜 | “date” | 메타태그 값으로 날짜가 입력됨 | +| 예 또는 아니요 | “yesno ” | 메타태그 값으로 Boolean 값이 입력됨 | + +- `value`: 메타태그의 데이터. format 에 맞게 입력하는 것을 권장한다 + +| ID | 값 예제 | 비고 | +| --- | --- | --- | +| 텍스트 | “한글과컴퓨터” | | +| 숫자 | “2022” | | +| 날짜 | “2022-07-18” | | +| 예 또는 아니요 | “yes”, “no” | “yes”, “no” 문자열만 유효함 | + +**반환값** + +성공 시 true 를 반환하며, 실패하면 false 를 반환한다. + +**비고** + +hwpx 에서만 사용가능합니다. + +**예제** + +1. 예제 스크립트 실행 결과: 현재 문서에 메타태그 정보를 삽입합니다. + +```js +var tag; +HwpCtrl.SetMetaTag("office", "text", "한글과컴퓨터"); +HwpCtrl.GetTextFile("HWPX", "", function (result) { + data = result; + HwpCtrl.Clear(1); + HwpCtrl.SetTextFile(data, "HWPX", "", function (result) { + tag = HwpCtrl.GetMetaTag("office"); + alert('name : '+ tag.NAME + '\n' + 'value : ' + tag.VALUE); + } +, null); +} +); +``` + +#### 1.2.48 `SetPos` + +**구문** + +```js +HwpCtrl.SetPos(list, para, pos) +``` + +**설명** + +캐럿을 문서 내 특정 위치로 위치시킨다. + +**파라미터** + +- `list`: 캐럿이 위치할 문서 내 리스트 아이디 +- `para`: 캐럿이 위치할 문단 아이디 +- `pos`: 캐럿이 위치할 문단 내 글자 단위 위치 + +**반환값** + +성공 시 true, 실패 시 false + +**예제** + +1. 문서 작성 +2. 캐럿 이동 +3. GetPos API 로 현재 Pos 정보 저장 +4. 다른 위치로 캐럿 이동 +5. 3단계에서 저장한 위치로 캐럿 이동 결과 : 캐럿 위치를 저장하고, 다시 이동한다. + +```js +posObj = HwpCtrl.GetPos(); +//GetPos API 로 현재 Pos 정보 저장 //... 다른 위치로 캐럿 이동 +HwpCtrl.SetPos(posObj.list, posObj.para, posObj.pos); +//3단계에서 저장한 위치로 캐럿 이동 +``` + +#### 1.2.49 `SetPosBySet` + +**구문** + +```js +HwpCtrl.SetPosBySet(pos) +``` + +**설명** + +캐럿을 문서 내 특정 위치로 위치시킵니다. SetPos 의 ParameterSet 버전) + +**파라미터** + +- `pos`: 캐럿의 위치에 대한 ParameterSet + +**반환값** + +성공하면 true 를 반환하고, 실패하면 false 를 반환합니다. + +**예제** + +1. 임의의 텍스트 작성 +2. 예제 스크립트 실행 +3. 현재 커서의 위치 메시지창 출력 +4. 1초 후 왼쪽으로 2칸 이동된 커서의 위치 메시지창 출력 결과: 커서 위치를 이동 + +```js +var posSet = null; +function SetPosTest() { + posSet = HwpCtrl.GetPosBySet(); + if ( posSet != null) { + alert(posSet.Item("List") +""+ posSet.Item("Para") +""+ posSet.Item("Pos")); + if (!HwpCtrl.SetPosBySet(posSet)) { + alert ("GetPos err"); + } +} +} +; +function CursorMove() { + HwpCtrl.Run('MoveLeft'); + HwpCtrl.Run('MoveLeft'); +``` + +setTimeout(() =& { SetPosTest(); },1000) + +```js +} +``` + +SetPosTest(); CursorMove(); + +#### 1.2.50 `SetTextFile` + +**구문** + +```js +HwpCtrl.SetTextFile(data, format, option, callback, callbackUserData) +``` + +**설명** + +문서를 문자열로 지정한다. + +**파라미터** + +- `data`: 문자열로 변경된 text 파일 +- `format`: 파일의 형식 + +| format | 설명 | 비고 | +| --- | --- | --- | +| HWP | Hwp native format | BASE64 로 인코딩되어 있다. 저장된 내용 을 다른 곳에서 보여줄 필요가 없다면 이 포맷을 사용하기를 권장합니다 | +| HWPML2X | HWP 형식과 호환 | 문서의 모든 정보를 유지 | +| HTML | 인터넷 문서 HTML 형식 | 한글 고유의 서식은 손실된다. | +| TEXT | 유니코드 텍스트 | 서식정보가 없는 텍스트만 저장 | +| JSON | json format | 문서의 정보를 Json 형태로 만든다. 문서의 모든 정보를 유지 | + +- option + +| option | 값 | 설명 | +| --- | --- | --- | +| insertfile | 현재 커서 이후에 지정된 파일 삽입 | | + +- `callback`: SetTextFile API 이후 수행될 함수 + +- result: 성공 실패 여부, callbackUserData 를 포함한 Object 를 인자로 받는다 +- `callback`: SetTextFile API 이후 수행될 함수 +- `callbackUserData`: callback 함수에 Argument 로 넘겨 받을 result 에 포함될 UserData, 생략 가능 + +**반환값** + +성공하면 true, 실패하면 false + +**비고** + +return 받은 boolean 값은 Argument 가 제대로 들어왔고, 함수가 제대로 동작을 하였는지를 판별할 수 있고, 서버를 통해서 문서가 제대로 SetTextFile 됬는지 여부는 콜백함수의 인자로 들어오는 result 에 서 확인할 수 있다. + +**예제** + +1. 문서 작성 +2. GetTextFile API 현재 문서 데이터 저장 +3. Clear API 로 현재 문서 초기화 +4. SetTextFile API 로 저장한 문서 다시 로드 결과 : 현재 문서의 데이터를 문자열 형태로 저장하고, 다시 불러온다. callback 방식 + +```js +// GetTextFile API 현재 문서 데이터 저장 +HwpCtrl.GetTextFile("HWP", "", function(result) { + data = result; +} +); +HwpCtrl.Clear(1); +// Clear API 로 현재 문서 초기화 // SetTextFile API 로 저장한 문서 다시 로드 +HwpCtrl.SetTextFile(data, "HWP", "", function(result) { + // 이후 수행 할 동작 +} +, null); +``` + +async await 방식 + +```js +// GetTextFile API 현재 문서 데이터 저장 data = await +HwpCtrl.GetTextFile("HWP", ""); +HwpCtrl.Clear(1); +// Clear API 로 현재 문서 초기화 // SetTextFile API 로 저장한 문서 다시 로드 await +HwpCtrl.SetTextFile(data, "HWP", ""); +``` + +#### 1.2.51 `ShowCaret` + +**구문** + +```js +HwpCtrl.ShowCaret(show) +``` + +**설명** + +캐럿을 숨김/보이기 처리한다. + +**파라미터** + +- `show`: 캐럿 표시 여부 + +**예제** + +1. 예제 스크립트 실행 결과 : 캐럿이 숨겨진다. + +```js +HwpCtrl.ShowCaret(false); +``` + +#### 1.2.52 `ShowHorizontalScroll` + +**구문** + +```js +HwpCtrl.ShowHorizontalScroll(show) +``` + +**설명** + +가로 스크롤을 화면에 숨김/보이기 처리한다. + +**파라미터** + +- `show`: 가로 스크롤 표시 여부 + +**반환값** + +성공하면 true, 실패하면 false + +**예제** + +1. 예제 스크립트 실행 결과 : 가로 스크롤이 감춰진다. + +```js +HwpCtrl.ShowHorizontalScroll(false); +``` + +#### 1.2.53 `ShowRibbon` + +**구문** + +```js +HwpCtrl.ShowRibbon(show) +``` + +**설명** + +리본 메뉴를 화면에 숨김/보이기 처리한다. + +**파라미터** + +- `show`: 캐럿 표시 여부 + +**반환값** + +성공하면 true, 실패하면 false + +**예제** + +1. 예제 스크립트 실행 결과 : Ribbon 메뉴가 감춰진다. + +```js +HwpCtrl.ShowRibbon(false); +``` + +#### 1.2.54 `ShowStatusBar` + +**구문** + +```js +HwpCtrl.ShowStatusBar(show) +``` + +**설명** + +상태바를 화면에 숨김/보이기 처리한다. + +**파라미터** + +- `show`: 상태바 표시 여부 + +**반환값** + +성공하면 true, 실패하면 false + +**예제** + +1. 예제 스크립트 실행 결과 : 상태바가 감춰진다. + +```js +HwpCtrl.ShowStatusBar(false); +``` + +#### 1.2.55 `ShowToolBar` + +**구문** + +```js +HwpCtrl.ShowStatusBar(show) +``` + +**설명** + +툴바 메뉴를 화면에 숨김/보이기 처리한다. + +**파라미터** + +- `show`: 상태바 표시 여부 + +**반환값** + +성공하면 true, 실패하면 false + +**예제** + +1. 예제 스크립트 실행 결과 : 툴바 메뉴가가 감춰진다. + +```js +HwpCtrl.ShowToolBar(false); +``` + +#### 1.2.56 `ShowVerticalScroll` + +**구문** + +```js +HwpCtrl.ShowVerticalScroll(show) +``` + +**설명** + +세로 스크롤을 화면에 숨김/보이기 처리한다. + +**파라미터** + +- `show`: 세로 스크롤 표시 여부 + +**반환값** + +성공하면 true, 실패하면 false + +**예제** + +1. 예제 스크립트 실행 결과 : 세로 스크롤이 감춰진다. + +```js +HwpCtrl.ShowVerticalScroll(false); +``` + +## 2. HwpAction + +### 2.1 Properties + +#### 2.1.1 `ActID` + +**구문** + +```js +act.ActID +``` + +**설명** + +액션 ID를 나타낸다. 읽기 전용.액션 ID의 종류 및 설명은 별도 문서 참조. + +**반환값** + +ActionID 를 String 타입으로 return 한다. + +**예제** + +1. 예제 스크립트 실행 결과 : CharShapeBold ActionID 메시지 창 + +```js +var act = HwpCtrl.CreateAction("CharShapeBold"); +alert("Action ID :"+ act.ActID); +``` + +#### 2.1.2 `SetID` + +**구문** + +```js +act.SetID +``` + +**설명** + +액션이 사용하는 parameter set ID를 나타낸다. 읽기 전용.액션 ID의 종류 및 설명은 별도 문서 참조. + +**반환값** + +액션이 사용하는 SetID 를 String 타입으로 return 한다. + +**예제** + +1. 예제 스크립트 실행 결과 : CharShapeBold 액션의 SetID 메시지 창 + +```js +var act = HwpCtrl.CreateAction("CharShapeBold"); +alert("Set ID :"+ act.SetID); +``` + +### 2.2 Methods + +#### 2.2.1 `CreateSet` + +**구문** + +```js +act.CreateSet() +``` + +**설명** + +액션과 대응하는 ParameterSet 객체를 생성한다. + +**반환값** + +성공 시 ParameterSet 객체를 return 한다.ParameterSet 을 사용하지 않는 Action 은 null 객체를 리 턴한다. + +**예제** + +1. 예제 스크립트 실행 결과 : 5x5 크기의 표를 삽입한다. + +```js +var tbact = HwpCtrl.CreateAction("TableCreate"); +var tbset = tbact.CreateSet(); +tbact.GetDefault(tbset); +tbset.SetItem("Rows", 5); +tbset.SetItem("Cols", 5); +tbact.Execute(tbset); +``` + +#### 2.2.2 `GetDefault` + +**구문** + +```js +act.GetDefault(param) +``` + +**설명** + +현재 상태에 따라 액션 실행에 필요한 인수를 구한다.액션 ID의 종류 및 설명은 별도 문서 참조. + +**파라미터** + +- `param`: 인수를 저장할 ParameterSet 객체 + +**반환값** + +성공 시 true, 실패 시 false + +**비고** + +예를 들어 글자모양의 액션의 경우, 현재 셀렉션 상태에 따라 param 의 아이템들이 채워진다.서브셋을 만들 경우에는 서브셋을 만든 후에 GetDefault 를 사용한다. + +**예제** + +1. 예제 스크립트 실행 결과 : 5x5 크기의 표를 삽입한다. + +```js +var tbact = HwpCtrl.CreateAction("TableCreate"); +var tbset = tbact.CreateSet(); +tbact.GetDefault(tbset); +tbset.SetItem("Rows", 5); +tbset.SetItem("Cols", 5); +tbact.Execute(tbset); +``` + +#### 2.2.3 `PopupDialog` + +**구문** + +```js +act.PopupDialog(param, callback, callbackUserData) +``` + +**설명** + +액션의 대화상자를 띄운다. + +**파라미터** + +- `param`: 액션의 실행을 제어할 인수. parameterSet 의 종류와 아이템의 의미는 액션이 정의한 바에 따라 다르다 +- `callback`: PopupDialog 동작을 마친 후 수행될 함수 + +- action: 콜백을 호출한 Action 객체 + +- param: 액션으로 전달된 ParameterSet 객체 + +- result: 대화상자 버튼 클릭 결과 true: 설정버튼, false: 취소버튼) + +- callbackUserData: 사용자가 콜백에 전달한 데이터 +- `callbackUserData`: 사용자가 콜백에 전달한 데이터 + +**반환값** + +성공 시 true, 실패 시 false + +**예제** + +1. 예제 스크립트 실행 결과 : 글자 속성 대화상자를 띄운다. + +```js +var dact = HwpCtrl.CreateAction("CharShape"); +// 액션 생성 +var dset = dact.CreateSet(); +dact.GetDefault(dset); +dact.PopupDialog(dset, function(action, paramSet, result, callbackUserData) { + if(result == true) { +``` + +action.Execute(paramSet); } }); + +#### 2.2.4 `Execute` + +**구문** + +```js +act.Execute(param, callback, callbackUserData) +``` + +**설명** + +지정한 인수로 액션을 실행한다. + +**파라미터** + +- `param`: 액션의 실행을 제어할 인수. parameterSet 의 종류와 아이템의 의미는 액션이 정의한 바에 따라 다르다 +- `callback`: Execute 동작 수행 후 실행될 함수 + +- action: 콜백을 호출한 Action 객체 + +- param: 액션으로 전달된 ParameterSet 객체 + +- result: 액션 실행 결과 true: 액션 성공, false: 액션 실패) + +- callbackUserData: 사용자가 콜백에 전달한 데이터 +- `callbackUserData`: 사용자가 콜백에 전달한 데이터 + +**반환값** + +성공 시 true, 실패 시 false + +**예제** + +1. 예제 스크립트 실행 결과 : 5x5 크기의 표를 삽입한다. + +```js +var tbact = HwpCtrl.CreateAction("TableCreate"); +var tbset = tbact.CreateSet(); +tbact.GetDefault(tbset); +tbset.SetItem("Rows", 5); +tbset.SetItem("Cols", 5); +tbact.Execute(tbset); +``` + +#### 2.2.5 `Run` + +**구문** + +```js +act.Run() +``` + +**설명** + +액션을 실행한다. + +**파라미터** + +- `param`: 액션의 실행을 제어할 인수. parameterSet 의 종류와 아이템의 의미는 액션이 정의한 바에 따라 다르다 +- `callback`: Run 동작을 마친 후 수행될 함수 + +- action: 콜백을 호출한 Action 객체 + +- param: 액션으로 전달된 ParameterSet 객체 + +- result: 대화상자 버튼 클릭 결과 true: 설정버튼, false: 취소버튼) + +- callbackUserData: 사용자가 콜백에 전달한 데이터 +- `callbackUserData`: 사용자가 콜백에 전달한 데이터 + +**반환값** + +성공 시 true, 실패 시 false + +**예제** + +1. 텍스트 입력 +2. 예제 스크립트 실행 결과 : Bold Action 실행대화상자가 없는 액션은 동기 방식으로 실행한다.) + +```js +var act = HwpCtrl.CreateAction("CharShapeBold"); +// 액션 생성 +act.Run(); +//액션 실행; +``` + +## 3. HwpParameterSet + +### 3.1 Properties + +#### 3.1.1 `Count` + +**구문** + +```js +pset.Count +``` + +**설명** + +현재 존재하는 아이템의 개수를 나타낸다. 읽기 전용. + +**반환값** + +아이템 개수를 Number 타입으로 리턴한다. + +**예제** + +1. 예제 스크립트 실행 결과 : 서브 셋 아이템 수 안내창 + +```js +var act = HwpCtrl.CreateAction("PageSetup"); +var pageSet = act.CreateSet(); +act.GetDefault(pageSet); +pageSet.SetItem("ApplyTo", 3); +var pset = pageSet.CreateItemSet("PageDef", "PageDef"); +pset.SetItem("TopMargin", 3401); +pset.SetItem("BottomMargin", 5669); +pset.SetItem("LeftMargin", 4251); +pset.SetItem("RightMargin", 4251); +pset.SetItem("HeaderLen", 0); +pset.SetItem("FooterLen", 0); +pset.SetItem("GutterLen", 0); +alert("Page 속성 서브 Set 의 아이템 수 :"+ pset.Count) +``` + +#### 3.1.2 `IsSet` + +**구문** + +```js +pset.IsSet +``` + +**설명** + +ParameterSet 타입인지 여부를 나타낸다. 읽기 전용. + +**반환값** + +ParameterSet 인 경우 true, ParameterArray 인 경우 false 를 리턴한다. + +**예제** + +1. 예제 스크립트 실행 결과 : 5 * 5 표를 삽입하고, Width, Height 를 조작한다. + +```js +var act = HwpCtrl.CreateAction("TableCreate"); +var pset = act.CreateSet(); +var idx = 0; +if(pset.IsSet) { + act.GetDefault(pset); + pset.SetItem("Rows", 5); + pset.SetItem("Cols", 5); + var colset = pset.CreateItemArray("ColWidth", 5); + var rowset = pset.CreateItemArray("RowHeight", 5); + if(colset.IsSet) { + return; + // colset 은 ParameterArray 타입이어야 한다. + } +for (idx = 0; idx < colset.Count; idx++) { + colset.SetItem(idx, 7200); +} +for (idx = 0; idx < rowset.Count; idx++) { + rowset.SetItem(idx, colset.Item(0)/2); + // Height 는 Width 의 절반으로 지정한다. +} +act.Execute(pset) +} +``` + +#### 3.1.3 `SetID` + +**구문** + +```js +pset.SetID +``` + +**설명** + +ParameterSet 의 ID를 나타낸다. 읽기 전용. + +**파라미터** + +- `itemid`: 아이템 ID + +**반환값** + +Set ID를 String 타입으로 리턴한다. + +**예제** + +결과 : SetID 안내창 TableCreate Action SetID : TableCreation) + +```js +var tbact = HwpCtrl.CreateAction("TableCreate"); +var tbset = tbact.CreateSet(); +alert("TableCreate Action SetID :"+ tbset.SetID); +``` + +### 3.2 Methods + +#### 3.2.1 `Clone` + +**구문** + +```js +pset.Clone() +``` + +**설명** + +ParameterSet 을 복사한다. + +**반환값** + +동일한 데이터를 가진 ParameterSet 을 복사하여 ParameterSet 객체 타입으로 리턴한다. + +**비고** + +동일한 ID를 가진 기존의 아이템은 삭제된다. + +**예제** + +1. 예제 스크립트 실행 결과 : 복사한 아이템이 동일한 지 여부 안내창 + +```js +var charShapeSet = HwpCtrl.CharShape; +var cloneSet = charShapeSet.Clone(); +if(cloneSet.IsEquivalent(charShapeSet)) { + alert("동일"); +} +``` + +#### 3.2.2 `CreateItemArray` + +**구문** + +```js +pset.CreateItemArray(itemid, count) +``` + +**설명** + +아이템으로 ParameterArray 타입의 배열을 생성한다. + +**파라미터** + +- `itemid`: 아이템 +- `IDcount`: 생성할 배열의 초기 크기 + +**반환값** + +인자에 따라 ParameterArray 를 생성하고, ParameterArray 객체 타입으로 리턴한다. + +**비고** + +동일한 ID를 가진 기존의 아이템은 삭제된다. + +**예제** + +1. 예제 스크립트 실행 결과 : 5 * 5 표를 삽입하고, Width, Height 를 조작한다. + +```js +var act = HwpCtrl.CreateAction("TableCreate"); +var pset = act.CreateSet(); +var idx = 0; +if(pset.IsSet) { + act.GetDefault(pset); + pset.SetItem("Rows", 5); + pset.SetItem("Cols", 5); + var colset = pset.CreateItemArray("ColWidth", 5); + var rowset = pset.CreateItemArray("RowHeight", 5); + if(colset.IsSet) { + return; + // colset 은 ParameterArray 타입이어야 한다. + } +for (idx = 0; idx < colset.Count; idx++) { + colset.SetItem(idx, 7200); +} +for (idx = 0; idx < rowset.Count; idx++) { + rowset.SetItem(idx, colset.Item(0)/2); + // Height 는 Width 의 절반으로 지정한다. +} +act.Execute(pset) +} +``` + +#### 3.2.3 `CreateItemSet` + +**구문** + +```js +pset.CreateItemSet(itemid, setid) +``` + +**설명** + +아이템으로 ParameterSet 을 생성한다. + +**파라미터** + +- `itemid`: 아이템 +- `IDsetid`: 생성할 ParameterSet 의 ID + +**반환값** + +생성된 서브 셋을 ParameterSet 객체 타입으로 리턴한다. + +**비고** + +parameter set 내부에 아이템으로 또 다른 parameter set 을 가지는 서브 셋의 개념이다. + +**예제** + +1. 예제 스크립트 실행 결과 : 문서 전체 용지의 속성이 변경된다. + +```js +var act = HwpCtrl.CreateAction("PageSetup"); +var pageSet = act.CreateSet(); +act.GetDefault(pageSet); +pageSet.SetItem("ApplyTo", 3); +var pset = pageSet.CreateItemSet("PageDef", "PageDef"); +pset.SetItem("TopMargin", 3401); +pset.SetItem("BottomMargin", 5669); +pset.SetItem("LeftMargin", 4251); +pset.SetItem("RightMargin", 4251); +pset.SetItem("HeaderLen", 0); +pset.SetItem("FooterLen", 0); +pset.SetItem("GutterLen", 0); +act.Execute(pageSet); +``` + +#### 3.2.4 `GetIntersection` + +**구문** + +```js +pset.GetIntersection(srcset) +``` + +**설명** + +두 ParameterSet 에 공통적으로 존재하고, 값도 동일한 아이템만으로 구성된 교집합으로 this 를 구성한다. + +**파라미터** + +- `srcset`: this 와 srcset 의 교집합이 this 에 저장된다 + +**예제** + +1. 텍스트 입력 후 글자 블록 +2. 예제 스크립트 실행 결과 : 블록된 텍스트의 글자 크기 속성을 제거하고 글자 크기를 10으로 다시 지정한다. + +```js +var charShapeSet = HwpCtrl.CharShape; +var cloneSet = charShapeSet.Clone(); +cloneSet.RemoveItem("Height"); +// cloneSet 에서 글자 크기 속성 제거 charShapeSet.GetIntersection(cloneSet); +// 두 셋의 교집합만 남긴다. +if(charShapeSet.Item("Height") === null) charShapeSet.SetItem("Height", 1000) HwpCtrl.CharShape = charShapeSet; +``` + +#### 3.2.5 `IsEquivalent` + +**구문** + +```js +pset.IsEquivalent(srcset) +``` + +**설명** + +두 ParameterSet 의 내용이 동일한 값을 가지고 있는지 검사한다. + +**파라미터** + +- `srcset`: this 와 srcset 을 비교한 결과를 리턴한다 + +**반환값** + +동일하면 true, 다르면 false + +**예제** + +1. 예제 스크립트 실행 결과 : 복사한 아이템이 동일한 지 여부 안내창 + +```js +var charShapeSet = HwpCtrl.CharShape; +var cloneSet = charShapeSet.Clone(); +if(cloneSet.IsEquivalent(charShapeSet)) { + alert("동일"); +} +``` + +#### 3.2.6 `Item` + +**구문** + +```js +pset.Item(itemid) +``` + +**설명** + +지정한 아이템의 값을 리턴한다. + +**파라미터** + +- `itemid`: 아이템 ID + +**반환값** + +아이템의 값, 만약 지정한 아이템이 존재하지 않으면 아이템의 포맷에 따라 0 또는 빈 문자열을 리턴한다. + +**예제** + +1. 글자 입력 +2. 입력한 글자 Selection +3. 예제 스크립트 실행 결과 : 만약 Selection 된 글자들의 글자 크기가 동일하고 20pt 미만이면 글자 크기를 20pt 로 설정한다. + +```js +var cs = HwpCtrl.CharShape; +if (cs.ItemExist("Height") && cs.Item("Height") < 2000) { + cs.SetItem("Height", 2000); + HwpCtrl.CharShape = cs; +} +``` + +#### 3.2.7 `ItemExist` + +**구문** + +```js +pset.ItemExist(itemid) +``` + +**설명** + +지정한 아이템이 존재하는지 검사한다. + +**파라미터** + +- `itemid`: 아이템 ID + +**반환값** + +존재하면 true, 존재하지 않으면 false + +**예제** + +1. 글자 입력 +2. 입력한 글자 Selection +3. 예제 스크립트 실행 결과 : 만약 Selection 된 글자들의 글자 크기가 동일하고 20pt 미만이면 글자 크기를 20pt 로 설정한다. + +```js +var cs = HwpCtrl.CharShape; +if (cs.ItemExist("Height") && cs.Item("Height") < 2000) { + cs.SetItem("Height", 2000); + HwpCtrl.CharShape = cs; +} +``` + +#### 3.2.8 `Merge` + +**구문** + +```js +pset.Merge(srcset) +``` + +**설명** + +두 ParameterSet 에 내용을 병합한다, 결과는 this 와 srcset 의 합집합이 this 에 저장된다. + +**파라미터** + +- `srcset`: this 와 srcset 이 병합되어 this 에 저장된다 + +**예제** + +1. 텍스트 입력 후 글자 블록 +2. 예제 스크립트 실행 결과 : ParameterSet 의 아이템을 제거하고, 병합한다. + +```js +var act = HwpCtrl.CreateAction("PageSetup"); +var pageSet = act.CreateSet(); +act.GetDefault(pageSet); +pageSet.SetItem("ApplyTo", 3); +var pset = pageSet.CreateItemSet("PageDef", "PageDef"); +pset.SetItem("TopMargin", 3401); +pset.SetItem("BottomMargin", 5669); +pset.SetItem("LeftMargin", 4251); +pset.SetItem("RightMargin", 4251); +pset.SetItem("HeaderLen", 0); +pset.SetItem("FooterLen", 0); +pset.SetItem("GutterLen", 0); +var origCount = pset.Count; +// 7 +var cloneSet = pset.Clone(); +var cloneCount = cloneSet.Count; +// 7 +pset.RemoveAll(); +//pset 의 모든 아이템을 삭제한다. +var removedCount = pset.Count; +// 0 +pset.Merge(cloneSet); +//pset 과 cloneSet 을 합친다. +var mergedCount = pset.Count; +//7 +act.Execute(pageSet); +alert("origCount : " + origCount + "\ncloneCount : " + cloneCount + "\nremovedCount : " + removedCount + "\nmergedCount : " + mergedCount); +``` + +#### 3.2.9 `RemoveAll` + +**구문** + +```js +pset.RemoveAll(setID) +``` + +**설명** + +ParameterSet 을 초기화한다. + +**파라미터** + +- `setid`: 새로 적용할 Set ID + +**비고** + +이미 존재하는 ParameterSet 객체를 이용해 새로운 타입의 ParameterSet 으로 초기화하여 재사용하는 목적에 사용된다. + +**예제** + +1. 텍스트 입력 후 글자 블록 +2. 예제 스크립트 실행 결과 : ParameterSet 의 아이템을 제거하고, 병합한다. + +```js +var act = HwpCtrl.CreateAction("PageSetup"); +var pageSet = act.CreateSet(); +act.GetDefault(pageSet); +pageSet.SetItem("ApplyTo", 3); +var pset = pageSet.CreateItemSet("PageDef", "PageDef"); +pset.SetItem("TopMargin", 3401); +pset.SetItem("BottomMargin", 5669); +pset.SetItem("LeftMargin", 4251); +pset.SetItem("RightMargin", 4251); +pset.SetItem("HeaderLen", 0); +pset.SetItem("FooterLen", 0); +pset.SetItem("GutterLen", 0); +var origCount = pset.Count; +// 7 +var cloneSet = pset.Clone(); +var cloneCount = cloneSet.Count; +// 7 +pset.RemoveAll(); +//pset 의 모든 아이템을 삭제한다. +var removedCount = pset.Count; +// 0 +pset.Merge(cloneSet); +//pset 과 cloneSet 을 합친다. +var mergedCount = pset.Count; +//7 +act.Execute(pageSet); +alert("origCount : " + origCount + "\ncloneCount : " + cloneCount + "\nremovedCount : " + removedCount + "\nmergedCount : " + mergedCount); +``` + +#### 3.2.10 `RemoveItem` + +**구문** + +```js +pset.RemoveItem(itemid) +``` + +**설명** + +지정한 아이템을 삭제한다. + +**파라미터** + +- `itemid`: 아이템 ID + +**예제** + +1. 텍스트 입력 후 글자 블록 +2. 예제 스크립트 실행 결과 : 블록된 텍스트의 글자 크기 속성을 제거하고 글자 크기를 10으로 다시 지정한다. + +```js +var charShapeSet = HwpCtrl.CharShape; +var cloneSet = charShapeSet.Clone(); +cloneSet.RemoveItem("Height"); +// cloneSet 에서 글자 크기 속성 제거 charShapeSet.GetIntersection(cloneSet); +// 두 셋의 교집합만 남긴다. +if(charShapeSet.Item("Height") === null) charShapeSet.SetItem("Height", 1000) HwpCtrl.CharShape = charShapeSet; +``` + +#### 3.2.11 `SetItem` + +**구문** + +```js +pset.SetItem(itemid, value) +``` + +**설명** + +지정한 아이템의 값을 설정한다. + +**파라미터** + +- `itemid`: 아이템 ID +- `value`: 설정할 값 + +**비고** + +이미 동일한 ID의 아이템이 존재하면 지정한 값으로 바뀌고, 존재하지 않으면 아이템이 생성된다. + +**예제** + +1. 예제 스크립트 실행 결과 : 문서 전체 용지의 속성이 변경된다. + +```js +var act = HwpCtrl.CreateAction("PageSetup"); +var pageSet = act.CreateSet(); +act.GetDefault(pageSet); +pageSet.SetItem("ApplyTo", 3); +var pset = pageSet.CreateItemSet("PageDef", "PageDef"); +pset.SetItem("TopMargin", 3401); +pset.SetItem("BottomMargin", 5669); +pset.SetItem("LeftMargin", 4251); +pset.SetItem("RightMargin", 4251); +pset.SetItem("HeaderLen", 0); +pset.SetItem("FooterLen", 0); +pset.SetItem("GutterLen", 0); +act.Execute(pageSet); +``` + +## 4. HwpParameterArray + +### 4.1 Properties + +#### 4.1.1 `Count` + +**구문** + +```js +itemArray.Count +``` + +**설명** + +배열의 크기를 나타낸다. + +**반환값** + +배열의 크기를 Number 타입으로 리턴한다. + +**비고** + +배열의 크기는 Runtime 에 동적으로 조절할 수 있다. + +**예제** + +1. 예제 스크립트 실행 결과 : 5 * 5 표를 삽입하고, Width, Height 를 조작한다. + +```js +var act = HwpCtrl.CreateAction("TableCreate"); +var pset = act.CreateSet(); +var idx = 0; +if(pset.IsSet) { + act.GetDefault(pset); + pset.SetItem("Rows", 5); + pset.SetItem("Cols", 5); + var colset = pset.CreateItemArray("ColWidth", 5); + var rowset = pset.CreateItemArray("RowHeight", 5); + if(colset.IsSet) { + return; + // colset 은 ParameterArray 타입이어야 한다. + } +for (idx = 0; idx < colset.Count; idx++) { + colset.SetItem(idx, 7200); +} +for (idx = 0; idx < rowset.Count; idx++) { + rowset.SetItem(idx, colset.Item(0)/2); + // Height 는 Width 의 절반으로 지정한다. +} +act.Execute(pset) +} +``` + +#### 4.1.2 `IsSet` + +**구문** + +```js +itemArray.IsSet +``` + +**설명** + +ParameterSet 타입인지 여부를 나타낸다. 읽기 전용. + +**반환값** + +ParameterSet 인 경우 true, ParameterArray 인 경우 false 를 리턴한다. + +**예제** + +1. 예제 스크립트 실행 결과 : 5 * 5 표를 삽입하고, Width, Height 를 조작한다. + +```js +var act = HwpCtrl.CreateAction("TableCreate"); +var pset = act.CreateSet(); +var idx = 0; +if(pset.IsSet) { + act.GetDefault(pset); + pset.SetItem("Rows", 5); + pset.SetItem("Cols", 5); + var colset = pset.CreateItemArray("ColWidth", 5); + var rowset = pset.CreateItemArray("RowHeight", 5); + if(colset.IsSet) { + return; + // colset 은 ParameterArray 타입이어야 한다. + } +for (idx = 0; idx < colset.Count; idx++) { + colset.SetItem(idx, 7200); +} +for (idx = 0; idx < rowset.Count; idx++) { + rowset.SetItem(idx, colset.Item(0)/2); + // Height 는 Width 의 절반으로 지정한다. +} +act.Execute(pset) +} +``` + +### 4.2 Methods + +#### 4.2.1 `Clone` + +**구문** + +```js +itemArray.Clone() +``` + +**설명** + +동일한 크기와 데이터를 갖는 ParameterArray 개체를 복사하여 리턴한다. + +**반환값** + +성공 시 복사된 ParameterArray 객체를 리턴한다.실패 시 null 을 리턴한다. + +**예제** + +결과 : Clone, Copy 결과 안내창 + +```js +var tset = HwpCtrl.CreateSet("TableCreation"); +var colset = tset.CreateItemArray("ColWidth", 1); +colset.SetItem(0, 7200); +var cloneColset = colset.Clone(); +var cloneColWidth = cloneColset.Item(0); +cloneColset.SetItem(0, 0); +var cloneColWidth2 = cloneColset.Item(0); +cloneColset.Copy(colset) var cloneColWidth3 = cloneColset.Item(0); +alert("Clone :"+ cloneColWidth +"\n 초기화 :"+ cloneColWidth2 +"\nCopy :"+ cloneColWidth3); +``` + +#### 4.2.2 `Copy` + +**구문** + +```js +itemArray.Copy(srcarray) +``` + +**설명** + +인자로 들어온 ParameterArray 의 내용을 복사한다. + +**파라미터** + +- `srcarray`: srcarray 의 내용이 그대로 this 로 복사된다 + +**예제** + +결과 : Clone, Copy 결과 안내창 + +```js +var tset = HwpCtrl.CreateSet("TableCreation"); +var colset = tset.CreateItemArray("ColWidth", 1); +colset.SetItem(0, 7200); +var cloneColset = colset.Clone(); +var cloneColWidth = cloneColset.Item(0); +cloneColset.SetItem(0, 0); +var cloneColWidth2 = cloneColset.Item(0); +cloneColset.Copy(colset) var cloneColWidth3 = cloneColset.Item(0); +alert("Clone :"+ cloneColWidth +"\n 초기화 :"+ cloneColWidth2 +"\nCopy :"+ cloneColWidth3); +``` + +#### 4.2.3 `Item` + +**구문** + +```js +itemArray.Item(index) +``` + +**설명** + +지정한 아이템의 값을 리턴한다. + +**파라미터** + +- `index`: 아이템 인덱스. 0부터 시작한다 + +**반환값** + +index 에 해당하는 아이템의 값을 리턴한다. + +**예제** + +1. 예제 스크립트 실행 결과 : 5 * 5 표를 삽입하고, Width, Height 를 조작한다. + +```js +var act = HwpCtrl.CreateAction("TableCreate"); +var pset = act.CreateSet(); +var idx = 0; +if(pset.IsSet) { + act.GetDefault(pset); + pset.SetItem("Rows", 5); + pset.SetItem("Cols", 5); + var colset = pset.CreateItemArray("ColWidth", 5); + var rowset = pset.CreateItemArray("RowHeight", 5); + if(colset.IsSet) { + return; + // colset 은 ParameterArray 타입이어야 한다. + } +for (idx = 0; idx < colset.Count; idx++) { + colset.SetItem(idx, 7200); +} +for (idx = 0; idx < rowset.Count; idx++) { + rowset.SetItem(idx, colset.Item(0)/2); + // Height 는 Width 의 절반으로 지정한다. +} +act.Execute(pset) +} +``` + +#### 4.2.4 `SetItem` + +**구문** + +```js +itemArray.SetItem(index, value) +``` + +**설명** + +지정한 원소의 값을 설정한다. + +**파라미터** + +- `index`: 아이템의 인덱스. 0부터 시작.value : 아이템의 값 + +**예제** + +1. 예제 스크립트 실행 결과 : 5 * 5 표를 삽입하고, Width, Height 를 조작한다. + +```js +var act = HwpCtrl.CreateAction("TableCreate"); +var pset = act.CreateSet(); +var idx = 0; +if(pset.IsSet) { + act.GetDefault(pset); + pset.SetItem("Rows", 5); + pset.SetItem("Cols", 5); + var colset = pset.CreateItemArray("ColWidth", 5); + var rowset = pset.CreateItemArray("RowHeight", 5); + if(colset.IsSet) { + return; + // colset 은 ParameterArray 타입이어야 한다. + } +for (idx = 0; idx < colset.Count; idx++) { + colset.SetItem(idx, 7200); +} +for (idx = 0; idx < rowset.Count; idx++) { + rowset.SetItem(idx, colset.Item(0)/2); + // Height 는 Width 의 절반으로 지정한다. +} +act.Execute(pset) +} +``` + +## 5. HwpCtrlCode + +### 5.1 Properties + +#### 5.1.1 `CtrlCh` + +**구문** + +```js +ctrlCode.CtrlCh +``` + +**설명** + +컨트롤 문자. 읽기 전용. + +**반환값** + +현재 컨트롤의 컨트롤 문자를 Number 타입으로 리턴한다. + +**비고** + +일반적으로 컨트롤 ID를 사용해 컨트롤의 종류를 판별하지만, 이보다 더 포괄적인 범주를 나타내는 컨 트롤 문자로 판별할 수도 있다. 예를 들어 각주와 미주는 ID는 다르지만, 컨트롤 문자는 17로 동일하다. 컨트롤 문자는 131 사이의 값을 사용한다. 그 중 CtrlCode 객체로 리턴 받을 수 있는 문자는 다음과 같다. + +| Ch | 설명 | +| --- | --- | +| 2 | 구역/단 정의 | +| 3 | 필드 시작 | +| 11 | 그리기 개체 / 표 | +| 15 | 주석 | +| 16 | 머리말 / 꼬리말 | +| 17 | 각주 / 미주 | +| 18 | 자동 번호 | +| 21 | 새번호/ 번호 감추기/ 번호 위치 | +| 22 | 책갈피 / 찾아보기 표시 | +| 23 | 덧말 / 글자 겹침,,, | + +**예제** + +1. 표, 그리기 개체, 누름틀 삽입 +2. 예제 스크립트 실행 결과 : 표, 그리기 개체만 삭제된다. + +```js +var ctrl = HwpCtrl.HeadCtrl; +while(ctrl) { + if (ctrl.CtrlCh == 11) { + // 그리기 개체 / 표 + HwpCtrl.DeleteCtrl(ctrl); + } +ctrl = ctrl.Next; +} +``` + +#### 5.1.2 `CtrlID` + +**구문** + +```js +ctrlCode.CtrlID +``` + +**설명** + +컨트롤 ID. 읽기 전용. + +**반환값** + +현재 컨트롤의 ID를 String 타입으로 리턴한다. + +**비고** + +컨트롤 ID는 컨트롤의 종류를 나타내기 위해 할당된 ID로서, 최대 4개의 문자로 구성된 문자열이다. 예 를 들어 표는 tbl, 각주는 fn% 이다.한글에서 현재까지 지원되는 모든 컨트롤의 ID는 다음 표 참조. + +| ID | ParameterSet ID | Initialization Set | 설명 | +| --- | --- | --- | --- | +| cold | ColDef | ColDef | 단 | +| secd | SecDef | SecDef | 구역 | +| fn | FootnoteShape | FootnoteShape | 각주 | +| en | FootnoteShape | FootnoteShape | 미주 | +| tbl | Table | TableCreation | 표 | +| eqed | EqEdit | EqEdit | 수식 | +| gso | ShapeObject | ShapeObject | 그리기 개체 | +| atno | AutoNum | AutoNum | 번호넣기 | +| nwno | AutoNum | AutoNum | 새번호로 | +| pgct | PageNumCtrl | PageNumCtrl | 페이지 번호 제어 | +| pghd | PageHiding | PageHiding | 감추기 | +| pgnp | PageNumPos | PageNumPos | 쪽번호 위치 | +| head | HeaderFooter | HeaderFooter | 머리말 | +| foot | HeaderFooter | HeaderFooter | 꼬리말 | +| %dte | FieldCtrl | FieldCtrl | 현재의 날짜/시간 필드 | +| %ddt | FieldCtrl | FieldCtrl | 파일 작성 날짜/시간 필드 | +| %pat | FieldCtrl | FieldCtrl | 문서 경로 필드 | +| %bmk | FieldCtrl | FieldCtrl | 블럭 책갈피 | +| %mmg | FieldCtrl | FieldCtrl | 메일 머지 | +| %xrf | FieldCtrl | FieldCtrl | 상호 참조 | +| %fmu | FieldCtrl | FieldCtrl | 계산식 | +| %clk | FieldCtrl | FieldCtrl | 누름틀 | +| %smr | FieldCtrl | FieldCtrl | 문서 요약 정보 필드 | +| %usr | FieldCtrl | FieldCtrl | 사용자 정보 필드 | +| %hlk | FieldCtrl | FieldCtrl | 하이퍼링크 | +| bokm | TextCtrl | TextCtrl | 책갈피 | +| idxm | IndexMark | IndexMark | 찾아보기 | +| tdut | Dutmal | Dutmal | 덧말 | +| tcmt | | | 주석 | + +```js +Initialization Set : HwpCtrl.InsertCtrl 에 지정할 수 있는 initparam 의 ParameterSet ID +``` + +**예제** + +1. 표 삽입 +2. 예제 스크립트 실행 결과 : 현재 문서에 있는 모든 표의 셀 간격이 1pt 로 설정된다. + +```js +var ctrl = HwpCtrl.HeadCtrl; +var tbset = HwpCtrl.CreateSet("Table"); +tbset.SetItem("CellSpacing", 100); +while(ctrl) { + if (ctrl.CtrlID =="tbl") { + // 그리기 개체 / 표 ctrl.Properties = tbset; + } +ctrl = ctrl.Next; +} +``` + +#### 5.1.3 `Next` + +**구문** + +```js +ctrlCode.Next +``` + +**설명** + +다음 컨트롤. 읽기 전용. + +**반환값** + +현재 컨트롤의 뒤 컨트롤을 CtrlCode 객체 타입으로 리턴한다. + +**비고** + +문서 중의 모든 컨트롤(표, 그림 등 특수 문자)들은 linked list 로 서로 연결되어 있는데, list 중 다음 + +컨트롤을 나타낸다. + +**예제** + +1. 표 삽입 +2. 예제 스크립트 실행 결과 : 현재 문서에 있는 모든 표의 셀 간격이 1pt 로 설정된다. + +```js +var ctrl = HwpCtrl.HeadCtrl; +var tbset = HwpCtrl.CreateSet("Table"); +tbset.SetItem("CellSpacing", 100); +while(ctrl) { + if (ctrl.CtrlID =="tbl") { + // 그리기 개체 / 표 ctrl.Properties = tbset; + } +ctrl = ctrl.Next; +} +``` + +#### 5.1.4 `Prev` + +**구문** + +```js +ctrlCode.Prev +``` + +**설명** + +이전 컨트롤. 읽기 전용. + +**반환값** + +현재 컨트롤의 앞 컨트롤을 CtrlCode 객체 타입으로 리턴한다. + +**비고** + +문서 중의 모든 컨트롤(표, 그림 등 특수 문자)들은 linked list 로 서로 연결되어 있는데, list 중 앞 컨트롤을 나타낸다. + +**예제** + +1. 표 입력 +2. 예제 스크립트 실행 결과 : 문서 중 모든 표의 셀 간격을 1pt 로 설정한다. + +```js +var ctrl = HwpCtrl.LastCtrl; +var tbset = HwpCtrl.CreateSet("Table"); +tbset.SetItem("CellSpacing", 100); +while(ctrl != null) { + if(ctrl.CtrlID =="tbl") { + ctrl.Properties = tbset; + } +ctrl = ctrl.Prev; +} +``` + +#### 5.1.5 `Properties` + +**구문** + +```js +ctrlCode.Properties +``` + +**설명** + +컨트롤의 속성을 나타낸다. + +**반환값** + +컨트롤의 속성을 ParameterSet 객체 타입으로 리턴한다. + +**비고** + +모든 컨트롤은 대응하는 ParameterSet 으로 속성을 읽고 쓸 수 있다. + +**예제** + +1. 표 삽입 +2. 예제 스크립트 실행 결과 : 현재 문서에 있는 모든 표의 셀 간격이 1pt 로 설정된다. + +```js +var ctrl = HwpCtrl.HeadCtrl; +var tbset = HwpCtrl.CreateSet("Table"); +tbset.SetItem("CellSpacing", 100); +while(ctrl) { + if (ctrl.CtrlID =="tbl") { + // 그리기 개체 / 표 ctrl.Properties = tbset; + } +ctrl = ctrl.Next; +} +``` diff --git a/apps/as/docs/approval-line-management/SPEC.md b/apps/as/docs/spec/approval-line-management/SPEC.md similarity index 100% rename from apps/as/docs/approval-line-management/SPEC.md rename to apps/as/docs/spec/approval-line-management/SPEC.md diff --git a/apps/as/docs/checklist-management/SPEC.md b/apps/as/docs/spec/checklist-management/SPEC.md similarity index 100% rename from apps/as/docs/checklist-management/SPEC.md rename to apps/as/docs/spec/checklist-management/SPEC.md diff --git a/apps/as/docs/code-management/SPEC.md b/apps/as/docs/spec/code-management/SPEC.md similarity index 100% rename from apps/as/docs/code-management/SPEC.md rename to apps/as/docs/spec/code-management/SPEC.md diff --git a/apps/as/docs/spec/dashboard/SPEC.md b/apps/as/docs/spec/dashboard/SPEC.md new file mode 100644 index 000000000..3ce6f9800 --- /dev/null +++ b/apps/as/docs/spec/dashboard/SPEC.md @@ -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` | `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 +``` diff --git a/apps/as/docs/essential-attachment-file-management/SPEC.md b/apps/as/docs/spec/essential-attachment-file-management/SPEC.md similarity index 100% rename from apps/as/docs/essential-attachment-file-management/SPEC.md rename to apps/as/docs/spec/essential-attachment-file-management/SPEC.md diff --git a/apps/as/docs/spec/my-adss-doc/SPEC.md b/apps/as/docs/spec/my-adss-doc/SPEC.md new file mode 100644 index 000000000..37ae2937d --- /dev/null +++ b/apps/as/docs/spec/my-adss-doc/SPEC.md @@ -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` | `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 +``` + diff --git a/apps/as/docs/spec/nti-hstry/SPEC.md b/apps/as/docs/spec/nti-hstry/SPEC.md new file mode 100644 index 000000000..eb6fb5463 --- /dev/null +++ b/apps/as/docs/spec/nti-hstry/SPEC.md @@ -0,0 +1,352 @@ +# 화면설계서: 알림이력 + +## 1. 기본 정보 + +| 항목 | 내용 | +| ----------- | -------------------------------------------- | +| 버전 | 1.4 | +| 프로세스명 | UI설계 | +| 화면명 | 알림이력 | +| 화면 ID | AS_SC0205011 | +| 요구사항 ID | NSUR-001-001 | +| 화면 유형 | [v] 조회 [ ] 입력 [ ] 수정 [ ] 삭제 [ ] 출력 | +| 메뉴 경로 | 메인 > 행정지원서비스 > 알림이력 | + +--- + +## 2. 화면 목적 + +로그인한 사용자가 **본인에게 발송된 행정지원(ADSS) 알림**을 기간·구분으로 조회한다. 목록의 처리 버튼으로 해당 알림을 읽음 처리한 뒤, 연계된 업무 화면으로 이동한다. + +--- + +## 3. 화면 구성 + +### 3-1. 전체 레이아웃 + +``` +┌─────────────────────────────────────────────────────────────────────────────────┐ +│ 브레드크럼: 메인 > 행정지원서비스 > 알림이력 │ +│ 페이지 타이틀: 알림이력 │ +├─────────────────────────────────────────────────────────────────────────────────┤ +│ [영역 A] 검색 필터 │ +│ 기간 [시작일] ~ [종료일] 구분 ▼ [검색] │ +├─────────────────────────────────────────────────────────────────────────────────┤ +│ [영역 B] 알림 이력 목록 │ +│ 타이틀: 알림 이력 목록 N개 ▼ │ +│ ┌────┬──────────────┬────────────────┬──────────┬────────┬────────┐ │ +│ │ No │ 알림일시 │ 알림내용 │ 연계업무 │ 상태 │ 처리 │ │ +│ ├────┼──────────────┼────────────────┼──────────┼────────┼────────┤ │ +│ │ … │ regDt │ notiTtl │ docSeComNm│ Badge │ 버튼 │ │ +│ └────┴──────────────┴────────────────┴──────────┴────────┴────────┘ │ +│ [페이지네이션] │ +└─────────────────────────────────────────────────────────────────────────────────┘ +``` + +### 3-2. 화면 데이터 흐름 + +``` +BEGIN + + // 최초 진입 + // 기간 기본값: 오늘 기준 최근 1개월 (시작일 = today-1M, 종료일 = today) + CALL findMyAsNotiList(NotiSearchDto + pageable) + DISPLAY 알림 이력 목록 그리드 (영역 B) + + ON [검색] 버튼 CLICK DO + CALL findMyAsNotiList(영역 A 조건 + page=0) + DISPLAY 알림 이력 목록 그리드 (영역 B) + END ON + + ON 페이지네이션 CHANGE DO + CALL findMyAsNotiList(영역 A 조건 + page/size) + DISPLAY 알림 이력 목록 그리드 (영역 B) + END ON + + ON 영역 B [처리] 버튼 CLICK DO + CALL readNoti(notiId) + // UI설계: 해당 알림에 연계된 URL로 이동 + NAVIGATE notiUrl + END ON + +END +``` + +--- + +## 4. 영역 A — 검색 필터 + +구성: **기간 · 구분** (+ 검색). + +서버 `findMyAsNotiList` 고정 정책 (클라이언트 미전달): + +- `recvUnqId` = 로그인 사용자 UUID +- `delYn` = `N` +- `notiType` = `ADSS` (행정지원 전용 API) + +### 4-1. 필드 정의 + +| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 | +| ---------- | ---------- | ---- | --------------------------- | ----------------- | ------------------------------------------------------------------------------------------------ | +| 기간(시작) | DatePicker | N | 오늘 − 1개월 (`YYYY-MM-DD`) | `startDt` | `NotiSearchDto.startDt` | +| 기간(종료) | DatePicker | N | 오늘 (`YYYY-MM-DD`) | `endDt` | `NotiSearchDto.endDt` | +| 구분 | Select | N | 전체(미선택) | `docSeComCd` | `INSTL` / `FNRS` / `SEWER` / `CNFM` / `PWRK` (또는 단축 `I`/`F`/`S`/`C`/`P`). 공통코드 `ASDOCSE` | + +### 4-2. 버튼 + +| 버튼명 | 동작 | +| ------ | -------------------------------------------------------------- | +| 검색 | 영역 A 조건으로 `findMyAsNotiList` 호출 → 영역 B 갱신 (page=0) | + +### 4-3. 콤보·기본값 주의 + +| 항목 | 내용 | +| -------------- | ---------------------------------------------------------- | +| 구분 = 전체 | `docSeComCd` 미전달. `notiType`은 서버가 `ADSS`로 고정 | +| 구분 옵션 소스 | 공통코드 `ASDOCSE` (`docSeComCd` ↔ `docSeComNm`) | +| 기간 | UI설계 상세설명: 기본 최근 1개월. `startDt`/`endDt`로 전달 | + +--- + +## 5. 영역 B — 알림 이력 목록 + +### 5-1. 그리드 컬럼 + +| 표시명 | Field | 비고 | +| -------- | ------------ | --------------------------------------------------------------- | +| No | (row index) | 페이지 오프셋 반영 | +| 알림일시 | `regDt` | 표시 `YYYY-MM-DD HH:mm` (응답 `yyyy-MM-dd HH:mm:ss`) | +| 알림내용 | `notiTtl` | 제목 문자열. UI 샘플은 `[YYYY-MM-DD] …` 접두 | +| 연계업무 | `docSeComNm` | 문서구분 공통코드명 (`docSeComCd` + `@CodeName` `ASDOCSE`) | +| 상태 | (derived) | `Badge` 렌더링. 판정 규칙은 §5-1-1 | +| 처리 | (action) | 상태별 버튼 라벨. 클릭 시 `readNoti` 후 `notiUrl`로 이동 (§5-3) | + +#### 5-1-1. 상태 UI 케이스 (`@common/components` `Badge`) + +판정에 사용: `notiPrcsCmptnYn`, `notReadYn`, `regDt` (오늘 기준 경과일). + +| 조건 | 라벨 | Badge | +| ------------------------------------------------------------------------------ | -------- | ----------------------------- | +| `notiPrcsCmptnYn` = `Y` | 처리완료 | `variant="default"` (primary) | +| `notiPrcsCmptnYn` = `N` 이고 `notReadYn` = `Y` 이고 `regDt`로부터 **7일 초과** | 처리지연 | `color="red"` | +| `notiPrcsCmptnYn` = `N` 이고 `notReadYn` = `Y` 이고 `regDt`로부터 **7일 이내** | 처리중 | `color="green"` | +| `notiPrcsCmptnYn` = `N` 이고 `notReadYn` = `N` 이고 `regDt`로부터 **3일 초과** | 지연 | `color="orange"` | +| `notiPrcsCmptnYn` = `N` 이고 `notReadYn` = `N` 이고 `regDt`로부터 **3일 이내** | 미조회 | `color="yellow"` | + +``` +IF notiPrcsCmptnYn = Y THEN + DISPLAY Badge "처리완료" (primary / variant=default) +ELSE IF notReadYn = Y THEN + IF daysSince(regDt) > 7 THEN + DISPLAY Badge "처리지연" (red) + ELSE + DISPLAY Badge "처리중" (green) + END IF +ELSE // notReadYn = N + IF daysSince(regDt) > 3 THEN + DISPLAY Badge "지연" (orange) + ELSE + DISPLAY Badge "미조회" (yellow) + END IF +END IF +``` + +- `daysSince(regDt)`: 오늘 날짜 − `regDt` 일자 (일 단위) +- `regDt`가 없으면 상태 Badge 표시 전략 TBD (미표시 또는 기본값) + +응답에 있으나 그리드에 **표시하지 않음**: + +| Field | 비고 | +| ----------------- | ---------------------------------------------------------------------- | +| `notiId` | 알림 ID — 행 키 | +| `recvUnqId` | 수신 회원 UUID (서버가 로그인 사용자로 고정) | +| `senderUnqId` | 발신 회원 UUID | +| `notiType` | 알림 종류 (`ADSS`) | +| `notiCn` | 알림 내용 (목록에서는 서버가 제외할 수 있음) | +| `notReadYn` | 확인 여부 `Y`/`N`. 상태 Badge 판정에 사용 (§5-1-1) | +| `notiUrl` | 처리 버튼 이동 URL. 컬럼 미표시 | +| `notiLnkgUnqNo` | 알림연결고유번호 | +| `docSeComCd` | 문서구분 공통코드. 연계업무 표시는 `docSeComNm` 사용 | +| `notiIdntyDt` | 알림 확인 일시 | +| `notiIdntyYn` | 알림 확인 여부 (`notReadYn`과 동일 의미). 상태 판정은 `notReadYn` 사용 | +| `notiPrcsCmptnYn` | 알림 처리 완료 여부. 상태 Badge 판정에 사용 (§5-1-1) | +| `notiPrcsCmptnDt` | 알림 처리 완료 일시 | + +### 5-2. 목록 메타·페이지네이션 + +| 항목 | 매핑 | +| ------------- | ------------------------------------------- | +| 건수 / size ▼ | 상단 우측 `N개` 셀렉트 → `Pageable.size` | +| page / size | `NotiSearchDto`의 `PageableParams` | +| 페이지 컨트롤 | Spring `Page` 표준 (`number`, `totalPages`) | + +### 5-3. 행 상호작용 + +| 동작 | 설명 | +| -------------- | --------------------------------------------------------------------------- | +| 처리 버튼 클릭 | `readNoti(notiId)`로 읽음 처리 후, 해당 알림에 연계된 URL(`notiUrl`)로 이동 | +| 행 클릭 | UI설계에 없음. 처리 버튼만 명시 | + +**처리 버튼 라벨** (상태 Badge §5-1-1 기준): + +| 상태 | 버튼명 | +| -------- | -------- | +| 미조회 | 처리하기 | +| 지연 | 처리하기 | +| 처리중 | 처리하기 | +| 처리지연 | 처리하기 | +| 처리완료 | 상세보기 | + +- `notiUrl`이 비어 있으면 이동하지 않음 (no-op). 읽음 처리는 `notiUrl` 유무와 관계없이 호출 +- 읽음 처리 성공 후 목록 쿼리(`notiBaseKey`)를 invalidate 하여 영역 B 상태를 갱신 + +--- + +## 6. API/DTO 명세 + +### 6-1. 공통 + +| 항목 | 값 | +| --------------- | ---------------------------------------------------------------------------------------- | +| Base prefix | `/cms-service/api/v1/noti` (`ENDPOINT.CMS_SERVICE.NOTI`) | +| 인증 | 로그인 사용자 UUID — 서버 `SecurityUtils.getLoggedInUserUnqIdUUID()` (클라이언트 미전달) | +| 클라이언트 모듈 | `@/entities/noti` — `notiService` / `useFindMyAsNotiList` / `useReadNoti` | + +### 6-2. 로그인한 사용자 행정지원 알림 목록 — `findMyAsNotiList` + +| 기능 | Method | URL | Request DTO | Response DTO | API | +| ---------------------------------- | ------ | ------------------- | --------------------------------- | ------------------------------------- | ------------------------------------------------------ | +| 로그인한 사용자 행정지원 알림 목록 | GET | `{BASE}/as/my/list` | Query: `NotiSearchDto` + Pageable | `PageResponse` | `notiService.findMyAsNotiList` / `useFindMyAsNotiList` | + +**Request (`NotiSearchDto` + Pageable)** + +| 필드 | 필수 | 설명 | +| ------------------------ | ---- | --------------------------------------------------------------------------- | +| `recvUnqId` | N | 수신 회원 UUID. 서버가 로그인 사용자로 덮어씀 | +| `senderUnqId` | N | 발신 회원 UUID. 본 화면 필터에 없음 | +| `notiType` | N | 알림 종류. 본 API는 서버가 `ADSS`로 고정 | +| `notiTtlKeyword` | N | 알림 제목 키워드. 본 화면 필터에 없음 | +| `notiCnKeyword` | N | 알림 내용 키워드. 본 화면 필터에 없음 | +| `notReadYn` | N | 알림 확인 여부 (`Y`/`N`). 본 화면 검색 필터에 없음 | +| `delYn` | N | 삭제 여부. 서버가 `N`으로 고정 | +| `startDt` | N | 시작일자 `YYYY-MM-DD` — 영역 A 기간(시작) | +| `endDt` | N | 종료일자 `YYYY-MM-DD` — 영역 A 기간(종료) | +| `docSeComCd` | N | 문서 구분 공통 코드 (`INSTL`/`FNRS`/`SEWER`/`CNFM`/`PWRK` 등) — 영역 A 구분 | +| `page` / `size` / `sort` | N | Spring Pageable | + +**Response (`NotiAsUserResponseDto`)** + +| 필드 | 타입 | 설명 | +| ----------------- | ------- | ------------------------------------------------------- | +| `notiId` | number | 알림 ID | +| `recvUnqId` | string | 수신 회원 UUID | +| `senderUnqId` | string | 발신 회원 UUID | +| `notiType` | string | 알림 종류 (`ADSS`) | +| `notiTtl` | string | 알림 제목 | +| `notiCn` | string? | 알림 내용 (목록에서 제외될 수 있음) | +| `notReadYn` | string? | 알림 확인 여부 (`Y`/`N`). 상태 Badge 판정 | +| `regDt` | string? | 등록 일시 `yyyy-MM-dd HH:mm:ss`. 상태 Badge 경과일 기준 | +| `notiUrl` | string? | 알림 URL 주소 | +| `notiLnkgUnqNo` | string? | 알림연결고유번호 | +| `docSeComCd` | string? | 문서구분 공통코드 | +| `docSeComNm` | string? | 문서구분 공통코드명 (`ASDOCSE`) | +| `notiIdntyDt` | string? | 알림 확인 일시 `yyyy-MM-dd HH:mm:ss` | +| `notiIdntyYn` | string? | 알림 확인 여부 (`Y`/`N`) | +| `notiPrcsCmptnYn` | string? | 알림 처리 완료 여부 (`Y`/`N`). 상태 Badge 판정 | +| `notiPrcsCmptnDt` | string? | 알림 처리 완료 일시 `yyyy-MM-dd HH:mm:ss` | + +### 6-3. 알림 읽음 처리 — `readNoti` + +| 기능 | Method | URL | Request DTO | Response DTO | API | +| -------------- | ------ | ---------------------- | ----------------------- | ---------------- | -------------------------------------- | +| 알림 읽음 처리 | PUT | `{BASE}/{notiId}/read` | Path: `notiId` (`Long`) | `Long` (알림 ID) | `notiService.readNoti` / `useReadNoti` | + +**Request** + +| 필드 | 위치 | 필수 | 설명 | +| -------- | ---- | ---- | ------- | +| `notiId` | Path | Y | 알림 ID | + +Body 없음. + +**Response** + +| 타입 | 설명 | +| ------ | ------------------------------ | +| number | 읽음 처리된 알림 ID (`notiId`) | + +화면 매핑: 영역 B [처리] 버튼 클릭 시 호출. 성공 시 `notiBaseKey` invalidate → `findMyAsNotiList` 재조회. + +--- + +## 7. UI 구현 가이드 + +### 7-1. 기술 스택·원칙 + +- UI: `@common/components` — DatePicker, Select, Button, DataTable, Pagination, **Badge** +- 목록: `@tanstack/react-table` + `useDataTable` (기존 `my-pbadms-sprt-list` 패턴) +- 데이터: React Query `useFindMyAsNotiList(params)`, `useReadNoti()` +- 검색 조건·페이지: URL `searchParams` 동기화 권장 (`useSearchParamsState` 스킬) +- 처리 컬럼: 행 클릭이 아니라 **버튼 클릭**으로 `readNoti` 후 `notiUrl` 이동 +- 상태 컬럼: §5-1-1 규칙으로 `Badge` 렌더링 (`notiPrcsCmptnYn` / `notReadYn` / `regDt`) +- 연계업무: `docSeComNm` 표시 + +### 7-2. 앱·파일 배치 + +| 구분 | 경로 | +| ------------ | --------------------------------------------------------------------- | +| 라우트 | `app/(main)/(with-base-layout)/noti/page.tsx` | +| 페이지 | `pages/noti/ui/noti-list-page.tsx` | +| 목록 그리드 | `pages/noti/ui/noti-list.tsx` | +| 테이블 컬럼 | `pages/noti/model/noti-table-columns.tsx` | +| 상태 판정 | `pages/noti/lib/resolve-noti-status.ts` | +| Entity API | `entities/noti/api/noti-service.ts` | +| Entity Hook | `entities/noti/api/queries.ts` → `useFindMyAsNotiList`, `useReadNoti` | +| Entity Model | `entities/noti/model/noti-interface.ts` | + +화면 레이어(`pages/noti`)·Entity(`findMyAsNotiList`, `readNoti`) 구현 완료. + +### 7-3. 영역별 UI 매핑 + +| 영역 | 컴포넌트 | 데이터 | +| ---- | ------------------- | -------------------------------------------------------------- | +| A | `noti-list-page` | 로컬 state → `NotiSearchDto` | +| B | DataTable + columns | `useFindMyAsNotiList` → `content[]`. 처리 버튼 → `useReadNoti` | + +### 7-4. 다이얼로그·라우팅 + +| 항목 | 내용 | +| ----------- | --------------------------------------------------------------------- | +| 다이얼로그 | 없음 (목록 전용) | +| 목록 라우트 | `/noti` | +| 상세 이동 | 처리 버튼 → `readNoti(notiId)` 후 `notiUrl` (절대/상대 경로 형식 TBD) | + +### 7-5. 데이터 흐름 + +``` +BEGIN + + // 최초 진입 + SET 기간 = 최근 1개월 // startDt / endDt 전달 + CALL findMyAsNotiList + DISPLAY 영역 B + + ON [검색] CLICK DO + CALL findMyAsNotiList // → 영역 B 갱신 + END ON + + ON 페이지 CHANGE DO + CALL findMyAsNotiList + END ON + + ON 영역 B [처리] 버튼 CLICK DO + CALL readNoti(notiId) // → 영역 B 상태 갱신 (invalidate) + IF NOT notiUrl THEN + // no-op (읽음 처리는 수행됨) + ELSE + NAVIGATE notiUrl + END IF + END ON + +END +``` diff --git a/apps/as/docs/review-manpower-management/SPEC.md b/apps/as/docs/spec/review-manpower-management/SPEC.md similarity index 100% rename from apps/as/docs/review-manpower-management/SPEC.md rename to apps/as/docs/spec/review-manpower-management/SPEC.md diff --git a/apps/as/middleware.ts b/apps/as/middleware.ts index f5bd866f3..dbd2a53f7 100644 --- a/apps/as/middleware.ts +++ b/apps/as/middleware.ts @@ -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")); diff --git a/apps/as/public/images/help.png b/apps/as/public/images/help.png new file mode 100644 index 000000000..e872802e4 Binary files /dev/null and b/apps/as/public/images/help.png differ diff --git a/apps/as/public/images/magnifying-glass-with-warning.png b/apps/as/public/images/magnifying-glass-with-warning.png new file mode 100644 index 000000000..b184da207 Binary files /dev/null and b/apps/as/public/images/magnifying-glass-with-warning.png differ diff --git a/apps/as/public/images/region_map.svg b/apps/as/public/images/region_map.svg new file mode 100644 index 000000000..f327bb9fc --- /dev/null +++ b/apps/as/public/images/region_map.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/as/public/images/region_map_bg.png b/apps/as/public/images/region_map_bg.png new file mode 100644 index 000000000..6f59f58cc Binary files /dev/null and b/apps/as/public/images/region_map_bg.png differ diff --git a/apps/as/public/webhwpctrl/hwpsdkmain.html b/apps/as/public/webhwpctrl/hwpsdkmain.html new file mode 100644 index 000000000..d24c7eee7 --- /dev/null +++ b/apps/as/public/webhwpctrl/hwpsdkmain.html @@ -0,0 +1,41 @@ + + + + + + + + Hwp SDK Wasm Sample + + + + + + + + +
+
+
+ 구성 중 ... 0% + +
+
+ +
+
+ + + + + + diff --git a/apps/as/public/webhwpctrl/index.html b/apps/as/public/webhwpctrl/index.html new file mode 100644 index 000000000..dcb8c3e45 --- /dev/null +++ b/apps/as/public/webhwpctrl/index.html @@ -0,0 +1,66 @@ + + + + + + + 웹한글 기안기 브리지 + + + + + + +
+ + + diff --git a/apps/as/src/entities/attach/api/attach-service.ts b/apps/as/src/entities/attach/api/attach-service.ts index 0af0a916d..2cb1fc35d 100644 --- a/apps/as/src/entities/attach/api/attach-service.ts +++ b/apps/as/src/entities/attach/api/attach-service.ts @@ -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 { + if (!atflCd) return []; + const response = await client.get(`${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}/${atflCd}`); + return response.data; +} async function deleteAttachmentByAtflId(atflId: string): Promise { if (!atflId) return; @@ -16,7 +22,15 @@ async function downloadAttachmentByAtflCd(atflCd: string): Promise { + return await client.get(`${ENDPOINT.ATTACH_SERVICE.DOWNLOAD_BY_ATFL_ID}/${atflId}`, { + responseType: "blob", + }); +} + export const attachService = { + findByAtflCd, deleteAttachmentByAtflId, downloadAttachmentByAtflCd, + downloadAttachmentByAtflId, } satisfies AttachService; diff --git a/apps/as/src/entities/attach/api/queries.ts b/apps/as/src/entities/attach/api/queries.ts index ed9efefbf..09a1ab1d9 100644 --- a/apps/as/src/entities/attach/api/queries.ts +++ b/apps/as/src/entities/attach/api/queries.ts @@ -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); }, }); } diff --git a/apps/as/src/entities/attach/index.ts b/apps/as/src/entities/attach/index.ts index 37258e6e5..ca804b4a0 100644 --- a/apps/as/src/entities/attach/index.ts +++ b/apps/as/src/entities/attach/index.ts @@ -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"; diff --git a/apps/as/src/entities/attach/model/attach-interface.ts b/apps/as/src/entities/attach/model/attach-interface.ts index d6b0a08d2..ef299a43c 100644 --- a/apps/as/src/entities/attach/model/attach-interface.ts +++ b/apps/as/src/entities/attach/model/attach-interface.ts @@ -17,6 +17,8 @@ export interface Attach { } export interface AttachService { + findByAtflCd: (atflCd: string) => Promise; deleteAttachmentByAtflId: (atflId: string) => Promise; downloadAttachmentByAtflCd: (atflCd: string) => Promise; + downloadAttachmentByAtflId: (atflId: string) => Promise; } diff --git a/apps/as/src/entities/board/api/post-queries.ts b/apps/as/src/entities/board/api/post-queries.ts new file mode 100644 index 000000000..4cf801e0a --- /dev/null +++ b/apps/as/src/entities/board/api/post-queries.ts @@ -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, + }); +} diff --git a/apps/as/src/entities/board/api/post-service.ts b/apps/as/src/entities/board/api/post-service.ts new file mode 100644 index 000000000..9cd25f976 --- /dev/null +++ b/apps/as/src/entities/board/api/post-service.ts @@ -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 { + const response = await client.get( + `${ENDPOINT.BOARD_SERVICE.POSTS}/user/${AS_DASHBOARD_BBS_ID}`, + { params }, + ); + return response.data; +} + +export const postApi: PostService = { + getUserPostList, +}; diff --git a/apps/as/src/entities/board/index.ts b/apps/as/src/entities/board/index.ts new file mode 100644 index 000000000..2bec23972 --- /dev/null +++ b/apps/as/src/entities/board/index.ts @@ -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"; diff --git a/apps/as/src/entities/board/model/post-interface.ts b/apps/as/src/entities/board/model/post-interface.ts new file mode 100644 index 000000000..31eca4e64 --- /dev/null +++ b/apps/as/src/entities/board/model/post-interface.ts @@ -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; + +export interface PostService { + getUserPostList(params?: UserPostSearchParams): Promise; +} diff --git a/apps/as/src/entities/dashboard/api/dashboard-service.ts b/apps/as/src/entities/dashboard/api/dashboard-service.ts new file mode 100644 index 000000000..2440c423c --- /dev/null +++ b/apps/as/src/entities/dashboard/api/dashboard-service.ts @@ -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 { + const response = await client.get(`${BASE}/main`, { + params: searchDto, + }); + return response.data; +} + +export const dashboardService = { + getMainDashboard, +} satisfies DashboardService; diff --git a/apps/as/src/entities/dashboard/api/queries.ts b/apps/as/src/entities/dashboard/api/queries.ts new file mode 100644 index 000000000..9274d520c --- /dev/null +++ b/apps/as/src/entities/dashboard/api/queries.ts @@ -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), + }); +} diff --git a/apps/as/src/entities/dashboard/index.ts b/apps/as/src/entities/dashboard/index.ts new file mode 100644 index 000000000..19d5b2c0b --- /dev/null +++ b/apps/as/src/entities/dashboard/index.ts @@ -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"; diff --git a/apps/as/src/entities/dashboard/model/dashboard-interface.ts b/apps/as/src/entities/dashboard/model/dashboard-interface.ts new file mode 100644 index 000000000..fb592680e --- /dev/null +++ b/apps/as/src/entities/dashboard/model/dashboard-interface.ts @@ -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; +} diff --git a/apps/as/src/entities/dcrag-to/api/dcrag-to-service.ts b/apps/as/src/entities/dcrag-to/api/dcrag-to-service.ts new file mode 100644 index 000000000..16c2ccdbd --- /dev/null +++ b/apps/as/src/entities/dcrag-to/api/dcrag-to-service.ts @@ -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 { + const response = await client.post(`${BASE}/documents/transfer`, body); + return response.data; +} + +async function requestDraftGeneration( + body: DcRagDraftGenerateRequestDto, +): Promise { + const response = await client.post( + `${BASE}/documents/draft/generate`, + body, + ); + return response.data; +} + +async function getDraftGenerationStatus(crtDmndUuid: string): Promise { + const response = await client.get( + `${BASE}/documents/draft/status/${crtDmndUuid}`, + ); + return response.data; +} + +async function requestCostVerification( + body: DcRagCostVerifyRequestDto, +): Promise { + const response = await client.post(`${BASE}/documents/cost/verify`, body); + return response.data; +} + +export const dcRagApiToService = { + requestDocumentTransfer, + requestDraftGeneration, + getDraftGenerationStatus, + requestCostVerification, +} satisfies DcRagApiToService; diff --git a/apps/as/src/entities/dcrag-to/api/queries.ts b/apps/as/src/entities/dcrag-to/api/queries.ts new file mode 100644 index 000000000..4e9fc4483 --- /dev/null +++ b/apps/as/src/entities/dcrag-to/api/queries.ts @@ -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 }); + }, + }); +} diff --git a/apps/as/src/entities/dcrag-to/index.ts b/apps/as/src/entities/dcrag-to/index.ts new file mode 100644 index 000000000..2944e98c6 --- /dev/null +++ b/apps/as/src/entities/dcrag-to/index.ts @@ -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"; diff --git a/apps/as/src/entities/dcrag-to/model/dcrag-to-interface.ts b/apps/as/src/entities/dcrag-to/model/dcrag-to-interface.ts new file mode 100644 index 000000000..d80f91049 --- /dev/null +++ b/apps/as/src/entities/dcrag-to/model/dcrag-to-interface.ts @@ -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; + requestDraftGeneration( + body: DcRagDraftGenerateRequestDto, + ): Promise; + getDraftGenerationStatus(crtDmndUuid: string): Promise; + requestCostVerification(body: DcRagCostVerifyRequestDto): Promise; +} diff --git a/apps/as/src/entities/dcrag-to/model/dcrag-to-payload-schema.ts b/apps/as/src/entities/dcrag-to/model/dcrag-to-payload-schema.ts new file mode 100644 index 000000000..7ad5827c2 --- /dev/null +++ b/apps/as/src/entities/dcrag-to/model/dcrag-to-payload-schema.ts @@ -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; +export type DcRagDocumentTransferRequestDto = z.infer; +export type DcRagDraftGenerateRequestDto = z.infer; +export type DcRagCostVerifyRequestDto = z.infer; diff --git a/apps/as/src/entities/design-cnstn/api/queries.ts b/apps/as/src/entities/design-cnstn/api/queries.ts index bafc9784a..c98a5ad15 100644 --- a/apps/as/src/entities/design-cnstn/api/queries.ts +++ b/apps/as/src/entities/design-cnstn/api/queries.ts @@ -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 }); }, }); } diff --git a/apps/as/src/entities/design-cnstn/index.ts b/apps/as/src/entities/design-cnstn/index.ts index ed6b6e1ae..cad262c32 100644 --- a/apps/as/src/entities/design-cnstn/index.ts +++ b/apps/as/src/entities/design-cnstn/index.ts @@ -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, diff --git a/apps/as/src/entities/elapr/api/elapr-prgrs-stts-service.ts b/apps/as/src/entities/elapr/api/elapr-prgrs-stts-service.ts index f1931d556..435827cf2 100644 --- a/apps/as/src/entities/elapr/api/elapr-prgrs-stts-service.ts +++ b/apps/as/src/entities/elapr/api/elapr-prgrs-stts-service.ts @@ -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> { - const response = await client.get>(`${BASE}/my/doc`, { + params: ElaprMyDocSearchDto, +): Promise> { + const response = await client.get>(`${BASE}/my/doc`, { params, }); return response.data; diff --git a/apps/as/src/entities/elapr/api/queries.ts b/apps/as/src/entities/elapr/api/queries.ts index e93c1540e..734c6cdf4 100644 --- a/apps/as/src/entities/elapr/api/queries.ts +++ b/apps/as/src/entities/elapr/api/queries.ts @@ -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), diff --git a/apps/as/src/entities/elapr/index.ts b/apps/as/src/entities/elapr/index.ts index adec3e931..4805fecc3 100644 --- a/apps/as/src/entities/elapr/index.ts +++ b/apps/as/src/entities/elapr/index.ts @@ -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 { diff --git a/apps/as/src/entities/elapr/lib/apln-validators.ts b/apps/as/src/entities/elapr/lib/apln-validators.ts index 91f1a95cf..8b75b4279 100644 --- a/apps/as/src/entities/elapr/lib/apln-validators.ts +++ b/apps/as/src/entities/elapr/lib/apln-validators.ts @@ -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 "저장할 결재선이 없습니다."; } diff --git a/apps/as/src/entities/elapr/lib/approval-utils.ts b/apps/as/src/entities/elapr/lib/approval-utils.ts index 00d7d7b2a..831c06949 100644 --- a/apps/as/src/entities/elapr/lib/approval-utils.ts +++ b/apps/as/src/entities/elapr/lib/approval-utils.ts @@ -6,11 +6,26 @@ export interface ApproverStatusConfig { } export const APPROVER_STATUS_MAP: Record = { - 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", } ); diff --git a/apps/as/src/entities/elapr/model/elapr-dmnd-interface.ts b/apps/as/src/entities/elapr/model/elapr-dmnd-interface.ts index a4eb14c7c..264f45db7 100644 --- a/apps/as/src/entities/elapr/model/elapr-dmnd-interface.ts +++ b/apps/as/src/entities/elapr/model/elapr-dmnd-interface.ts @@ -7,6 +7,8 @@ export interface ElaprDmndSaveRequestDto { elaprDocNo: string; dmndTtl: string; dmndCn: string; + elaprCycl?: number; + docPrgrsSttsComCd?: string; elaprPrgrsSttsList?: ElaprPrgrsSttsSaveRequestDto[]; } diff --git a/apps/as/src/entities/elapr/model/elapr-prgrs-stts-interface.ts b/apps/as/src/entities/elapr/model/elapr-prgrs-stts-interface.ts index 7c24ff8eb..1633b95f8 100644 --- a/apps/as/src/entities/elapr/model/elapr-prgrs-stts-interface.ts +++ b/apps/as/src/entities/elapr/model/elapr-prgrs-stts-interface.ts @@ -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; findMyElaperDocList( - params: ElaprPrgrsSttsMyDocSearchParams, - ): Promise>; + params: ElaprMyDocSearchDto, + ): Promise>; } diff --git a/apps/as/src/entities/elapr/ui/elapr-approval-dialog.tsx b/apps/as/src/entities/elapr/ui/elapr-approval-dialog.tsx index c4cb3956d..ad9abfa3e 100644 --- a/apps/as/src/entities/elapr/ui/elapr-approval-dialog.tsx +++ b/apps/as/src/entities/elapr/ui/elapr-approval-dialog.tsx @@ -314,8 +314,8 @@ function ElaprApprovalDialog({ - - + + diff --git a/apps/as/src/entities/elapr/ui/elapr-doc-prgrs-status.tsx b/apps/as/src/entities/elapr/ui/elapr-doc-prgrs-status.tsx index 731e39a07..4be4cf0c3 100644 --- a/apps/as/src/entities/elapr/ui/elapr-doc-prgrs-status.tsx +++ b/apps/as/src/entities/elapr/ui/elapr-doc-prgrs-status.tsx @@ -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 = ( +
{isRejected && rjctRsn ? ( {statusBadge} @@ -49,11 +51,16 @@ function ElaprDocPrgrsStep({ item }: { item: ElaprDocPrgrsClientResponseDto }) { )}
- [{item.atrzSeComNm || "-"}] - {item.aprvrNm || item.aprvrUuid || "-"} +

+ [{item.atrzSeComNm || "-"}]{" "} + {item.aprvrNm || item.aprvrUuid || "-"} +

{metaParts.length > 0 && ( - {metaParts.join(" · ")} + {metaParts.join(" · ")} + )} + {item.atrzDt && ( + {dayjs(item.atrzDt).format("YYYY-MM-DD")} )}
@@ -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 ( -
- - - -
+ +
+ + + +
+
); } if (steps.length === 0) { - return
결재 정보가 없습니다.
; + return null; } return ( -
- {steps.map((item, index) => ( - -
- {index > 0 && } -
- -
- ))} -
+ +
+ {steps.map((item, index) => ( + + {index > 0 && ( +
+ {} +
+ )} + +
+ ))} +
+
); } diff --git a/apps/as/src/entities/elapr/ui/sortable-prgrs-stts-row.tsx b/apps/as/src/entities/elapr/ui/sortable-prgrs-stts-row.tsx index 601d272f2..3c2bb457a 100644 --- a/apps/as/src/entities/elapr/ui/sortable-prgrs-stts-row.tsx +++ b/apps/as/src/entities/elapr/ui/sortable-prgrs-stts-row.tsx @@ -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 = ( (`${BASE}/${id}/tech-review`, body); } +async function requestExamine(docNo: string, body: FnrsExamineRequestDto): Promise { + const response = await client.post(`${BASE}/${docNo}/request-examine`, body); + return response.data; +} + +async function requestActnReview(docNo: string, body: EnvoActnReviewReqDto): Promise { + const response = await client.post(`${BASE}/${docNo}/request-actn-review`, body); + return response.data; +} + async function submitSupplement(id: string, body: FnrsCnsltnApplySaveRequestDto): Promise { const response = await client.post(`${BASE}/${id}/submitsupplement`, body); return response.data; @@ -68,6 +83,15 @@ async function remove(fnrsCnsltnDocNo: string): Promise { await client.delete(`${BASE}/${fnrsCnsltnDocNo}`); } +async function findFileVrfcResults( + fnrsCnsltnDocNo: string, +): Promise { + const response = await client.get( + `${BASE}/${fnrsCnsltnDocNo}/file-vrfc-results`, + ); + return response.data; +} + async function changePrgrsSttsComCd(body: DocPrgrsUpdateRequestDto): Promise { await client.post(`${BASE}/doc/updatePrgrsStts`, body); } @@ -79,8 +103,11 @@ export const fnrsCnsltnMngService = { saveApply, update, techReview, + requestExamine, + requestActnReview, submitSupplement, submitReqSupplement, remove, + findFileVrfcResults, changePrgrsSttsComCd, } satisfies FnrsCnsltnMngService; diff --git a/apps/as/src/entities/fnrs-cnsltn/api/queries.ts b/apps/as/src/entities/fnrs-cnsltn/api/queries.ts index 127e87e08..357aa58c0 100644 --- a/apps/as/src/entities/fnrs-cnsltn/api/queries.ts +++ b/apps/as/src/entities/fnrs-cnsltn/api/queries.ts @@ -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 }); }, }); } diff --git a/apps/as/src/entities/fnrs-cnsltn/index.ts b/apps/as/src/entities/fnrs-cnsltn/index.ts index 645065ffe..31bc2f528 100644 --- a/apps/as/src/entities/fnrs-cnsltn/index.ts +++ b/apps/as/src/entities/fnrs-cnsltn/index.ts @@ -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"; diff --git a/apps/as/src/entities/fnrs-cnsltn/model/fnrs-cnsltn-interface.ts b/apps/as/src/entities/fnrs-cnsltn/model/fnrs-cnsltn-interface.ts index d0a43a71b..8e50d5d79 100644 --- a/apps/as/src/entities/fnrs-cnsltn/model/fnrs-cnsltn-interface.ts +++ b/apps/as/src/entities/fnrs-cnsltn/model/fnrs-cnsltn-interface.ts @@ -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>; @@ -170,8 +198,11 @@ export interface FnrsCnsltnMngService { saveApply(body: FnrsCnsltnApplySaveRequestDto): Promise; update(fnrsCnsltnDocNo: string, body: FnrsCnsltnMngUpdateRequestDto): Promise; techReview(id: string, body: FnrsCnsltnApplySaveRequestDto): Promise; + requestExamine(docNo: string, body: FnrsExamineRequestDto): Promise; + requestActnReview(docNo: string, body: EnvoActnReviewReqDto): Promise; submitSupplement(id: string, body: FnrsCnsltnApplySaveRequestDto): Promise; submitReqSupplement(id: string, body: FnrsCnsltnApplySaveRequestDto): Promise; remove(fnrsCnsltnDocNo: string): Promise; + findFileVrfcResults(fnrsCnsltnDocNo: string): Promise; changePrgrsSttsComCd(body: DocPrgrsUpdateRequestDto): Promise; } diff --git a/apps/as/src/entities/fnrs-cnsltn/model/fnrs-cnsltn-payload-schema.ts b/apps/as/src/entities/fnrs-cnsltn/model/fnrs-cnsltn-payload-schema.ts new file mode 100644 index 000000000..a34b22ad6 --- /dev/null +++ b/apps/as/src/entities/fnrs-cnsltn/model/fnrs-cnsltn-payload-schema.ts @@ -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; +export type EnvoActnReviewReqDto = z.infer; diff --git a/apps/as/src/entities/fnrs-shar-se/model/fnrs-shar-se-interface.ts b/apps/as/src/entities/fnrs-shar-se/model/fnrs-shar-se-interface.ts index ab60a868f..97adb34b6 100644 --- a/apps/as/src/entities/fnrs-shar-se/model/fnrs-shar-se-interface.ts +++ b/apps/as/src/entities/fnrs-shar-se/model/fnrs-shar-se-interface.ts @@ -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 메서드명·시그니처와 동일 */ diff --git a/apps/as/src/entities/instl-sncn/model/instl-sncn-interface.ts b/apps/as/src/entities/instl-sncn/model/instl-sncn-interface.ts index 4bbd7ae24..a5e4ce791 100644 --- a/apps/as/src/entities/instl-sncn/model/instl-sncn-interface.ts +++ b/apps/as/src/entities/instl-sncn/model/instl-sncn-interface.ts @@ -148,6 +148,8 @@ export interface InstlSncnMngResponseDto extends InstlSncnMngSaveRequestDto { taskSeCdNm: string; /** 진행상태공통코드명 */ prgrsSttsCdNm: string; + /** 기술검토회신이력여부 */ + techRplyHstryYn: "Y" | "N"; /** 재이용시설구분공통코드명 */ rtlztnFcltSeComNm: string; /** 재이용용도구분공통코드명 */ diff --git a/apps/as/src/entities/noti/api/noti-service.ts b/apps/as/src/entities/noti/api/noti-service.ts new file mode 100644 index 000000000..649d6d17b --- /dev/null +++ b/apps/as/src/entities/noti/api/noti-service.ts @@ -0,0 +1,23 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { PageResponse } from "@/shared/model"; + +import type { NotiAsUserResponseDto, NotiSearchDto, NotiService } from "../model/noti-interface"; + +const BASE = ENDPOINT.CMS_SERVICE.NOTI; + +async function findMyAsNotiList( + params: NotiSearchDto, +): Promise> { + const response = await client.get>(`${BASE}/as/my/list`, { + params, + }); + return response.data; +} + +async function readNoti(notiId: number): Promise { + const response = await client.put(`${BASE}/${notiId}/read`); + return response.data; +} + +export const notiService = { findMyAsNotiList, readNoti } satisfies NotiService; diff --git a/apps/as/src/entities/noti/api/queries.ts b/apps/as/src/entities/noti/api/queries.ts new file mode 100644 index 000000000..0d55e3279 --- /dev/null +++ b/apps/as/src/entities/noti/api/queries.ts @@ -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] }); + }, + }); +} diff --git a/apps/as/src/entities/noti/index.ts b/apps/as/src/entities/noti/index.ts new file mode 100644 index 000000000..dc57023cb --- /dev/null +++ b/apps/as/src/entities/noti/index.ts @@ -0,0 +1,3 @@ +export { notiService } from "./api/noti-service"; +export { notiBaseKey, useFindMyAsNotiList, useReadNoti } from "./api/queries"; +export type { NotiAsUserResponseDto, NotiSearchDto, NotiService } from "./model/noti-interface"; diff --git a/apps/as/src/entities/noti/model/noti-interface.ts b/apps/as/src/entities/noti/model/noti-interface.ts new file mode 100644 index 000000000..d81f5fdca --- /dev/null +++ b/apps/as/src/entities/noti/model/noti-interface.ts @@ -0,0 +1,41 @@ +import type { PageableParams, PageResponse } from "@/shared/model"; + +/** 백엔드 NotiAsUserResponseDto */ +export interface NotiAsUserResponseDto { + notiId: number; + recvUnqId: string; + senderUnqId: string; + notiType: string; + notiTtl: string; + notiCn?: string; + notReadYn?: string; + regDt?: string; + notiUrl?: string; + notiLnkgUnqNo?: string; + docSeComCd?: string; + docSeComNm?: string; + notiIdntyDt?: string; + notiIdntyYn?: string; + notiPrcsCmptnYn?: string; + notiPrcsCmptnDt?: string; +} + +/** 백엔드 NotiSearchDto + Pageable */ +export interface NotiSearchDto extends PageableParams { + recvUnqId?: string; + senderUnqId?: string; + notiType?: string; + notiTtlKeyword?: string; + notiCnKeyword?: string; + notReadYn?: string; + delYn?: string; + startDt?: string; + endDt?: string; + docSeComCd?: string; +} + +/** Controller public 메서드명·시그니처와 동일 */ +export interface NotiService { + findMyAsNotiList(params: NotiSearchDto): Promise>; + readNoti(notiId: number): Promise; +} diff --git a/apps/as/src/entities/pwrk-cnsltn-doc-crt-rslt/api/pwrk-cnsltn-doc-crt-rslt-service.ts b/apps/as/src/entities/pwrk-cnsltn-doc-crt-rslt/api/pwrk-cnsltn-doc-crt-rslt-service.ts new file mode 100644 index 000000000..82346e7c4 --- /dev/null +++ b/apps/as/src/entities/pwrk-cnsltn-doc-crt-rslt/api/pwrk-cnsltn-doc-crt-rslt-service.ts @@ -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 { + const response = await client.get( + `${BASE}/${crtDmndUuid}/latest`, + ); + return response.data; +} + +async function openWebHwpLatest(crtDmndUuid: string) { + return client.get(`${BASE}/${crtDmndUuid}/webhwp/open`, { + responseType: "blob", + }); +} + +async function download(crtDmndUuid: string, verSn: number) { + return client.get(`${BASE}/${crtDmndUuid}/${verSn}/download`, { + responseType: "blob", + }); +} + +async function downloadLatest(crtDmndUuid: string) { + return client.get(`${BASE}/${crtDmndUuid}/download`, { + responseType: "blob", + }); +} + +async function saveMultipart( + saveDto: PwrkCnsltnDocCrtRsltSaveRequestDto, + file?: Blob | File, + fileName?: string, +): Promise { + 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(BASE, formData, { + headers: { "Content-Type": undefined }, + }); + return response.data; +} + +async function updateMultipart( + crtDmndUuid: string, + verSn: number, + formData: FormData, +): Promise { + const response = await client.put( + `${BASE}/${crtDmndUuid}/${verSn}`, + formData, + { headers: { "Content-Type": undefined } }, + ); + return response.data; +} + +export const pwrkCnsltnDocCrtRsltService = { + findLatestByCrtDmndUuid, + openWebHwpLatest, + download, + downloadLatest, + saveMultipart, + updateMultipart, +} satisfies PwrkCnsltnDocCrtRsltService; diff --git a/apps/as/src/entities/pwrk-cnsltn-doc-crt-rslt/index.ts b/apps/as/src/entities/pwrk-cnsltn-doc-crt-rslt/index.ts new file mode 100644 index 000000000..6784fdcbd --- /dev/null +++ b/apps/as/src/entities/pwrk-cnsltn-doc-crt-rslt/index.ts @@ -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"; diff --git a/apps/as/src/entities/pwrk-cnsltn-doc-crt-rslt/model/pwrk-cnsltn-doc-crt-rslt-interface.ts b/apps/as/src/entities/pwrk-cnsltn-doc-crt-rslt/model/pwrk-cnsltn-doc-crt-rslt-interface.ts new file mode 100644 index 000000000..338e4ce5c --- /dev/null +++ b/apps/as/src/entities/pwrk-cnsltn-doc-crt-rslt/model/pwrk-cnsltn-doc-crt-rslt-interface.ts @@ -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; + openWebHwpLatest(crtDmndUuid: string): Promise>; + download(crtDmndUuid: string, verSn: number): Promise>; + downloadLatest(crtDmndUuid: string): Promise>; + saveMultipart( + saveDto: PwrkCnsltnDocCrtRsltSaveRequestDto, + file?: Blob | File, + fileName?: string, + ): Promise; + updateMultipart( + crtDmndUuid: string, + verSn: number, + formData: FormData, + ): Promise; +} diff --git a/apps/as/src/entities/pwrk-cnsltn-doc-crt-rslt/model/pwrk-cnsltn-doc-crt-rslt-payload-schema.ts b/apps/as/src/entities/pwrk-cnsltn-doc-crt-rslt/model/pwrk-cnsltn-doc-crt-rslt-payload-schema.ts new file mode 100644 index 000000000..c9ed3b7e1 --- /dev/null +++ b/apps/as/src/entities/pwrk-cnsltn-doc-crt-rslt/model/pwrk-cnsltn-doc-crt-rslt-payload-schema.ts @@ -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 +>; diff --git a/apps/as/src/entities/pwrk-cnsltn-mng/api/pwrk-cnsltn-mng-service.ts b/apps/as/src/entities/pwrk-cnsltn-mng/api/pwrk-cnsltn-mng-service.ts new file mode 100644 index 000000000..ac27e8f5d --- /dev/null +++ b/apps/as/src/entities/pwrk-cnsltn-mng/api/pwrk-cnsltn-mng-service.ts @@ -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 { + const response = await client.get(`${BASE}/updocno/${upDocNo}`); + return response.data; +} + +export const pwrkCnsltnMngService = { + findByUpDocNo, +} satisfies PwrkCnsltnMngService; diff --git a/apps/as/src/entities/pwrk-cnsltn-mng/api/queries.ts b/apps/as/src/entities/pwrk-cnsltn-mng/api/queries.ts new file mode 100644 index 000000000..a5ba4cd8e --- /dev/null +++ b/apps/as/src/entities/pwrk-cnsltn-mng/api/queries.ts @@ -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, + }); +} diff --git a/apps/as/src/entities/pwrk-cnsltn-mng/index.ts b/apps/as/src/entities/pwrk-cnsltn-mng/index.ts new file mode 100644 index 000000000..58a3dc75d --- /dev/null +++ b/apps/as/src/entities/pwrk-cnsltn-mng/index.ts @@ -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"; diff --git a/apps/as/src/entities/pwrk-cnsltn-mng/lib/find-completed-draft.ts b/apps/as/src/entities/pwrk-cnsltn-mng/lib/find-completed-draft.ts new file mode 100644 index 000000000..2faa27222 --- /dev/null +++ b/apps/as/src/entities/pwrk-cnsltn-mng/lib/find-completed-draft.ts @@ -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), + ); +} diff --git a/apps/as/src/entities/pwrk-cnsltn-mng/model/pwrk-cnsltn-mng-interface.ts b/apps/as/src/entities/pwrk-cnsltn-mng/model/pwrk-cnsltn-mng-interface.ts new file mode 100644 index 000000000..fe5c4e5a4 --- /dev/null +++ b/apps/as/src/entities/pwrk-cnsltn-mng/model/pwrk-cnsltn-mng-interface.ts @@ -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; +} diff --git a/apps/as/src/entities/rvw-mnpw/api/rvw-mnpw-service.ts b/apps/as/src/entities/rvw-mnpw/api/rvw-mnpw-service.ts index 2e99f3fba..3bda05a06 100644 --- a/apps/as/src/entities/rvw-mnpw/api/rvw-mnpw-service.ts +++ b/apps/as/src/entities/rvw-mnpw/api/rvw-mnpw-service.ts @@ -41,11 +41,7 @@ async function findById( } async function saveRvwMnpwMng(body: RvwMnpwMngSaveRequestDto): Promise { - const { atrzSeComCd, ognzSn, userUuid } = body; - const response = await client.post( - `${BASE}/${atrzSeComCd}/${ognzSn}/${userUuid}`, - body, - ); + const response = await client.post(BASE, body); return response.data; } diff --git a/apps/as/src/entities/sewer-bsc-plan/api/queries.ts b/apps/as/src/entities/sewer-bsc-plan/api/queries.ts index 063e1bd00..32808b49f 100644 --- a/apps/as/src/entities/sewer-bsc-plan/api/queries.ts +++ b/apps/as/src/entities/sewer-bsc-plan/api/queries.ts @@ -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), }); }, }); diff --git a/apps/as/src/entities/sewer-bsc-plan/api/sewer-bsc-plan-service.ts b/apps/as/src/entities/sewer-bsc-plan/api/sewer-bsc-plan-service.ts index 013b1d384..c8c0b298b 100644 --- a/apps/as/src/entities/sewer-bsc-plan/api/sewer-bsc-plan-service.ts +++ b/apps/as/src/entities/sewer-bsc-plan/api/sewer-bsc-plan-service.ts @@ -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 { - const response = await client.get(`${BASE}/${sewerBscPlanDocNo}`); +async function findById(sewerBscPlanDocNo: string): Promise { + const response = await client.get(`${BASE}/${sewerBscPlanDocNo}`); return response.data; } @@ -75,8 +77,11 @@ async function submitReqSupplement( id: string, chgCycl: number, body: SewerBscPlanApplySaveRequestDto, -): Promise { - const response = await client.post(`${BASE}/${id}/${chgCycl}/reqsupplement`, body); +): Promise { + const response = await client.post( + `${BASE}/${id}/${chgCycl}/reqsupplement`, + body, + ); return response.data; } diff --git a/apps/as/src/entities/sewer-bsc-plan/index.ts b/apps/as/src/entities/sewer-bsc-plan/index.ts index eecf354cd..fd98c82ff 100644 --- a/apps/as/src/entities/sewer-bsc-plan/index.ts +++ b/apps/as/src/entities/sewer-bsc-plan/index.ts @@ -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"; diff --git a/apps/as/src/entities/sewer-bsc-plan/model/sewer-bsc-plan-apply-payload-schema.ts b/apps/as/src/entities/sewer-bsc-plan/model/sewer-bsc-plan-apply-payload-schema.ts new file mode 100644 index 000000000..0060467b7 --- /dev/null +++ b/apps/as/src/entities/sewer-bsc-plan/model/sewer-bsc-plan-apply-payload-schema.ts @@ -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; +export type SewerBscPlanDtlSaveRequestDto = z.infer; +export type SewerBscPlanOtlnItem = z.infer; +export type CklstResultItem = z.infer; +export type SewerBscPlanFnrsCstItem = z.infer; +export type SewerBscPlanSplmntDmndItem = z.infer; +export type SewerBscPlanRvwOpnnItem = z.infer; +export type EsntlAtchFileItem = z.infer; +export type SewerBscPlanBizCstItem = z.infer; +export type SewerBscPlanTmplntDtlItem = z.infer; +export type SewerBscPlanTmplntItem = z.infer; +export type SewerBscPlanTmplntTmprStrgItem = z.infer; +export type SewerBscPlanApplySaveRequestDto = z.infer; diff --git a/apps/as/src/entities/sewer-bsc-plan/model/sewer-bsc-plan-interface.ts b/apps/as/src/entities/sewer-bsc-plan/model/sewer-bsc-plan-interface.ts index cc5f533d8..0bab9d6f0 100644 --- a/apps/as/src/entities/sewer-bsc-plan/model/sewer-bsc-plan-interface.ts +++ b/apps/as/src/entities/sewer-bsc-plan/model/sewer-bsc-plan-interface.ts @@ -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>; - findById(sewerBscPlanDocNo: string): Promise; + findById(sewerBscPlanDocNo: string): Promise; save(body: SewerBscPlanSaveRequestDto): Promise; update( sewerBscPlanDocNo: string, @@ -346,6 +221,6 @@ export interface SewerBscPlanService { id: string, chgCycl: number, body: SewerBscPlanApplySaveRequestDto, - ): Promise; + ): Promise; techReview(id: string, body: SewerBscPlanApplySaveRequestDto): Promise; } diff --git a/apps/as/src/pages/approval-line-management/ui/elapr-tmplt-list.tsx b/apps/as/src/pages/approval-line-management/ui/elapr-tmplt-list.tsx index 7955f8b6b..790c6e408 100644 --- a/apps/as/src/pages/approval-line-management/ui/elapr-tmplt-list.tsx +++ b/apps/as/src/pages/approval-line-management/ui/elapr-tmplt-list.tsx @@ -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(null); const [rowSelection, setRowSelection] = useState({}); @@ -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({ diff --git a/apps/as/src/pages/approval-line-management/ui/sortable-apln-row.tsx b/apps/as/src/pages/approval-line-management/ui/sortable-apln-row.tsx index b0a23ee1e..d9f3ddf73 100644 --- a/apps/as/src/pages/approval-line-management/ui/sortable-apln-row.tsx +++ b/apps/as/src/pages/approval-line-management/ui/sortable-apln-row.tsx @@ -82,7 +82,13 @@ function SortableAplnRow({ { + void handleFileUpload(e.target.files); + e.target.value = ""; + }} + /> + + + +
onOpenChange(false)}> 취소 - diff --git a/apps/as/src/pages/esntl-atch-file-mng/ui/esntl-atch-file-list.tsx b/apps/as/src/pages/esntl-atch-file-mng/ui/esntl-atch-file-list.tsx index 62a728073..0872198f8 100644 --- a/apps/as/src/pages/esntl-atch-file-mng/ui/esntl-atch-file-list.tsx +++ b/apps/as/src/pages/esntl-atch-file-mng/ui/esntl-atch-file-list.tsx @@ -6,6 +6,7 @@ import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { getCoreRowModel, type PaginationState } from "@tanstack/react-table"; import { useDataTable } from "@/shared/lib"; +import { useDownloadAttachmentByAtflCd } from "@/entities/attach"; import { useCodeList } from "@/entities/code"; import { type EsntlAtchFileMng, @@ -37,6 +38,7 @@ function EsntlAtchFileList({ }: EsntlAtchFileListProps) { const { confirm } = useConfirm(); const { data: docSeCodes } = useCodeList("ASDOCSE"); + const { mutate: downloadTemplate } = useDownloadAttachmentByAtflCd(); const [dialogMode, setDialogMode] = useState<"create" | "edit" | null>(null); const [editingRow, setEditingRow] = useState(null); @@ -64,6 +66,13 @@ function EsntlAtchFileList({ [confirm, deleteMutation], ); + const handleDownloadTemplate = useCallback( + (atflCd: string) => { + downloadTemplate(atflCd); + }, + [downloadTemplate], + ); + const columns = useMemo( () => createEsntlAtchFileColumns({ @@ -73,8 +82,9 @@ function EsntlAtchFileList({ setDialogMode("edit"); }, onDelete: handleDelete, + onDownloadTemplate: handleDownloadTemplate, }), - [docSeCodes, handleDelete], + [docSeCodes, handleDelete, handleDownloadTemplate], ); const { tableComponent } = useDataTable({ @@ -102,6 +112,7 @@ function EsntlAtchFileList({ esntlAtchFileNm: values.esntlAtchFileNm, esntlAtchYn: values.esntlAtchYn, esntlAtchFileRmrk: values.esntlAtchFileRmrk || undefined, + atchFileGroupId: values.atchFileGroupId || undefined, }; if (dialogMode === "create") { diff --git a/apps/as/src/pages/fnrs-cnsltn/index.ts b/apps/as/src/pages/fnrs-cnsltn/index.ts index 0193bd7c8..0531c9fb8 100644 --- a/apps/as/src/pages/fnrs-cnsltn/index.ts +++ b/apps/as/src/pages/fnrs-cnsltn/index.ts @@ -1,3 +1,4 @@ +export { FnrsDetailGate } from "./ui/fnrs-detail-gate"; export { FnrsEditPage } from "./ui/fnrs-edit-page"; export { FnrsFormPage } from "./ui/fnrs-form-page"; export { FnrsListPage } from "./ui/fnrs-list-page"; diff --git a/apps/as/src/pages/fnrs-cnsltn/lib/ai-pending-utils.ts b/apps/as/src/pages/fnrs-cnsltn/lib/ai-pending-utils.ts new file mode 100644 index 000000000..1ac079216 --- /dev/null +++ b/apps/as/src/pages/fnrs-cnsltn/lib/ai-pending-utils.ts @@ -0,0 +1,21 @@ +const AI_PENDING_STATUS_CODES = ["FV", "AC", "AV"] as const; + +type AiPendingStatusCode = (typeof AI_PENDING_STATUS_CODES)[number]; + +const AI_PENDING_MESSAGES: Record = { + FV: "AI 파일 검증이 진행 중입니다", + AC: "AI 실무협의서 초안 생성이 진행 중입니다", + AV: "AI 금액 검증이 진행 중입니다", +}; + +function isAiPendingStatus(prgrsSttsComCd: string): boolean { + return AI_PENDING_STATUS_CODES.includes(prgrsSttsComCd as AiPendingStatusCode); +} + +function getAiPendingMessage(prgrsSttsComCd: string): string { + if (!isAiPendingStatus(prgrsSttsComCd)) return ""; + return AI_PENDING_MESSAGES[prgrsSttsComCd as AiPendingStatusCode]; +} + +export { AI_PENDING_STATUS_CODES, getAiPendingMessage, isAiPendingStatus }; +export type { AiPendingStatusCode }; diff --git a/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts b/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts index 6fdba8073..1b6ba18b4 100644 --- a/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts +++ b/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts @@ -8,7 +8,10 @@ export const FNRS_CNSLTN_VIEW_BUTTON_ACTION = { sncnAprv: "sncnAprv", rvwRply: "rvwRply", rqstTechRvw: "rqstTechRvw", + fileSplmnt: "fileSplmnt", dsgntRvwr: "dsgntRvwr", + pwrkCnsltnDocCrt: "pwrkCnsltnDocCrt", + pwrkCnsltnDocMng: "pwrkCnsltnDocMng", rvwStrg: "rvwStrg", rvwCmptn: "rvwCmptn", dmndSplmnt: "dmndSplmnt", @@ -43,17 +46,19 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record > = { ER: { - ATRZ01: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.dsgntRvwr], + ATRZ01: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.fileSplmnt, FNRS_CNSLTN_VIEW_BUTTON_ACTION.dsgntRvwr], }, TE: { ATRZ02: [ + FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocCrt, + FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng, FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwStrg, FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwCmptn, FNRS_CNSLTN_VIEW_BUTTON_ACTION.dmndSplmnt, @@ -61,6 +66,9 @@ const ROLE_USER01_ATRZ_STATUS_ACTIONS: Record< ATRZ03: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzAprv], ATRZ99: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzRply], }, + RK: { + ATRZ02: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng], + }, }; function resolveButtonConfigs(actions: FnrsCnsltnViewButtonAction[]): FnrsCnsltnViewButtonAction[] { @@ -69,17 +77,20 @@ function resolveButtonConfigs(actions: FnrsCnsltnViewButtonAction[]): FnrsCnsltn interface GetButtonConfigsOptions { hasPendingApprovalRequest?: boolean; + hasCompletedDraft?: boolean; } +const PENDING_APPROVAL_ACTIONS = new Set([ + FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwCmptn, + FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzAprv, + FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzRply, +]); + function resolveRoleUser01AtrzButtonActions( prgrsSttsComCd: string, atrzSeComCdList: string[], options?: GetButtonConfigsOptions, ): FnrsCnsltnViewButtonAction[] { - if (prgrsSttsComCd === "TE" && options?.hasPendingApprovalRequest === false) { - return []; - } - const atrzStatusMap = ROLE_USER01_ATRZ_STATUS_ACTIONS[prgrsSttsComCd]; if (!atrzStatusMap || atrzSeComCdList.length === 0) return []; @@ -97,7 +108,20 @@ function resolveRoleUser01AtrzButtonActions( } } - return actions; + return actions.filter((action) => { + if (prgrsSttsComCd === "TE" && options?.hasCompletedDraft != null) { + if (action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng) { + return options.hasCompletedDraft; + } + if (action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocCrt) { + return !options.hasCompletedDraft; + } + } + + const needsPendingApproval = PENDING_APPROVAL_ACTIONS.has(action); + + return !needsPendingApproval || options?.hasPendingApprovalRequest !== false; + }); } export function needsRoleUser01AtrzResolve( diff --git a/apps/as/src/pages/fnrs-cnsltn/lib/fnrs-cnsltn-regist-schema.ts b/apps/as/src/pages/fnrs-cnsltn/lib/fnrs-cnsltn-regist-schema.ts index 8bfc0c317..e925ab321 100644 --- a/apps/as/src/pages/fnrs-cnsltn/lib/fnrs-cnsltn-regist-schema.ts +++ b/apps/as/src/pages/fnrs-cnsltn/lib/fnrs-cnsltn-regist-schema.ts @@ -85,7 +85,7 @@ export const fnrsCnsltnRegistSchema = z.object({ ), splmntDmndList: z.array( z.object({ - splmntDmndSn: z.number().optional(), + splmntDmndSn: z.number().nullish(), splmntArtclComCd: z.string().optional(), splmntArtclCdNm: z.string().nullish(), splmntDmndCn: z.string().optional(), diff --git a/apps/as/src/pages/fnrs-cnsltn/lib/form-data-mapper.ts b/apps/as/src/pages/fnrs-cnsltn/lib/form-data-mapper.ts index d30765961..09e4817f5 100644 --- a/apps/as/src/pages/fnrs-cnsltn/lib/form-data-mapper.ts +++ b/apps/as/src/pages/fnrs-cnsltn/lib/form-data-mapper.ts @@ -1,6 +1,6 @@ import type { IUserInfo } from "@/shared/store"; import type { FnrsCnsltnMngDetResponseDto } from "@/entities/fnrs-cnsltn"; -import { mapFnrsSharDetToFormList } from "@/widgets/pbadms-sprt"; +import { mapFnrsSharDetToFormList, toEsntlAtchFileSaveList } from "@/widgets/pbadms-sprt"; import type { FnrsCnsltnRegistFormData } from "./fnrs-cnsltn-regist-schema"; import { parseTaskSeComCdFromApi } from "./fnrs-cnsltn-regist-schema"; @@ -10,20 +10,6 @@ function mapSelectedValue(value?: string | null): "Y" | "N" | null { return null; } -function mapEsntlAtchFileList(esntlFileList: FnrsCnsltnMngDetResponseDto["esntlFileList"]) { - const esntlMap = new Map(); - - 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: FnrsCnsltnMngDetResponseDto): FnrsCnsltnRegistFormData { const detDto = data.detDto; if (!detDto) { @@ -94,7 +80,7 @@ export function mapDetailToFormData(data: FnrsCnsltnMngDetResponseDto): FnrsCnsl selected: mapSelectedValue(item.rvwRsltAnmlyYn), picRvwCn: item.picRvwCn, })) ?? [], - esntlAtchFileList: mapEsntlAtchFileList(esntlFileList), + esntlAtchFileList: toEsntlAtchFileSaveList(esntlFileList), splmntDmndList: splmntDmndList?.map((item) => ({ splmntDmndSn: item.splmntDmndSn, diff --git a/apps/as/src/pages/fnrs-cnsltn/lib/save-payload-utils.ts b/apps/as/src/pages/fnrs-cnsltn/lib/save-payload-utils.ts index fb34d7d0e..6a81db9ee 100644 --- a/apps/as/src/pages/fnrs-cnsltn/lib/save-payload-utils.ts +++ b/apps/as/src/pages/fnrs-cnsltn/lib/save-payload-utils.ts @@ -4,9 +4,8 @@ import type { BizSclTreeDto, BzScaleItem, CklstResultItem, - EsntlAtchFileItem, - EsntlAtchFileMngTreeResponseDto, FnrsCnsltnApplySaveRequestDto, + FnrsCnsltnFileVrfcRsltResponseDto, FnrsCnsltnMngDetResponseDto, FnrsCnsltnMngResponseDto, FnrsCnsltnSplmntDmndItem, @@ -14,6 +13,7 @@ import type { import { mapFnrsSharDetToFormList, type SplmntDmndDialogConfirmPayload, + toEsntlAtchFileSaveList, toFnrsSharSaveList, } from "@/widgets/pbadms-sprt"; @@ -44,22 +44,6 @@ export function toBizCstSaveDto(bizCstList: BizCstTreeDto[]): BizCstItem[] { })); } -export function toEsntlAtchFileSaveDto( - esntlFileList: EsntlAtchFileMngTreeResponseDto[], -): EsntlAtchFileItem[] { - const esntlMap = new Map(); - - 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 buildSavePayload( data: FnrsCnsltnMngDetResponseDto, checklistFormState: ChecklistFormState, @@ -81,7 +65,7 @@ export function buildSavePayload( bzScaleList: toBzScaleSaveDto(bizSclList), bizCstList: toBizCstSaveDto(bizCstList), cklstResultList: toCklstSaveDto(cklstResultList, checklistFormState, reviewStep), - esntlAtchFileList: toEsntlAtchFileSaveDto(esntlFileList), + esntlAtchFileList: toEsntlAtchFileSaveList(esntlFileList), fnrsSharList: toFnrsSharSaveList(mapFnrsSharDetToFormList(data.fnrsSharList ?? [])), }; } @@ -128,6 +112,23 @@ export function buildSupplementReqPayload( }; } +export function buildFileSplmntReqPayload( + data: FnrsCnsltnMngDetResponseDto, + selected: FnrsCnsltnFileVrfcRsltResponseDto[], + checklistFormState: ChecklistFormState, +): FnrsCnsltnApplySaveRequestDto { + const newItems: FnrsCnsltnSplmntDmndItem[] = selected.map((row) => ({ + splmntArtclComCd: row.fileSeComCd, + splmntDmndCn: row.vrfcRsltCn, + cmptnYn: "N", + })); + + return { + ...buildSavePayload(data, checklistFormState, "LOCAL", "SL"), + splmntDmndList: [...newItems], + }; +} + export function toSplmntDmndSaveItem(item: FnrsCnsltnSplmntDmndItem): FnrsCnsltnSplmntDmndItem { return { splmntDmndSn: item.splmntDmndSn, diff --git a/apps/as/src/pages/fnrs-cnsltn/model/columns.tsx b/apps/as/src/pages/fnrs-cnsltn/model/columns.tsx index ba660c342..fff55d21a 100644 --- a/apps/as/src/pages/fnrs-cnsltn/model/columns.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/model/columns.tsx @@ -11,29 +11,33 @@ import { Badge } from "@common/components"; const columnHelper = createColumnHelper(); const MAIN_STEP_LABELS = { - save: "저장", - apply: "신청완료", + apply: "재원협의 신청", + envoReview: "환경청 검토", techReview: "기술검토", reply: "검토결과 회신", } as const; -const TECH_REVIEW_STATUS_CODES = new Set(["LE", "ER", "EE", "TE", "TS", "TR", "TP"]); +export const PROGRESS_STATUS_COLUMNS = [ + { label: "재원협의 신청 (시군)", sttsComCd: "ST" }, + { label: "재원협의 신청 (시도)", sttsComCd: "LE" }, + { label: "환경청 검토", sttsComCd: "ER" }, + { label: "기술검토", sttsComCd: "TE" }, + { label: "기술검토 회신", sttsComCd: "TP" }, + { label: "환경청 검토 결과 회신", sttsComCd: "EP" }, +]; -type BadgeState = "completed" | "current" | "pending" | "action"; +type BadgeState = "completed" | "current" | "pending"; interface ProgressBadge { label: string; state: BadgeState; } -function buildProgressBadges(prgrsSttsComCd: string, prgrsSttsCdNm: string): ProgressBadge[] { - const techReviewLabel = prgrsSttsCdNm || MAIN_STEP_LABELS.techReview; - const replyLabel = prgrsSttsCdNm || MAIN_STEP_LABELS.reply; - +function buildProgressBadges(prgrsSttsComCd: string, techRplyHstryYn: "Y" | "N"): ProgressBadge[] { if (prgrsSttsComCd === "SV") { return [ - { label: MAIN_STEP_LABELS.save, state: "current" }, { label: MAIN_STEP_LABELS.apply, state: "pending" }, + { label: MAIN_STEP_LABELS.envoReview, state: "pending" }, { label: MAIN_STEP_LABELS.techReview, state: "pending" }, { label: MAIN_STEP_LABELS.reply, state: "pending" }, ]; @@ -41,41 +45,61 @@ function buildProgressBadges(prgrsSttsComCd: string, prgrsSttsCdNm: string): Pro if (prgrsSttsComCd === "ST") { return [ - { label: MAIN_STEP_LABELS.save, state: "completed" }, { label: MAIN_STEP_LABELS.apply, state: "current" }, + { label: MAIN_STEP_LABELS.envoReview, state: "pending" }, { label: MAIN_STEP_LABELS.techReview, state: "pending" }, { label: MAIN_STEP_LABELS.reply, state: "pending" }, ]; } - if (TECH_REVIEW_STATUS_CODES.has(prgrsSttsComCd)) { + if (prgrsSttsComCd === "LE") { return [ - { label: MAIN_STEP_LABELS.save, state: "completed" }, { label: MAIN_STEP_LABELS.apply, state: "completed" }, - { label: techReviewLabel, state: "action" }, + { label: MAIN_STEP_LABELS.envoReview, state: "current" }, + { label: MAIN_STEP_LABELS.techReview, state: "pending" }, { label: MAIN_STEP_LABELS.reply, state: "pending" }, ]; } + if (prgrsSttsComCd === "ER" || prgrsSttsComCd === "TE" || prgrsSttsComCd === "SL") { + return [ + { label: MAIN_STEP_LABELS.apply, state: "completed" }, + { label: MAIN_STEP_LABELS.envoReview, state: "completed" }, + { label: MAIN_STEP_LABELS.techReview, state: "current" }, + { label: MAIN_STEP_LABELS.reply, state: "pending" }, + ]; + } + + if (prgrsSttsComCd === "TP") { + return [ + { label: MAIN_STEP_LABELS.apply, state: "completed" }, + { label: MAIN_STEP_LABELS.envoReview, state: "completed" }, + { + label: MAIN_STEP_LABELS.techReview, + state: techRplyHstryYn === "Y" ? "completed" : "pending", + }, + { label: MAIN_STEP_LABELS.reply, state: "current" }, + ]; + } + if (prgrsSttsComCd === "EP") { return [ - { label: MAIN_STEP_LABELS.save, state: "completed" }, { label: MAIN_STEP_LABELS.apply, state: "completed" }, - { label: MAIN_STEP_LABELS.techReview, state: "completed" }, - { label: replyLabel, state: "action" }, - ]; - } - - if (prgrsSttsComCd === "TA" || prgrsSttsComCd === "TB") { - return [ - { label: MAIN_STEP_LABELS.save, state: "completed" }, - { label: MAIN_STEP_LABELS.apply, state: "completed" }, - { label: MAIN_STEP_LABELS.techReview, state: "completed" }, + { label: MAIN_STEP_LABELS.envoReview, state: "completed" }, + { + label: MAIN_STEP_LABELS.techReview, + state: techRplyHstryYn === "Y" ? "completed" : "pending", + }, { label: MAIN_STEP_LABELS.reply, state: "completed" }, ]; } - return [{ label: prgrsSttsCdNm || prgrsSttsComCd || "-", state: "current" }]; + return [ + { label: MAIN_STEP_LABELS.apply, state: "pending" }, + { label: MAIN_STEP_LABELS.envoReview, state: "pending" }, + { label: MAIN_STEP_LABELS.techReview, state: "pending" }, + { label: MAIN_STEP_LABELS.reply, state: "pending" }, + ]; } function getBadgeColor(state: BadgeState) { @@ -86,8 +110,6 @@ function getBadgeColor(state: BadgeState) { return "blue"; case "pending": return "lightGray"; - case "action": - return "orange"; } } @@ -97,11 +119,11 @@ function formatOptional(value: string | undefined) { interface ProgressStepBadgeProps { prgrsSttsComCd: string; - prgrsSttsCdNm: string; + techRplyHstryYn: "Y" | "N"; } -function ProgressStepBadge({ prgrsSttsComCd, prgrsSttsCdNm }: ProgressStepBadgeProps) { - const badges = buildProgressBadges(prgrsSttsComCd, prgrsSttsCdNm); +function ProgressStepBadge({ prgrsSttsComCd, techRplyHstryYn }: ProgressStepBadgeProps) { + const badges = buildProgressBadges(prgrsSttsComCd, techRplyHstryYn); return (
@@ -154,7 +176,7 @@ export function createFnrsCnsltnColumns() { cell: ({ row }) => ( ), }), diff --git a/apps/as/src/pages/fnrs-cnsltn/model/file-vrfc-splmnt-columns.tsx b/apps/as/src/pages/fnrs-cnsltn/model/file-vrfc-splmnt-columns.tsx new file mode 100644 index 000000000..c677c9924 --- /dev/null +++ b/apps/as/src/pages/fnrs-cnsltn/model/file-vrfc-splmnt-columns.tsx @@ -0,0 +1,73 @@ +"use client"; + +import { createColumnHelper } from "@tanstack/react-table"; + +import type { FnrsCnsltnFileVrfcRsltResponseDto } from "@/entities/fnrs-cnsltn"; +import { Checkbox } from "@common/components"; + +const FILE_SE_COM_CD_MAP = { + ESNTL: "필수", + ETC: "기타", +}; + +const columnHelper = createColumnHelper(); + +function formatOptional(value: string | undefined) { + return value?.trim() ? value : "-"; +} + +export function getFileVrfcRsltRowKey(row: FnrsCnsltnFileVrfcRsltResponseDto, index: number) { + if (row.atflId) return row.atflId; + return [row.esntlAtchFileId, row.fileSeComCd, row.orgnlFileNm, String(index)] + .filter(Boolean) + .join("::"); +} + +export function createFileVrfcSplmntColumns() { + return [ + columnHelper.display({ + id: "select", + header: ({ table }) => ( + table.toggleAllRowsSelected(!!checked)} + onClick={(event) => event.stopPropagation()} + aria-label="전체 선택" + /> + ), + meta: { align: "center", width: 48 }, + cell: ({ row }) => ( + row.toggleSelected(!!checked)} + onClick={(event) => event.stopPropagation()} + aria-label="행 선택" + /> + ), + }), + columnHelper.accessor("fileSeComCd", { + header: () =>

파일유형

, + meta: { align: "left" }, + cell: ({ getValue }) => ( +

{formatOptional(FILE_SE_COM_CD_MAP[getValue() as keyof typeof FILE_SE_COM_CD_MAP])}

+ ), + }), + columnHelper.accessor("fileSeNm", { + header: () =>

파일구분

, + meta: { align: "left" }, + cell: ({ getValue }) =>

{formatOptional(getValue())}

, + }), + columnHelper.accessor("vrfcRsltCn", { + header: "검증내역", + meta: { align: "left" }, + cell: ({ getValue }) => formatOptional(getValue()), + }), + ]; +} diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/components/checklist-task-panel.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/components/checklist-task-panel.tsx index 1e4d186e6..6b38af620 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/components/checklist-task-panel.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/components/checklist-task-panel.tsx @@ -46,12 +46,7 @@ function ChecklistTaskPanel({ .forEach((row) => { nextResults[buildChecklistRowKey(row, selectedReviewStep)] = value; }); - return patchChecklistFormState( - prev, - { results: nextResults }, - allRows, - selectedReviewStep, - ); + return patchChecklistFormState(prev, { results: nextResults }, allRows, selectedReviewStep); }); } @@ -77,6 +72,7 @@ function ChecklistTaskPanel({ diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/components/file-vrfc-splmnt-dialog.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/components/file-vrfc-splmnt-dialog.tsx new file mode 100644 index 000000000..1395883d2 --- /dev/null +++ b/apps/as/src/pages/fnrs-cnsltn/ui/components/file-vrfc-splmnt-dialog.tsx @@ -0,0 +1,168 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; + +import type { UseQueryResult } from "@tanstack/react-query"; +import { getCoreRowModel, type RowSelectionState } from "@tanstack/react-table"; + +import { useDataTable } from "@/shared/lib"; +import type { PageResponse } from "@/shared/model"; +import { + type FnrsCnsltnFileVrfcRsltResponseDto, + useFnrsCnsltnFileVrfcResults, +} from "@/entities/fnrs-cnsltn"; +import { + Button, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@common/components"; + +import { + createFileVrfcSplmntColumns, + getFileVrfcRsltRowKey, +} from "../../model/file-vrfc-splmnt-columns"; + +interface FileVrfcSplmntDialogProps { + open: boolean; + fnrsCnsltnDocNo: string; + isSubmitting?: boolean; + onClose: () => void; + onConfirm: (selected: FnrsCnsltnFileVrfcRsltResponseDto[]) => void; +} + +function toFileVrfcPageResponse( + rows: FnrsCnsltnFileVrfcRsltResponseDto[], +): PageResponse { + const content = rows.filter((row) => row.vrfcSttsComCd === "WARNING"); + const totalElements = content.length; + return { + content, + pageable: { + sort: { empty: true, sorted: false, unsorted: true }, + offset: 0, + pageNumber: 0, + pageSize: totalElements || 1, + paged: false, + unpaged: true, + }, + sort: { empty: true, sorted: false, unsorted: true }, + totalPages: 1, + totalElements, + number: 0, + numberOfElements: totalElements, + size: totalElements || 1, + first: true, + last: true, + empty: totalElements === 0, + }; +} + +function FileVrfcSplmntDialog({ + open, + fnrsCnsltnDocNo, + isSubmitting = false, + onClose, + onConfirm, +}: FileVrfcSplmntDialogProps) { + const [rowSelection, setRowSelection] = useState({}); + + const listQuery = useFnrsCnsltnFileVrfcResults(open ? fnrsCnsltnDocNo : null); + + const pageData = useMemo( + () => (listQuery.data ? toFileVrfcPageResponse(listQuery.data) : undefined), + [listQuery.data], + ); + + const pageQuery = useMemo( + () => + ({ + isPending: listQuery.isPending, + isError: listQuery.isError, + isSuccess: listQuery.isSuccess, + data: pageData, + error: listQuery.error, + status: listQuery.status, + fetchStatus: listQuery.fetchStatus, + }) as UseQueryResult>, + [ + listQuery.error, + listQuery.fetchStatus, + listQuery.isError, + listQuery.isPending, + listQuery.isSuccess, + listQuery.status, + pageData, + ], + ); + + const columns = useMemo(() => createFileVrfcSplmntColumns(), []); + + const { tableComponent } = useDataTable({ + query: pageQuery, + tableOptions: { + columns, + state: { rowSelection }, + enableRowSelection: true, + enableMultiRowSelection: true, + getRowId: (row, index) => getFileVrfcRsltRowKey(row, index), + onRowSelectionChange: (updater) => { + const next = typeof updater === "function" ? updater(rowSelection) : updater; + setRowSelection(next); + }, + getCoreRowModel: getCoreRowModel(), + }, + dataTableOptions: { + viewPagination: false, + }, + }); + + const selectedRows = useMemo(() => { + const selectedIds = new Set(Object.keys(rowSelection)); + const content = pageData?.content ?? []; + return content.filter((row, index) => selectedIds.has(getFileVrfcRsltRowKey(row, index))); + }, [pageData?.content, rowSelection]); + + useEffect(() => { + if (!open) setRowSelection({}); + }, [open]); + + function handleOpenChange(nextOpen: boolean) { + if (!nextOpen) onClose(); + } + + function handleConfirm() { + if (selectedRows.length === 0) return; + onConfirm(selectedRows); + } + + return ( + + + + 파일보완 + + +
{tableComponent}
+ + + + + +
+
+ ); +} + +export { FileVrfcSplmntDialog }; +export type { FileVrfcSplmntDialogProps }; diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx index b4461ab1b..e3a6fad4f 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx @@ -22,10 +22,13 @@ import { import { type DocPrgrsUpdateRequestDto, type FnrsCnsltnApplySaveRequestDto, - fnrsCnsltnMngBaseKey, + type FnrsCnsltnFileVrfcRsltResponseDto, + fnrsCnsltnKeys, type FnrsCnsltnMngDetResponseDto, fnrsCnsltnMngService, + type FnrsExamineRequestDto, } from "@/entities/fnrs-cnsltn"; +import { findCompletedPwrkDraft, usePwrkCnsltnMngByUpDocNo } from "@/entities/pwrk-cnsltn-mng"; import { SplmntDmndDialog, type SplmntDmndDialogConfirmPayload } from "@/widgets/pbadms-sprt"; import { Button } from "@common/components"; @@ -37,6 +40,7 @@ import { } from "../../lib/button-config"; import { computeReviewStepChecklistProgress } from "../../lib/checklist-utils"; import { + buildFileSplmntReqPayload, buildSavePayload, buildSupplementReqPayload, buildTechReviewReplyPayload, @@ -45,6 +49,9 @@ import { import { type ChecklistFormState } from "../../model/view-types"; import type { TechReviewOpinionSectionHandle } from "../sections/tech-review-opinion-section"; +import { FileVrfcSplmntDialog } from "./file-vrfc-splmnt-dialog"; +import { PwrkCnsltnDocMngDialog } from "./pwrk-cnsltn-doc-mng-dialog"; + interface FloatingActionButtonsProps { data: FnrsCnsltnMngDetResponseDto; docSeComCd: string; @@ -56,9 +63,17 @@ interface FloatingActionButtonsProps { checklistFormState: ChecklistFormState; techReviewOpinionRef?: React.RefObject; opnrNm?: string; + isAiPending?: boolean; } -type ActiveDialog = "elaprTmplt" | "rjct" | "dmndSplmnt" | "elaprApproval" | null; +type ActiveDialog = + | "elaprTmplt" + | "rjct" + | "dmndSplmnt" + | "fileSplmnt" + | "elaprApproval" + | "pwrkCnsltnDocMng" + | null; function FloatingActionButtons({ data, @@ -71,6 +86,7 @@ function FloatingActionButtons({ checklistFormState, techReviewOpinionRef, opnrNm, + isAiPending = false, }: FloatingActionButtonsProps) { const router = useRouter(); const { confirm } = useConfirm(); @@ -85,8 +101,14 @@ function FloatingActionButtons({ const needsAtrzResolve = needsRoleUser01AtrzResolve(dtlInstCd, prgrsSttsComCd); const needsTeApprovalCheck = needsAtrzResolve && prgrsSttsComCd === "TE"; + const needsPwrkDraftCheck = dtlInstCd === "ROLE_USER01" && ["TE", "RK"].includes(prgrsSttsComCd); const { data: elaprDocPrgrsList = [], isLoading: isElaprDocPrgrsLoading } = useElaprDocPrgrsByDocNo(needsTeApprovalCheck ? docNo : null); + const { data: pwrkCnsltnData, isLoading: isPwrkCnsltnLoading } = usePwrkCnsltnMngByUpDocNo( + docNo, + needsPwrkDraftCheck, + ); + const hasCompletedDraft = Boolean(findCompletedPwrkDraft(pwrkCnsltnData)); const hasPendingApprovalRequest = hasCurrentUserPendingApprovalRequest( elaprDocPrgrsList, @@ -96,17 +118,22 @@ function FloatingActionButtons({ const actionButtons = useMemo(() => { if (needsAtrzResolve && isRvwLoginUserLoading) return []; if (needsTeApprovalCheck && isElaprDocPrgrsLoading) return []; + if (needsPwrkDraftCheck && isPwrkCnsltnLoading) return []; return getButtonConfigs(dtlInstCd, prgrsSttsComCd, atrzSeComCdList, { hasPendingApprovalRequest: needsTeApprovalCheck ? hasPendingApprovalRequest : undefined, + hasCompletedDraft: needsPwrkDraftCheck ? hasCompletedDraft : undefined, }); }, [ atrzSeComCdList, dtlInstCd, + hasCompletedDraft, hasPendingApprovalRequest, isElaprDocPrgrsLoading, + isPwrkCnsltnLoading, isRvwLoginUserLoading, needsAtrzResolve, needsTeApprovalCheck, + needsPwrkDraftCheck, prgrsSttsComCd, ]); @@ -145,7 +172,7 @@ function FloatingActionButtons({ onSuccess: () => { if (successMessage) alert(successMessage); queryClient.invalidateQueries({ - queryKey: [...fnrsCnsltnMngBaseKey, "findById", fnrsCnsltnDocNo], + queryKey: fnrsCnsltnKeys.detail(fnrsCnsltnDocNo), }); }, onError: (error) => { @@ -201,7 +228,6 @@ function FloatingActionButtons({ request: payload, successMessage: "신청 완료되었습니다.", }); - queryClient.invalidateQueries({ queryKey: fnrsCnsltnMngBaseKey }); } async function handleDelClick() { @@ -224,7 +250,6 @@ function FloatingActionButtons({ request: payload, successMessage: "고시되었습니다.", }); - queryClient.invalidateQueries({ queryKey: fnrsCnsltnMngBaseKey }); } async function handleEnvoSbmsnClick() { @@ -238,7 +263,6 @@ function FloatingActionButtons({ request: payload, successMessage: "제출되었습니다.", }); - queryClient.invalidateQueries({ queryKey: fnrsCnsltnMngBaseKey }); } async function handleRjctClick() { @@ -252,7 +276,6 @@ function FloatingActionButtons({ request: payload, successMessage: "반려되었습니다.", }); - queryClient.invalidateQueries({ queryKey: fnrsCnsltnMngBaseKey }); } async function handleSncnAprvClick() { @@ -266,7 +289,6 @@ function FloatingActionButtons({ request: payload, successMessage: "인가승인되었습니다.", }); - queryClient.invalidateQueries({ queryKey: fnrsCnsltnMngBaseKey }); } function getReviewOpinionFormData() { @@ -304,17 +326,14 @@ function FloatingActionButtons({ } async function handleRqstTechRvwClick() { - const payload: DocPrgrsUpdateRequestDto = { - docNo, - docSeComCd, - prgrsSttsComCd: "ER", + const payload: FnrsExamineRequestDto = { + prgrsSttsComCd: "FV", }; await executeAction({ action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstTechRvw, request: payload, successMessage: "기술검토 의뢰가 완료되었습니다.", }); - queryClient.invalidateQueries({ queryKey: fnrsCnsltnMngBaseKey }); } async function handleElaprTmpltConfirm({ aplnRows }: ElaprTmpltPickerConfirmPayload) { @@ -322,6 +341,8 @@ function FloatingActionButtons({ elaprDocNo: docNo, dmndTtl: data.detDto?.bizNm ?? "", dmndCn: data.detDto?.bizCn ?? "", + elaprCycl: data.detDto?.prgrsSttsComCd === "RK" ? 2 : 1, + docPrgrsSttsComCd: "AC", elaprPrgrsSttsList: buildElaprPrgrsSttsListFromAplnRows(aplnRows), }; await executeAction({ @@ -400,6 +421,20 @@ function FloatingActionButtons({ }); } + function handleFileSplmntClick() { + setActiveDialog("fileSplmnt"); + } + + async function handleFileSplmntConfirm(selected: FnrsCnsltnFileVrfcRsltResponseDto[]) { + setActiveDialog(null); + const requestPayload = buildFileSplmntReqPayload(data, selected, checklistFormState); + await executeAction({ + action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.fileSplmnt, + request: requestPayload, + successMessage: "파일보완 요청이 처리되었습니다.", + }); + } + return ( <>
@@ -407,114 +442,156 @@ function FloatingActionButtons({ - {canEdit && ( - - )} - {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.strg) && ( - - )} - {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.aply) && ( - - )} - {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.del) && ( - - )} - {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.fnrsCnsltnAncmnt) && ( - - )} - {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.envoSbmsn) && ( - - )} - {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rjct) && ( - - )} - {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.sncnAprv) && ( - - )} - {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply) && ( - - )} - {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstTechRvw) && ( - - )} - {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.dsgntRvwr) && ( - - )} - {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwStrg) && ( - - )} - {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwCmptn) && ( - - )} - {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.dmndSplmnt) && ( - - )} - {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzAprv) && ( - - )} - {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzRply) && ( - + {!isAiPending && ( + <> + {canEdit && ( + + )} + {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.strg) && ( + + )} + {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.aply) && ( + + )} + {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.del) && ( + + )} + {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.fnrsCnsltnAncmnt) && ( + + )} + {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.envoSbmsn) && ( + + )} + {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rjct) && ( + + )} + {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.sncnAprv) && ( + + )} + {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply) && ( + + )} + {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstTechRvw) && ( + + )} + {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.fileSplmnt) && ( + + )} + {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.dsgntRvwr) && ( + + )} + {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocCrt) && ( + + )} + {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng) && ( + + )} + {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwStrg) && ( + + )} + {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwCmptn) && ( + + )} + {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.dmndSplmnt) && ( + + )} + {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzAprv) && ( + + )} + {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzRply) && ( + + )} + )}
- setActiveDialog(null)} - docSeComCd={docSeComCd} - onConfirm={handleElaprTmpltConfirm} - /> + {!isAiPending && ( + <> + setActiveDialog(null)} + docSeComCd={docSeComCd} + onConfirm={handleElaprTmpltConfirm} + /> - setActiveDialog(null)} - isSubmitting={isProcessing} - onConfirm={handleDmndSplmntConfirm} - /> + setActiveDialog(null)} + isSubmitting={isProcessing} + onConfirm={handleDmndSplmntConfirm} + /> - setActiveDialog(open ? "elaprApproval" : null)} - elaprDocNo={data.detDto?.fnrsCnsltnDocNo ?? fnrsCnsltnDocNo} - currentUserUuid={userInfo?.userUnqId ?? ""} - onSuccess={() => { - queryClient.invalidateQueries({ - queryKey: [...fnrsCnsltnMngBaseKey, "findById", fnrsCnsltnDocNo], - }); - }} - /> + setActiveDialog(null)} + onConfirm={handleFileSplmntConfirm} + /> + + setActiveDialog(open ? "elaprApproval" : null)} + elaprDocNo={data.detDto?.fnrsCnsltnDocNo ?? fnrsCnsltnDocNo} + currentUserUuid={userInfo?.userUnqId ?? ""} + onSuccess={() => { + queryClient.invalidateQueries({ + queryKey: fnrsCnsltnKeys.detail(fnrsCnsltnDocNo), + }); + }} + /> + + setActiveDialog(null)} + /> + + )} ); } @@ -590,6 +667,19 @@ async function handleDmndSplmnt( } } +async function handleRequestExamine( + context: ActionHandlerContext, +): Promise { + try { + const docNo = context.data.detDto?.fnrsCnsltnDocNo; + if (!docNo) throw new Error("문서번호가 없습니다."); + await fnrsCnsltnMngService.requestExamine(docNo, context.request); + context.onSuccess?.(); + } catch (error) { + context.onError?.(error as Error); + } +} + async function handleAtrzAprv(context: ActionHandlerContext): Promise { // TODO: 결재 승인 처리 API 호출 context.onSuccess?.(); @@ -609,8 +699,11 @@ const buttonActionHandlers: Record = rjct: handleChangePrgrsSttsComCd, sncnAprv: handleChangePrgrsSttsComCd, rvwRply: handleTechReview, - rqstTechRvw: handleChangePrgrsSttsComCd, + rqstTechRvw: handleRequestExamine, + fileSplmnt: handleDmndSplmnt, dsgntRvwr: handleDsgntRvwr, + pwrkCnsltnDocCrt: () => Promise.resolve(), + pwrkCnsltnDocMng: () => Promise.resolve(), rvwStrg: handleTechReview, rvwCmptn: handleTechReview, dmndSplmnt: handleDmndSplmnt, diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx new file mode 100644 index 000000000..2e9ac175d --- /dev/null +++ b/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx @@ -0,0 +1,351 @@ +"use client"; + +import { useEffect, useState } from "react"; +import dynamic from "next/dynamic"; + +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { Paperclip, X } from "lucide-react"; + +import { getDefaultFileKey, useAlertStore, useTus } from "@/shared/lib"; +import { TextShimmer } from "@/shared/ui"; +import { + attachService, + parseFilenameFromResponse, + useDownloadAttachmentByAtflId, +} from "@/entities/attach"; +import { + pwrkCnsltnDocCrtRsltSaveRequestSchema, + pwrkCnsltnDocCrtRsltService, +} from "@/entities/pwrk-cnsltn-doc-crt-rslt"; +import { + findCompletedPwrkDraft, + pwrkCnsltnMngKeys, + usePwrkCnsltnMngByUpDocNo, +} from "@/entities/pwrk-cnsltn-mng"; +import { + Button, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + Skeleton, +} from "@common/components"; + +const HangulGiangi = dynamic( + () => import("@/widgets/hangulgiangi").then((module) => module.HangulGiangi), + { + ssr: false, + loading: () => , + }, +); + +const PWRK_ATCH_FILE_KEY_PREFIX = "pwrk-cnsltn-doc::"; + +interface PwrkCnsltnDocMngDialogProps { + open: boolean; + fnrsCnsltnDocNo: string; + onClose: () => void; +} + +interface UploadedAtchFile { + fileName: string; + fileKey: string; + atflId?: string; + atflCd?: string; +} + +function createPwrkAtchUploadKey(file: File) { + return `${PWRK_ATCH_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`; +} + +function PwrkCnsltnDocMngDialog({ open, fnrsCnsltnDocNo, onClose }: PwrkCnsltnDocMngDialogProps) { + const queryClient = useQueryClient(); + const alert = useAlertStore((state) => state.setMessage); + const downloadByAtflId = useDownloadAttachmentByAtflId(); + const [atchFileGroupId, setAtchFileGroupId] = useState(""); + const [uploadedFiles, setUploadedFiles] = useState([]); + + const { + data: pwrkCnsltnData, + isLoading: isPwrkCnsltnLoading, + isError: isPwrkCnsltnError, + } = usePwrkCnsltnMngByUpDocNo(fnrsCnsltnDocNo, open); + const completedDraft = findCompletedPwrkDraft(pwrkCnsltnData); + const crtDmndUuid = completedDraft?.crtDmndUuid; + + const { + data: documentBlob, + isLoading: isDocumentLoading, + isError: isDocumentError, + } = useQuery({ + queryKey: ["pwrk-cnsltn-doc-crt-rslt", "open-latest", crtDmndUuid], + queryFn: () => pwrkCnsltnDocCrtRsltService.openWebHwpLatest(crtDmndUuid!), + select: (response) => response.data, + enabled: open && Boolean(crtDmndUuid), + }); + + const documentFileName = `실무협의서_${pwrkCnsltnData?.pwrkCnsltnDocNo ?? crtDmndUuid}.hwpx`; + const isLoading = isPwrkCnsltnLoading || isDocumentLoading; + const isError = isPwrkCnsltnError || isDocumentError; + + const { uploadFileAsync, isUploading, uploads } = useTus({ + onSuccess: (atflCd, atflId, _file, fileKey) => { + if (!atflCd || !atflId) return; + setAtchFileGroupId(atflCd); + setUploadedFiles((prev) => + prev.map((item) => (item.fileKey === fileKey ? { ...item, atflCd, atflId } : item)), + ); + }, + onError: (_error, _file, fileKey) => { + setUploadedFiles((prev) => prev.filter((item) => item.fileKey !== fileKey)); + }, + }); + + useEffect(() => { + if (!open) { + setAtchFileGroupId(""); + setUploadedFiles([]); + return; + } + + const initialGroupId = pwrkCnsltnData?.atchFileGroupId ?? ""; + setAtchFileGroupId(initialGroupId); + + if (!initialGroupId) { + setUploadedFiles([]); + return; + } + + let cancelled = false; + void attachService.findByAtflCd(initialGroupId).then((files) => { + if (cancelled) return; + setUploadedFiles( + files.map((file) => ({ + fileName: file.orgnlFileNm, + fileKey: `${PWRK_ATCH_FILE_KEY_PREFIX}${file.atflId}`, + atflId: file.atflId, + atflCd: file.atflCd, + })), + ); + }); + + return () => { + cancelled = true; + }; + }, [open, pwrkCnsltnData?.atchFileGroupId]); + + function handleOpenChange(nextOpen: boolean) { + if (!nextOpen) onClose(); + } + + async function handleSaveDocument(file: Blob) { + if (!completedDraft) throw new Error("저장할 실무협의서 초안이 없습니다."); + + const saveDto = pwrkCnsltnDocCrtRsltSaveRequestSchema.parse({ + crtDmndUuid: completedDraft.crtDmndUuid, + pwrkCnsltnDocNo: completedDraft.pwrkCnsltnDocNo, + atchFileGroupId: atchFileGroupId || undefined, + }); + + const savedDocument = await pwrkCnsltnDocCrtRsltService.saveMultipart( + saveDto, + file, + documentFileName, + ); + await Promise.all([ + queryClient.invalidateQueries({ + queryKey: pwrkCnsltnMngKeys.byUpDocNo(fnrsCnsltnDocNo), + }), + queryClient.invalidateQueries({ + queryKey: ["pwrk-cnsltn-doc-crt-rslt", "open-latest", completedDraft.crtDmndUuid], + }), + ]); + alert(`실무협의서를 저장했습니다. (버전 ${savedDocument.verSn})`); + } + + async function handleDownload() { + if (!completedDraft) throw new Error("다운로드할 실무협의서 초안이 없습니다."); + + const response = await pwrkCnsltnDocCrtRsltService.downloadLatest(completedDraft.crtDmndUuid); + const objectUrl = window.URL.createObjectURL(response.data); + const anchor = document.createElement("a"); + anchor.href = objectUrl; + anchor.download = parseFilenameFromResponse(response); + document.body.appendChild(anchor); + anchor.click(); + anchor.remove(); + window.URL.revokeObjectURL(objectUrl); + } + + async function handleAtchFileUpload(files: FileList | null) { + if (!files || files.length === 0) return; + + const fileArray = Array.from(files); + let groupId = atchFileGroupId; + + for (const file of fileArray) { + const fileKey = createPwrkAtchUploadKey(file); + + setUploadedFiles((prev) => [...prev, { fileName: file.name, fileKey }]); + + try { + const atflCd = await uploadFileAsync(file, { + fileKey, + metadata: groupId ? { atflCd: groupId } : undefined, + }); + + if (!groupId && atflCd) { + groupId = atflCd; + setAtchFileGroupId(atflCd); + } + } catch { + // onError callback handles cleanup + } + } + } + + async function handleAtchFileRemove(fileIndex: number) { + const fileToRemove = uploadedFiles[fileIndex]; + if (!fileToRemove) return; + + uploads[fileToRemove.fileKey]?.abort(); + + if (fileToRemove.atflId) { + try { + await attachService.deleteAttachmentByAtflId(fileToRemove.atflId); + } catch (error) { + console.error("실무협의서 첨부파일 삭제 실패:", error); + alert("첨부파일 삭제에 실패했습니다."); + return; + } + } + + const nextFiles = uploadedFiles.filter((_, index) => index !== fileIndex); + setUploadedFiles(nextFiles); + + if (nextFiles.length === 0) { + setAtchFileGroupId(""); + } + } + + function handleAtchFileDownload(atflId?: string) { + if (!atflId) return; + downloadByAtflId.mutate(atflId); + } + + const isAnyAtchUploading = uploadedFiles.some((item) => isUploading[item.fileKey]); + + return ( + + + + 실무협의서 관리 + + + {isLoading ? : null} + {isError ? ( +

+ 실무협의서 정보를 불러오지 못했습니다. +

+ ) : null} + {!isLoading && !isError && !completedDraft ? ( +

+ 생성 완료된 실무협의서 초안이 없습니다. +

+ ) : null} + {completedDraft ? ( + documentBlob ? ( + + ) : null + ) : null} + + {completedDraft ? ( +
+
+

첨부파일

+ +
+ {uploadedFiles.length > 0 ? ( +
+ {uploadedFiles.map((item, index) => { + const uploading = isUploading[item.fileKey] ?? false; + + return ( +
+ + + +
+ ); + })} +
+ ) : ( +

등록된 첨부파일이 없습니다.

+ )} +
+ ) : null} + + + + +
+
+ ); +} + +export { PwrkCnsltnDocMngDialog }; +export type { PwrkCnsltnDocMngDialogProps }; diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-detail-gate.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-detail-gate.tsx new file mode 100644 index 000000000..55fbb04ad --- /dev/null +++ b/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-detail-gate.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { useUserStore } from "@/shared/lib"; +import { useFnrsCnsltnMng } from "@/entities/fnrs-cnsltn"; +import { canEditPbadmsDoc, PbadmsDetailGate } from "@/widgets/pbadms-sprt"; + +import { FnrsViewPage } from "./fnrs-view-page"; + +interface FnrsDetailGateProps { + id: string; +} + +function FnrsDetailGate({ id }: FnrsDetailGateProps) { + const userInfo = useUserStore((state) => state.userInfo); + const { data, isLoading, isFetching, isError } = useFnrsCnsltnMng(id); + const canEdit = canEditPbadmsDoc({ + docSeComCd: "FNRS", + prgrsSttsComCd: data?.detDto?.prgrsSttsComCd, + rgtr: data?.detDto?.rgtr, + loggedInUserUnqId: userInfo?.userUnqId, + }); + + return ( + + + + ); +} + +export { FnrsDetailGate }; diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-edit-page.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-edit-page.tsx index f3fc0090a..01d08f11d 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-edit-page.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-edit-page.tsx @@ -35,7 +35,7 @@ function FnrsEditPage({ id }: FnrsEditPageProps) { return
상세 정보를 찾을 수 없습니다.
; } - return ; + return ; } export { FnrsEditPage }; diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-form-page.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-form-page.tsx index e85efd562..671b70f98 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-form-page.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-form-page.tsx @@ -9,10 +9,12 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useQueryClient } from "@tanstack/react-query"; import { useAlertStore, useUserStore } from "@/shared/lib"; +import { SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui"; import { useAcntgSeMngTree } from "@/entities/acntg"; +import { useEsntlAtchFileMngTree } from "@/entities/esntl"; import { type FnrsCnsltnApplySaveRequestDto, - fnrsCnsltnMngBaseKey, + fnrsCnsltnKeys, type FnrsCnsltnMngDetResponseDto, fnrsCnsltnMngService, } from "@/entities/fnrs-cnsltn"; @@ -27,7 +29,9 @@ import { ContactInfoFormSection, FnrsSharFormSection, isFormChecklistComplete, + isFormEsntlAtchComplete, PrivacyConsentSection, + ProgressStatusSection, recalculateFnrsSharTotals, SplmntDmndFormSection, toFnrsSharSaveList, @@ -41,10 +45,10 @@ import { formatTaskSeComCdForApi, } from "../lib/fnrs-cnsltn-regist-schema"; import { getCreateDefaultValues, mapDetailToFormData } from "../lib/form-data-mapper"; +import { PROGRESS_STATUS_COLUMNS } from "../model/columns"; import { BasicInfoFormSection } from "./sections/basic-info-form-section"; interface FnrsFormPageProps { - id?: string; mode: "create" | "edit"; initialData?: FnrsCnsltnMngDetResponseDto; createDefaults?: { @@ -55,7 +59,7 @@ interface FnrsFormPageProps { }; } -function FnrsFormPage({ id, mode, initialData, createDefaults }: FnrsFormPageProps) { +function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps) { const queryClient = useQueryClient(); const router = useRouter(); const { confirm } = useConfirm(); @@ -67,6 +71,7 @@ function FnrsFormPage({ id, mode, initialData, createDefaults }: FnrsFormPagePro const { data: masterSharTree = [] } = useFnrsSharSeMngTree(); const { data: acntgTree = [] } = useAcntgSeMngTree(); + const { data: essentialFiles = [] } = useEsntlAtchFileMngTree({ docSeComCd: "FNRS" }); const sharTree = isEditMode && initialData?.fnrsSharList?.length ? initialData.fnrsSharList : masterSharTree; const sharItems = useMemo(() => buildFnrsSharItems(sharTree), [sharTree]); @@ -83,7 +88,7 @@ function FnrsFormPage({ id, mode, initialData, createDefaults }: FnrsFormPagePro function buildFormPayload( data: FnrsCnsltnRegistFormData, - prgrsSttsComCd: "SV" | "ST" | "TE", + prgrsSttsComCd: string, ): FnrsCnsltnApplySaveRequestDto | undefined { const withTotals = recalculateFnrsSharTotals(data.fnrsSharList, sharItems); const validationMessage = validateBizCstSharBalance( @@ -118,7 +123,7 @@ function FnrsFormPage({ id, mode, initialData, createDefaults }: FnrsFormPagePro // eslint-disable-next-line @typescript-eslint/no-unused-vars ({ splmntAtchFileNm, splmntAtchFileAtflId, splmntDmndAtchFileNm, ...item }) => ({ ...item, - ...(prgrsSttsComCd === "TE" ? { cmptnYn: "Y" } : null), + ...(prgrsSttsComCd !== "SV" && prgrsSttsComCd !== "ST" && { cmptnYn: "Y" }), }), ), }; @@ -134,11 +139,12 @@ function FnrsFormPage({ id, mode, initialData, createDefaults }: FnrsFormPagePro } async function submitSupplementForm(data: FnrsCnsltnRegistFormData): Promise { - const docNo = initialData?.detDto?.fnrsCnsltnDocNo; - if (!docNo) return undefined; + const beforePrgrsSttsComCd = initialData?.detDto?.beforePrgrsSttsComCd; + if (!beforePrgrsSttsComCd) return undefined; - const payload = buildFormPayload(data, "TE"); - if (!payload) return undefined; + const docNo = initialData?.detDto?.fnrsCnsltnDocNo; + const payload = buildFormPayload(data, beforePrgrsSttsComCd); + if (!docNo || !payload) return undefined; return fnrsCnsltnMngService.submitSupplement(docNo, payload); } @@ -166,11 +172,8 @@ function FnrsFormPage({ id, mode, initialData, createDefaults }: FnrsFormPagePro if (!result) return; alert("보완완료 처리되었습니다."); + await queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all }); router.push(`/fnrs-cnsltn/${docNo}`); - queryClient.invalidateQueries({ queryKey: fnrsCnsltnMngBaseKey }); - queryClient.invalidateQueries({ - queryKey: [...fnrsCnsltnMngBaseKey, "findById", docNo], - }); } catch (error) { console.error("보완완료 실패:", error); alert("보완완료 처리 중 오류가 발생했습니다."); @@ -188,12 +191,8 @@ function FnrsFormPage({ id, mode, initialData, createDefaults }: FnrsFormPagePro if (!fnrsCnsltnDocNo) return; alert("저장되었습니다."); + await queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all }); router.push(`/fnrs-cnsltn/${fnrsCnsltnDocNo}/edit`); - if (isEditMode && id) { - queryClient.invalidateQueries({ - queryKey: [...fnrsCnsltnMngBaseKey, "findById", id], - }); - } } catch (error) { console.error("저장 실패:", error); alert("저장 중 오류가 발생했습니다."); @@ -210,19 +209,19 @@ function FnrsFormPage({ id, mode, initialData, createDefaults }: FnrsFormPagePro return; } + if (!isFormEsntlAtchComplete(essentialFiles, data.esntlAtchFileList)) { + alert("필수 첨부파일을 모두 등록해야 신청이 가능합니다."); + return; + } + try { setIsSubmitting(true); const fnrsCnsltnDocNo = await submitForm(data, "ST"); if (!fnrsCnsltnDocNo) return; alert("신청 완료되었습니다."); + await queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all }); router.push(`/fnrs-cnsltn/${fnrsCnsltnDocNo}`); - queryClient.invalidateQueries({ queryKey: fnrsCnsltnMngBaseKey }); - if (id) { - queryClient.invalidateQueries({ - queryKey: [...fnrsCnsltnMngBaseKey, "findById", id], - }); - } } catch (error) { console.error("신청 실패:", error); alert("신청 중 오류가 발생했습니다."); @@ -250,6 +249,7 @@ function FnrsFormPage({ id, mode, initialData, createDefaults }: FnrsFormPagePro setIsSubmitting(true); await fnrsCnsltnMngService.remove(docNo); alert("삭제되었습니다."); + await queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all }); router.push("/fnrs-cnsltn"); } catch (error) { console.error("삭제 실패:", error); @@ -261,72 +261,86 @@ function FnrsFormPage({ id, mode, initialData, createDefaults }: FnrsFormPagePro return ( -
- - - - - - - - - - -
-
- - {!isSplmntPrgrsStts ? ( - <> - - {isEditMode && ( - <> - - - - )} - - ) : ( - - )} + + +
+
-
+
+ {initialData?.detDto?.fnrsCnsltnDocNo && ( + + )} + + + + + + + + + + +
+
+ + {!isSplmntPrgrsStts ? ( + <> + + {isEditMode && ( + <> + + + + )} + + ) : ( + + )} +
+
+
+ ); diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-list.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-list.tsx index 6f584074d..e09a86b1a 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-list.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-list.tsx @@ -5,7 +5,7 @@ import { useRouter } from "next/navigation"; import { getCoreRowModel, type PaginationState } from "@tanstack/react-table"; -import { useDataTable, useUserStore } from "@/shared/lib"; +import { useDataTable } from "@/shared/lib"; import { FnrsCnsltnMngResponseDto, type FnrsCnsltnMngSearchDto, @@ -24,7 +24,6 @@ interface FnrsListProps { } function FnrsList({ searchParams, pagination, onPaginationChange }: FnrsListProps) { - const userInfo = useUserStore((state) => state.userInfo); const router = useRouter(); const [isTaskSeDialogOpen, setIsTaskSeDialogOpen] = useState(false); @@ -44,13 +43,7 @@ function FnrsList({ searchParams, pagination, onPaginationChange }: FnrsListProp const columns = useMemo(() => createFnrsCnsltnColumns(), []); const handleRowClick = (row: FnrsCnsltnMngResponseDto) => { - const canEdit = - (row.prgrsSttsComCd === "SV" || row.prgrsSttsComCd === "SL") && - row.rgtr === userInfo?.userUnqId; - - router.push( - canEdit ? `/fnrs-cnsltn/${row.fnrsCnsltnDocNo}/edit` : `/fnrs-cnsltn/${row.fnrsCnsltnDocNo}`, - ); + router.push(`/fnrs-cnsltn/${row.fnrsCnsltnDocNo}`); }; const { tableComponent } = useDataTable({ diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-view-page.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-view-page.tsx index 3ec2522d1..2e85018b0 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-view-page.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-view-page.tsx @@ -3,18 +3,24 @@ import { useMemo, useRef } from "react"; import { useUserStore } from "@/shared/lib"; +import { SectionCollapsibleGroup, SectionCollapsibleToggle, TextShimmer } from "@/shared/ui"; import { ElaprDocPrgrsStatus } from "@/entities/elapr"; import { useFnrsCnsltnMng } from "@/entities/fnrs-cnsltn"; import { useRvwMnpwMngLoginUserList } from "@/entities/rvw-mnpw"; -import { SectionCollapsible, SupplementDemandSection } from "@/widgets/pbadms-sprt"; +import { + AttachmentsViewSection, + ProgressStatusSection, + SupplementDemandSection, +} from "@/widgets/pbadms-sprt"; import { Skeleton } from "@common/components"; +import { getAiPendingMessage, isAiPendingStatus } from "../lib/ai-pending-utils"; import { needsRoleUser01AtrzResolve } from "../lib/button-config"; import { getEditableReviewStep } from "../lib/review-step-utils"; import { EMPTY_CKLST_RESULT_LIST, useChecklistFormState } from "../lib/use-checklist-form-state"; +import { PROGRESS_STATUS_COLUMNS } from "../model/columns"; import { FloatingActionButtons } from "./components/floating-action-buttons"; -import { AttachmentsSection } from "./sections/attachments-section"; import { BasicInfoSection } from "./sections/basic-info-section"; import { BusinessCostSection } from "./sections/business-cost-section"; import { BusinessScaleSection } from "./sections/business-scale-section"; @@ -71,59 +77,85 @@ function FnrsViewPage({ id }: FnrsViewPageProps) { return
상세 정보를 찾을 수 없습니다.
; } - const detDto = data.detDto; - const bizSclList = data.bizSclList ?? []; - const bizCstList = data.bizCstList ?? []; - const cklstResultList = data.cklstResultList ?? []; - const esntlFileList = data.esntlFileList ?? []; - const etcFileList = data.etcFileList ?? []; - const rvwList = data.rvwList ?? []; - const splmntDmndList = data.splmntDmndList ?? []; - const fnrsSharList = data.fnrsSharList ?? []; + const { + detDto, + bizSclList = [], + bizCstList = [], + cklstResultList = [], + esntlFileList = [], + etcFileList = [], + rvwList = [], + splmntDmndList = [], + fnrsSharList = [], + sttsChgHstryList = [], + } = data; const editableReviewStep = getEditableReviewStep( userInfo?.dtlInstCd, detDto.prgrsSttsComCd || "", ); + const isAiPending = isAiPendingStatus(detDto.prgrsSttsComCd || ""); return ( -
- - - - - - - - - - - - - +
+ +
+ + {isAiPending && ( +
+ + + + + + {getAiPendingMessage(detDto.prgrsSttsComCd || "")} + +
+ )} +
+
+ + + + + + + + + + + + +
+
); } diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/sections/attachments-section.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/sections/attachments-section.tsx deleted file mode 100644 index 3dbbdf1fd..000000000 --- a/apps/as/src/pages/fnrs-cnsltn/ui/sections/attachments-section.tsx +++ /dev/null @@ -1,118 +0,0 @@ -"use client"; - -import { formatFileSize } from "@/shared/lib/file-utils"; -import { useDownloadAttachmentByAtflCd } from "@/entities/attach"; -import type { - AtflMngClientResponseDto, - EsntlAtchFileMngTreeResponseDto, -} from "@/entities/fnrs-cnsltn"; -import { SectionCollapsible } from "@/widgets/pbadms-sprt"; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components"; - -interface AttachmentsSectionProps { - esntlFileList: EsntlAtchFileMngTreeResponseDto[]; - etcFileList: AtflMngClientResponseDto[]; -} - -function AttachmentsSection({ esntlFileList, etcFileList }: AttachmentsSectionProps) { - const { mutate: downloadAttachment } = useDownloadAttachmentByAtflCd(); - - return ( - <> - -
- - - - 필수 자료명 - 파일명 - 크기 - - - - {esntlFileList.length === 0 ? ( - - - 조회된 필수 첨부파일 항목이 없습니다. - - - ) : ( - esntlFileList.map((file) => ( - - - {file.esntlAtchFileNm} - {file.esntlAtchYn === "Y" ? ( - * - ) : null} - - - {file.atflCd ? ( - downloadAttachment(file.atflCd ?? "")} - target="_blank" - rel="noreferrer" - > - {file.orgnlFileNm || "다운로드"} - - ) : ( - 첨부없음 - )} - - {formatFileSize(file.atflFlieSz)} - - )) - )} - -
-
-
- - -
- - - - 파일명 - 크기 - - - - {etcFileList.length === 0 ? ( - - - 등록된 기타첨부파일이 없습니다. - - - ) : ( - etcFileList.map((file) => ( - - - {file.atflCd ? ( - downloadAttachment(file.atflCd)} - target="_blank" - rel="noreferrer" - > - {file.orgnlFileNm || file.physFileNm || file.atflId} - - ) : ( - {file.orgnlFileNm || file.physFileNm || file.atflId} - )} - - - {formatFileSize(file.atflFlieSz)} - - - )) - )} - -
-
-
- - ); -} - -export { AttachmentsSection }; diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/sections/basic-info-form-section.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/sections/basic-info-form-section.tsx index 751312ac6..759b1f0df 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/sections/basic-info-form-section.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/sections/basic-info-form-section.tsx @@ -3,10 +3,10 @@ import { useState } from "react"; import { Controller, useFormContext } from "react-hook-form"; +import { SectionCollapsible } from "@/shared/ui"; import { Field, FieldError, FieldGroup, FieldLabel, FieldSet } from "@/shared/ui/field"; import { findCodeLabel, useCodeList } from "@/entities/code"; import { type BnsApplyData, BnsPickerDialog } from "@/widgets/bns-picker"; -import { SectionCollapsible } from "@/widgets/pbadms-sprt"; import { Button, Checkbox, diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/sections/basic-info-section.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/sections/basic-info-section.tsx index 939af68fd..48f14eceb 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/sections/basic-info-section.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/sections/basic-info-section.tsx @@ -1,7 +1,7 @@ "use client"; +import { SectionCollapsible } from "@/shared/ui"; import type { FnrsCnsltnMngResponseDto } from "@/entities/fnrs-cnsltn"; -import { SectionCollapsible } from "@/widgets/pbadms-sprt"; import { Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components"; interface BasicInfoSectionProps { diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/sections/business-cost-section.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/sections/business-cost-section.tsx index 285fe8bed..d892187f6 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/sections/business-cost-section.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/sections/business-cost-section.tsx @@ -2,9 +2,9 @@ import { useMemo } from "react"; +import { SectionCollapsible } from "@/shared/ui"; import type { BizCstTreeDto } from "@/entities/fnrs-cnsltn"; -import { calcDiffPercent, fmtNumber, indentPx } from "@/widgets/pbadms-sprt"; -import { SectionCollapsible } from "@/widgets/pbadms-sprt"; +import { fmtNumber, indentPx } from "@/widgets/pbadms-sprt"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components"; import { sortBizCstListByTreePath } from "../../lib/business-cost-utils"; @@ -46,10 +46,6 @@ function BusinessCostSection({ bizCstList }: BusinessCostSectionProps) { ) : ( sortedBizCstList.map((node) => { const pl = indentPx(node.indAcntgSeCdNm); - const bscPlanAmt = Number(node.bscPlanAmt) || 0; - const budgetReflct = Number(node.bgtRfltAmt) || 0; - const tstmDesignAmt = Number(node.tstmDesignAmt) || 0; - const diff = tstmDesignAmt - budgetReflct; const showSubIcon = node.treeLevel >= 3; return ( @@ -63,21 +59,21 @@ function BusinessCostSection({ bizCstList }: BusinessCostSectionProps) { )} -
{fmtNumber(bscPlanAmt)}
+
{fmtNumber(node.bgtRfltAmt)}
-
{fmtNumber(budgetReflct)}
+
{fmtNumber(node.bscPlanAmt)}
-
{fmtNumber(tstmDesignAmt)}
+
{fmtNumber(node.tstmDesignAmt)}
- {diff > 0 ? `+${fmtNumber(diff)}` : fmtNumber(diff)} + {node.chgAmt > 0 ? `+${fmtNumber(node.chgAmt)}` : fmtNumber(node.chgAmt)}
-
{calcDiffPercent(diff, budgetReflct)}
+
{fmtNumber(node.chgRt)}%
); diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/sections/business-scale-section.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/sections/business-scale-section.tsx index f8849cf8f..46cd6418d 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/sections/business-scale-section.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/sections/business-scale-section.tsx @@ -1,8 +1,8 @@ "use client"; +import { SectionCollapsible } from "@/shared/ui"; import type { BizSclTreeDto } from "@/entities/fnrs-cnsltn"; import { fmtNumber, indentPx } from "@/widgets/pbadms-sprt"; -import { SectionCollapsible } from "@/widgets/pbadms-sprt"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components"; interface BusinessScaleSectionProps { @@ -33,10 +33,6 @@ function BusinessScaleSection({ bizSclList }: BusinessScaleSectionProps) { ) : ( bizSclList.map((node) => { const pl = indentPx(node.indCnstpSeCdNm); - const bscPlanQty = Number(node.bscPlanQty) || 0; - const budgetReflct = Number(node.bgtRfltQty) || 0; - const tstmDesignQty = Number(node.tstmDesignQty) || 0; - const diff = tstmDesignQty - budgetReflct; return ( @@ -44,17 +40,17 @@ function BusinessScaleSection({ bizSclList }: BusinessScaleSectionProps) { {node.cnstpSeCdNm} -
{fmtNumber(bscPlanQty)}
+
{fmtNumber(node.bscPlanQty)}
-
{fmtNumber(budgetReflct)}
+
{fmtNumber(node.bgtRfltQty)}
-
{fmtNumber(tstmDesignQty)}
+
{fmtNumber(node.tstmDesignQty)}
- {diff > 0 ? `+${fmtNumber(diff)}` : fmtNumber(diff)} + {node.chgQty > 0 ? `+${fmtNumber(node.chgQty)}` : fmtNumber(node.chgQty)}
diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/sections/checklist-section.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/sections/checklist-section.tsx index 80671876a..e50f5a6b8 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/sections/checklist-section.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/sections/checklist-section.tsx @@ -2,8 +2,8 @@ import { type Dispatch, type SetStateAction, useEffect, useMemo, useState } from "react"; +import { SectionCollapsible } from "@/shared/ui"; import type { CklstRvwRsltTreeDto } from "@/entities/fnrs-cnsltn"; -import { SectionCollapsible } from "@/widgets/pbadms-sprt"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; import { @@ -71,6 +71,10 @@ function ChecklistSection({ const { total, completed, percent } = progress; + if (cklstResultList.length === 0) { + return null; + } + return (
diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/sections/contact-info-section.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/sections/contact-info-section.tsx index 7443460e8..f5273fb33 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/sections/contact-info-section.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/sections/contact-info-section.tsx @@ -1,7 +1,7 @@ "use client"; +import { SectionCollapsible } from "@/shared/ui"; import type { FnrsCnsltnMngResponseDto } from "@/entities/fnrs-cnsltn"; -import { SectionCollapsible } from "@/widgets/pbadms-sprt"; import { Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components"; interface ContactInfoSectionProps { diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/sections/fnrs-shar-section.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/sections/fnrs-shar-section.tsx index 198261c41..3d4e3aa93 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/sections/fnrs-shar-section.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/sections/fnrs-shar-section.tsx @@ -2,8 +2,9 @@ import { useMemo } from "react"; +import { SectionCollapsible } from "@/shared/ui"; import type { FnrsSharBizCstTreeDto } from "@/entities/fnrs-shar-se"; -import { fmtNumber, indentPx, SectionCollapsible } from "@/widgets/pbadms-sprt"; +import { fmtNumber, indentPx } from "@/widgets/pbadms-sprt"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components"; interface FnrsSharSectionProps { @@ -60,8 +61,14 @@ function FnrsSharSection({ fnrsSharList }: FnrsSharSectionProps) {
{fmtNumber(node.chgRqtAmt)}
- - + +
+ {node.chgAmt > 0 ? `+${fmtNumber(node.chgAmt)}` : fmtNumber(node.chgAmt)} +
+
+ +
{fmtNumber(node.chgRt)}%
+
{node.rmrkCn ?? ""} ); diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/sections/tech-review-opinion-section.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/sections/tech-review-opinion-section.tsx index bc7a51af6..bdbd4e622 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/sections/tech-review-opinion-section.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/sections/tech-review-opinion-section.tsx @@ -2,12 +2,15 @@ import { forwardRef, useEffect, useImperativeHandle, useState } from "react"; +import { Paperclip } from "lucide-react"; + import { PROXY_HOST } from "@/shared/config"; import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib"; +import { TextShimmer } from "@/shared/ui"; +import { SectionCollapsible } from "@/shared/ui"; import { Field, FieldLabel, FieldSet } from "@/shared/ui/field"; import { attachService } from "@/entities/attach"; import type { FnrsCnsltnRvwOpnnResponseDto } from "@/entities/fnrs-cnsltn"; -import { SectionCollapsible } from "@/widgets/pbadms-sprt"; import { Button, RadioGroup, @@ -71,12 +74,10 @@ function hasFormContent(formData: ReviewOpinionFormData) { const TechReviewOpinionSection = forwardRef< TechReviewOpinionSectionHandle, TechReviewOpinionSectionProps ->(function TechReviewOpinionSection({ - rvwList, - editableReviewStep, - atrzSeComCdList = [], - isAtrzLoading = false, -}, ref) { +>(function TechReviewOpinionSection( + { rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false }, + ref, +) { const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02"); const [formData, setFormData] = useState({ ...EMPTY_FORM_DATA, @@ -247,7 +248,9 @@ const TechReviewOpinionSection = forwardRef< {uploadedFile ? (
- {uploadedFile.fileName} + + {uploadedFile.fileName} + diff --git a/apps/as/src/pages/instl-sncn/ui/components/floating-action-buttons.tsx b/apps/as/src/pages/instl-sncn/ui/components/floating-action-buttons.tsx index e3c0d8119..1334c93b8 100644 --- a/apps/as/src/pages/instl-sncn/ui/components/floating-action-buttons.tsx +++ b/apps/as/src/pages/instl-sncn/ui/components/floating-action-buttons.tsx @@ -334,6 +334,8 @@ function FloatingActionButtons({ elaprDocNo: docNo, dmndTtl: data.detDto.bizNm ?? "", dmndCn: data.detDto.bizCn ?? "", + elaprCycl: 1, + docPrgrsSttsComCd: data.detDto?.prgrsSttsComCd, elaprPrgrsSttsList: buildElaprPrgrsSttsListFromAplnRows(aplnRows), }; @@ -511,6 +513,7 @@ function FloatingActionButtons({ /> setActiveDialog(null)} isSubmitting={isProcessing} diff --git a/apps/as/src/pages/instl-sncn/ui/instl-detail-gate.tsx b/apps/as/src/pages/instl-sncn/ui/instl-detail-gate.tsx new file mode 100644 index 000000000..321f92940 --- /dev/null +++ b/apps/as/src/pages/instl-sncn/ui/instl-detail-gate.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { useUserStore } from "@/shared/lib"; +import { useInstlSncnDetail } from "@/entities/instl-sncn"; +import { canEditPbadmsDoc, PbadmsDetailGate } from "@/widgets/pbadms-sprt"; + +import { InstlViewPage } from "./instl-view-page"; + +interface InstlSncnDetailGateProps { + id: string; +} + +function InstlSncnDetailGate({ id }: InstlSncnDetailGateProps) { + const userInfo = useUserStore((state) => state.userInfo); + const { data, isLoading, isError } = useInstlSncnDetail(id); + const shouldRedirectToEdit = canEditPbadmsDoc({ + docSeComCd: "INSTL", + prgrsSttsComCd: data?.detDto.prgrsSttsComCd, + rgtr: data?.detDto.rgtr, + loggedInUserUnqId: userInfo?.userUnqId, + }); + + return ( + + + + ); +} + +export { InstlSncnDetailGate }; diff --git a/apps/as/src/pages/instl-sncn/ui/instl-edit-page.tsx b/apps/as/src/pages/instl-sncn/ui/instl-edit-page.tsx index 9f432fd5e..bc6737369 100644 --- a/apps/as/src/pages/instl-sncn/ui/instl-edit-page.tsx +++ b/apps/as/src/pages/instl-sncn/ui/instl-edit-page.tsx @@ -35,7 +35,7 @@ function InstlEditPage({ id }: InstlEditPageProps) { return
수정 대상을 찾을 수 없습니다.
; } - return ; + return ; } export { InstlEditPage }; diff --git a/apps/as/src/pages/instl-sncn/ui/instl-form-page.tsx b/apps/as/src/pages/instl-sncn/ui/instl-form-page.tsx index 766712e35..adf563c65 100644 --- a/apps/as/src/pages/instl-sncn/ui/instl-form-page.tsx +++ b/apps/as/src/pages/instl-sncn/ui/instl-form-page.tsx @@ -9,6 +9,8 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useQueryClient } from "@tanstack/react-query"; import { useAlertStore, useUserStore } from "@/shared/lib"; +import { SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui"; +import { useEsntlAtchFileMngTree } from "@/entities/esntl"; import { type InstlSncnApplySaveRequestDto, instlSncnKeys, @@ -22,7 +24,9 @@ import { ChecklistFormSection, ContactInfoFormSection, isFormChecklistComplete, + isFormEsntlAtchComplete, PrivacyConsentSection, + ProgressStatusSection, SplmntDmndFormSection, } from "@/widgets/pbadms-sprt"; import { Button } from "@common/components"; @@ -33,10 +37,10 @@ import { type InstlSncnRegistFormData, instlSncnRegistSchema, } from "../lib/instl-sncn-regist-schema"; +import { PROGRESS_STATUS_COLUMNS } from "../model/columns"; import { BasicInfoFormSection } from "./sections/basic-info-form-section"; interface InstlFormPageProps { - id?: string; mode: "create" | "edit"; initialData?: InstlSncnMngDetResponse; createDefaults?: { @@ -47,7 +51,7 @@ interface InstlFormPageProps { }; } -function InstlFormPage({ id, mode, initialData, createDefaults }: InstlFormPageProps) { +function InstlFormPage({ mode, initialData, createDefaults }: InstlFormPageProps) { const queryClient = useQueryClient(); const router = useRouter(); const { confirm } = useConfirm(); @@ -57,6 +61,8 @@ function InstlFormPage({ id, mode, initialData, createDefaults }: InstlFormPageP const isEditMode = mode === "edit"; const isSplmntPrgrsStts = initialData?.detDto.prgrsSttsComCd === "SL"; + const { data: essentialFiles = [] } = useEsntlAtchFileMngTree({ docSeComCd: "INSTL" }); + const form = useForm({ resolver: zodResolver(instlSncnRegistSchema), mode: "onChange", @@ -133,8 +139,7 @@ function InstlFormPage({ id, mode, initialData, createDefaults }: InstlFormPageP alert("보완완료 처리되었습니다."); router.push(`/instl-sncn/${docNo}`); - queryClient.invalidateQueries({ queryKey: instlSncnKeys.list() }); - queryClient.invalidateQueries({ queryKey: instlSncnKeys.detail(docNo) }); + queryClient.invalidateQueries({ queryKey: instlSncnKeys.all }); } catch (error) { console.error("보완완료 실패:", error); alert("보완완료 처리 중 오류가 발생했습니다."); @@ -153,9 +158,7 @@ function InstlFormPage({ id, mode, initialData, createDefaults }: InstlFormPageP alert("저장되었습니다."); router.push(`/instl-sncn/${instlSncnDocNo}/edit`); - if (isEditMode && id) { - queryClient.invalidateQueries({ queryKey: instlSncnKeys.detail(id) }); - } + queryClient.invalidateQueries({ queryKey: instlSncnKeys.all }); } catch (error) { console.error("저장 실패:", error); alert("저장 중 오류가 발생했습니다."); @@ -172,6 +175,11 @@ function InstlFormPage({ id, mode, initialData, createDefaults }: InstlFormPageP return; } + if (!isFormEsntlAtchComplete(essentialFiles, data.esntlAtchFileList)) { + alert("필수 첨부파일을 모두 등록해야 신청이 가능합니다."); + return; + } + try { setIsSubmitting(true); const instlSncnDocNo = await submitForm(data, "ST"); @@ -179,10 +187,7 @@ function InstlFormPage({ id, mode, initialData, createDefaults }: InstlFormPageP alert("신청 완료되었습니다."); router.push(`/instl-sncn/${instlSncnDocNo}`); - queryClient.invalidateQueries({ queryKey: instlSncnKeys.list() }); - if (id) { - queryClient.invalidateQueries({ queryKey: instlSncnKeys.detail(id) }); - } + queryClient.invalidateQueries({ queryKey: instlSncnKeys.all }); } catch (error) { console.error("신청 실패:", error); alert("신청 중 오류가 발생했습니다."); @@ -218,68 +223,80 @@ function InstlFormPage({ id, mode, initialData, createDefaults }: InstlFormPageP return ( -
- - - - - - - - - -
-
- - {!isSplmntPrgrsStts ? ( - <> - - {isEditMode && ( - <> - - - - )} - - ) : ( - + + + +
+ {initialData?.detDto.instlSncnDocNo && ( + )} + + + + + + + + + +
+
+ + {!isSplmntPrgrsStts ? ( + <> + + {isEditMode && ( + <> + + + + )} + + ) : ( + + )} +
+
-
+ ); diff --git a/apps/as/src/pages/instl-sncn/ui/instl-list.tsx b/apps/as/src/pages/instl-sncn/ui/instl-list.tsx index e54b41578..43981e896 100644 --- a/apps/as/src/pages/instl-sncn/ui/instl-list.tsx +++ b/apps/as/src/pages/instl-sncn/ui/instl-list.tsx @@ -5,7 +5,7 @@ import { useRouter } from "next/navigation"; import { getCoreRowModel, type PaginationState } from "@tanstack/react-table"; -import { useDataTable, useUserStore } from "@/shared/lib"; +import { useDataTable } from "@/shared/lib"; import { type InstlSncnMngResponseDto, type InstlSncnMngSearchParams, @@ -24,7 +24,6 @@ interface InstlListProps { } function InstlList({ searchParams, pagination, onPaginationChange }: InstlListProps) { - const userInfo = useUserStore((state) => state.userInfo); const router = useRouter(); const [isTaskSeDialogOpen, setIsTaskSeDialogOpen] = useState(false); @@ -44,13 +43,7 @@ function InstlList({ searchParams, pagination, onPaginationChange }: InstlListPr const columns = useMemo(() => createInstlSncnColumns(), []); const handleRowClick = (row: InstlSncnMngResponseDto) => { - const canEdit = - (row.prgrsSttsComCd === "SV" || row.prgrsSttsComCd === "SL") && - row.rgtr === userInfo?.userUnqId; - - router.push( - canEdit ? `/instl-sncn/${row.instlSncnDocNo}/edit` : `/instl-sncn/${row.instlSncnDocNo}`, - ); + router.push(`/instl-sncn/${row.instlSncnDocNo}`); }; const { tableComponent } = useDataTable({ diff --git a/apps/as/src/pages/instl-sncn/ui/instl-view-page.tsx b/apps/as/src/pages/instl-sncn/ui/instl-view-page.tsx index 094072f1a..6a4f5ad2b 100644 --- a/apps/as/src/pages/instl-sncn/ui/instl-view-page.tsx +++ b/apps/as/src/pages/instl-sncn/ui/instl-view-page.tsx @@ -3,18 +3,23 @@ import { useMemo, useRef } from "react"; import { useUserStore } from "@/shared/lib"; +import { SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui"; import { ElaprDocPrgrsStatus } from "@/entities/elapr"; import { useInstlSncnDetail } from "@/entities/instl-sncn"; import { useRvwMnpwMngLoginUserList } from "@/entities/rvw-mnpw"; -import { SectionCollapsible, SupplementDemandSection } from "@/widgets/pbadms-sprt"; +import { + AttachmentsViewSection, + ProgressStatusSection, + SupplementDemandSection, +} from "@/widgets/pbadms-sprt"; import { Skeleton } from "@common/components"; import { needsRoleUser01AtrzResolve } from "../lib/button-config"; import { getEditableReviewStep } from "../lib/review-step-utils"; import { EMPTY_CKLST_RESULT_LIST, useChecklistFormState } from "../lib/use-checklist-form-state"; +import { PROGRESS_STATUS_COLUMNS } from "../model/columns"; import { FloatingActionButtons } from "./components/floating-action-buttons"; -import { AttachmentsSection } from "./sections/attachments-section"; import { BasicInfoSection } from "./sections/basic-info-section"; import { BusinessCostSection } from "./sections/business-cost-section"; import { BusinessScaleSection } from "./sections/business-scale-section"; @@ -79,6 +84,7 @@ function InstlViewPage({ id }: InstlViewPageProps) { etcFileList, rvwList = [], splmntDmndList = [], + sttsChgHstryList = [], } = data; const editableReviewStep = getEditableReviewStep( @@ -87,42 +93,53 @@ function InstlViewPage({ id }: InstlViewPageProps) { ); return ( -
- - - - - - - - - - - - +
+ + +
+ + + + + + + + + + + +
+
); } diff --git a/apps/as/src/pages/instl-sncn/ui/sections/attachments-section.tsx b/apps/as/src/pages/instl-sncn/ui/sections/attachments-section.tsx deleted file mode 100644 index d518e80dc..000000000 --- a/apps/as/src/pages/instl-sncn/ui/sections/attachments-section.tsx +++ /dev/null @@ -1,118 +0,0 @@ -"use client"; - -import { formatFileSize } from "@/shared/lib/file-utils"; -import { useDownloadAttachmentByAtflCd } from "@/entities/attach"; -import type { - AtflMngClientResponseDto, - EsntlAtchFileMngTreeResponseDto, -} from "@/entities/instl-sncn"; -import { SectionCollapsible } from "@/widgets/pbadms-sprt"; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components"; - -interface AttachmentsSectionProps { - esntlFileList: EsntlAtchFileMngTreeResponseDto[]; - etcFileList: AtflMngClientResponseDto[]; -} - -function AttachmentsSection({ esntlFileList, etcFileList }: AttachmentsSectionProps) { - const { mutate: downloadAttachment } = useDownloadAttachmentByAtflCd(); - - return ( - <> - -
- - - - 필수 자료명 - 파일명 - 크기 - - - - {esntlFileList.length === 0 ? ( - - - 조회된 필수 첨부파일 항목이 없습니다. - - - ) : ( - esntlFileList.map((file) => ( - - - {file.esntlAtchFileNm} - {file.esntlAtchYn === "Y" ? ( - * - ) : null} - - - {file.atflCd ? ( - downloadAttachment(file.atflCd ?? "")} - target="_blank" - rel="noreferrer" - > - {file.orgnlFileNm || "다운로드"} - - ) : ( - 첨부없음 - )} - - {formatFileSize(file.atflFlieSz)} - - )) - )} - -
-
-
- - -
- - - - 파일명 - 크기 - - - - {etcFileList.length === 0 ? ( - - - 등록된 기타첨부파일이 없습니다. - - - ) : ( - etcFileList.map((file) => ( - - - {file.atflCd ? ( - downloadAttachment(file.atflCd)} - target="_blank" - rel="noreferrer" - > - {file.orgnlFileNm || file.physFileNm || file.atflId} - - ) : ( - {file.orgnlFileNm || file.physFileNm || file.atflId} - )} - - - {formatFileSize(file.atflFlieSz)} - - - )) - )} - -
-
-
- - ); -} - -export { AttachmentsSection }; diff --git a/apps/as/src/pages/instl-sncn/ui/sections/basic-info-form-section.tsx b/apps/as/src/pages/instl-sncn/ui/sections/basic-info-form-section.tsx index e9c8aa8b2..6ddc0c8d5 100644 --- a/apps/as/src/pages/instl-sncn/ui/sections/basic-info-form-section.tsx +++ b/apps/as/src/pages/instl-sncn/ui/sections/basic-info-form-section.tsx @@ -3,10 +3,10 @@ import { useState } from "react"; import { Controller, useFormContext } from "react-hook-form"; +import { SectionCollapsible } from "@/shared/ui"; import { Field, FieldError, FieldGroup, FieldLabel, FieldSet } from "@/shared/ui/field"; import { findCodeLabel, useCodeList } from "@/entities/code"; import { type BnsApplyData, BnsPickerDialog } from "@/widgets/bns-picker"; -import { SectionCollapsible } from "@/widgets/pbadms-sprt"; import { Button, Checkbox, diff --git a/apps/as/src/pages/instl-sncn/ui/sections/basic-info-section.tsx b/apps/as/src/pages/instl-sncn/ui/sections/basic-info-section.tsx index 5413f0e0a..08362043c 100644 --- a/apps/as/src/pages/instl-sncn/ui/sections/basic-info-section.tsx +++ b/apps/as/src/pages/instl-sncn/ui/sections/basic-info-section.tsx @@ -1,7 +1,7 @@ "use client"; +import { SectionCollapsible } from "@/shared/ui"; import type { InstlSncnMngResponseDto } from "@/entities/instl-sncn"; -import { SectionCollapsible } from "@/widgets/pbadms-sprt"; import { Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components"; interface BasicInfoSectionProps { diff --git a/apps/as/src/pages/instl-sncn/ui/sections/business-cost-section.tsx b/apps/as/src/pages/instl-sncn/ui/sections/business-cost-section.tsx index 998f796bd..7de55ee64 100644 --- a/apps/as/src/pages/instl-sncn/ui/sections/business-cost-section.tsx +++ b/apps/as/src/pages/instl-sncn/ui/sections/business-cost-section.tsx @@ -2,9 +2,9 @@ import { useMemo } from "react"; +import { SectionCollapsible } from "@/shared/ui"; import type { BizCstTreeDto } from "@/entities/instl-sncn"; -import { calcDiffPercent, fmtNumber, indentPx } from "@/widgets/pbadms-sprt"; -import { SectionCollapsible } from "@/widgets/pbadms-sprt"; +import { fmtNumber, indentPx } from "@/widgets/pbadms-sprt"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components"; import { sortBizCstListByTreePath } from "../../lib/business-cost-utils"; @@ -46,10 +46,6 @@ function BusinessCostSection({ bizCstList }: BusinessCostSectionProps) { ) : ( sortedBizCstList.map((node) => { const pl = indentPx(node.indAcntgSeCdNm); - const bscPlanAmt = Number(node.bscPlanAmt) || 0; - const budgetReflct = Number(node.bgtRfltAmt) || 0; - const tstmDesignAmt = Number(node.tstmDesignAmt) || 0; - const diff = tstmDesignAmt - budgetReflct; const showSubIcon = node.treeLevel >= 3; return ( @@ -63,21 +59,21 @@ function BusinessCostSection({ bizCstList }: BusinessCostSectionProps) { )} -
{fmtNumber(bscPlanAmt)}
+
{fmtNumber(node.bgtRfltAmt)}
-
{fmtNumber(budgetReflct)}
+
{fmtNumber(node.bscPlanAmt)}
-
{fmtNumber(tstmDesignAmt)}
+
{fmtNumber(node.tstmDesignAmt)}
- {diff > 0 ? `+${fmtNumber(diff)}` : fmtNumber(diff)} + {node.chgAmt > 0 ? `+${fmtNumber(node.chgAmt)}` : fmtNumber(node.chgAmt)}
-
{calcDiffPercent(diff, budgetReflct)}
+
{fmtNumber(node.chgRt)}%
); diff --git a/apps/as/src/pages/instl-sncn/ui/sections/business-scale-section.tsx b/apps/as/src/pages/instl-sncn/ui/sections/business-scale-section.tsx index 70d93ad14..ca78cfec9 100644 --- a/apps/as/src/pages/instl-sncn/ui/sections/business-scale-section.tsx +++ b/apps/as/src/pages/instl-sncn/ui/sections/business-scale-section.tsx @@ -1,8 +1,8 @@ "use client"; +import { SectionCollapsible } from "@/shared/ui"; import type { BizSclTreeDto } from "@/entities/instl-sncn"; import { fmtNumber, indentPx } from "@/widgets/pbadms-sprt"; -import { SectionCollapsible } from "@/widgets/pbadms-sprt"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components"; interface BusinessScaleSectionProps { @@ -33,10 +33,6 @@ function BusinessScaleSection({ bizSclList }: BusinessScaleSectionProps) { ) : ( bizSclList.map((node) => { const pl = indentPx(node.indCnstpSeCdNm); - const bscPlanQty = Number(node.bscPlanQty) || 0; - const budgetReflct = Number(node.bgtRfltQty) || 0; - const tstmDesignQty = Number(node.tstmDesignQty) || 0; - const diff = tstmDesignQty - budgetReflct; return ( @@ -44,17 +40,17 @@ function BusinessScaleSection({ bizSclList }: BusinessScaleSectionProps) { {node.cnstpSeCdNm} -
{fmtNumber(bscPlanQty)}
+
{fmtNumber(node.bscPlanQty)}
-
{fmtNumber(budgetReflct)}
+
{fmtNumber(node.bgtRfltQty)}
-
{fmtNumber(tstmDesignQty)}
+
{fmtNumber(node.tstmDesignQty)}
- {diff > 0 ? `+${fmtNumber(diff)}` : fmtNumber(diff)} + {node.chgQty > 0 ? `+${fmtNumber(node.chgQty)}` : fmtNumber(node.chgQty)}
diff --git a/apps/as/src/pages/instl-sncn/ui/sections/checklist-section.tsx b/apps/as/src/pages/instl-sncn/ui/sections/checklist-section.tsx index 0846d2b3e..a24c7dd9b 100644 --- a/apps/as/src/pages/instl-sncn/ui/sections/checklist-section.tsx +++ b/apps/as/src/pages/instl-sncn/ui/sections/checklist-section.tsx @@ -2,8 +2,8 @@ import { type Dispatch, type SetStateAction, useEffect, useMemo, useState } from "react"; +import { SectionCollapsible } from "@/shared/ui"; import type { CklstRvwRsltTreeDto, ReviewStepCode } from "@/entities/instl-sncn"; -import { SectionCollapsible } from "@/widgets/pbadms-sprt"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; import { @@ -70,6 +70,10 @@ function ChecklistSection({ const { total, completed, percent } = progress; + if (cklstResultList.length === 0) { + return null; + } + return (
diff --git a/apps/as/src/pages/instl-sncn/ui/sections/contact-info-section.tsx b/apps/as/src/pages/instl-sncn/ui/sections/contact-info-section.tsx index 33108ee80..930e8b375 100644 --- a/apps/as/src/pages/instl-sncn/ui/sections/contact-info-section.tsx +++ b/apps/as/src/pages/instl-sncn/ui/sections/contact-info-section.tsx @@ -1,7 +1,7 @@ "use client"; +import { SectionCollapsible } from "@/shared/ui"; import type { InstlSncnMngResponseDto } from "@/entities/instl-sncn"; -import { SectionCollapsible } from "@/widgets/pbadms-sprt"; import { Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components"; interface ContactInfoSectionProps { diff --git a/apps/as/src/pages/instl-sncn/ui/sections/tech-review-opinion-section.tsx b/apps/as/src/pages/instl-sncn/ui/sections/tech-review-opinion-section.tsx index 2227492ed..ba787fd61 100644 --- a/apps/as/src/pages/instl-sncn/ui/sections/tech-review-opinion-section.tsx +++ b/apps/as/src/pages/instl-sncn/ui/sections/tech-review-opinion-section.tsx @@ -2,12 +2,15 @@ import { forwardRef, useEffect, useImperativeHandle, useState } from "react"; +import { Paperclip } from "lucide-react"; + import { PROXY_HOST } from "@/shared/config"; import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib"; +import { TextShimmer } from "@/shared/ui"; +import { SectionCollapsible } from "@/shared/ui"; import { Field, FieldLabel, FieldSet } from "@/shared/ui/field"; import { attachService } from "@/entities/attach"; import type { InstlSncnRvwOpnnResponseDto, ReviewStepCode } from "@/entities/instl-sncn"; -import { SectionCollapsible } from "@/widgets/pbadms-sprt"; import { Button, RadioGroup, @@ -67,12 +70,10 @@ function hasFormContent(formData: ReviewOpinionFormData) { const TechReviewOpinionSection = forwardRef< TechReviewOpinionSectionHandle, TechReviewOpinionSectionProps ->(function TechReviewOpinionSection({ - rvwList, - editableReviewStep, - atrzSeComCdList = [], - isAtrzLoading = false, -}, ref) { +>(function TechReviewOpinionSection( + { rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false }, + ref, +) { const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02"); const [formData, setFormData] = useState({ ...EMPTY_FORM_DATA, @@ -240,7 +241,9 @@ const TechReviewOpinionSection = forwardRef< {uploadedFile ? (
- {uploadedFile.fileName} + + {uploadedFile.fileName} + + + + + +
+ ); +} + +export { MyPbadmsSprtListPage }; diff --git a/apps/as/src/pages/my-pbadms-sprt/ui/my-pbadms-sprt-list.tsx b/apps/as/src/pages/my-pbadms-sprt/ui/my-pbadms-sprt-list.tsx new file mode 100644 index 000000000..06e64ee55 --- /dev/null +++ b/apps/as/src/pages/my-pbadms-sprt/ui/my-pbadms-sprt-list.tsx @@ -0,0 +1,88 @@ +"use client"; + +import { useMemo } from "react"; +import { useRouter } from "next/navigation"; + +import { getCoreRowModel, type PaginationState } from "@tanstack/react-table"; + +import { useDataTable } from "@/shared/lib"; +import { + type ElaprMyDocSearchDto, + type MyAdssDocResponseDto, + useElaprPrgrsSttsMyDocPage, +} from "@/entities/elapr"; + +import { resolveMyPbadmsSprtDetailPath } from "../lib/resolve-detail-path"; +import { createMyPbadmsSprtColumns } from "../model/my-pbadms-sprt-table-columns"; + +interface MyPbadmsSprtListProps { + searchParams: ElaprMyDocSearchDto; + pagination: PaginationState; + onPaginationChange: (pagination: PaginationState) => void; +} + +function toOptionalParam(value: string | undefined) { + const trimmed = value?.trim(); + return trimmed ? trimmed : undefined; +} + +function MyPbadmsSprtList({ searchParams, pagination, onPaginationChange }: MyPbadmsSprtListProps) { + const router = useRouter(); + + const queryParams = useMemo( + (): ElaprMyDocSearchDto => ({ + startDate: toOptionalParam(searchParams.startDate), + endDate: toOptionalParam(searchParams.endDate), + docSeComCd: toOptionalParam(searchParams.docSeComCd), + prgrsSttsComCd: toOptionalParam(searchParams.prgrsSttsComCd), + page: pagination.pageIndex, + size: pagination.pageSize, + sort: searchParams.sort, + }), + [searchParams, pagination], + ); + + const pageQuery = useElaprPrgrsSttsMyDocPage(queryParams); + const columns = useMemo( + () => + createMyPbadmsSprtColumns({ + pageIndex: pagination.pageIndex, + pageSize: pagination.pageSize, + }), + [pagination.pageIndex, pagination.pageSize], + ); + + const handleRowClick = (row: MyAdssDocResponseDto) => { + const path = resolveMyPbadmsSprtDetailPath(row); + if (path) router.push(path); + }; + + const { tableComponent } = useDataTable({ + query: pageQuery, + tableOptions: { + columns, + state: { pagination }, + onPaginationChange: (updater) => { + const next = typeof updater === "function" ? updater(pagination) : updater; + onPaginationChange(next); + }, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + }, + dataTableOptions: { + viewPagination: true, + onRowClick: handleRowClick, + }, + }); + + return ( +
+
+

내 신청 목록

+
+ {tableComponent} +
+ ); +} + +export { MyPbadmsSprtList }; diff --git a/apps/as/src/pages/nti-hstry/index.ts b/apps/as/src/pages/nti-hstry/index.ts new file mode 100644 index 000000000..4df9ff6e9 --- /dev/null +++ b/apps/as/src/pages/nti-hstry/index.ts @@ -0,0 +1 @@ +export { NtiHstryListPage } from "./ui/nti-hstry-list-page"; diff --git a/apps/as/src/pages/nti-hstry/lib/resolve-noti-status.ts b/apps/as/src/pages/nti-hstry/lib/resolve-noti-status.ts new file mode 100644 index 000000000..19ff1f3fa --- /dev/null +++ b/apps/as/src/pages/nti-hstry/lib/resolve-noti-status.ts @@ -0,0 +1,61 @@ +import { dayjs } from "@/shared/lib"; +import type { NotiAsUserResponseDto } from "@/entities/noti"; +import type { BadgeProps } from "@common/components"; + +export type NotiStatusLabel = "처리완료" | "처리지연" | "처리중" | "지연" | "미조회"; + +export interface NotiStatusUi { + label: NotiStatusLabel; + badgeProps: Pick; + actionLabel: string; +} + +function daysSince(regDt: string) { + return dayjs().startOf("day").diff(dayjs(regDt).startOf("day"), "day"); +} + +export function resolveNotiStatus( + row: Pick, +): NotiStatusUi | null { + if (row.notiPrcsCmptnYn === "Y") { + return { + label: "처리완료", + badgeProps: { variant: "default", size: "lg" }, + actionLabel: "상세보기", + }; + } + + if (!row.regDt) return null; + + const days = daysSince(row.regDt); + const isRead = row.notReadYn === "Y"; + + if (isRead) { + if (days > 7) { + return { + label: "처리지연", + badgeProps: { color: "red", size: "lg" }, + actionLabel: "처리하기", + }; + } + return { + label: "처리중", + badgeProps: { color: "green", size: "lg" }, + actionLabel: "처리하기", + }; + } + + if (days > 3) { + return { + label: "지연", + badgeProps: { color: "orange", size: "lg" }, + actionLabel: "처리하기", + }; + } + + return { + label: "미조회", + badgeProps: { color: "yellow" }, + actionLabel: "처리하기", + }; +} diff --git a/apps/as/src/pages/nti-hstry/model/noti-table-columns.tsx b/apps/as/src/pages/nti-hstry/model/noti-table-columns.tsx new file mode 100644 index 000000000..24ddc6eaf --- /dev/null +++ b/apps/as/src/pages/nti-hstry/model/noti-table-columns.tsx @@ -0,0 +1,91 @@ +"use client"; + +import { createColumnHelper } from "@tanstack/react-table"; + +import { dayjs } from "@/shared/lib"; +import type { NotiAsUserResponseDto } from "@/entities/noti"; +import { Badge, Button } from "@common/components"; + +import { resolveNotiStatus } from "../lib/resolve-noti-status"; + +const columnHelper = createColumnHelper(); + +function formatOptional(value: string | number | undefined | null) { + if (value === undefined || value === null) return "-"; + const text = String(value).trim(); + return text ? text : "-"; +} + +function formatDateTime(value: string | undefined) { + return value ? dayjs(value).format("YYYY-MM-DD HH:mm") : "-"; +} + +interface CreateNotiColumnsOptions { + pageIndex: number; + pageSize: number; + isActionPending: boolean; + onAction: (row: NotiAsUserResponseDto) => void; +} + +export function createNotiColumns({ + pageIndex, + pageSize, + isActionPending, + onAction, +}: CreateNotiColumnsOptions) { + return [ + columnHelper.display({ + id: "no", + header: "No", + meta: { align: "center" }, + cell: ({ row }) => pageIndex * pageSize + row.index + 1, + }), + columnHelper.accessor("regDt", { + header: "알림일시", + meta: { align: "center" }, + cell: ({ getValue }) => formatDateTime(getValue()), + }), + columnHelper.accessor("notiTtl", { + header: "알림내용", + meta: { align: "left" }, + cell: ({ getValue }) => formatOptional(getValue()), + }), + columnHelper.accessor("docSeComNm", { + header: "연계업무", + meta: { align: "center" }, + cell: ({ row }) => formatOptional(row.original.docSeComNm || row.original.docSeComCd), + }), + columnHelper.display({ + id: "status", + header: "상태", + meta: { align: "center" }, + cell: ({ row }) => { + const status = resolveNotiStatus(row.original); + if (!status) return "-"; + return {status.label}; + }, + }), + columnHelper.display({ + id: "action", + header: "처리", + meta: { align: "center" }, + cell: ({ row }) => { + const status = resolveNotiStatus(row.original); + const label = status?.actionLabel ?? "처리하기"; + return ( +
event.stopPropagation()}> + +
+ ); + }, + }), + ]; +} diff --git a/apps/as/src/pages/nti-hstry/ui/nti-hstry-list-page.tsx b/apps/as/src/pages/nti-hstry/ui/nti-hstry-list-page.tsx new file mode 100644 index 000000000..bffec4476 --- /dev/null +++ b/apps/as/src/pages/nti-hstry/ui/nti-hstry-list-page.tsx @@ -0,0 +1,128 @@ +"use client"; + +import { useState } from "react"; + +import type { PaginationState } from "@tanstack/react-table"; + +import { dayjs } from "@/shared/lib"; +import { useCodeList } from "@/entities/code"; +import type { NotiSearchDto } from "@/entities/noti"; +import { + Button, + InputDatePicker, + Label, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@common/components"; + +import { NtiHstryList } from "./nti-hstry-list"; + +const DEFAULT_PAGE_INDEX = 0; +const DEFAULT_PAGE_SIZE = 10; +const ALL_SELECT_VALUE = "__ALL__"; + +function defaultStartDate() { + return dayjs().subtract(1, "month").toDate(); +} + +function defaultEndDate() { + return dayjs().toDate(); +} + +function formatDateParam(date: Date | null | undefined) { + return date ? dayjs(date).format("YYYY-MM-DD") : ""; +} + +function buildDefaultSearchParams(): NotiSearchDto { + return { + startDt: formatDateParam(defaultStartDate()), + endDt: formatDateParam(defaultEndDate()), + docSeComCd: "", + page: DEFAULT_PAGE_INDEX, + size: DEFAULT_PAGE_SIZE, + }; +} + +function NtiHstryListPage() { + const [startDate, setStartDate] = useState(defaultStartDate); + const [endDate, setEndDate] = useState(defaultEndDate); + const [docSeComCd, setDocSeComCd] = useState(""); + const [pagination, setPagination] = useState({ + pageIndex: DEFAULT_PAGE_INDEX, + pageSize: DEFAULT_PAGE_SIZE, + }); + const [searchParams, setSearchParams] = useState(buildDefaultSearchParams); + + const { data: docSeCodes, isPending: isDocSeLoading } = useCodeList("ASDOCSE"); + + const handleSearch = (event: React.FormEvent) => { + event.preventDefault(); + setPagination((prev) => ({ ...prev, pageIndex: DEFAULT_PAGE_INDEX })); + setSearchParams({ + startDt: formatDateParam(startDate), + endDt: formatDateParam(endDate), + docSeComCd, + page: DEFAULT_PAGE_INDEX, + size: pagination.pageSize, + }); + }; + + return ( +
+
+
+
+ + setStartDate(date ?? null)} + className="w-[12rem]" + /> + ~ + setEndDate(date ?? null)} + className="w-[12rem]" + /> +
+ +
+ + +
+ + +
+
+ + +
+ ); +} + +export { NtiHstryListPage }; diff --git a/apps/as/src/pages/nti-hstry/ui/nti-hstry-list.tsx b/apps/as/src/pages/nti-hstry/ui/nti-hstry-list.tsx new file mode 100644 index 000000000..be270e2d3 --- /dev/null +++ b/apps/as/src/pages/nti-hstry/ui/nti-hstry-list.tsx @@ -0,0 +1,114 @@ +"use client"; + +import { useCallback, useMemo } from "react"; +import { useRouter } from "next/navigation"; + +import { getCoreRowModel, type PaginationState } from "@tanstack/react-table"; + +import { useDataTable } from "@/shared/lib"; +import { + type NotiAsUserResponseDto, + type NotiSearchDto, + useFindMyAsNotiList, + useReadNoti, +} from "@/entities/noti"; + +import { createNotiColumns } from "../model/noti-table-columns"; + +interface NtiHstryListProps { + searchParams: NotiSearchDto; + pagination: PaginationState; + onPaginationChange: (pagination: PaginationState) => void; +} + +function toOptionalParam(value: string | undefined) { + const trimmed = value?.trim(); + return trimmed ? trimmed : undefined; +} + +function NtiHstryList({ searchParams, pagination, onPaginationChange }: NtiHstryListProps) { + const router = useRouter(); + const { mutateAsync: readNoti, isPending: isReadPending } = useReadNoti(); + + const queryParams = useMemo( + (): NotiSearchDto => ({ + startDt: toOptionalParam(searchParams.startDt), + endDt: toOptionalParam(searchParams.endDt), + docSeComCd: toOptionalParam(searchParams.docSeComCd), + page: pagination.pageIndex, + size: pagination.pageSize, + sort: searchParams.sort, + }), + [searchParams, pagination], + ); + + const pageQuery = useFindMyAsNotiList(queryParams); + + const resolveNotiUrl = (row: NotiAsUserResponseDto) => { + const notiUrl = row.notiUrl?.trim(); + if (!notiUrl) return null; + + if (row.docSeComCd === "SEWER") { + const parts = notiUrl.split("/"); + if (parts.length > 1) { + parts.pop(); + return parts.join("/"); + } + } + + return notiUrl; + }; + + const handleAction = useCallback( + async (row: NotiAsUserResponseDto) => { + if (row.notReadYn === "N") await readNoti(row.notiId); + + const notiUrl = resolveNotiUrl(row); + if (!notiUrl) return; + + router.push(notiUrl); + }, + [readNoti, router], + ); + + const columns = useMemo( + () => + createNotiColumns({ + pageIndex: pagination.pageIndex, + pageSize: pagination.pageSize, + isActionPending: isReadPending, + onAction: (row) => { + void handleAction(row); + }, + }), + [pagination.pageIndex, pagination.pageSize, isReadPending, handleAction], + ); + + const { tableComponent } = useDataTable({ + query: pageQuery, + tableOptions: { + columns, + state: { pagination }, + onPaginationChange: (updater) => { + const next = typeof updater === "function" ? updater(pagination) : updater; + onPaginationChange(next); + }, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + }, + dataTableOptions: { + viewPagination: true, + }, + }); + + return ( +
+
+

알림 이력 목록

+
+ {tableComponent} +
+ ); +} + +export { NtiHstryList }; diff --git a/apps/as/src/pages/sewer-bsc-plan/index.ts b/apps/as/src/pages/sewer-bsc-plan/index.ts index 39f824683..f8329573d 100644 --- a/apps/as/src/pages/sewer-bsc-plan/index.ts +++ b/apps/as/src/pages/sewer-bsc-plan/index.ts @@ -1,4 +1,6 @@ +export { SewerBscPlanDetailGate } from "./ui/sewer-bsc-plan-detail-gate"; export { SewerBscPlanEditPage } from "./ui/sewer-bsc-plan-edit-page"; export { SewerBscPlanFormPage } from "./ui/sewer-bsc-plan-form-page"; export { SewerBscPlanListPage } from "./ui/sewer-bsc-plan-list-page"; export { SewerBscPlanRegistPage } from "./ui/sewer-bsc-plan-regist-page"; +export { SewerBscPlanViewPage } from "./ui/sewer-bsc-plan-view-page"; diff --git a/apps/as/src/pages/sewer-bsc-plan/lib/biz-cst-matrix-utils.ts b/apps/as/src/pages/sewer-bsc-plan/lib/biz-cst-matrix-utils.ts index 41ff8e9da..74867acd6 100644 --- a/apps/as/src/pages/sewer-bsc-plan/lib/biz-cst-matrix-utils.ts +++ b/apps/as/src/pages/sewer-bsc-plan/lib/biz-cst-matrix-utils.ts @@ -1,4 +1,4 @@ -import type { CostItemDef } from "@/widgets/pbadms-sprt/lib/business-cost-form-utils"; +import { type CostItemDef, roundAmount } from "@/widgets/pbadms-sprt"; export interface BizCstMatrixItem { planStpNo: number; @@ -45,9 +45,11 @@ function recalculateBizCstMatrix( if (!item.readonly || !item.children) continue; for (const planStpNo of PLAN_STEPS) { - const childSum = item.children.reduce( - (sum, childCode) => sum + getAmt(next, childCode, planStpNo), - 0, + const childSum = roundAmount( + item.children.reduce( + (sum, childCode) => sum + getAmt(next, childCode, planStpNo), + 0, + ), ); const current = getAmt(next, item.code, planStpNo); if (current !== childSum) { diff --git a/apps/as/src/pages/sewer-bsc-plan/lib/button-config.ts b/apps/as/src/pages/sewer-bsc-plan/lib/button-config.ts new file mode 100644 index 000000000..062dcf7e1 --- /dev/null +++ b/apps/as/src/pages/sewer-bsc-plan/lib/button-config.ts @@ -0,0 +1,129 @@ +export const SEWER_BSC_PLAN_VIEW_BUTTON_ACTION = { + strg: "strg", + aply: "aply", + del: "del", + rvwRply: "rvwRply", + rqstTechRvw: "rqstTechRvw", + dsgntRvwr: "dsgntRvwr", + rvwStrg: "rvwStrg", + rvwCmptn: "rvwCmptn", + dmndSplmnt: "dmndSplmnt", + atrzAprv: "atrzAprv", + atrzRply: "atrzRply", +} as const; + +export type SewerBscPlanViewButtonAction = + (typeof SEWER_BSC_PLAN_VIEW_BUTTON_ACTION)[keyof typeof SEWER_BSC_PLAN_VIEW_BUTTON_ACTION]; + +const ROLE_STATUS_BUTTON_ACTIONS: Record> = { + ROLE_USER05: { + "": [ + SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.strg, + SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.aply, + SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.del, + ], + SV: [ + SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.strg, + SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.aply, + SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.del, + ], + }, + ROLE_USER03: { + LE: [SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.rvwRply, SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.rqstTechRvw], + TP: [SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.rvwRply], + }, +}; + +const ROLE_USER01_ATRZ_STATUS_CODES = ["ER", "TE"] as const; + +const ROLE_USER01_ATRZ_STATUS_ACTIONS: Record< + string, + Record +> = { + ER: { + ATRZ01: [SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.dsgntRvwr], + }, + TE: { + ATRZ02: [ + SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.rvwStrg, + SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.rvwCmptn, + SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.dmndSplmnt, + ], + ATRZ03: [SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.atrzAprv], + ATRZ99: [SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.atrzRply], + }, +}; + +function resolveButtonConfigs( + actions: SewerBscPlanViewButtonAction[], +): SewerBscPlanViewButtonAction[] { + return actions.map((action) => SEWER_BSC_PLAN_VIEW_BUTTON_ACTION[action]); +} + +interface GetButtonConfigsOptions { + hasPendingApprovalRequest?: boolean; +} + +function resolveRoleUser01AtrzButtonActions( + prgrsSttsComCd: string, + atrzSeComCdList: string[], + options?: GetButtonConfigsOptions, +): SewerBscPlanViewButtonAction[] { + if (prgrsSttsComCd === "TE" && options?.hasPendingApprovalRequest === false) { + return []; + } + + const atrzStatusMap = ROLE_USER01_ATRZ_STATUS_ACTIONS[prgrsSttsComCd]; + if (!atrzStatusMap || atrzSeComCdList.length === 0) return []; + + const seen = new Set(); + const actions: SewerBscPlanViewButtonAction[] = []; + + for (const atrzSeComCd of atrzSeComCdList) { + const mappedActions = atrzStatusMap[atrzSeComCd]; + if (!mappedActions) continue; + + for (const action of mappedActions) { + if (seen.has(action)) continue; + seen.add(action); + actions.push(action); + } + } + + return actions; +} + +export function needsRoleUser01AtrzResolve( + dtlInstCd: string | undefined, + prgrsSttsComCd: string, +): boolean { + return ( + dtlInstCd === "ROLE_USER01" && + ROLE_USER01_ATRZ_STATUS_CODES.includes( + prgrsSttsComCd as (typeof ROLE_USER01_ATRZ_STATUS_CODES)[number], + ) + ); +} + +export function getButtonConfigs( + dtlInstCd: string | undefined, + prgrsSttsComCd: string, + atrzSeComCdList?: string[], + options?: GetButtonConfigsOptions, +): SewerBscPlanViewButtonAction[] { + if (!dtlInstCd) return []; + + if (needsRoleUser01AtrzResolve(dtlInstCd, prgrsSttsComCd)) { + return resolveButtonConfigs( + resolveRoleUser01AtrzButtonActions(prgrsSttsComCd, atrzSeComCdList ?? [], options), + ); + } + + const statusMap = ROLE_STATUS_BUTTON_ACTIONS[dtlInstCd]; + if (!statusMap) return []; + + const actions = statusMap[prgrsSttsComCd]; + if (!actions) return []; + + return resolveButtonConfigs(actions); +} diff --git a/apps/as/src/pages/sewer-bsc-plan/lib/checklist-utils.ts b/apps/as/src/pages/sewer-bsc-plan/lib/checklist-utils.ts new file mode 100644 index 000000000..89e82a3d8 --- /dev/null +++ b/apps/as/src/pages/sewer-bsc-plan/lib/checklist-utils.ts @@ -0,0 +1,178 @@ +import type { CklstResultItem, CklstRvwRsltTreeDto } from "@/entities/sewer-bsc-plan"; + +import { + type ChecklistFormState, + type ChecklistProgress, + type ChecklistRowKey, + type ChecklistStepMap, + REVIEW_STEP_CODES, + type ReviewStepCode, +} from "../model/view-types"; + +export function buildChecklistRowKey( + row: CklstRvwRsltTreeDto, + reviewStep?: ReviewStepCode, +): ChecklistRowKey { + const step = reviewStep ?? (row.rvwStpComCd as ReviewStepCode) ?? "LOCAL"; + return `${row.taskSeComCd}_${row.cklstSn}_${row.cklstArtclSn}_${row.cklstRvwSn}_${step}`; +} + +export function groupByReviewStep(rows: CklstRvwRsltTreeDto[]): ChecklistStepMap { + const base: ChecklistStepMap = { LOCAL: [], ENV: [], KECO: [] }; + rows.forEach((row) => { + const step = (row.rvwStpComCd ?? "LOCAL") as ReviewStepCode; + if (REVIEW_STEP_CODES.includes(step)) base[step].push(row); + }); + return base; +} + +export function extractTaskCodes(rows: CklstRvwRsltTreeDto[]): string[] { + return Array.from(new Set(rows.map((row) => row.taskSeComCd).filter(Boolean))) as string[]; +} + +export function filterChecklistRows( + rows: CklstRvwRsltTreeDto[], + taskCode: string | null, +): CklstRvwRsltTreeDto[] { + return rows.filter((row) => (taskCode ? row.taskSeComCd === taskCode : true)).slice(); +} + +export function getChecklistDisplayRows( + rows: CklstRvwRsltTreeDto[], + taskCode?: string | null, +): CklstRvwRsltTreeDto[] { + const filtered = filterChecklistRows(rows, taskCode ?? null); + const seen = new Set(); + const deduped: CklstRvwRsltTreeDto[] = []; + + for (const row of filtered) { + const dedupeKey = `${row.taskSeComCd}_${row.cklstRvwSn}`; + if (seen.has(dedupeKey)) continue; + seen.add(dedupeKey); + deduped.push(row); + } + + return deduped; +} + +export function computeChecklistProgress( + rows: CklstRvwRsltTreeDto[], + results: ChecklistFormState["results"], +): ChecklistProgress { + const activeRows = rows.filter((row) => row.bchckUseYn === "Y"); + const total = activeRows.length; + const completed = activeRows.reduce((acc, row) => { + const key = buildChecklistRowKey(row); + const val = results[key] ?? row.rvwRsltAnmlyYn ?? ""; + return acc + (val ? 1 : 0); + }, 0); + const percent = total > 0 ? Math.round((completed / total) * 100) : 0; + return { total, completed, percent }; +} + +export function filterChecklistRowsByReviewStep( + rows: CklstRvwRsltTreeDto[], + reviewStep: ReviewStepCode, +): CklstRvwRsltTreeDto[] { + return rows.filter((row) => row.rvwStpComCd === reviewStep); +} + +export function computeReviewStepChecklistProgress( + rows: CklstRvwRsltTreeDto[], + results: ChecklistFormState["results"], + reviewStep: ReviewStepCode, +): ChecklistProgress { + const activeRows = getChecklistDisplayRows(rows).filter((row) => row.bchckUseYn === "Y"); + const total = activeRows.length; + const completed = activeRows.reduce((acc, row) => { + const key = buildChecklistRowKey(row, reviewStep); + const val = results[key]; + return acc + (val === "Y" || val === "N" ? 1 : 0); + }, 0); + const percent = total > 0 ? Math.round((completed / total) * 100) : 100; + return { total, completed, percent }; +} + +export function buildChecklistFormState(rows: CklstRvwRsltTreeDto[]): ChecklistFormState { + const results: ChecklistFormState["results"] = {}; + const comments: ChecklistFormState["comments"] = {}; + rows.forEach((row) => { + const key = buildChecklistRowKey(row); + if (row.rvwRsltAnmlyYn === "Y" || row.rvwRsltAnmlyYn === "N") { + results[key] = row.rvwRsltAnmlyYn; + } + if (row.picRvwCn) comments[key] = row.picRvwCn; + }); + return { + results, + comments, + progress: computeChecklistProgress(rows, results), + }; +} + +export function mergeChecklistFormStateFromServer( + prev: ChecklistFormState, + allRows: CklstRvwRsltTreeDto[], +): ChecklistFormState { + const results = { ...prev.results }; + const comments = { ...prev.comments }; + + for (const row of allRows) { + const step = row.rvwStpComCd as ReviewStepCode | undefined; + if (!step || !REVIEW_STEP_CODES.includes(step)) continue; + + const key = buildChecklistRowKey(row); + if ((row.rvwRsltAnmlyYn === "Y" || row.rvwRsltAnmlyYn === "N") && results[key] === undefined) { + results[key] = row.rvwRsltAnmlyYn; + } + if (row.picRvwCn && !comments[key]) { + comments[key] = row.picRvwCn; + } + } + + return { + results, + comments, + progress: computeChecklistProgress(getChecklistDisplayRows(allRows), results), + }; +} + +export function patchChecklistFormState( + prev: ChecklistFormState, + patch: Partial>, + allRows: CklstRvwRsltTreeDto[], + reviewStep?: ReviewStepCode, +): ChecklistFormState { + const results = patch.results ?? prev.results; + const comments = patch.comments ?? prev.comments; + const progress = reviewStep + ? computeReviewStepChecklistProgress(allRows, results, reviewStep) + : computeChecklistProgress(getChecklistDisplayRows(allRows), results); + + return { + results, + comments, + progress, + }; +} + +export function toCklstSaveDto( + rows: CklstRvwRsltTreeDto[], + formState: ChecklistFormState, + reviewStep: ReviewStepCode, +): CklstResultItem[] { + return getChecklistDisplayRows(rows) + .filter((row) => row.bchckUseYn === "Y") + .map((row) => { + const key = buildChecklistRowKey(row, reviewStep); + const selected = formState.results[key] ?? null; + return { + cklstSn: row.cklstSn, + cklstArtclSn: row.cklstArtclSn, + cklstRvwSn: row.cklstRvwSn, + rvwStpComCd: reviewStep, + selected: selected ?? undefined, + picRvwCn: formState.comments[key] ?? null, + }; + }); +} diff --git a/apps/as/src/pages/sewer-bsc-plan/lib/fnrs-cst-form-utils.ts b/apps/as/src/pages/sewer-bsc-plan/lib/fnrs-cst-form-utils.ts index a6ed6b94b..57de61985 100644 --- a/apps/as/src/pages/sewer-bsc-plan/lib/fnrs-cst-form-utils.ts +++ b/apps/as/src/pages/sewer-bsc-plan/lib/fnrs-cst-form-utils.ts @@ -1,4 +1,4 @@ -import type { CostItemDef } from "@/widgets/pbadms-sprt/lib/business-cost-form-utils"; +import { type CostItemDef, roundAmount } from "@/widgets/pbadms-sprt"; import { type BizCstMatrixItem,getAmt, PLAN_STEPS } from "./biz-cst-matrix-utils"; @@ -31,11 +31,11 @@ function getBizCstStepTotal( } function sumFnrsCstLeaf(item: FnrsCstItem): number { - return ( + return roundAmount( (item.gvexpAmt ?? 0) + - (item.lcexpAmt ?? 0) + - (item.orgprBdchgAmt ?? 0) + - (item.etcAmt ?? 0) + (item.lcexpAmt ?? 0) + + (item.orgprBdchgAmt ?? 0) + + (item.etcAmt ?? 0), ); } @@ -55,7 +55,7 @@ function calcFnrsCstRemaining( const bizTotal = getBizCstStepTotal(bizCstList, costItems, planStpNo); const fnrsItem = fnrsCstList.find((item) => item.planStpNo === planStpNo); const sharTotal = fnrsItem ? sumFnrsCstLeaf(fnrsItem) : 0; - return { planStpNo, remaining: bizTotal - sharTotal }; + return { planStpNo, remaining: roundAmount(bizTotal - sharTotal) }; }); } diff --git a/apps/as/src/pages/sewer-bsc-plan/lib/form-data-mapper.ts b/apps/as/src/pages/sewer-bsc-plan/lib/form-data-mapper.ts index 7a6b1f0ac..9c405f337 100644 --- a/apps/as/src/pages/sewer-bsc-plan/lib/form-data-mapper.ts +++ b/apps/as/src/pages/sewer-bsc-plan/lib/form-data-mapper.ts @@ -3,6 +3,8 @@ import type { SewerBscPlanApplySaveRequestDto, SewerBscPlanDetResponseDto, } from "@/entities/sewer-bsc-plan"; +import { sewerBscPlanApplySaveSchema } from "@/entities/sewer-bsc-plan"; +import { toEsntlAtchFileSaveList } from "@/widgets/pbadms-sprt"; import type { BizCstMatrixItem } from "./biz-cst-matrix-utils"; import { recalcFnrsCstSumAmt } from "./fnrs-cst-form-utils"; @@ -25,20 +27,6 @@ function mapSelectedValue(value?: string | null): "Y" | "N" | null { return null; } -function mapEsntlAtchFileList(esntlFileList: SewerBscPlanDetResponseDto["esntlFileList"]) { - const esntlMap = new Map(); - - 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, - })); -} - function createEmptyTmplntDtlList(swtrmtFcltCd: string) { return TMPLNT_DTL_STEPS.map((planStpNo) => ({ planStpNo, @@ -217,7 +205,7 @@ export function mapDetailToFormData(data: SewerBscPlanDetResponseDto): SewerBscP selected: mapSelectedValue(item.rvwRsltAnmlyYn), picRvwCn: item.picRvwCn, })) ?? [], - esntlAtchFileList: mapEsntlAtchFileList(data.esntlFileList), + esntlAtchFileList: toEsntlAtchFileSaveList(data.esntlFileList), splmntDmndList: data.splmntDmndList?.map((item) => ({ splmntDmndSn: item.splmntDmndSn, @@ -244,43 +232,33 @@ export function buildApplyPayload( prgrsSttsComCd: "SV" | "LE" | "TE", options?: { isEditMode?: boolean; docNo?: string }, ): SewerBscPlanApplySaveRequestDto { - const { taskSeComCd, ...sewerBscPlanRest } = data.sewerBscPlan; + const docNo = options?.isEditMode ? options.docNo : undefined; - return { + return sewerBscPlanApplySaveSchema.parse({ sewerBscPlan: { - ...sewerBscPlanRest, - taskSeComCd: taskSeComCd ?? "", - sewerBscPlanDocNo: options?.isEditMode ? options.docNo : undefined, + ...data.sewerBscPlan, + sewerBscPlanDocNo: docNo, + taskSeComCd: data.sewerBscPlan.taskSeComCd ?? "", prgrsSttsComCd, }, sewerBscPlanDtl: { ...data.sewerBscPlanDtl, - sewerBscPlanDocNo: options?.isEditMode ? options.docNo : undefined, + sewerBscPlanDocNo: docNo, }, sewerBscPlanOtlnList: data.sewerBscPlanOtlnList, bizCstList: data.bizCstList, fnrsCstList: data.fnrsCstList.map((item) => recalcFnrsCstSumAmt(item)), - sewerBscPlanTmplntList: data.sewerBscPlanTmplntList.map( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - ({ atchFileNm, swtrmtFcltNm, ...item }) => ({ - swtrmtFcltCd: item.swtrmtFcltCd, - atchFileGroupId: item.atchFileGroupId, - sewerBscPlanTmplntDtlList: item.sewerBscPlanTmplntDtlList, - }), - ), + sewerBscPlanTmplntList: data.sewerBscPlanTmplntList, cklstResultList: data.cklstResultList.map((item) => ({ ...item, selected: item.selected ?? undefined, })), esntlAtchFileList: data.esntlAtchFileList, - splmntDmndList: data.splmntDmndList.map( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - ({ splmntAtchFileNm, splmntAtchFileAtflId, splmntDmndAtchFileNm, ...item }) => ({ - ...item, - ...(prgrsSttsComCd === "TE" ? { cmptnYn: "Y" } : null), - }), - ), - }; + splmntDmndList: data.splmntDmndList.map((item) => ({ + ...item, + cmptnYn: prgrsSttsComCd === "TE" ? "Y" : item.cmptnYn, + })), + }); } export { createEmptyTmplntDtlList }; diff --git a/apps/as/src/pages/sewer-bsc-plan/lib/payload-utils.ts b/apps/as/src/pages/sewer-bsc-plan/lib/payload-utils.ts new file mode 100644 index 000000000..07224c8ef --- /dev/null +++ b/apps/as/src/pages/sewer-bsc-plan/lib/payload-utils.ts @@ -0,0 +1,158 @@ +import type { + SewerBscPlanApplySaveRequestDto, + SewerBscPlanDetResponseDto, + SewerBscPlanSplmntDmndItem, +} from "@/entities/sewer-bsc-plan"; +import { type SplmntDmndDialogConfirmPayload, toEsntlAtchFileSaveList } from "@/widgets/pbadms-sprt"; + +import type { ChecklistFormState, ReviewStepCode } from "../model/view-types"; + +import { toCklstSaveDto } from "./checklist-utils"; +import { recalcFnrsCstSumAmt } from "./fnrs-cst-form-utils"; +import { type ReviewOpinionGetFormDataResult, toRvwOpnnSaveDto } from "./review-opinion-utils"; + +export function buildSavePayload( + data: SewerBscPlanDetResponseDto, + checklistFormState: ChecklistFormState, + reviewStep: ReviewStepCode, + prgrsSttsComCd: string, +): SewerBscPlanApplySaveRequestDto { + const detDto = data.detDto; + const dtl = data.activeDtlDto; + if (!detDto) { + throw new Error("상세 정보(detDto)가 없습니다."); + } + + const cklstResultRows = data.cklstResultRows ?? []; + const esntlFileList = data.esntlFileList ?? []; + + return { + sewerBscPlan: { + sewerBscPlanDocNo: detDto.sewerBscPlanDocNo, + admdstCd: detDto.admdstCd, + envoSeComCd: detDto.envoSeComCd, + sewerBscPlanSeComCd: detDto.sewerBscPlanSeComCd, + sewerBscPlanNm: detDto.sewerBscPlanNm, + taskSeComCd: detDto.taskSeComCd, + prgrsSttsComCd, + }, + sewerBscPlanDtl: { + sewerBscPlanDocNo: detDto.sewerBscPlanDocNo, + chgCycl: dtl?.chgCycl ?? detDto.chgCycl, + aplcntNm: dtl?.aplcntNm, + aplcntMblTelno: dtl?.aplcntMblTelno, + aplcntOgdpNm: dtl?.aplcntOgdpNm, + aplcntEmlAddr: dtl?.aplcntEmlAddr, + dsgnrNm: dtl?.dsgnrNm, + dsgnrTelno: dtl?.dsgnrTelno, + dsgnrOgdpNm: dtl?.dsgnrOgdpNm, + dsgnrEmlAddr: dtl?.dsgnrEmlAddr, + planYr: dtl?.planYr, + crtrYr: dtl?.crtrYr, + bizOtlnCn: dtl?.bizOtlnCn, + frlvlGoalYr: dtl?.frlvlGoalYr, + sclvlGoalYr: dtl?.sclvlGoalYr, + thlvlGoalYr: dtl?.thlvlGoalYr, + folvlGoalYr: dtl?.folvlGoalYr, + atchFileGroupId: dtl?.atchFileGroupId, + }, + sewerBscPlanOtlnList: (data.otlnList ?? []).map((item) => ({ + planStpNo: item.planStpNo, + sewerPlanPpltnCnt: item.sewerPlanPpltnCnt, + sewerPrcsPpltnCnt: item.sewerPrcsPpltnCnt, + sewerPsplyRt: item.sewerPsplyRt, + })), + bizCstList: (data.bizCstList ?? []).map((item) => ({ + planStpNo: item.planStpNo, + acntgSeCd: item.acntgSeCd, + cstrnAmt: item.cstrnAmt, + })), + fnrsCstList: (data.fnrsCstList ?? []).map((item) => + recalcFnrsCstSumAmt({ + planStpNo: item.planStpNo ?? 1, + gvexpAmt: item.gvexpAmt ?? 0, + lcexpAmt: item.lcexpAmt ?? 0, + orgprBdchgAmt: item.orgprBdchgAmt ?? 0, + etcAmt: item.etcAmt ?? 0, + sumAmt: item.sumAmt ?? 0, + }), + ), + sewerBscPlanTmplntList: (data.tmplntList ?? []).map((item) => ({ + swtrmtFcltCd: item.swtrmtFcltCd, + atchFileGroupId: item.atchFileGroupId, + sewerBscPlanTmplntDtlList: (item.sewerBscPlanTmplntDtlList ?? []).map((dtlItem) => ({ + planStpNo: dtlItem.planStpNo, + swtrmtFcltCd: dtlItem.swtrmtFcltCd ?? item.swtrmtFcltCd, + prcsZoneArea: dtlItem.prcsZoneArea, + swtrmtPpltnCnt: dtlItem.swtrmtPpltnCnt, + planSwqty: dtlItem.planSwqty, + fcltCpct: dtlItem.fcltCpct, + sprsysDrtwPplLen: dtlItem.sprsysDrtwPplLen, + sprsysExlnPplLen: dtlItem.sprsysExlnPplLen, + cbsysPplLen: dtlItem.cbsysPplLen, + icptgPplLen: dtlItem.icptgPplLen, + drgCnt: dtlItem.drgCnt, + drtwRlyPumpCnt: dtlItem.drtwRlyPumpCnt, + drtwPumpCnt: dtlItem.drtwPumpCnt, + exlnPumpCnt: dtlItem.exlnPumpCnt, + })), + })), + cklstResultList: toCklstSaveDto(cklstResultRows, checklistFormState, reviewStep), + esntlAtchFileList: toEsntlAtchFileSaveList(esntlFileList), + }; +} + +export function buildTechReviewReplyPayload( + data: SewerBscPlanDetResponseDto, + checklistFormState: ChecklistFormState, + reviewStep: ReviewStepCode, + prgrsSttsComCd: string, + techReViewSaveCd: string, + reviewOpinionFormData?: ReviewOpinionGetFormDataResult | null, + opnrNm?: string, +): SewerBscPlanApplySaveRequestDto { + return { + ...buildSavePayload(data, checklistFormState, reviewStep, prgrsSttsComCd), + techReViewSaveCd, + rvwStpCd: reviewStep, + rvwOpnnList: toRvwOpnnSaveDto(data, reviewOpinionFormData, opnrNm), + }; +} + +export function buildSupplementReqPayload( + data: SewerBscPlanDetResponseDto, + payload: SplmntDmndDialogConfirmPayload, + checklistFormState: ChecklistFormState, +): SewerBscPlanApplySaveRequestDto { + const existingItems: SewerBscPlanSplmntDmndItem[] = (data.splmntDmndList ?? []).map((item) => + toSplmntDmndSaveItem(item), + ); + + const newItems: SewerBscPlanSplmntDmndItem[] = payload.items.map((item) => ({ + splmntDmndSn: null, + splmntArtclComCd: item.splmntArtclComCd, + splmntDmndCn: item.splmntDmndCn, + splmntDmndAtchFileGroupId: item.splmntDmndAtchFileGroupId, + cmptnYn: "N", + })); + + return { + ...buildSavePayload(data, checklistFormState, "LOCAL", "SL"), + splmntDmndList: [...existingItems, ...newItems], + }; +} + +export function toSplmntDmndSaveItem(item: SewerBscPlanSplmntDmndItem): SewerBscPlanSplmntDmndItem { + return { + splmntDmndSn: item.splmntDmndSn, + splmntArtclComCd: item.splmntArtclComCd, + splmntDmndCn: item.splmntDmndCn, + splmntDmndDt: item.splmntDmndDt, + splmntDmndAtchFileGroupId: item.splmntDmndAtchFileGroupId, + splmntCn: item.splmntCn, + splmntPicNm: item.splmntPicNm, + splmntCmptnDt: item.splmntCmptnDt, + splmntAtchFileGroupId: item.splmntAtchFileGroupId, + cmptnYn: item.cmptnYn, + }; +} diff --git a/apps/as/src/pages/sewer-bsc-plan/lib/review-opinion-utils.ts b/apps/as/src/pages/sewer-bsc-plan/lib/review-opinion-utils.ts new file mode 100644 index 000000000..91b11b67c --- /dev/null +++ b/apps/as/src/pages/sewer-bsc-plan/lib/review-opinion-utils.ts @@ -0,0 +1,87 @@ +import type { + SewerBscPlanDetResponseDto, + SewerBscPlanRvwOpnnItem, + SewerBscPlanRvwOpnnResponseDto, +} from "@/entities/sewer-bsc-plan"; + +export interface ReviewOpinionFormData { + rvwRsltCn: string; + rvwRsltAnmlyYn: "Y" | "N" | ""; + atchFileGroupId: string; + opnrSeComCd: string; + rvwOpnnSn?: number; +} + +export interface ReviewOpinionGetFormDataResult { + formData: ReviewOpinionFormData; + rvwList: SewerBscPlanRvwOpnnResponseDto[]; +} + +function hasReviewOpinionContent(formData: ReviewOpinionFormData) { + return Boolean(formData.rvwRsltCn.trim() || formData.rvwRsltAnmlyYn || formData.atchFileGroupId); +} + +function mapServerItemToSaveDto(item: SewerBscPlanRvwOpnnResponseDto): SewerBscPlanRvwOpnnItem | null { + if (item.rvwOpnnSn == null) return null; + + return { + rvwOpnnSn: item.rvwOpnnSn, + opnrSeComCd: item.opnrSeComCd, + opnrNm: item.opnrNm, + rvwRsltCn: item.rvwRsltCn, + rvwRsltAnmlyYn: item.rvwRsltAnmlyYn || undefined, + atchFileGroupId: item.atchFileGroupId || undefined, + }; +} + +function mapFormDataToSaveDto( + formData: ReviewOpinionFormData, + opnrNm?: string, +): SewerBscPlanRvwOpnnItem { + const base: SewerBscPlanRvwOpnnItem = { + opnrSeComCd: formData.opnrSeComCd, + opnrNm: opnrNm ?? "", + rvwRsltCn: formData.rvwRsltCn, + rvwRsltAnmlyYn: formData.rvwRsltAnmlyYn || undefined, + atchFileGroupId: formData.atchFileGroupId || undefined, + }; + + if (formData.rvwOpnnSn != null) { + return { ...base, rvwOpnnSn: formData.rvwOpnnSn }; + } + + return base; +} + +export function toRvwOpnnSaveDto( + data: SewerBscPlanDetResponseDto, + result: ReviewOpinionGetFormDataResult | null | undefined, + opnrNm?: string, +): SewerBscPlanRvwOpnnItem[] | undefined { + if (!result || !data.detDto) return undefined; + + const { formData, rvwList } = result; + + const list: SewerBscPlanRvwOpnnItem[] = rvwList + .map((item) => mapServerItemToSaveDto(item)) + .filter((item): item is SewerBscPlanRvwOpnnItem => item != null); + + if (!hasReviewOpinionContent(formData)) { + return list.length > 0 ? list : undefined; + } + + const formDto = mapFormDataToSaveDto(formData, opnrNm); + + if (formData.rvwOpnnSn != null) { + const existingIndex = list.findIndex((item) => item.rvwOpnnSn === formData.rvwOpnnSn); + if (existingIndex >= 0) { + list[existingIndex] = formDto; + } else { + list.push(formDto); + } + } else { + list.push(formDto); + } + + return list.length > 0 ? list : undefined; +} diff --git a/apps/as/src/pages/sewer-bsc-plan/lib/review-step-utils.ts b/apps/as/src/pages/sewer-bsc-plan/lib/review-step-utils.ts new file mode 100644 index 000000000..bce365857 --- /dev/null +++ b/apps/as/src/pages/sewer-bsc-plan/lib/review-step-utils.ts @@ -0,0 +1,44 @@ +import { + PBADMS_PROGRESS_ORDER, + REVIEW_STEP_CODES, + REVIEW_STEP_ROLE_RANK, + type ReviewStepCode, + ROLE_TO_REVIEW_STEP, +} from "../model/view-types"; + +export function getProgressStepCount(prgrsSttsComCd?: string): number { + const idx = PBADMS_PROGRESS_ORDER.indexOf( + prgrsSttsComCd as (typeof PBADMS_PROGRESS_ORDER)[number], + ); + if (idx <= PBADMS_PROGRESS_ORDER.indexOf("SV")) return 1; + if (idx <= PBADMS_PROGRESS_ORDER.indexOf("LE")) return 2; + return 3; +} + +export function getRoleStepCount(dtlInstCd?: string): number { + const mapped = dtlInstCd ? ROLE_TO_REVIEW_STEP[dtlInstCd] : undefined; + return mapped ? REVIEW_STEP_ROLE_RANK[mapped] : 0; +} + +export function getVisibleReviewSteps( + prgrsSttsComCd: string, + userRoleCode?: string, +): ReviewStepCode[] { + const count = Math.min(getProgressStepCount(prgrsSttsComCd), getRoleStepCount(userRoleCode)); + return REVIEW_STEP_CODES.slice(0, count); +} + +export function getEditableReviewStep( + dtlInstCd?: string, + prgrsSttsComCd?: string, +): ReviewStepCode | undefined { + if (dtlInstCd === "ROLE_USER03" && (prgrsSttsComCd === "LE" || prgrsSttsComCd === "TP")) { + return "ENV"; + } + + if (dtlInstCd === "ROLE_USER01" && prgrsSttsComCd === "TE") { + return "KECO"; + } + + return undefined; +} diff --git a/apps/as/src/pages/sewer-bsc-plan/lib/sewer-bsc-plan-regist-schema.ts b/apps/as/src/pages/sewer-bsc-plan/lib/sewer-bsc-plan-regist-schema.ts index 19d0bdf90..416b1f401 100644 --- a/apps/as/src/pages/sewer-bsc-plan/lib/sewer-bsc-plan-regist-schema.ts +++ b/apps/as/src/pages/sewer-bsc-plan/lib/sewer-bsc-plan-regist-schema.ts @@ -1,38 +1,27 @@ import { z } from "zod"; -const tmplntDtlSchema = z.object({ - planStpNo: z.number(), - swtrmtFcltCd: z.string(), - 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(), -}); +import { + sewerBscPlanBizCstItemSchema, + sewerBscPlanCklstResultItemSchema, + sewerBscPlanDtlSaveSchema, + sewerBscPlanEsntlAtchFileItemSchema, + sewerBscPlanFnrsCstItemSchema, + sewerBscPlanOtlnItemSchema, + sewerBscPlanSaveSchema, + sewerBscPlanSplmntDmndItemSchema, + sewerBscPlanTmplntDtlItemSchema, + sewerBscPlanTmplntItemSchema, +} from "@/entities/sewer-bsc-plan"; export const sewerBscPlanRegistSchema = z.object({ - sewerBscPlan: z.object({ - sewerBscPlanDocNo: z.string().optional(), + sewerBscPlan: sewerBscPlanSaveSchema.extend({ sewerBscPlanNm: z.string().min(1, "기본계획명은 필수입니다"), - sewerBscPlanSeComCd: z.string().optional(), sewerBscPlanSeCdNm: z.string().optional(), - taskSeComCd: z.string().optional(), taskSeCdNm: z.string().optional(), - admdstCd: z.string().optional(), - envoSeComCd: z.string().optional(), - prgrsSttsComCd: z.string().optional(), sido: z.string().optional(), - gugun: z.string().optional(), + gugun: z.string().nullish(), }), - sewerBscPlanDtl: z.object({ - chgCycl: z.number().optional(), + sewerBscPlanDtl: sewerBscPlanDtlSaveSchema.extend({ planYr: z.string().min(1, "계획년도는 필수입니다"), crtrYr: z.string().min(1, "기준년도는 필수입니다"), frlvlGoalYr: z.string().min(1, "1단계 목표연도는 필수입니다"), @@ -40,10 +29,6 @@ export const sewerBscPlanRegistSchema = z.object({ thlvlGoalYr: z.string().min(1, "3단계 목표연도는 필수입니다"), folvlGoalYr: z.string().min(1, "4단계 목표연도는 필수입니다"), bizOtlnCn: z.string().min(1, "계획개요는 필수입니다"), - aplcntNm: z.string().optional(), - aplcntMblTelno: z.string().optional(), - aplcntOgdpNm: z.string().optional(), - aplcntEmlAddr: z.string().optional(), dsgnrNm: z.string().min(1, "설계자 이름은 필수입니다"), dsgnrTelno: z .string() @@ -54,74 +39,60 @@ export const sewerBscPlanRegistSchema = z.object({ .string() .min(1, "설계자 이메일은 필수입니다") .email("올바른 이메일 형식이 아닙니다"), - atchFileGroupId: z.string().optional(), }), sewerBscPlanOtlnList: z.array( - z.object({ + sewerBscPlanOtlnItemSchema.extend({ planStpNo: z.number(), - sewerPlanPpltnCnt: z.number().optional(), - sewerPrcsPpltnCnt: z.number().optional(), - sewerPsplyRt: z.number().optional(), }), ), bizCstList: z.array( - z.object({ + sewerBscPlanBizCstItemSchema.extend({ planStpNo: z.number(), acntgSeCd: z.string(), cstrnAmt: z.number(), }), ), fnrsCstList: z.array( - z.object({ + sewerBscPlanFnrsCstItemSchema.extend({ planStpNo: z.number(), - gvexpAmt: z.number().optional(), - lcexpAmt: z.number().optional(), - orgprBdchgAmt: z.number().optional(), - etcAmt: z.number().optional(), - sumAmt: z.number().optional(), }), ), sewerBscPlanTmplntList: z.array( - z.object({ + sewerBscPlanTmplntItemSchema.extend({ swtrmtFcltCd: z.string(), swtrmtFcltNm: z.string().nullish(), - atchFileGroupId: z.string().optional(), atchFileNm: z.string().optional(), - sewerBscPlanTmplntDtlList: z.array(tmplntDtlSchema).optional(), + sewerBscPlanTmplntDtlList: z + .array( + sewerBscPlanTmplntDtlItemSchema.extend({ + planStpNo: z.number(), + swtrmtFcltCd: z.string(), + }), + ) + .optional(), }), ), cklstResultList: z.array( - z.object({ + sewerBscPlanCklstResultItemSchema.extend({ cklstSn: z.number(), cklstArtclSn: z.number(), cklstRvwSn: z.number(), - rvwStpComCd: z.string(), selected: z.enum(["Y", "N"]).nullable(), picRvwCn: z.string().nullable().optional(), }), ), esntlAtchFileList: z.array( - z.object({ + sewerBscPlanEsntlAtchFileItemSchema.extend({ esntlAtchFileId: z.string(), atchFileGroupId: z.string(), }), ), splmntDmndList: z.array( - z.object({ - splmntDmndSn: z.number().optional(), - splmntArtclComCd: z.string().optional(), + sewerBscPlanSplmntDmndItemSchema.extend({ splmntArtclCdNm: z.string().nullish(), - splmntDmndCn: z.string().optional(), - splmntDmndDt: z.string().optional(), - splmntDmndAtchFileGroupId: z.string().optional(), splmntDmndAtchFileNm: z.string().optional(), - splmntCn: z.string().optional(), - splmntPicNm: z.string().optional(), - splmntCmptnDt: z.string().optional(), - splmntAtchFileGroupId: z.string().optional(), splmntAtchFileNm: z.string().optional(), splmntAtchFileAtflId: z.string().optional(), - cmptnYn: z.string().optional(), }), ), privacyConsent: z.enum(["Y", "N"], { @@ -130,6 +101,3 @@ export const sewerBscPlanRegistSchema = z.object({ }); export type SewerBscPlanRegistFormData = z.infer; - -/** 체크리스트·필수첨부 docSeComCd — CMS 합의 전 임시값 */ -export const SEWER_BSC_PLAN_DOC_SE_COM_CD = "SEWER"; diff --git a/apps/as/src/pages/sewer-bsc-plan/lib/use-checklist-form-state.ts b/apps/as/src/pages/sewer-bsc-plan/lib/use-checklist-form-state.ts new file mode 100644 index 000000000..2bfbb58f7 --- /dev/null +++ b/apps/as/src/pages/sewer-bsc-plan/lib/use-checklist-form-state.ts @@ -0,0 +1,61 @@ +import { useCallback, useEffect, useState } from "react"; + +import type { CklstRvwRsltTreeDto } from "@/entities/sewer-bsc-plan"; + +import { + type ChecklistFormState, + DEFAULT_CHECKLIST_PROGRESS, + type ReviewStepCode, +} from "../model/view-types"; + +import { mergeChecklistFormStateFromServer, patchChecklistFormState } from "./checklist-utils"; + +const EMPTY_CKLST_RESULT_LIST: CklstRvwRsltTreeDto[] = []; + +function isSameChecklistFormState(a: ChecklistFormState, b: ChecklistFormState): boolean { + if ( + a.progress.total !== b.progress.total || + a.progress.completed !== b.progress.completed || + a.progress.percent !== b.progress.percent + ) { + return false; + } + + const aResultKeys = Object.keys(a.results); + if (aResultKeys.length !== Object.keys(b.results).length) return false; + if (!aResultKeys.every((key) => a.results[key] === b.results[key])) return false; + + const aCommentKeys = Object.keys(a.comments); + if (aCommentKeys.length !== Object.keys(b.comments).length) return false; + + return aCommentKeys.every((key) => a.comments[key] === b.comments[key]); +} + +function useChecklistFormState(cklstResultList: CklstRvwRsltTreeDto[] = EMPTY_CKLST_RESULT_LIST) { + const [formState, setFormState] = useState({ + results: {}, + comments: {}, + progress: DEFAULT_CHECKLIST_PROGRESS, + }); + + useEffect(() => { + setFormState((prev) => { + const next = mergeChecklistFormStateFromServer(prev, cklstResultList); + return isSameChecklistFormState(prev, next) ? prev : next; + }); + }, [cklstResultList]); + + const patchFormState = useCallback( + ( + patch: Partial>, + reviewStep?: ReviewStepCode, + ) => { + setFormState((prev) => patchChecklistFormState(prev, patch, cklstResultList, reviewStep)); + }, + [cklstResultList], + ); + + return { formState, setFormState, patchFormState }; +} + +export { EMPTY_CKLST_RESULT_LIST, useChecklistFormState }; diff --git a/apps/as/src/pages/sewer-bsc-plan/model/columns.tsx b/apps/as/src/pages/sewer-bsc-plan/model/columns.tsx index 4a9b03a9a..4dc1af0c6 100644 --- a/apps/as/src/pages/sewer-bsc-plan/model/columns.tsx +++ b/apps/as/src/pages/sewer-bsc-plan/model/columns.tsx @@ -11,30 +11,30 @@ import { Badge } from "@common/components"; const columnHelper = createColumnHelper(); const MAIN_STEP_LABELS = { - save: "저장", - apply: "신청완료", + apply: "기본계획 신청", envoReview: "환경청 검토", techReview: "기술검토", reply: "검토결과 회신", - supplement: "보완요청", } as const; -const TECH_REVIEW_STATUS_CODES = new Set(["EE", "ER", "TE", "TS", "TR", "TP"]); +export const PROGRESS_STATUS_COLUMNS = [ + { label: "기본계획 신청 (시군)", sttsComCd: "LE" }, + { label: "환경청 검토", sttsComCd: "ER" }, + { label: "기술검토", sttsComCd: "TE" }, + { label: "기술검토 회신", sttsComCd: "TP" }, + { label: "횐경청 검토 결과 회신", sttsComCd: "EP" }, +]; -type BadgeState = "completed" | "current" | "pending" | "action"; +type BadgeState = "completed" | "current" | "pending"; interface ProgressBadge { label: string; state: BadgeState; } -function buildProgressBadges(prgrsSttsComCd: string, prgrsSttsCdNm: string): ProgressBadge[] { - const techReviewLabel = prgrsSttsCdNm || MAIN_STEP_LABELS.techReview; - const replyLabel = prgrsSttsCdNm || MAIN_STEP_LABELS.reply; - +function buildProgressBadges(prgrsSttsComCd: string, techRplyHstryYn: "Y" | "N"): ProgressBadge[] { if (prgrsSttsComCd === "SV") { return [ - { label: MAIN_STEP_LABELS.save, state: "current" }, { label: MAIN_STEP_LABELS.apply, state: "pending" }, { label: MAIN_STEP_LABELS.envoReview, state: "pending" }, { label: MAIN_STEP_LABELS.techReview, state: "pending" }, @@ -44,9 +44,8 @@ function buildProgressBadges(prgrsSttsComCd: string, prgrsSttsCdNm: string): Pro if (prgrsSttsComCd === "ST") { return [ - { label: MAIN_STEP_LABELS.save, state: "completed" }, - { label: MAIN_STEP_LABELS.apply, state: "completed" }, - { label: MAIN_STEP_LABELS.envoReview, state: "action" }, + { label: MAIN_STEP_LABELS.apply, state: "current" }, + { label: MAIN_STEP_LABELS.envoReview, state: "pending" }, { label: MAIN_STEP_LABELS.techReview, state: "pending" }, { label: MAIN_STEP_LABELS.reply, state: "pending" }, ]; @@ -54,54 +53,52 @@ function buildProgressBadges(prgrsSttsComCd: string, prgrsSttsCdNm: string): Pro if (prgrsSttsComCd === "LE") { return [ - { label: MAIN_STEP_LABELS.save, state: "completed" }, { label: MAIN_STEP_LABELS.apply, state: "completed" }, - { label: MAIN_STEP_LABELS.envoReview, state: "completed" }, - { label: MAIN_STEP_LABELS.reply, state: "action" }, - ]; - } - - if (prgrsSttsComCd === "SL") { - return [ - { label: MAIN_STEP_LABELS.save, state: "completed" }, - { label: MAIN_STEP_LABELS.apply, state: "completed" }, - { label: MAIN_STEP_LABELS.envoReview, state: "completed" }, + { label: MAIN_STEP_LABELS.envoReview, state: "current" }, { label: MAIN_STEP_LABELS.techReview, state: "pending" }, - { label: MAIN_STEP_LABELS.supplement, state: "action" }, + { label: MAIN_STEP_LABELS.reply, state: "pending" }, ]; } - if (TECH_REVIEW_STATUS_CODES.has(prgrsSttsComCd)) { + if (prgrsSttsComCd === "ER" || prgrsSttsComCd === "TE" || prgrsSttsComCd === "SL") { return [ - { label: MAIN_STEP_LABELS.save, state: "completed" }, { label: MAIN_STEP_LABELS.apply, state: "completed" }, { label: MAIN_STEP_LABELS.envoReview, state: "completed" }, - { label: techReviewLabel, state: "action" }, + { label: MAIN_STEP_LABELS.techReview, state: "current" }, { label: MAIN_STEP_LABELS.reply, state: "pending" }, ]; } + if (prgrsSttsComCd === "TP") { + return [ + { label: MAIN_STEP_LABELS.apply, state: "completed" }, + { label: MAIN_STEP_LABELS.envoReview, state: "completed" }, + { + label: MAIN_STEP_LABELS.techReview, + state: techRplyHstryYn === "Y" ? "completed" : "pending", + }, + { label: MAIN_STEP_LABELS.reply, state: "current" }, + ]; + } + if (prgrsSttsComCd === "EP") { return [ - { label: MAIN_STEP_LABELS.save, state: "completed" }, { label: MAIN_STEP_LABELS.apply, state: "completed" }, { label: MAIN_STEP_LABELS.envoReview, state: "completed" }, - { label: MAIN_STEP_LABELS.techReview, state: "completed" }, - { label: replyLabel, state: "action" }, - ]; - } - - if (prgrsSttsComCd === "TA" || prgrsSttsComCd === "TB") { - return [ - { label: MAIN_STEP_LABELS.save, state: "completed" }, - { label: MAIN_STEP_LABELS.apply, state: "completed" }, - { label: MAIN_STEP_LABELS.envoReview, state: "completed" }, - { label: MAIN_STEP_LABELS.techReview, state: "completed" }, + { + label: MAIN_STEP_LABELS.techReview, + state: techRplyHstryYn === "Y" ? "completed" : "pending", + }, { label: MAIN_STEP_LABELS.reply, state: "completed" }, ]; } - return [{ label: prgrsSttsCdNm || prgrsSttsComCd || "-", state: "current" }]; + return [ + { label: MAIN_STEP_LABELS.apply, state: "pending" }, + { label: MAIN_STEP_LABELS.envoReview, state: "pending" }, + { label: MAIN_STEP_LABELS.techReview, state: "pending" }, + { label: MAIN_STEP_LABELS.reply, state: "pending" }, + ]; } function getBadgeColor(state: BadgeState) { @@ -112,8 +109,6 @@ function getBadgeColor(state: BadgeState) { return "blue"; case "pending": return "lightGray"; - case "action": - return "orange"; } } @@ -123,11 +118,11 @@ function formatOptional(value: string | undefined) { interface ProgressStepBadgeProps { prgrsSttsComCd: string; - prgrsSttsCdNm: string; + techRplyHstryYn: "Y" | "N"; } -function ProgressStepBadge({ prgrsSttsComCd, prgrsSttsCdNm }: ProgressStepBadgeProps) { - const badges = buildProgressBadges(prgrsSttsComCd, prgrsSttsCdNm); +function ProgressStepBadge({ prgrsSttsComCd, techRplyHstryYn }: ProgressStepBadgeProps) { + const badges = buildProgressBadges(prgrsSttsComCd, techRplyHstryYn); return (
@@ -186,7 +181,7 @@ export function createSewerBscPlanColumns() { cell: ({ row }) => ( ), }), diff --git a/apps/as/src/pages/sewer-bsc-plan/model/view-types.ts b/apps/as/src/pages/sewer-bsc-plan/model/view-types.ts new file mode 100644 index 000000000..48e5b61f7 --- /dev/null +++ b/apps/as/src/pages/sewer-bsc-plan/model/view-types.ts @@ -0,0 +1,69 @@ +import type { + AtflMngClientResponseDto, + CklstRvwRsltTreeDto, + EsntlAtchFileMngTreeResponseDto, + SewerBscPlanDetResponseDto, + SewerBscPlanResponseDto, + SewerBscPlanRvwOpnnResponseDto, +} from "@/entities/sewer-bsc-plan"; + +export type ReviewStepCode = "LOCAL" | "ENV" | "KECO"; + +export type { + AtflMngClientResponseDto, + CklstRvwRsltTreeDto, + EsntlAtchFileMngTreeResponseDto, + SewerBscPlanDetResponseDto, + SewerBscPlanResponseDto, + SewerBscPlanRvwOpnnResponseDto, +}; + +export type ChecklistRowKey = string; + +export interface ChecklistProgress { + total: number; + completed: number; + percent: number; +} + +export const DEFAULT_CHECKLIST_PROGRESS: ChecklistProgress = { + total: 0, + completed: 0, + percent: 0, +}; + +export interface ChecklistFormState { + results: Record; + comments: Record; + progress: ChecklistProgress; +} + +export type ChecklistStepMap = Record; + +export const REVIEW_STEP_CODES: ReviewStepCode[] = ["LOCAL", "ENV", "KECO"]; + +export const REVIEW_STEP_LABELS: Record = { + LOCAL: "지자체", + ENV: "환경청", + KECO: "환경공단", +}; + +export const TASK_CODE_LABELS: Record = { + CP: "부분변경", + AP: "전체변경", + MP: "병합", +}; + +export const PBADMS_PROGRESS_ORDER = ["SV", "SL", "TA", "EP", "LE", "TP", "ER", "TE"] as const; + +export const ROLE_TO_REVIEW_STEP: Record = { + ROLE_USER05: "LOCAL", + ROLE_USER03: "ENV", + ROLE_USER01: "KECO", +}; + +export const REVIEW_STEP_ROLE_RANK: Record = { + LOCAL: 1, + ENV: 2, + KECO: 3, +}; diff --git a/apps/as/src/pages/sewer-bsc-plan/ui/components/checklist-task-panel.tsx b/apps/as/src/pages/sewer-bsc-plan/ui/components/checklist-task-panel.tsx new file mode 100644 index 000000000..843c6534c --- /dev/null +++ b/apps/as/src/pages/sewer-bsc-plan/ui/components/checklist-task-panel.tsx @@ -0,0 +1,196 @@ +import type { Dispatch, SetStateAction } from "react"; + +import type { CklstRvwRsltTreeDto } from "@/entities/sewer-bsc-plan"; +import { + Button, + cn, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, + Textarea, +} from "@common/components"; + +import { buildChecklistRowKey, patchChecklistFormState } from "../../lib/checklist-utils"; +import type { ReviewStepCode } from "../../model/view-types"; +import { type ChecklistFormState, ROLE_TO_REVIEW_STEP } from "../../model/view-types"; + +interface ChecklistTaskPanelProps { + selectedReviewStep: ReviewStepCode; + selectedRows: CklstRvwRsltTreeDto[]; + allRows: CklstRvwRsltTreeDto[]; + formState: ChecklistFormState; + setFormState: Dispatch>; + userRoleCode?: string; +} + +function ChecklistTaskPanel({ + selectedReviewStep, + selectedRows, + allRows, + formState, + setFormState, + userRoleCode, +}: ChecklistTaskPanelProps) { + const currentUserReviewStep = userRoleCode ? ROLE_TO_REVIEW_STEP[userRoleCode] : undefined; + const isStepEditable = currentUserReviewStep === selectedReviewStep; + + function handleSelectAll(value: "Y" | "N") { + if (!isStepEditable) return; + setFormState((prev) => { + const nextResults = { ...prev.results }; + selectedRows + .filter((row) => row.bchckUseYn === "Y") + .forEach((row) => { + nextResults[buildChecklistRowKey(row, selectedReviewStep)] = value; + }); + return patchChecklistFormState(prev, { results: nextResults }, allRows, selectedReviewStep); + }); + } + + return ( +
+ + + + + 검토항목 + + 주요 검토내용 + 검토 결과 + {selectedReviewStep !== "LOCAL" ? ( + + 기술검토 의견 + + ) : null} + + + + + + + + + + + + {selectedRows.length === 0 ? ( + + + 조회된 체크리스트 항목이 없습니다. + + + ) : ( + selectedRows.map((row) => { + const key = buildChecklistRowKey(row, selectedReviewStep); + const isFieldVisible = row.bchckUseYn === "Y"; + const isLocalReviewStep = selectedReviewStep === "LOCAL"; + + return ( + + {row.cklstArtclNm} + + {row.rvwCn} + + {isFieldVisible && ( + <> + + { + if (!isStepEditable) return; + setFormState((prev) => + patchChecklistFormState( + prev, + { results: { ...prev.results, [key]: "Y" } }, + allRows, + selectedReviewStep, + ), + ); + }} + className={cn(isStepEditable ? "cursor-pointer" : "cursor-not-allowed")} + /> + + + { + if (!isStepEditable) return; + setFormState((prev) => + patchChecklistFormState( + prev, + { results: { ...prev.results, [key]: "N" } }, + allRows, + selectedReviewStep, + ), + ); + }} + className={cn(isStepEditable ? "cursor-pointer" : "cursor-not-allowed")} + /> + + + )} + {!isLocalReviewStep && isFieldVisible && ( + +