From c1d6cd7b8e5ad34ca1b7c53bfb364b26cf943499 Mon Sep 17 00:00:00 2001 From: kiminchan Date: Thu, 23 Jul 2026 17:26:08 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A1=B0=EC=82=AC=EC=82=AC=EC=97=85=20?= =?UTF-8?q?=EB=B3=B4=EA=B3=A0=EC=84=9C=20=EB=8B=A4=EC=9A=B4=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=EB=B0=8F=20=EC=83=81=EC=84=B8=ED=98=84=ED=99=A9=20?= =?UTF-8?q?=EA=B7=B8=EB=A6=AC=EB=93=9C=20=EB=B3=B4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../002-qa-SFR-010-004~007-조사사업-통합검증결과.md | 10 ++ .../001-fe-SFR-010-004~007-조사사업-통합구현기록.md | 9 ++ .../features/exmn/api/exmn-business-api.ts | 4 + .../features/exmn/ui/ExmnRegistrationPage.tsx | 98 +++++++++++++++---- .../exmn/ui/ExmnStatusDetailPanel.tsx | 22 ++++- 5 files changed, 122 insertions(+), 21 deletions(-) diff --git a/apps/sn/docs/작업검증/SFR-010-004~SFR-010-007-조사사업/002-qa-SFR-010-004~007-조사사업-통합검증결과.md b/apps/sn/docs/작업검증/SFR-010-004~SFR-010-007-조사사업/002-qa-SFR-010-004~007-조사사업-통합검증결과.md index 33ade2c2c..583f483e3 100644 --- a/apps/sn/docs/작업검증/SFR-010-004~SFR-010-007-조사사업/002-qa-SFR-010-004~007-조사사업-통합검증결과.md +++ b/apps/sn/docs/작업검증/SFR-010-004~SFR-010-007-조사사업/002-qa-SFR-010-004~007-조사사업-통합검증결과.md @@ -53,6 +53,7 @@ | 2026-07-23 | 조사현황 4차 클릭 구조 교체 | 목록을 공통 Table 스타일의 화면 전용 표로 교체하고 모든 데이터 셀을 전체 면적 button으로 구성, 공통 `rowClick` 제거 | | 2026-07-23 | 조사관리 상세현황 사용자 최종 확인 | 전체 행 선택, 관로·맨홀 탭, 목록·상세 배치와 상세 집계 표시 확인 완료. 조사관리 상세현황 화면 단계 완료 판정 | | 2026-07-23 | git status·문서 정합성 점검 | 추적 코드 8개·신규 코드 15개와 FE 문서 4개를 대조하고 작업카드·구현·검증 문서의 단계 상태와 실제 파일 목록을 동기화 | +| 2026-07-23 | 사업보고서 다운로드 정적 확인 | 첨부 ID가 있는 보고서 셀의 파란 밑줄 링크, 행 클릭 전파 차단, 기존 첨부서비스 Blob 다운로드, 샘플 미연결 안내 확인 | ## 3. FE-0 검증 결과 @@ -161,6 +162,15 @@ - 사용자의 로그인된 실제 화면 최종 확인으로 행 전체 선택, 관로·맨홀 전환, 목록·상세 배치와 상세 집계 표시를 완료 판정했다. - 남은 검증은 실 API/백엔드 연동과 후속 상세보기·다운로드 구현이다. 지도보기는 보류다. +## 11. 조사사업 목록 사업보고서 다운로드 검증 결과 + +- `bizRptpAtchFileId`가 있는 행만 보고서 텍스트에 파란색·밑줄·hover 스타일을 적용했다. +- 보고서 링크 클릭 시 `stopPropagation()`으로 조사사업 행 선택 동작과 파일 다운로드 동작을 분리했다. +- 기존 첨부서비스 경로 `/attach-service/api/v1/download/{atflId}`와 공통 `fileDownload()`를 재사용했다. +- 실제 파일명 원천이 목록 응답에 없으므로 현재 다운로드명은 `${사업명}_사업보고서.pdf`다. +- `sample=Y`는 실제 첨부서비스를 호출하지 않고 안내를 표시한다. +- 실제 첨부 ID를 사용한 브라우저 다운로드와 PDF 열림 확인은 백엔드·첨부 데이터 준비 후 수행해야 한다. + ## 7. 흡수한 기존 문서 - 없음 diff --git a/apps/sn/docs/작업내용/SFR-010-004~SFR-010-007-조사사업/001-fe-SFR-010-004~007-조사사업-통합구현기록.md b/apps/sn/docs/작업내용/SFR-010-004~SFR-010-007-조사사업/001-fe-SFR-010-004~007-조사사업-통합구현기록.md index 83e7dcba3..d3383fc11 100644 --- a/apps/sn/docs/작업내용/SFR-010-004~SFR-010-007-조사사업/001-fe-SFR-010-004~007-조사사업-통합구현기록.md +++ b/apps/sn/docs/작업내용/SFR-010-004~SFR-010-007-조사사업/001-fe-SFR-010-004~007-조사사업-통합구현기록.md @@ -267,6 +267,15 @@ - 사업등록 popup은 확정 크기 `920×620`, 처리분구 영역 `21rem`으로 복구했다. 결과등록은 `980×340`을 유지한다. - 따라서 조사사업 목록·상세의 폭 정렬은 유지하면서 WindowPopup의 기존 확정 디자인에는 전역 CSS 변경이 전파되지 않는다. +### 9.15 조사사업 목록 사업보고서 직접 다운로드 + +- 정보검색 보고서 탭의 파란 밑줄 파일 링크와 기존 운영 화면의 첨부서비스 Blob 다운로드 패턴을 재사용했다. +- 조사사업 목록에서 `bizRptpAtchFileId`가 있는 `보고서=있음` 셀만 파란색·밑줄·hover 색상으로 표시한다. +- 링크 클릭은 행 선택 이벤트의 전파를 중단하고 `/attach-service/api/v1/download/{atflId}`에서 원본 Blob을 받은 뒤 `${사업명}_사업보고서.pdf`로 내려받는다. +- `reportYn=Y`여도 첨부파일 ID가 없으면 다운로드 링크로 표시하지 않는다. +- `sample=Y`의 샘플 첨부 ID는 실제 파일로 위장하지 않고 다운로드 미지원 안내만 표시한다. +- 신규 백엔드 API·DTO·DB 변경은 없으며 결과등록에서 저장한 `bizRptpAtchFileId`를 그대로 사용한다. + ## 10. 2026-07-23 git status 기준 실제 수정 파일 ### 10.1 추적 코드 변경 9개 diff --git a/apps/sn/src/features/exmn/api/exmn-business-api.ts b/apps/sn/src/features/exmn/api/exmn-business-api.ts index 0f270fce3..4c9e8d90a 100644 --- a/apps/sn/src/features/exmn/api/exmn-business-api.ts +++ b/apps/sn/src/features/exmn/api/exmn-business-api.ts @@ -107,6 +107,10 @@ export const createExmnTemplates = async ( export const downloadExmnTemplates = async (exmnBizId: string): Promise => getBlobData(`${EXMN_BUSINESS_API_PATH}/${encodeURIComponent(exmnBizId)}/templates/download`); +/** 사업보고서 첨부파일 ID로 기존 첨부서비스의 원본 파일을 내려받는다. */ +export const downloadExmnBusinessReport = async (attachmentId: string): Promise => + getBlobData(`/attach-service/api/v1/download/${encodeURIComponent(attachmentId)}`); + /** 사업 파일별 첨부서비스 키 쌍을 조사사업에 연결한다. */ export const updateExmnAttachments = async ( exmnBizId: string, diff --git a/apps/sn/src/features/exmn/ui/ExmnRegistrationPage.tsx b/apps/sn/src/features/exmn/ui/ExmnRegistrationPage.tsx index 3c3db45df..e873d76bb 100644 --- a/apps/sn/src/features/exmn/ui/ExmnRegistrationPage.tsx +++ b/apps/sn/src/features/exmn/ui/ExmnRegistrationPage.tsx @@ -1,6 +1,7 @@ "use client"; import LoadingOverlay from "@/components/ui/LoadingBar"; +import { fileDownload } from "@/components/lib/utils"; import { CtpvSggPrcsFcltSelectBox } from "@/components/ui/ctpvSggPrcsFcltSelectBox"; import { getStartYearArray } from "@/components/ui/YearRangeSelectBox"; import { openWindowPopup } from "@/features/windowPopup/lib/openWindowPopup"; @@ -20,7 +21,10 @@ import { SearchDiv, SelectBox } from "@common/business_components/ui"; import type { UiTableSort } from "@common/business_components/ui"; import ListDiv from "@common/business_components/ui/list_div"; import { useEffect, useMemo, useRef, useState } from "react"; -import { getExmnBusinesses } from "../api/exmn-business-api"; +import { + downloadExmnBusinessReport, + getExmnBusinesses, +} from "../api/exmn-business-api"; import type { ExmnBusiness, ExmnBusinessSearchParams, @@ -66,7 +70,7 @@ const EMPTY_FILTERS: BusinessFilters = { bizNm: "", }; -type Notice = { type: "success" | "error"; text: string } | null; +type Notice = { type: "success" | "error" | "info"; text: string } | null; /** 서버·샘플 목록에 같은 비교 규칙을 적용해 정렬 헤더와 실제 행 순서를 일치시킨다. */ const sortBusinessList = ( @@ -273,6 +277,33 @@ export function ExmnRegistrationPage() { await fetchBusinesses(draftFilters, sort); }; + /** 목록의 보고서 링크에서 첨부서비스 원본 PDF를 내려받는다. */ + const handleReportDownload = async (business: ExmnBusiness) => { + const attachmentId = business.bizRptpAtchFileId?.trim(); + if (!attachmentId) { + setNotice({ type: "error", text: "다운로드할 사업보고서 첨부파일 정보가 없습니다." }); + return; + } + if (sampleMode) { + setNotice({ type: "info", text: "샘플 모드에서는 실제 사업보고서 파일을 다운로드하지 않습니다." }); + return; + } + + setIsLoading(true); + setNotice(null); + try { + const reportBlob = await downloadExmnBusinessReport(attachmentId); + fileDownload(reportBlob, `${business.bizNm}_사업보고서.pdf`); + } catch (error) { + setNotice({ + type: "error", + text: error instanceof Error ? error.message : "사업보고서 다운로드 중 오류가 발생했습니다.", + }); + } finally { + setIsLoading(false); + } + }; + /** 공통 WindowPopup 체계로 사업정보 등록 화면을 연다. */ const openBusinessRegister = () => { setNotice(null); @@ -406,7 +437,18 @@ export function ExmnRegistrationPage() { {notice && ( -
{notice.text}
+
+ {notice.text} +
)} @@ -422,16 +464,18 @@ export function ExmnRegistrationPage() { )} -
+
+
{BUSINESS_TABLE_HEAD.map((head) => ( @@ -513,17 +557,36 @@ export function ExmnRegistrationPage() { className="cursor-pointer" onClick={() => setSelectedBusinessId(business.exmnBizId)} > - {BUSINESS_TABLE_HEAD.map((head) => ( - - - - ))} + {BUSINESS_TABLE_HEAD.map((head) => { + const isReportCell = head.id === "reportYnText"; + const reportAvailable = isReportCell && Boolean(business.bizRptpAtchFileId?.trim()); + return ( + + + + ); + })} ); }) : ( @@ -535,6 +598,7 @@ export function ExmnRegistrationPage() { )}
+