Compare commits
15
Commits
c2396f093b
...
df60bd0b40
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df60bd0b40 | ||
|
|
41e6f1cf48 | ||
|
|
0802ba191a | ||
|
|
ad57ed241d | ||
|
|
f17cb420e3 | ||
|
|
5e43d69d8e | ||
|
|
9971e512c8 | ||
|
|
0c77ed9bba | ||
|
|
e759c96ade | ||
|
|
f602b75268 | ||
|
|
11d0378060 | ||
|
|
8ed4f959cb | ||
|
|
e5f7778861 | ||
|
|
2d086a05b2 | ||
|
|
6ef4603a7c |
@@ -76,7 +76,7 @@ function AprvrPickerDialog({ open, onOpenChange, atrzSeComCd, onSelect }: AprvrP
|
||||
배치된 검토인력이 없습니다.
|
||||
</p>
|
||||
) : (
|
||||
<Table caption="결재자 선택 목록으로 성명, 부서, 직위, 직급, 선택 정보 제공" className="w-full">
|
||||
<Table caption="검토인력 선택 목록으로 성명, 부서, 직위, 직급, 선택 정보 제공" className="w-full">
|
||||
<colgroup>
|
||||
<col width="25%" />
|
||||
<col width="25%" />
|
||||
|
||||
@@ -162,7 +162,7 @@ function ElaprAplnPickerGrid({
|
||||
</p>
|
||||
) : (
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
<Table caption="결재선 선택 목록으로 결재순서, 결재구분, 결재자 정보 제공" className="w-full">
|
||||
<Table caption="결재선 관리 목록으로 결재순서, 결재구분, 결재자, 관리 정보 제공" className="w-full">
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
|
||||
@@ -315,7 +315,7 @@ function ElaprApprovalDialog({
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<Table caption="결재 진행 현황 목록으로 결재순서, 결재구분, 결재자, 전결자 지정, 결재상태, 관리 정보 제공" className="w-full">
|
||||
<Table caption="결재 진행 단계 목록으로 결재순서, 결재구분, 결재자, 전결자 지정, 결재상태, 관리 정보 제공" className="w-full">
|
||||
<colgroup>
|
||||
<col width="8%" />
|
||||
<col width="8%" />
|
||||
|
||||
@@ -66,7 +66,7 @@ function ElaprTmpltSelectTable({
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "결재서식 선택 목록",
|
||||
caption: "전자결재서식 목록으로 결재서식명 정보 제공",
|
||||
onRowClick: (row) => {
|
||||
setRowSelection({ [row.elaprTmpltId]: true });
|
||||
onSelectTmplt(row);
|
||||
|
||||
@@ -73,7 +73,7 @@ function AprvrPickerDialog({
|
||||
배치된 검토인력이 없습니다.
|
||||
</p>
|
||||
) : (
|
||||
<Table caption="검토인력 목록으로 성명, 부서, 직위, 직급, 선택 정보 제공" className="w-full">
|
||||
<Table caption="검토인력 선택 목록으로 성명, 부서, 직위, 직급, 선택 정보 제공" className="w-full">
|
||||
<colgroup>
|
||||
<col width="25%" />
|
||||
<col width="25%" />
|
||||
|
||||
@@ -185,7 +185,7 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) {
|
||||
</p>
|
||||
) : (
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
<Table caption="결재선 목록으로 결재순서, 결재구분, 결재자 정보 제공" className="w-full">
|
||||
<Table caption="결재선 관리 목록으로 결재순서, 결재구분, 결재자 ID, 결재자, 등록일시, 관리 정보 제공" className="w-full">
|
||||
<colgroup>
|
||||
<col width="5%" />
|
||||
<col width="10%" />
|
||||
|
||||
@@ -90,7 +90,7 @@ function ElaprTmpltList({ selectedTmpltId, onSelectTmplt }: ElaprTmpltListProps)
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "결재서식 목록으로 결재서식명 정보 제공",
|
||||
caption: "전자결재선 관리 목록으로 결재서식명, 문서구분, 등록일시, 관리 정보 제공",
|
||||
onRowClick: (row) => {
|
||||
setRowSelection({ [row.elaprTmpltId]: true });
|
||||
onSelectTmplt(row);
|
||||
|
||||
@@ -70,7 +70,7 @@ function CklstArtclList({ cklstSn }: CklstArtclListProps) {
|
||||
Success: () => (
|
||||
<DataTable
|
||||
table={table}
|
||||
caption="체크리스트 항목 목록: 항목명, 검토내용, 검토결과 정보 제공"
|
||||
caption="점검항목 관리 목록으로 점검항목코드, 점검항목명, 사용여부, 등록일시, 관리 정보 제공"
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
@@ -83,7 +83,7 @@ function CklstMngList({ selectedCklstSn, onSelectCklst }: CklstMngListProps) {
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "체크리스트 목록",
|
||||
caption: "점검표 관리 목록으로 점검표일련번호, 점검표명, 문서구분, 업무구분, 사용여부, 등록일시, 관리 정보 제공",
|
||||
onRowClick: (row) => {
|
||||
setRowSelection({ [String(row.cklstSn)]: true });
|
||||
onSelectCklst(row);
|
||||
|
||||
@@ -51,7 +51,7 @@ function CklstPreviewDialog({
|
||||
{displayRows.length === 0 ? (
|
||||
<p className="py-6 text-center text-muted-foreground">표시할 항목이 없습니다.</p>
|
||||
) : (
|
||||
<Table caption={`체크리스트 ${cklstNm} 미리보기`}>
|
||||
<Table caption={`체크리스트 미리보기 ${cklstNm} 목록으로 점검항목, 주요 검토내용, 검토 여부 정보 제공`}>
|
||||
<colgroup>
|
||||
<col width="25%" />
|
||||
<col width="50%" />
|
||||
|
||||
@@ -277,7 +277,7 @@ function CklstRvwGrid({ selectedCklst }: CklstRvwGridProps) {
|
||||
등록된 항목이 없습니다. [추가]로 1단계 항목을 등록하세요.
|
||||
</p>
|
||||
) : (
|
||||
<Table caption="체크리스트 항목 목록" className="w-full">
|
||||
<Table caption="체크리스트 항목 목록으로 점검항목, 주요 검토내용, 사용 여부, 체크 사용, 정렬 순서, 구분, 관리 정보 제공" className="w-full">
|
||||
<colgroup>
|
||||
<col width="16%" />
|
||||
<col width="36%" />
|
||||
|
||||
@@ -103,7 +103,7 @@ function AcntgCodeTabPanel() {
|
||||
</p>
|
||||
) : (
|
||||
<div className="max-h-[60rem] overflow-y-auto px-4 pb-4 pt-1">
|
||||
<Table caption="회계구분 코드 목록" className="w-full">
|
||||
<Table caption="회계코드 목록으로 코드, 코드명, 설명, 사용 여부, 정렬순서, 구분, 관리 정보 제공" className="w-full">
|
||||
<colgroup>
|
||||
<col width="14%" />
|
||||
<col width="16%" />
|
||||
|
||||
@@ -103,7 +103,7 @@ function CnstpCodeTabPanel() {
|
||||
</p>
|
||||
) : (
|
||||
<div className="max-h-[60rem] overflow-y-auto px-4 pb-4 pt-1">
|
||||
<Table caption="공종구분 코드 목록" className="w-full">
|
||||
<Table caption="공사코드 목록으로 코드, 코드명, 설명, 숫자 여부, 소숫점 단위, 사용 여부, 정렬순서, 구분, 관리 정보 제공" className="w-full">
|
||||
<colgroup>
|
||||
<col width="14%" />
|
||||
<col width="16%" />
|
||||
|
||||
@@ -103,7 +103,7 @@ function FnrsSharCodeTabPanel() {
|
||||
</p>
|
||||
) : (
|
||||
<div className="max-h-[60rem] overflow-y-auto px-4 pb-4 pt-1">
|
||||
<Table caption="재원분담 코드 목록" className="w-full">
|
||||
<Table caption="재원분담구분코드 목록으로 코드, 코드명, 설명, 사용 여부, 정렬순서, 구분, 관리 정보 제공" className="w-full">
|
||||
<colgroup>
|
||||
<col width="14%" />
|
||||
<col width="16%" />
|
||||
|
||||
@@ -39,7 +39,7 @@ export function DashboardNoticeTable() {
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "공지사항 목록",
|
||||
caption: "공지사항 목록으로 번호, 제목, 작성자, 작성일, 조회수 정보 제공",
|
||||
variant: "line",
|
||||
onRowClick: handleRowClick,
|
||||
},
|
||||
|
||||
@@ -59,7 +59,7 @@ function DesignList({ searchParams, pagination, onPaginationChange }: DesignList
|
||||
manualPagination: true,
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "설계검토 목록",
|
||||
caption: "설계자문 목록으로 시도, 시군구, 사업명, 업무구분, 담당자, 해당기관, 진행상황, 신청일 정보 제공",
|
||||
viewPagination: true,
|
||||
onRowClick: handleRowClick,
|
||||
},
|
||||
|
||||
@@ -36,7 +36,7 @@ function BasicInfoFormSection() {
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="기본 정보">
|
||||
<Table caption="사업 기본 정보 입력" variant="secondary">
|
||||
<Table caption="기본 정보 입력으로 사업명, 사업기간, 사업내용, 구분, 신청 지자체 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
|
||||
@@ -11,7 +11,7 @@ interface BasicInfoSectionProps {
|
||||
function BasicInfoSection({ det }: BasicInfoSectionProps) {
|
||||
return (
|
||||
<SectionCollapsible title="기본 정보">
|
||||
<Table caption="사업 기본 정보" variant="secondary">
|
||||
<Table caption="기본 정보로 사업명, 사업기간, 사업내용, 환경청구분, 구분, 신청지자체 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
|
||||
@@ -13,7 +13,7 @@ function BusinessScaleSection({ bizSclList }: BusinessScaleSectionProps) {
|
||||
return (
|
||||
<SectionCollapsible title="사업규모">
|
||||
<div className="overflow-x-auto">
|
||||
<Table caption="사업규모 목록: 구분, 기본계획, 예산반영, 금회설계, 증감 정보 제공" variant="secondary">
|
||||
<Table caption="사업규모 목록으로 구분, 기본계획(A), 예산반영(B), 금회설계시(C), 증감(C-B) 정보 제공" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[30rem]">구분</TableHead>
|
||||
|
||||
@@ -11,7 +11,7 @@ interface ContactInfoSectionProps {
|
||||
function ContactInfoSection({ det }: ContactInfoSectionProps) {
|
||||
return (
|
||||
<SectionCollapsible title="담당자 정보">
|
||||
<Table caption="사업 담당자 정보" variant="secondary">
|
||||
<Table caption="담당자 정보로 신청자, 이름, 연락처, 소속, 이메일, 설계자, 이름, 연락처, 소속, 이메일 정보 제공" variant="secondary">
|
||||
<TableHeader>
|
||||
{det.aplcntNm && (
|
||||
<>
|
||||
|
||||
@@ -180,7 +180,7 @@ const TechReviewOpinionSection = forwardRef<
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
||||
<Table caption="검토의견 입력" variant="secondary">
|
||||
<Table caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead scope="row" className="w-[12rem]">의견내용</TableHead>
|
||||
@@ -289,7 +289,7 @@ const TechReviewOpinionSection = forwardRef<
|
||||
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<Table caption="검토의견 목록: 의견자, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
|
||||
<Table caption="검토의견 목록으로 의견자명, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="55%" />
|
||||
|
||||
@@ -100,7 +100,7 @@ function EsntlAtchFileList({
|
||||
manualPagination: true,
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "필수 첨부파일 관리 목록",
|
||||
caption: "필수첨부파일 목록으로 필수첨부파일아이디, 문서구분, 필수첨부파일명, 필수첨부여부, 비고, 양식 내려받기, 관리 정보 제공",
|
||||
viewPagination: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -53,7 +53,7 @@ function ChecklistTaskPanel({
|
||||
return (
|
||||
<>
|
||||
<div className="overflow-x-auto">
|
||||
<Table caption="체크리스트 검토 입력: 검토항목, 주요 검토내용, 검토결과 정보 제공" variant="secondary">
|
||||
<Table caption={`체크리스트 목록으로 검토항목, 주요 검토내용, 검토 결과${selectedReviewStep !== "LOCAL" ? ", 기술검토 의견" : ""} 정보 제공`} variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[20rem]" rowSpan={2}>
|
||||
|
||||
@@ -115,7 +115,7 @@ function FileVrfcSplmntDialog({
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "파일 검증 보완요청 목록",
|
||||
caption: "파일보완 목록으로 선택, 파일유형, 파일구분, 검증내역 정보 제공",
|
||||
viewPagination: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -42,7 +42,7 @@ function PwrkCnsltnDocPjtcoAjmtTable({
|
||||
}
|
||||
|
||||
return (
|
||||
<Table caption="사업비 조정 내역: 구분, 검증결과, 검증금액 정보 제공" variant="secondary">
|
||||
<Table caption="금액검증결과 목록으로 구분, 검증결과, 검증금액 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="30%" />
|
||||
<col width="50%" />
|
||||
|
||||
@@ -157,7 +157,7 @@ function CnsltnListTable({
|
||||
const isSomeSelected = selectedCount > 0 && !isAllSelected;
|
||||
|
||||
return (
|
||||
<Table caption="자문 검토결과 목록: 구분, 검토의견 정보 제공" variant="secondary">
|
||||
<Table caption="검토 결과 조회 목록으로 선택, 구분, 검토의견 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="4%" />
|
||||
<col width="4%" />
|
||||
|
||||
@@ -59,7 +59,7 @@ function FnrsList({ searchParams, pagination, onPaginationChange }: FnrsListProp
|
||||
manualPagination: true,
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "재원협의 목록",
|
||||
caption: "재원협의 목록으로 시도, 시군구, 사업명, 업무구분, 담당자, 해당기관, 진행상황, 신청일 정보 제공",
|
||||
viewPagination: true,
|
||||
onRowClick: handleRowClick,
|
||||
},
|
||||
|
||||
@@ -49,7 +49,7 @@ function BasicInfoFormSection() {
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="기본 정보">
|
||||
<Table caption="재원협의 기본 정보 입력" variant="secondary">
|
||||
<Table caption="기본 정보 입력으로 사업명, 사업기간, 사업내용, 구분, 신청 지자체, 시설종류, 재이용용도 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
|
||||
@@ -11,7 +11,7 @@ interface BasicInfoSectionProps {
|
||||
function BasicInfoSection({ det }: BasicInfoSectionProps) {
|
||||
return (
|
||||
<SectionCollapsible title="기본 정보">
|
||||
<Table caption="재원협의 기본 정보" variant="secondary">
|
||||
<Table caption="기본 정보로 사업명, 사업기간, 사업내용, 환경청구분, 구분, 신청지자체, 확정예산사업번호, 예산신청연도 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
|
||||
@@ -20,7 +20,7 @@ function BusinessCostSection({ bizCstList }: BusinessCostSectionProps) {
|
||||
<SectionCollapsible title="총사업비">
|
||||
<div className="overflow-x-auto">
|
||||
<p className="mb-2 text-right font-normal text-muted-foreground">(단위: 백만원)</p>
|
||||
<Table caption="총사업비 내역: 구분, 예산반영, 표준사업비, 금회설계, 증감 정보 제공" variant="secondary" className="table-fixed">
|
||||
<Table caption="총사업비 목록으로 구분, 예산반영(A), 표준사업비(B), 금회설계시(C), 증감 정보 제공" variant="secondary" className="table-fixed">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[30rem]" rowSpan={2}>
|
||||
|
||||
@@ -13,7 +13,7 @@ function BusinessScaleSection({ bizSclList }: BusinessScaleSectionProps) {
|
||||
return (
|
||||
<SectionCollapsible title="사업규모">
|
||||
<div className="overflow-x-auto">
|
||||
<Table caption="사업규모 내역: 구분, 기본계획, 예산반영, 금회설계, 증감 정보 제공" variant="secondary">
|
||||
<Table caption="사업규모 목록으로 구분, 기본계획(A), 예산반영(B), 금회설계시(C), 증감(C-B) 정보 제공" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[30rem]">구분</TableHead>
|
||||
|
||||
@@ -11,7 +11,7 @@ interface ContactInfoSectionProps {
|
||||
function ContactInfoSection({ det }: ContactInfoSectionProps) {
|
||||
return (
|
||||
<SectionCollapsible title="담당자 정보">
|
||||
<Table caption="재원협의 담당자 정보" variant="secondary">
|
||||
<Table caption="담당자 정보로 신청자, 이름, 연락처, 소속, 이메일, 설계자, 이름, 연락처, 소속, 이메일 정보 제공" variant="secondary">
|
||||
<TableHeader>
|
||||
{det.aplcntNm && (
|
||||
<>
|
||||
|
||||
@@ -21,7 +21,7 @@ function FnrsSharSection({ fnrsSharList }: FnrsSharSectionProps) {
|
||||
<SectionCollapsible title="재원분담">
|
||||
<div className="overflow-x-auto">
|
||||
<p className="mb-2 text-right font-normal text-muted-foreground">(단위: 백만원)</p>
|
||||
<Table caption="재원분담 내역: 구분, 현행, 변경요구, 증감, 비고 정보 제공" variant="secondary" className="table-fixed">
|
||||
<Table caption="재원분담 목록으로 구분, 현행(A), 변경요구(B), 증감, 비고 정보 제공" variant="secondary" className="table-fixed">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[24rem]" rowSpan={2}>
|
||||
|
||||
@@ -189,7 +189,7 @@ const TechReviewOpinionSection = forwardRef<
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
||||
<Table caption="검토의견 입력" variant="secondary">
|
||||
<Table caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[12rem]">의견내용</TableHead>
|
||||
@@ -303,7 +303,7 @@ const TechReviewOpinionSection = forwardRef<
|
||||
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<Table caption="검토의견 목록: 의견자, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
|
||||
<Table caption="검토의견 목록으로 의견자명, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="55%" />
|
||||
|
||||
@@ -52,7 +52,7 @@ function ChecklistTaskPanel({
|
||||
return (
|
||||
<>
|
||||
<div className="overflow-x-auto">
|
||||
<Table caption="체크리스트 검토 입력: 검토항목, 주요 검토내용, 검토결과 정보 제공" variant="secondary">
|
||||
<Table caption={`체크리스트 목록으로 검토항목, 주요 검토내용, 검토 결과${selectedReviewStep !== "LOCAL" ? ", 기술검토 의견" : ""} 정보 제공`} variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[20rem]" rowSpan={2}>
|
||||
|
||||
@@ -59,7 +59,7 @@ function InstlList({ searchParams, pagination, onPaginationChange }: InstlListPr
|
||||
manualPagination: true,
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "설치승인 목록",
|
||||
caption: "설치인가 목록으로 시도, 시군구, 사업명, 업무구분, 담당자, 해당기관, 진행상황, 신청일 정보 제공",
|
||||
viewPagination: true,
|
||||
onRowClick: handleRowClick,
|
||||
},
|
||||
|
||||
@@ -53,7 +53,7 @@ function BasicInfoFormSection() {
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="기본 정보">
|
||||
<Table caption="설치승인 기본 정보 입력" variant="secondary">
|
||||
<Table caption="기본 정보 입력으로 사업명, 사업기간, 사업내용, 구분, 신청 지자체, 사업구분, 시설종류, 재이용용도 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
|
||||
@@ -11,7 +11,7 @@ interface BasicInfoSectionProps {
|
||||
function BasicInfoSection({ det }: BasicInfoSectionProps) {
|
||||
return (
|
||||
<SectionCollapsible title="기본 정보">
|
||||
<Table caption="설치승인 기본 정보" variant="secondary">
|
||||
<Table caption="기본 정보로 사업명, 사업기간, 사업내용, 구분, 신청지자체, 사업구분 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
|
||||
@@ -20,7 +20,7 @@ function BusinessCostSection({ bizCstList }: BusinessCostSectionProps) {
|
||||
<SectionCollapsible title="총사업비">
|
||||
<div className="overflow-x-auto">
|
||||
<p className="mb-2 text-right font-normal text-muted-foreground">(단위: 백만원)</p>
|
||||
<Table caption="총사업비 내역: 구분, 예산반영, 표준사업비, 금회설계, 증감 정보 제공" variant="secondary" className="table-fixed">
|
||||
<Table caption="총사업비 목록으로 구분, 예산반영(A), 표준사업비(B), 금회설계시(C), 증감 정보 제공" variant="secondary" className="table-fixed">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[30rem]" rowSpan={2}>
|
||||
|
||||
@@ -13,7 +13,7 @@ function BusinessScaleSection({ bizSclList }: BusinessScaleSectionProps) {
|
||||
return (
|
||||
<SectionCollapsible title="사업규모">
|
||||
<div className="overflow-x-auto">
|
||||
<Table caption="사업규모 내역: 구분, 기본계획, 예산반영, 금회설계, 증감 정보 제공" variant="secondary">
|
||||
<Table caption="사업규모 목록으로 구분, 기본계획(A), 예산반영(B), 금회설계시(C), 증감(C-B) 정보 제공" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[30rem]">구분</TableHead>
|
||||
|
||||
@@ -11,7 +11,7 @@ interface ContactInfoSectionProps {
|
||||
function ContactInfoSection({ det }: ContactInfoSectionProps) {
|
||||
return (
|
||||
<SectionCollapsible title="담당자 정보">
|
||||
<Table caption="설치승인 담당자 정보" variant="secondary">
|
||||
<Table caption="담당자 정보로 신청자, 이름, 연락처, 소속, 이메일, 설계자, 이름, 연락처, 소속, 이메일 정보 제공" variant="secondary">
|
||||
<TableHeader>
|
||||
{det.aplcntNm && (
|
||||
<>
|
||||
|
||||
@@ -180,7 +180,7 @@ const TechReviewOpinionSection = forwardRef<
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
||||
<Table caption="검토의견 입력" variant="secondary">
|
||||
<Table caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead scope="row" className="w-[12rem]">의견내용</TableHead>
|
||||
@@ -301,7 +301,7 @@ const TechReviewOpinionSection = forwardRef<
|
||||
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<Table caption="검토의견 목록: 의견자, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
|
||||
<Table caption="검토의견 목록으로 의견자명, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="55%" />
|
||||
|
||||
@@ -70,7 +70,7 @@ function MyPbadmsSprtList({ searchParams, pagination, onPaginationChange }: MyPb
|
||||
manualPagination: true,
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "나의 행정지원 목록",
|
||||
caption: "내 신청 목록으로 No, 구분, 신청일, 진행상태, 완료(예정)일, 담당자, 담당자연락처 정보 제공",
|
||||
viewPagination: true,
|
||||
onRowClick: handleRowClick,
|
||||
},
|
||||
|
||||
@@ -97,7 +97,7 @@ function NtiHstryList({ searchParams, pagination, onPaginationChange }: NtiHstry
|
||||
manualPagination: true,
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "알림톡 발송 이력 목록",
|
||||
caption: "알림 이력 목록으로 No, 알림일시, 알림내용, 연계업무, 상태, 처리 정보 제공",
|
||||
viewPagination: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -33,7 +33,7 @@ function InPrgrsDocRcvrList({ selectedRcvrKeys, onRcvrSelectionChange }: InPrgrs
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
...rowSelectionOptions,
|
||||
},
|
||||
dataTableOptions: { caption: "진행중 문서 담당자 수신자 목록" },
|
||||
dataTableOptions: { caption: "현재 진행중 문서 담당자(신청자) 목록으로 선택, 문서번호, 문서명, 신청자, 상태 정보 제공" },
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -34,7 +34,7 @@ function RvwMnpwRcvrList({ selectedRcvrKeys, onRcvrSelectionChange }: RvwMnpwRcv
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
...rowSelectionOptions,
|
||||
},
|
||||
dataTableOptions: { caption: "검토인력 수신자 목록" },
|
||||
dataTableOptions: { caption: "검토인력 목록으로 선택, 이름, 소속, 전화번호 정보 제공" },
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -35,7 +35,7 @@ function SndngList({
|
||||
|
||||
return (
|
||||
<div className="max-h-[40rem] overflow-auto rounded-md border">
|
||||
<Table caption="알림톡 발송 대상 목록: 이름, 전화번호, 변수, 버튼 정보 제공" variant="line" className="text-sm">
|
||||
<Table caption={`발송 목록으로 ${["이름", "전화번호", ...variableNames.map((variableName) => `#${variableName}`), "버튼"].join(", ")} 정보 제공`} variant="line" className="text-sm">
|
||||
<TableHeader className="top-0">
|
||||
<TableRow>
|
||||
<TableHead className="min-w-[100px]">이름</TableHead>
|
||||
|
||||
@@ -52,7 +52,7 @@ function ChecklistTaskPanel({
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<Table caption="체크리스트 검토 입력: 검토항목, 주요 검토내용, 검토결과 정보 제공" variant="secondary">
|
||||
<Table caption={`체크리스트 목록으로 검토항목, 주요 검토내용, 검토 결과${selectedReviewStep !== "LOCAL" ? ", 기술검토 의견" : ""} 정보 제공`} variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[20rem]" rowSpan={2}>
|
||||
|
||||
@@ -35,7 +35,7 @@ function PreviousPlanAreaTable({ data }: PreviousPlanAreaTableProps) {
|
||||
const yearCells = getPlanYearCells(data);
|
||||
|
||||
return (
|
||||
<Table caption="이전 기본계획 처리구역 현황" variant="secondary">
|
||||
<Table caption="처리구역별 현황 목록으로 구분, 당초, 기준, 변경 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="16%" />
|
||||
<col width="9.3%" />
|
||||
|
||||
@@ -202,7 +202,7 @@ function PreviousPlanFileSection({ clsfSystmNm }: PreviousPlanFileSectionProps)
|
||||
) : files.length === 0 ? (
|
||||
<p className="py-8 text-center text-lg text-muted-foreground">파일이 없습니다.</p>
|
||||
) : (
|
||||
<Table caption="이전 기본계획 첨부파일 목록" variant="secondary">
|
||||
<Table caption="기본계획파일 목록으로 번호, 파일명, 등록일, 다운로드 정보 제공" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="!py-2 !text-xl">번호</TableHead>
|
||||
|
||||
@@ -13,7 +13,7 @@ function PreviousPlanSummaryTable({ data }: PreviousPlanSummaryTableProps) {
|
||||
const yearCells = getPlanYearCells(data);
|
||||
|
||||
return (
|
||||
<Table caption="이전 기본계획 목표연도" variant="secondary">
|
||||
<Table caption="총설 목록으로 구분, 당초, 기준, 변경 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
|
||||
@@ -57,7 +57,7 @@ function BasicInfoFormSection() {
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="기본계획 신청">
|
||||
<Table caption="기본계획 정보 입력" variant="secondary">
|
||||
<Table caption="기본계획 신청 입력으로 기본계획명, 계획년도, 기준년도, 1단계 목표연도, 2단계 목표연도, 3단계 목표연도, 4단계 목표연도, 계획개요, 구분, 신청 지자체 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
|
||||
@@ -18,7 +18,7 @@ function BasicInfoSection({ det, dtl }: BasicInfoSectionProps) {
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="기본계획 신청">
|
||||
<Table caption="기본계획 정보" variant="secondary">
|
||||
<Table caption="기본계획 신청으로 기본계획명, 계획년도, 기준년도, 1단계 목표연도, 2단계 목표연도, 3단계 목표연도, 4단계 목표연도, 계획개요, 구분, 신청 지자체 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
|
||||
@@ -75,7 +75,7 @@ function BizCstMatrixFormSection() {
|
||||
<SectionCollapsible title="총사업비">
|
||||
<div className="overflow-x-auto">
|
||||
<p className="mb-2 text-right font-normal text-muted-foreground">(단위: 백만원)</p>
|
||||
<Table caption="단계별 총사업비 입력" variant="secondary" className="table-fixed">
|
||||
<Table caption="총사업비 목록으로 구분, 1단계, 2단계, 3단계, 4단계 정보 제공" variant="secondary" className="table-fixed">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
<col width="22%" />
|
||||
|
||||
@@ -62,7 +62,7 @@ function BizCstMatrixSection({ bizCstList, dtl }: BizCstMatrixSectionProps) {
|
||||
<SectionCollapsible title="총사업비">
|
||||
<div className="overflow-x-auto">
|
||||
<p className="mb-2 text-right font-normal text-muted-foreground">(단위: 백만원)</p>
|
||||
<Table caption="단계별 총사업비 내역" variant="secondary" className="table-fixed">
|
||||
<Table caption="총사업비 목록으로 구분, 1단계, 2단계, 3단계, 4단계 정보 제공" variant="secondary" className="table-fixed">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
<col width="22%" />
|
||||
|
||||
@@ -13,7 +13,7 @@ function ContactInfoSection({ dtl }: ContactInfoSectionProps) {
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="담당자 정보">
|
||||
<Table caption="기본계획 담당자 정보" variant="secondary">
|
||||
<Table caption="담당자 정보로 신청자, 이름, 연락처, 소속, 이메일, 설계자, 이름, 연락처, 소속, 이메일 정보 제공" variant="secondary">
|
||||
<TableHeader>
|
||||
{dtl.aplcntNm ? (
|
||||
<>
|
||||
|
||||
@@ -85,7 +85,7 @@ function FnrsCstFormSection() {
|
||||
<h3 className="text-xl font-semibold">세부별 총사업비(①) - 재원분담 총사업비(②)</h3>
|
||||
<span className="font-normal text-muted-foreground">(단위: 백만원)</span>
|
||||
</div>
|
||||
<Table caption="총사업비 대비 재원분담 합계 비교" variant="secondary" className="table-fixed">
|
||||
<Table caption="세부별 총사업비(①) - 재원분담 총사업비(②) 목록으로 구분, 1단계, 2단계, 3단계, 4단계 정보 제공" variant="secondary" className="table-fixed">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
<col width="22%" />
|
||||
@@ -125,7 +125,7 @@ function FnrsCstFormSection() {
|
||||
<h3 className="text-xl font-semibold">재원분담</h3>
|
||||
<span className="font-normal text-muted-foreground">(단위: 백만원)</span>
|
||||
</div>
|
||||
<Table caption="단계별 재원분담 입력" variant="secondary" className="table-fixed">
|
||||
<Table caption="재원분담 목록으로 구분, 1단계, 2단계, 3단계, 4단계 정보 제공" variant="secondary" className="table-fixed">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
<col width="22%" />
|
||||
|
||||
@@ -79,7 +79,7 @@ function FnrsCstSection({ fnrsCstList, dtl }: FnrsCstSectionProps) {
|
||||
<h3 className="text-xl font-semibold">세부별 총사업비(①) - 재원분담 총사업비(②)</h3>
|
||||
<span className="font-normal text-muted-foreground">(단위: 백만원)</span>
|
||||
</div>
|
||||
<Table caption="단계별 재원분담 내역" variant="secondary" className="table-fixed">
|
||||
<Table caption="재원분담 목록으로 구분, 1단계, 2단계, 3단계, 4단계 정보 제공" variant="secondary" className="table-fixed">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
<col width="22%" />
|
||||
|
||||
@@ -79,7 +79,7 @@ function OutlineFormSection() {
|
||||
이전기본계획조회
|
||||
</Button>
|
||||
</div>
|
||||
<Table caption="사업개요 단계별 입력" variant="secondary">
|
||||
<Table caption="사업개요 목록으로 구분, 1단계, 2단계, 3단계, 4단계 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
<col width="22%" />
|
||||
|
||||
@@ -29,7 +29,7 @@ function OutlineSection({ otlnList, dtl }: OutlineSectionProps) {
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="사업개요">
|
||||
<Table caption="사업개요 단계별 현황" variant="secondary">
|
||||
<Table caption="사업개요 목록으로 구분, 1단계, 2단계, 3단계, 4단계 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
<col width="22%" />
|
||||
|
||||
@@ -187,7 +187,7 @@ const TechReviewOpinionSection = forwardRef<
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
||||
<Table caption="검토의견 입력" variant="secondary">
|
||||
<Table caption="검토의견 작성으로 의견내용, 검토결과, 첨부파일 정보 제공" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[12rem]">의견내용</TableHead>
|
||||
@@ -308,7 +308,7 @@ const TechReviewOpinionSection = forwardRef<
|
||||
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<Table caption="검토의견 목록: 의견자, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
|
||||
<Table caption="검토의견 목록으로 의견자명, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="55%" />
|
||||
|
||||
@@ -147,7 +147,7 @@ function TmplntFacilityMatrix({
|
||||
const originalData = Array.isArray(data) ? (data[0] ?? null) : null;
|
||||
|
||||
return (
|
||||
<Table caption="처리시설별 단계별 계획 입력" variant="secondary">
|
||||
<Table caption="처리시설별 하수도정비기본계획 목록으로 구분, 당초, 현재, 변경 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
<col width="9.75%" />
|
||||
@@ -317,7 +317,7 @@ function TmplntFacilityAttachments({
|
||||
return (
|
||||
<div className="mt-6 space-y-2">
|
||||
<h3 className="mb-4 text-xl font-semibold">처리구역 필수 첨부파일</h3>
|
||||
<Table caption="처리구역 필수 첨부파일" variant="secondary">
|
||||
<Table caption="처리구역 필수 첨부파일 목록으로 파일명, 파일 확장자, 파일첨부 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="40%" />
|
||||
<col width="15%" />
|
||||
|
||||
@@ -102,7 +102,7 @@ function TmplntFacilityMatrix({
|
||||
const originalData = Array.isArray(data) ? (data[0] ?? null) : null;
|
||||
|
||||
return (
|
||||
<Table caption="처리시설별 단계별 계획 현황" variant="secondary">
|
||||
<Table caption="처리시설별 하수도정비기본계획 목록으로 구분, 당초, 현재, 변경 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
<col width="9.75%" />
|
||||
@@ -242,7 +242,7 @@ function TmplntSection({ tmplntList, det, dtl }: TmplntSectionProps) {
|
||||
{(item.atchFileGroupId || item.atchFileInfo?.orgnlFileNm) && (
|
||||
<div className="mt-6 space-y-2">
|
||||
<h3 className="mb-4 text-xl font-semibold">처리구역 필수 첨부파일</h3>
|
||||
<Table caption="처리구역 첨부파일" variant="secondary">
|
||||
<Table caption="처리구역 필수 첨부파일 목록으로 파일명, 파일첨부 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="35%" />
|
||||
<col width="65%" />
|
||||
|
||||
@@ -117,7 +117,7 @@ function SewerBscPlanApplyDialog({ open, onOpenChange, onConfirm }: SewerBscPlan
|
||||
{isPending ? (
|
||||
<Skeleton className="h-[16rem] w-full" />
|
||||
) : (
|
||||
<Table caption="기본계획 신청 정보 입력" variant="secondary">
|
||||
<Table caption="하수도기본계획 신청으로 시도, 시군구, 계획구분, 업무구분 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
|
||||
@@ -63,7 +63,7 @@ function SewerBscPlanList({ searchParams, pagination, onPaginationChange }: Sewe
|
||||
manualPagination: true,
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "하수도 기본계획 목록",
|
||||
caption: "하수도기본계획 목록으로 시도, 시군구, 기본계획명, 계획구분, 업무구분, 담당자, 해당기관, 진행상황, 등록일 정보 제공",
|
||||
viewPagination: true,
|
||||
onRowClick: handleRowClick,
|
||||
},
|
||||
|
||||
@@ -134,7 +134,7 @@ function BnsPickerDialog({ open, onClose, onSelect }: BnsPickerDialogProps) {
|
||||
manualPagination: true,
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "사업 검색 결과 목록",
|
||||
caption: "사업찾기 목록으로 선택, 환경청, 시도, 시군구, 사업명, 예산신청년도, 사업시작년도, 사업종료년도, 총사업비 정보 제공",
|
||||
viewPagination: true,
|
||||
onRowClick: (row) => {
|
||||
setRowSelection({ [getBnsListRowKey(row)]: true });
|
||||
@@ -331,7 +331,7 @@ function BnsPickerDialog({ open, onClose, onSelect }: BnsPickerDialogProps) {
|
||||
|
||||
function BnsBasicInfoTable({ detail }: { detail: BnsDetailResponseDto }) {
|
||||
return (
|
||||
<Table caption="사업 검색 결과 목록" variant="secondary" className="w-full">
|
||||
<Table caption="기본정보로 시도, 구군, 대권역, 단위유역, 중권역, 환경청, 회계구분, 단위사업, 세부사업, 내역사업, 세부내역, 세세부내역, 사업시작년도, 사업종료년도, 보조율(%), 사업상태, 사업명, 사업위치, 기본계획상의 목표년도, 사업추진현황, 상위계획, 기술진단, 사업내용, 비고 정보 제공" variant="secondary" className="w-full">
|
||||
<TableBody>
|
||||
<DetailPairRow
|
||||
leftLabel="시도"
|
||||
@@ -404,7 +404,7 @@ function BnsBasicInfoTable({ detail }: { detail: BnsDetailResponseDto }) {
|
||||
|
||||
function BnsOperationTable({ detail }: { detail: BnsDetailResponseDto }) {
|
||||
return (
|
||||
<Table caption="선택 사업 상세 정보" variant="secondary" className="w-full">
|
||||
<Table caption="운영현황 및 사업효과로 유입하수처리장, 처리방식, 시설용량(현행), 시설용량(사업후), 처리구역면적(현행), 처리구역면적(사업후), 총인구(현행), 처리인구(현행), 처리인구(사업후), 하수관로, 배수설비 총가구수, 펌프장(개소), 방류수계, 방류하천, 사업후 예상유입하수량, 사업후 유입수질(예상BOD), 계획방류수질, 하천수질, 표준사업비, 기타사업물량 정보 제공" variant="secondary" className="w-full">
|
||||
<TableBody>
|
||||
<DetailPairRow
|
||||
leftLabel="유입하수처리장"
|
||||
|
||||
@@ -380,7 +380,7 @@ function AttachmentsFormSection({
|
||||
{essentialFiles.length > 0 && (
|
||||
<div className="px-2">
|
||||
<h3 className="mb-4 text-xl font-semibold">필수 첨부파일</h3>
|
||||
<Table caption="필수 첨부파일 목록: 파일명, 파일 확장자, 파일 첨부 정보 제공" variant="secondary" className="table-fixed">
|
||||
<Table caption="필수 첨부파일 목록으로 파일명, 파일 확장자, 파일첨부 정보 제공" variant="secondary" className="table-fixed">
|
||||
<colgroup>
|
||||
<col width="44%" />
|
||||
<col width="12%" />
|
||||
|
||||
@@ -17,7 +17,7 @@ function AttachmentsViewSection({ esntlFileList, etcFileList }: AttachmentsViewS
|
||||
{esntlFileList.length > 0 && (
|
||||
<SectionCollapsible title="필수 첨부파일">
|
||||
<div className="overflow-x-auto">
|
||||
<Table caption="첨부파일 목록" variant="secondary">
|
||||
<Table caption="필수 첨부파일 목록으로 필수 자료명, 파일명, 크기 정보 제공" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>필수 자료명</TableHead>
|
||||
@@ -77,7 +77,7 @@ function AttachmentsViewSection({ esntlFileList, etcFileList }: AttachmentsViewS
|
||||
{etcFileList.length > 0 && (
|
||||
<SectionCollapsible title="기타 첨부파일">
|
||||
<div className="overflow-x-auto">
|
||||
<Table caption="첨부파일 목록" variant="secondary">
|
||||
<Table caption="기타 첨부파일 목록으로 파일명, 크기 정보 제공" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>파일명</TableHead>
|
||||
|
||||
@@ -58,7 +58,7 @@ function BizCstTotalDisplayTable({ costItems, sharItems }: BizCstTotalDisplayTab
|
||||
<h3 className="text-xl font-semibold">세부별 총사업비(①) - 재원분담 총사업비(②)</h3>
|
||||
<span className="font-normal text-muted-foreground">(단위: 백만원)</span>
|
||||
</div>
|
||||
<Table caption="세부별 총사업비와 재원분담 총사업비 비교" variant="secondary">
|
||||
<Table caption="세부별 총사업비(①) - 재원분담 총사업비(②) 목록으로 구분, ① 예산반영(A) - ② 현행(A), ① 금회설계시(C) - ② 변경요구(B) 정보 제공" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>구분</TableHead>
|
||||
|
||||
@@ -102,7 +102,7 @@ function BusinessCostFormSection() {
|
||||
<SectionCollapsible title="세부별 총사업비">
|
||||
<div className="overflow-x-auto">
|
||||
<p className="mb-2 text-right font-normal text-muted-foreground">(단위: 백만원)</p>
|
||||
<Table caption="세부별 총사업비 입력: 구분, 예산반영, 표준사업비, 금회설계, 증감 정보 제공" variant="secondary" className="table-fixed">
|
||||
<Table caption="세부별 총사업비 목록으로 구분, 예산반영(A), 표준사업비(B), 금회설계시(C), 증감 정보 제공" variant="secondary" className="table-fixed">
|
||||
<colgroup>
|
||||
<col width="20%" />
|
||||
<col width="20%" />
|
||||
|
||||
@@ -102,7 +102,7 @@ function BusinessScaleFormSection() {
|
||||
<SectionCollapsible title="사업규모">
|
||||
<div className="overflow-x-auto">
|
||||
<p className="mb-2 text-right font-normal text-muted-foreground">(단위: 백만원)</p>
|
||||
<Table caption="사업규모 입력: 구분, 기본계획, 예산반영, 금회설계, 증감 정보 제공" variant="secondary">
|
||||
<Table caption="사업규모 목록으로 구분, 기본계획(A), 예산반영(B), 금회설계시(C), 증감(C-B) 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="20%" />
|
||||
<col width="20%" />
|
||||
|
||||
@@ -199,7 +199,7 @@ function ChecklistFormSection({ docSeComCd, fieldPrefix }: ChecklistFormSectionP
|
||||
{checklistTabs.map((tab) => {
|
||||
return (
|
||||
<TabsContent key={tab.code} value={tab.code} className="space-y-4">
|
||||
<Table caption={`${tab.label} 체크리스트 입력: 검토항목, 주요 검토내용, 검토결과 정보 제공`} variant="secondary">
|
||||
<Table caption={`${tab.label} 체크리스트 목록으로 검토항목, 주요 검토내용, 검토 결과 정보 제공`} variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead rowSpan={2} className="w-[30rem]">
|
||||
|
||||
@@ -23,7 +23,7 @@ function ContactInfoFormSection({ fieldPrefix }: ContactInfoFormSectionProps) {
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="담당자 정보">
|
||||
<Table caption="담당자 정보 입력" variant="secondary">
|
||||
<Table caption="담당자 정보 입력으로 신청자, 이름, 연락처, 소속, 이메일, 설계자, 이름, 연락처, 소속, 이메일 정보 제공" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead scope="row" className="w-[20rem]" rowSpan={2}>
|
||||
|
||||
@@ -200,7 +200,7 @@ function FnrsSharFormSection({ initialSharTree, initialSharAtchFiles }: FnrsShar
|
||||
<h3 className="text-xl font-semibold">재원분담</h3>
|
||||
<span className="font-normal text-muted-foreground">(단위: 백만원)</span>
|
||||
</div>
|
||||
<Table caption="재원분담 입력: 구분, 현행, 변경요구, 증감, 비고 정보 제공" variant="secondary" className="table-fixed">
|
||||
<Table caption="재원분담 목록으로 구분, 현행(A), 변경요구(B), 증감, 비고 정보 제공" variant="secondary" className="table-fixed">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[24rem]" rowSpan={2}>
|
||||
|
||||
@@ -105,7 +105,7 @@ function PbadmsTaskApplyDialog({
|
||||
{isPending ? (
|
||||
<Skeleton className="h-[16rem] w-full" />
|
||||
) : (
|
||||
<Table caption="행정지원 신청 정보 입력" variant="secondary">
|
||||
<Table caption={`${title}으로 시도, 시군구, 업무구분 정보 제공`} variant="secondary">
|
||||
<colgroup>
|
||||
<col width="20%" />
|
||||
<col width="30%" />
|
||||
|
||||
@@ -23,7 +23,7 @@ function PrivacyConsentSection() {
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="개인정보 수집/이용 동의">
|
||||
<Table caption="개인정보 수집·이용 동의 내용" variant="secondary">
|
||||
<Table caption="개인정보 수집/이용 동의로 개인정보 수집 및 이용 정보 제공" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[20rem]" data-required>
|
||||
|
||||
@@ -57,7 +57,7 @@ function ProgressStatusSection({
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="진행 상태">
|
||||
<Table caption="행정지원 진행상태" variant="secondary" className="table-fixed">
|
||||
<Table caption={`진행 상태 목록으로 ${columns.map((column) => column.label).join(", ")} 정보 제공`} variant="secondary" className="table-fixed">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{columns.map((column) => (
|
||||
|
||||
@@ -239,7 +239,7 @@ function SplmntDmndDialog({
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<Table caption="보완 요청 입력: 보완 항목, 보완 의견, 첨부파일 정보 제공" variant="secondary">
|
||||
<Table caption={`보완 내용 목록으로 보완 항목, 보완 의견, 첨부파일${rows.length > 1 ? ", 관리" : ""} 정보 제공`} variant="secondary">
|
||||
<colgroup>
|
||||
<col width="20%" />
|
||||
<col width="50%" />
|
||||
|
||||
@@ -240,7 +240,7 @@ function SplmntDmndFormSection({ initialData, readOnly = false }: SplmntDmndForm
|
||||
return (
|
||||
<SectionCollapsible title="보완요청">
|
||||
<div className="overflow-x-auto">
|
||||
<Table caption="보완요청 및 처리 현황: 보완항목, 요청일시, 요청내용, 완료내용, 첨부파일, 완료여부 정보 제공" variant="secondary">
|
||||
<Table caption="보완요청 목록으로 보완항목, 보완요청일시, 보완요청내용, 보완완료내용, 파일첨부, 완료여부 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
<col width="14%" />
|
||||
|
||||
@@ -28,7 +28,7 @@ function SupplementDemandSection({ splmntDmndList }: SupplementDemandSectionProp
|
||||
return (
|
||||
<SectionCollapsible title="보완요청 및 완료 현황">
|
||||
<div className="overflow-x-auto">
|
||||
<Table caption="보완요청 현황" variant="secondary">
|
||||
<Table caption="보완요청 및 완료 현황 목록으로 보완항목, 보완요청일시, 보완요청내용, 보완완료일시, 보완완료내용, 완료여부 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="14%" />
|
||||
<col width="8%" />
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
## Project
|
||||
This repository contains a Next.js web application.
|
||||
|
||||
## Portable paths
|
||||
|
||||
Resolve paths from this `apps/dm/AGENTS.md` file, not from a developer-specific drive or the current shell directory.
|
||||
|
||||
- `FE_APP_ROOT`: directory containing this file (`apps/dm`)
|
||||
- `FE_REPO_ROOT`: `FE_APP_ROOT/../..` (`frontend`)
|
||||
- `WORKSPACE_ROOT`: `FE_REPO_ROOT/..`
|
||||
- `COMMON_ROOT_AGENTS`: `WORKSPACE_ROOT/AGENTS.md`
|
||||
- `BE_REPO_ROOT`: `WORKSPACE_ROOT/IIPBS` (Backend repository root)
|
||||
- `BE_SERVICE_ROOT`: `BE_REPO_ROOT/sewerage-service`
|
||||
|
||||
Use an explicitly supplied repository path when the repositories are installed under different parent directories. Verify a calculated path exists before using it.
|
||||
|
||||
Main technologies:
|
||||
- Next.js
|
||||
- TypeScript
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
## Project
|
||||
This repository contains a Next.js web application.
|
||||
|
||||
## Portable paths
|
||||
|
||||
Resolve paths from this `apps/fo/AGENTS.md` file, not from a developer-specific drive or the current shell directory.
|
||||
|
||||
- `FE_APP_ROOT`: directory containing this file (`apps/fo`)
|
||||
- `FE_REPO_ROOT`: `FE_APP_ROOT/../..` (`frontend`)
|
||||
- `WORKSPACE_ROOT`: `FE_REPO_ROOT/..`
|
||||
- `COMMON_ROOT_AGENTS`: `WORKSPACE_ROOT/AGENTS.md`
|
||||
- `BE_REPO_ROOT`: `WORKSPACE_ROOT/IIPBS` (Backend repository root)
|
||||
- `BE_SERVICE_ROOT`: `BE_REPO_ROOT/sewerage-service`
|
||||
|
||||
Use an explicitly supplied repository path when the repositories are installed under different parent directories. Verify a calculated path exists before using it.
|
||||
|
||||
Main technologies:
|
||||
- Next.js
|
||||
- TypeScript
|
||||
|
||||
+35
-3
@@ -43,14 +43,46 @@
|
||||
|
||||
## 공통 시작 기준
|
||||
|
||||
Codex 작업 루트는 가능하면 `../../../../../`로 잡고, 공통 시작 기준은 `AGENTS.md`를 따른다.
|
||||
개발자마다 드라이브와 상위 작업 폴더 위치가 다르므로 특정 드라이브·사용자 디렉터리의 절대경로를 기준으로 사용하지 않는다.
|
||||
|
||||
경로 별칭은 `apps/sn/AGENTS.md`가 있는 디렉터리(`FE_APP_ROOT`)를 기준으로 다음과 같이 해석한다. 상대경로는 현재 셸의 실행 위치가 아니라 이 파일의 위치를 기준으로 계산한다.
|
||||
|
||||
- `FE_APP_ROOT`: 이 파일이 있는 `apps/sn` 디렉터리
|
||||
- `FE_REPO_ROOT`: `FE_APP_ROOT/../..`
|
||||
- `WORKSPACE_ROOT`: `FE_APP_ROOT/../../..`
|
||||
- `COMMON_ROOT_AGENTS`: `WORKSPACE_ROOT/AGENTS.md`
|
||||
- `BE_REPO_ROOT`: `WORKSPACE_ROOT/IIPBS` (Backend Git 저장소 루트)
|
||||
- `BE_SERVICE_ROOT`: `BE_REPO_ROOT/sewerage-service` (하수관로 백엔드 서비스 모듈)
|
||||
- `COMMON_DOCS_ROOT`: `BE_REPO_ROOT/docs`
|
||||
- `FE_APP_DOCS_ROOT`: `FE_APP_ROOT/docs`
|
||||
|
||||
경로 결정은 아래 우선순위를 따른다.
|
||||
|
||||
1. 작업 환경이나 사용자가 `WORKSPACE_ROOT`, `COMMON_DOCS_ROOT`, `BE_REPO_ROOT`, `BE_SERVICE_ROOT` 등의 유효한 경로를 명시한 경우 그 값을 우선한다. 저장소를 서로 다른 상위 폴더에 배치한 개발자도 이 방식으로 사용할 수 있다.
|
||||
2. 명시값이 없으면 위의 `FE_APP_ROOT` 기준 상대경로로 계산한다. 드라이브 문자나 사용자별 상위 디렉터리 이름이 달라도 같은 저장소 배치 구조를 사용할 수 있다.
|
||||
3. 계산한 경로는 사용 전에 대상 파일이나 디렉터리의 존재 여부를 확인한다. 없거나 후보가 여러 개인 경우 특정 절대경로를 추측하지 말고 현재 저장소에서 확인 가능한 작업만 진행한 뒤 필요한 경로를 사용자에게 확인한다.
|
||||
4. 작업 문서와 결과 보고에는 특별한 사유가 없으면 개발자별 절대경로 대신 위 경로 별칭이나 저장소 기준 상대경로를 기록한다.
|
||||
|
||||
PowerShell에서 현재 프론트엔드 저장소를 기준으로 확인할 때는 다음처럼 동적으로 계산할 수 있다. 각 단계는 앞 단계에서 찾은 저장소 위치를 기반으로 하므로 개발자별 설치 경로에 의존하지 않는다.
|
||||
|
||||
```powershell
|
||||
$feRepoRoot = (git rev-parse --show-toplevel).Trim()
|
||||
$feAppRoot = Join-Path $feRepoRoot 'apps/sn'
|
||||
$workspaceRoot = Split-Path $feRepoRoot -Parent
|
||||
$commonRootAgents = Join-Path $workspaceRoot 'AGENTS.md'
|
||||
$beRepoRoot = Join-Path $workspaceRoot 'IIPBS'
|
||||
$beServiceRoot = Join-Path $beRepoRoot 'sewerage-service'
|
||||
$commonDocsRoot = Join-Path $beRepoRoot 'docs'
|
||||
```
|
||||
|
||||
공통 시작 기준은 `COMMON_ROOT_AGENTS`를 따른다.
|
||||
|
||||
이 파일은 프론트엔드 `sn` 앱 세부 기준이다.
|
||||
|
||||
백엔드(`sewerage-service`)와 함께 수정하거나 API/응답/DB/인증/권한 연동 영향이 있는 작업은 아래 파일을 함께 확인한다.
|
||||
|
||||
- 공통 시작 기준: `AGENTS.md`
|
||||
- 백엔드 세부 기준: `BE_REPO_ROOT/AGENTS.md`
|
||||
- 백엔드 세부 기준: `BE_SERVICE_ROOT/AGENTS.md`
|
||||
- 프론트엔드 `sn` 앱 기준: `FE_APP_ROOT/AGENTS.md`
|
||||
|
||||
---
|
||||
@@ -230,7 +262,7 @@ VSCode나 별도 터미널에서 `sn` Next 개발 서버가 실행 중이면 `ne
|
||||
|
||||
### 프론트엔드 실행 프로세스 관리
|
||||
|
||||
- 모든 세션·역할·문서/검증 섹션에 공통 D:/workspace/AGENTS.md의 테스트 파일 정리 기준을 적용한다. 에이전트가 테스트를 위해 만든 파일은 정식 회귀 테스트·재사용 가능한 소스·fixture·mock·임시 라우트를 포함해 위치와 관계없이 결과를 요약한 뒤 작업 종료 전에 삭제한다(2026-09-23 사용자 지시). 전용 빈 폴더와 끊어진 실행 명령도 정리한다. 명시 보존 요청 파일·최종 산출물·사용자 원본/복구 백업은 유지한다. 기존 타 작업자 테스트 자산은 삭제 권한을 먼저 확인한다. 사용자 dev server의 .next·공유 캐시·node_modules는 포괄 삭제하지 않는다.
|
||||
- 모든 세션·역할·문서/검증 섹션에 `COMMON_ROOT_AGENTS`의 테스트 파일 정리 기준을 적용한다. 에이전트가 테스트를 위해 만든 파일은 정식 회귀 테스트·재사용 가능한 소스·fixture·mock·임시 라우트를 포함해 위치와 관계없이 결과를 요약한 뒤 작업 종료 전에 삭제한다(2026-09-23 사용자 지시). 전용 빈 폴더와 끊어진 실행 명령도 정리한다. 명시 보존 요청 파일·최종 산출물·사용자 원본/복구 백업은 유지한다. 기존 타 작업자 테스트 자산은 삭제 권한을 먼저 확인한다. 사용자 dev server의 .next·공유 캐시·node_modules는 포괄 삭제하지 않는다.
|
||||
|
||||
- Codex 에이전트가 작업 또는 검증을 위해 직접 기동한 `next dev`, `dev:sn`, Turborepo dev와 기타 프론트엔드 장시간 실행 프로세스는 작업 종료 전에 반드시 종료한다.
|
||||
- 셸, 백그라운드, 숨김 창 등 VSCode 밖에서 에이전트가 기동한 프로세스도 종료 대상이다.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
import { beginMapNavigation } from "@/features/map-search/lib/map-navigation";
|
||||
|
||||
import {
|
||||
useCallback,
|
||||
@@ -91,6 +92,8 @@ export const useExmnResultMap = (options: Options) => {
|
||||
async (job: ExmnMapJobType, filters: ExmnMapFilters) => {
|
||||
const map = mapRef.current;
|
||||
if (!map) return;
|
||||
const navigation = entry.jobType === job && entry.exmnBizId && entry.trtdstId
|
||||
? beginMapNavigation(map) : null;
|
||||
removeLayer(job);
|
||||
const state = layers.current[job];
|
||||
const sequence = state.sequence;
|
||||
@@ -184,7 +187,7 @@ export const useExmnResultMap = (options: Options) => {
|
||||
const extent = selectedSource.getExtent();
|
||||
if (
|
||||
fittedContext.current !== contextKey &&
|
||||
extent.every(Number.isFinite)
|
||||
extent.every(Number.isFinite) && navigation?.commit(extent)
|
||||
) {
|
||||
map
|
||||
.getView()
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* 우측 정보 화면의 심선·위선 목록, 선택 상태와 화면 생명주기 캐시를 관리한다.
|
||||
*/
|
||||
"use client";
|
||||
import { mapSearchNavigationOptions } from "@/features/map-search/lib/map-navigation";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { RefObject } from "react";
|
||||
@@ -23,6 +24,7 @@ type FldngHstryGisLayer = {
|
||||
showSearchResultsFromGeoJSON: (
|
||||
layerTypeName: string,
|
||||
featureId: string,
|
||||
options?: { beforeFocus: (extent: unknown) => boolean },
|
||||
) => void | Promise<void>;
|
||||
};
|
||||
|
||||
@@ -127,11 +129,14 @@ const displayCachedFldngHstryFeatures = (
|
||||
gisLayer: FldngHstryGisLayer,
|
||||
layerTypeName: string,
|
||||
features: Feature[],
|
||||
beforeFocus: (extent: unknown) => boolean,
|
||||
) => {
|
||||
const matched = Object.values(GisApp.layerCode ?? {}).find(
|
||||
(item: any) => item?.lyrTyNm === layerTypeName,
|
||||
) as any;
|
||||
if (!matched || features.length === 0) return;
|
||||
// 캐시를 재사용해도 최신 선택의 유효 도형일 때만 검색조건을 초기화한다.
|
||||
if (!beforeFocus(features[0]?.getGeometry()?.getExtent())) return;
|
||||
|
||||
gisLayer.clearSearchLayer(matched);
|
||||
const layer = gisLayer.ensureSearchLayer(matched);
|
||||
@@ -148,17 +153,20 @@ const showFldngHstryFeature = async (
|
||||
featureId: string,
|
||||
) => {
|
||||
const cacheKey = `${layerTypeName}:${featureId}`;
|
||||
/** 캐시 대기와 신규 WFS 조회가 동일한 이동 성공/취소 계약을 사용한다. */
|
||||
const navigationOptions = mapSearchNavigationOptions((GisApp.Module.core as any).map);
|
||||
const cachedRequest = cache.get(cacheKey);
|
||||
if (cachedRequest) {
|
||||
displayCachedFldngHstryFeatures(
|
||||
gisLayer,
|
||||
layerTypeName,
|
||||
await cachedRequest,
|
||||
navigationOptions.beforeFocus,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await gisLayer.showSearchResultsFromGeoJSON(layerTypeName, featureId);
|
||||
await gisLayer.showSearchResultsFromGeoJSON(layerTypeName, featureId, navigationOptions);
|
||||
const source = gisLayer.searchLayer?.getSource?.();
|
||||
if (!source) return;
|
||||
|
||||
|
||||
@@ -77,6 +77,8 @@ const DEFAULT_FILTERS: InftwtResultFilters = {
|
||||
};
|
||||
|
||||
const postRowSelectMessage = (row: InftwtResultRow) => {
|
||||
// 같은 클릭의 message/storage 전달을 식별한다.
|
||||
const requestId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
if (typeof window !== "undefined") {
|
||||
try {
|
||||
window.localStorage.setItem(
|
||||
@@ -84,6 +86,7 @@ const postRowSelectMessage = (row: InftwtResultRow) => {
|
||||
JSON.stringify({
|
||||
row,
|
||||
timestamp: Date.now(),
|
||||
requestId,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -96,6 +99,7 @@ const postRowSelectMessage = (row: InftwtResultRow) => {
|
||||
{
|
||||
type: INFTWT_RESULT_ROW_SELECT_MESSAGE,
|
||||
row,
|
||||
requestId,
|
||||
},
|
||||
window.location.origin,
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
import { beginMapNavigation, fitMapSelection, mapSearchNavigationOptions } from "@/features/map-search/lib/map-navigation";
|
||||
|
||||
/** 침입수 분석결과 선택 시 부모 지도에 결과 레이어를 표시하는 훅이다. */
|
||||
import { useCallback, useEffect, useRef, useState, type MutableRefObject } from "react";
|
||||
@@ -35,7 +36,6 @@ type UseInftwtResultMapParams = {
|
||||
const INFTWT_ZONE_LAYER_ID = "SN_INFTWT_RESULT_ZONE_LAYER";
|
||||
const INFTWT_PIPE_LAYER_ID = "SN_INFTWT_RESULT_PIPE_LAYER";
|
||||
const DEFAULT_RESULT_FIT_PADDING: [number, number, number, number] = [120, 120, 120, 120];
|
||||
const INFTWT_PROC_SEARCH_EVENT = "SN_INFTWT_PROC_SEARCH";
|
||||
|
||||
const zoneStyle = new Style({
|
||||
fill: new Fill({
|
||||
@@ -302,7 +302,7 @@ export const useInftwtResultMap = ({
|
||||
return;
|
||||
}
|
||||
|
||||
map.getView?.().fit?.(extent, {
|
||||
fitMapSelection(map, extent, {
|
||||
duration: 350,
|
||||
padding: DEFAULT_RESULT_FIT_PADDING,
|
||||
maxZoom: oilqPipeExtent ? 17 : shouldUseZoneExtent ? 15 : 16,
|
||||
@@ -319,7 +319,7 @@ export const useInftwtResultMap = ({
|
||||
if (normalizedSggCd && normalizedSggCd !== "0") {
|
||||
const sigunguCenter = getSigunguCenter(normalizedSggCd);
|
||||
if (sigunguCenter) {
|
||||
moveMapToCenter(gisMaps, sigunguCenter, 12);
|
||||
moveMapToCenter(gisMaps, sigunguCenter, 12, mapSearchNavigationOptions(gisMaps.current).beforeFocus);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -328,7 +328,7 @@ export const useInftwtResultMap = ({
|
||||
if (normalizedCtpvCd && normalizedCtpvCd !== "0") {
|
||||
const sidoCenterCoordinate = sidoCenter[normalizedCtpvCd.slice(0, 2)];
|
||||
if (sidoCenterCoordinate) {
|
||||
moveMapToCenter(gisMaps, sidoCenterCoordinate, 11);
|
||||
moveMapToCenter(gisMaps, sidoCenterCoordinate, 11, mapSearchNavigationOptions(gisMaps.current).beforeFocus);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -395,8 +395,17 @@ export const useInftwtResultMap = ({
|
||||
[clearLayers, ensurePipeLayer, ensureZoneLayer, gisMaps, moveToStableFocus],
|
||||
);
|
||||
|
||||
/** message/storage가 전달하는 동일 선택 요청을 한 번만 처리한다. */
|
||||
const selectionRequestsRef = useRef(new Set<string>());
|
||||
const handleRowSelect = useCallback(
|
||||
async (row: InftwtResultRow) => {
|
||||
async (row: InftwtResultRow, requestId?: string) => {
|
||||
if (requestId) {
|
||||
const requests = selectionRequestsRef.current;
|
||||
if (requests.has(requestId)) return;
|
||||
requests.add(requestId);
|
||||
if (requests.size > 64) requests.delete(requests.values().next().value!);
|
||||
}
|
||||
const navigation = beginMapNavigation(gisMaps.current);
|
||||
try {
|
||||
activeResultRowRef.current = row;
|
||||
setFeatureData(null);
|
||||
@@ -412,21 +421,9 @@ export const useInftwtResultMap = ({
|
||||
mapViewCacheRef.current.set(row.inftwtAnlsSn, mapView);
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(INFTWT_PROC_SEARCH_EVENT, {
|
||||
detail: {
|
||||
ctpvCd: row.ctpvCd ?? "0",
|
||||
sggCd: row.sggCd ?? "0",
|
||||
swtrmtFcltNm: row.swtrmtFcltNm ?? "",
|
||||
suppressMapMove: (mapView.oilqPipeCount ?? 0) > 0,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!navigation.isCurrent()) return;
|
||||
setActiveMapView(mapView);
|
||||
void renderMapView(mapView);
|
||||
await renderMapView(mapView);
|
||||
} catch (error) {
|
||||
console.error("移⑥엯??遺꾩꽍寃곌낵 吏???곌퀎 以??ㅻ쪟:", error);
|
||||
alert("침입수 분석결과를 지도에 연계하지 못했습니다.");
|
||||
@@ -439,16 +436,16 @@ export const useInftwtResultMap = ({
|
||||
const handlePopupMessage = (event: MessageEvent) => {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
if (event.data?.type !== INFTWT_RESULT_ROW_SELECT_MESSAGE || !event.data?.row) return;
|
||||
void handleRowSelect(event.data.row as InftwtResultRow);
|
||||
void handleRowSelect(event.data.row as InftwtResultRow, event.data.requestId);
|
||||
};
|
||||
|
||||
const handleStorageEvent = (event: StorageEvent) => {
|
||||
if (event.key !== INFTWT_RESULT_ROW_SELECT_STORAGE_KEY || !event.newValue) return;
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(event.newValue) as { row?: InftwtResultRow };
|
||||
const payload = JSON.parse(event.newValue) as { row?: InftwtResultRow; requestId?: string };
|
||||
if (!payload?.row) return;
|
||||
void handleRowSelect(payload.row);
|
||||
void handleRowSelect(payload.row, payload.requestId);
|
||||
} catch (error) {
|
||||
console.warn("移⑥엯??遺꾩꽍寃곌낵 storage ?숆린??媛믪쓣 ?댁꽍?섏? 紐삵뻽?듬땲??", error);
|
||||
}
|
||||
|
||||
@@ -79,6 +79,8 @@ const DEFAULT_FILTERS: RdiiResultFilters = {
|
||||
};
|
||||
|
||||
const postRowSelectMessage = (row: RdiiResultRow) => {
|
||||
// 같은 클릭의 message/storage 전달을 식별한다.
|
||||
const requestId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
if (typeof window !== "undefined") {
|
||||
try {
|
||||
window.localStorage.setItem(
|
||||
@@ -86,6 +88,7 @@ const postRowSelectMessage = (row: RdiiResultRow) => {
|
||||
JSON.stringify({
|
||||
row,
|
||||
timestamp: Date.now(),
|
||||
requestId,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -98,6 +101,7 @@ const postRowSelectMessage = (row: RdiiResultRow) => {
|
||||
{
|
||||
type: RDII_RESULT_ROW_SELECT_MESSAGE,
|
||||
row,
|
||||
requestId,
|
||||
},
|
||||
window.location.origin,
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/** RDII 분석결과 선택 시 부모 지도에 결과 레이어를 표시하는 훅이다. */
|
||||
"use client";
|
||||
import { beginMapNavigation, fitMapSelection, mapSearchNavigationOptions } from "@/features/map-search/lib/map-navigation";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState, type MutableRefObject } from "react";
|
||||
import GeoJSON from "ol/format/GeoJSON.js";
|
||||
@@ -32,7 +33,6 @@ type UseRdiiResultMapParams = {
|
||||
|
||||
const RDII_PIPE_LAYER_ID = "SN_RDII_RESULT_PIPE_LAYER";
|
||||
const DEFAULT_RESULT_FIT_PADDING: [number, number, number, number] = [120, 120, 120, 120];
|
||||
const PROC_SEARCH_EVENT = "SN_INFTWT_PROC_SEARCH";
|
||||
|
||||
const pipeStyle = [
|
||||
new Style({
|
||||
@@ -186,7 +186,7 @@ export const useRdiiResultMap = ({
|
||||
return;
|
||||
}
|
||||
|
||||
map.getView?.().fit?.(pipeExtent, {
|
||||
fitMapSelection(map, pipeExtent, {
|
||||
duration: 350,
|
||||
padding: DEFAULT_RESULT_FIT_PADDING,
|
||||
maxZoom: 16,
|
||||
@@ -201,7 +201,7 @@ export const useRdiiResultMap = ({
|
||||
if (normalizedSggCd && normalizedSggCd !== "0") {
|
||||
const sigunguCenter = getSigunguCenter(normalizedSggCd);
|
||||
if (sigunguCenter) {
|
||||
moveMapToCenter(gisMaps, sigunguCenter, 12);
|
||||
moveMapToCenter(gisMaps, sigunguCenter, 12, mapSearchNavigationOptions(gisMaps.current).beforeFocus);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -210,7 +210,7 @@ export const useRdiiResultMap = ({
|
||||
if (normalizedCtpvCd && normalizedCtpvCd !== "0") {
|
||||
const sidoCenterCoordinate = sidoCenter[normalizedCtpvCd.slice(0, 2)];
|
||||
if (sidoCenterCoordinate) {
|
||||
moveMapToCenter(gisMaps, sidoCenterCoordinate, 11);
|
||||
moveMapToCenter(gisMaps, sidoCenterCoordinate, 11, mapSearchNavigationOptions(gisMaps.current).beforeFocus);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -260,38 +260,37 @@ export const useRdiiResultMap = ({
|
||||
[clearLayers, ensurePipeLayer, gisMaps, moveToStableFocus],
|
||||
);
|
||||
|
||||
/** message/storage가 전달하는 동일 선택 요청을 한 번만 처리한다. */
|
||||
const selectionRequestsRef = useRef(new Set<string>());
|
||||
const handleRowSelect = useCallback(
|
||||
async (row: RdiiResultRow) => {
|
||||
async (row: RdiiResultRow, requestId?: string) => {
|
||||
if (requestId) {
|
||||
const requests = selectionRequestsRef.current;
|
||||
if (requests.has(requestId)) return;
|
||||
requests.add(requestId);
|
||||
if (requests.size > 64) requests.delete(requests.values().next().value!);
|
||||
}
|
||||
const navigation = beginMapNavigation(gisMaps.current);
|
||||
try {
|
||||
activeResultRowRef.current = row;
|
||||
setFeatureData(null);
|
||||
setIsOpen(false);
|
||||
setIsSearchModalOpen(false);
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(PROC_SEARCH_EVENT, {
|
||||
detail: {
|
||||
ctpvCd: row.ctpvCd ?? "0",
|
||||
sggCd: row.sggCd ?? "0",
|
||||
swtrmtFcltNm: row.swtrmtFcltNm ?? "",
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const cachedMapView = mapViewCacheRef.current.get(row.rainInfwtAnlsSn);
|
||||
if (cachedMapView) {
|
||||
if (!navigation.isCurrent()) return;
|
||||
setActiveMapView(cachedMapView);
|
||||
void renderMapView(cachedMapView);
|
||||
await renderMapView(cachedMapView);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = await getRawJson(`/sewerage-service/api/v1/rdiiAnlsRslt/mapView/${row.rainInfwtAnlsSn}`);
|
||||
const mapView = (payload?.data ?? payload) as RdiiResultMapViewResponse;
|
||||
mapViewCacheRef.current.set(row.rainInfwtAnlsSn, mapView);
|
||||
if (!navigation.isCurrent()) return;
|
||||
setActiveMapView(mapView);
|
||||
void renderMapView(mapView);
|
||||
await renderMapView(mapView);
|
||||
} catch (error) {
|
||||
console.error("RDII 분석결과 지도 연계 중 오류:", error);
|
||||
alert("RDII 분석결과를 지도에 연계하지 못했습니다.");
|
||||
@@ -304,16 +303,16 @@ export const useRdiiResultMap = ({
|
||||
const handlePopupMessage = (event: MessageEvent) => {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
if (event.data?.type !== RDII_RESULT_ROW_SELECT_MESSAGE || !event.data?.row) return;
|
||||
void handleRowSelect(event.data.row as RdiiResultRow);
|
||||
void handleRowSelect(event.data.row as RdiiResultRow, event.data.requestId);
|
||||
};
|
||||
|
||||
const handleStorageEvent = (event: StorageEvent) => {
|
||||
if (event.key !== RDII_RESULT_ROW_SELECT_STORAGE_KEY || !event.newValue) return;
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(event.newValue) as { row?: RdiiResultRow };
|
||||
const payload = JSON.parse(event.newValue) as { row?: RdiiResultRow; requestId?: string };
|
||||
if (!payload?.row) return;
|
||||
void handleRowSelect(payload.row);
|
||||
void handleRowSelect(payload.row, payload.requestId);
|
||||
} catch (error) {
|
||||
console.warn("RDII 분석결과 storage 동기화 값을 해석하지 못했습니다.", error);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState, useRef } from "react";
|
||||
|
||||
import { windowPopupDefinitions } from "@/features/window-popup/model/popup-definitions";
|
||||
import {
|
||||
@@ -462,26 +462,37 @@ export default function WaterRiskPopupPage(_props: WindowPopupContentProps) {
|
||||
setSelectedResultKey("");
|
||||
};
|
||||
|
||||
/** 늦게 끝난 이전 행 요청이 최신 선택을 덮지 않도록 식별한다. */
|
||||
const mapRequestRef = useRef<string>("");
|
||||
|
||||
/** 선택 행의 실제 GeoJSON을 조회한 뒤 부모 지도 창에 전달한다. */
|
||||
const handleSelectResultRow = async (row: WaterRiskResultRow) => {
|
||||
const requestId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
mapRequestRef.current = requestId;
|
||||
postMessageToOpener(WINDOW_POPUP_MESSAGE_TYPE.WATER_RISK_SELECT_RESULT, {
|
||||
phase: "loading", requestId,
|
||||
});
|
||||
setSelectedResultKey(buildResultRowKey(row));
|
||||
setIsMapLoading(true);
|
||||
setErrorMessage("");
|
||||
try {
|
||||
const mapView = await fetchWaterRiskMapView(row);
|
||||
if (mapRequestRef.current !== requestId) return;
|
||||
postMessageToOpener(WINDOW_POPUP_MESSAGE_TYPE.WATER_RISK_SELECT_RESULT, {
|
||||
requestId,
|
||||
areaId: row.areaId,
|
||||
resultRow: row,
|
||||
mapView,
|
||||
});
|
||||
} catch (error) {
|
||||
if (mapRequestRef.current !== requestId) return;
|
||||
setErrorMessage(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "통수위험 지도 결과를 조회하지 못했습니다.",
|
||||
);
|
||||
} finally {
|
||||
setIsMapLoading(false);
|
||||
if (mapRequestRef.current === requestId) setIsMapLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"use client";
|
||||
import { fitMapSelection } from "@/features/map-search/lib/map-navigation";
|
||||
|
||||
import type * as React from "react";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import Feature from "ol/Feature.js";
|
||||
@@ -122,7 +124,7 @@ export function useFloodHistorySelection({
|
||||
if (!gisMaps.current) return;
|
||||
|
||||
const geometry = new Polygon([detail.areaRing]);
|
||||
gisMaps.current.getView?.().fit?.(geometry.getExtent(), {
|
||||
fitMapSelection(gisMaps.current, geometry.getExtent(), {
|
||||
duration: 350,
|
||||
padding: FLDNG_HSTRY_SELECTION_PADDING,
|
||||
maxZoom: 15,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"use client";
|
||||
import { fitMapSelection, mapSearchNavigationOptions } from "@/features/map-search/lib/map-navigation";
|
||||
|
||||
import type { useFloodPredictionMap } from "@/features/fldng-predc-prst/lib/use-flood-prediction-map";
|
||||
import {
|
||||
useEffect,
|
||||
@@ -46,7 +48,7 @@ export function useMapRuntimeBridge({
|
||||
const fid = data.payload?.fid;
|
||||
if (!layerNm || !fid || !core?.showSearchResultsFromGeoJSON) return;
|
||||
|
||||
await core.showSearchResultsFromGeoJSON(layerNm, fid);
|
||||
await core.showSearchResultsFromGeoJSON(layerNm, fid, mapSearchNavigationOptions(gisMaps.current));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -64,7 +66,7 @@ export function useMapRuntimeBridge({
|
||||
return;
|
||||
}
|
||||
|
||||
await core.showMultipleSearchResultsFromGeoJSON(layerNm, fids);
|
||||
await core.showMultipleSearchResultsFromGeoJSON(layerNm, fids, mapSearchNavigationOptions(gisMaps.current));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -80,7 +82,7 @@ export function useMapRuntimeBridge({
|
||||
) {
|
||||
return;
|
||||
}
|
||||
map.getView().fit(extent, {
|
||||
fitMapSelection(map, extent, {
|
||||
duration: 300,
|
||||
padding: [50, 50, 50, 50],
|
||||
maxZoom: 16,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"use client";
|
||||
import { beginMapNavigation } from "@/features/map-search/lib/map-navigation";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import Feature from "ol/Feature.js";
|
||||
import GeoJSON from "ol/format/GeoJSON.js";
|
||||
@@ -35,6 +37,9 @@ export function useWaterRiskLayer({
|
||||
mapReady,
|
||||
onSelectResult,
|
||||
}: Options) {
|
||||
/** 팝업의 조회 시작부터 지도 이동까지 같은 요청 세대를 사용한다. */
|
||||
const selectionNavigationRef = useRef<ReturnType<typeof beginMapNavigation> | null>(null);
|
||||
const selectionRequestRef = useRef<string | undefined>(undefined);
|
||||
const waterRiskPopupWindowRef = useRef<Window | null>(null);
|
||||
|
||||
const waterRiskPopupCloseWatcherRef = useRef<number | null>(null);
|
||||
@@ -247,7 +252,7 @@ export function useWaterRiskLayer({
|
||||
const renderWaterRiskSelection = useCallback(
|
||||
(areaId: string | null, mapView: WaterRiskMapView | null) => {
|
||||
const map = gisMaps.current;
|
||||
if (!map) return;
|
||||
if (!map || !selectionNavigationRef.current?.isCurrent()) return;
|
||||
|
||||
const areaSource = ensureWaterRiskAreaLayer();
|
||||
if (!areaSource) return;
|
||||
@@ -318,6 +323,7 @@ export function useWaterRiskLayer({
|
||||
const extent = focusGeometry.getExtent();
|
||||
const [, minY = 0, maxX = 0, maxY = 0] = extent;
|
||||
setWaterRiskInfoAnchor([maxX, (minY + maxY) / 2]);
|
||||
if (!selectionNavigationRef.current?.commit(extent)) return;
|
||||
map.getView().fit(extent, {
|
||||
duration: 300,
|
||||
padding: [120, 120, 120, 120],
|
||||
@@ -389,6 +395,17 @@ export function useWaterRiskLayer({
|
||||
}
|
||||
|
||||
if (data?.type === WINDOW_POPUP_MESSAGE_TYPE.WATER_RISK_SELECT_RESULT) {
|
||||
if (data.payload?.phase === "loading") {
|
||||
selectionRequestRef.current = data.payload.requestId;
|
||||
selectionNavigationRef.current = beginMapNavigation(gisMaps.current);
|
||||
return;
|
||||
}
|
||||
if (data.payload?.requestId) {
|
||||
if (data.payload.requestId !== selectionRequestRef.current ||
|
||||
!selectionNavigationRef.current?.isCurrent()) return;
|
||||
} else {
|
||||
selectionNavigationRef.current = beginMapNavigation(gisMaps.current);
|
||||
}
|
||||
const areaId = String(data.payload?.areaId ?? "").trim();
|
||||
if (!areaId) return;
|
||||
const resultRow = data.payload?.resultRow as
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { mapSearchNavigationOptions } from "@/features/map-search/lib/map-navigation";
|
||||
/**
|
||||
* 📄 useBasicTab Hook
|
||||
* 검색 모달 기본정보 탭 훅
|
||||
@@ -189,7 +190,7 @@ export default function useBasicTab() {
|
||||
const layerNm = row.lyrTypeNm;
|
||||
const fid = row.fid;
|
||||
const core = GisApp.Module.layer as any;
|
||||
await core.showSearchResultsFromGeoJSON(layerNm, fid);
|
||||
await core.showSearchResultsFromGeoJSON(layerNm, fid, mapSearchNavigationOptions((GisApp.Module.core as any).map));
|
||||
setIsLoading(false);
|
||||
setIsSearchModalOpen(false); // SearchModal 닫기
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { mapSearchNavigationOptions } from "@/features/map-search/lib/map-navigation";
|
||||
/**
|
||||
* 📄 useOperationTab Hook
|
||||
* 검색 모달 유지관리 정보 탭 훅
|
||||
@@ -284,7 +285,7 @@ export default function useOperationTab() {
|
||||
const layerNm = row.lyrTypeNm;
|
||||
const fid = row.fid;
|
||||
const core = GisApp.Module.layer as any;
|
||||
await core.showSearchResultsFromGeoJSON(layerNm, fid);
|
||||
await core.showSearchResultsFromGeoJSON(layerNm, fid, mapSearchNavigationOptions((GisApp.Module.core as any).map));
|
||||
setIsLoading(false);
|
||||
setIsSearchModalOpen(false); // SearchModal 닫기
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { mapSearchNavigationOptions } from "@/features/map-search/lib/map-navigation";
|
||||
/**
|
||||
* 📄 useSearchModal Hook
|
||||
* 검색 모달 훅
|
||||
@@ -198,7 +199,7 @@ export default function useSearchModal() {
|
||||
const layerNm = row.lyrTypeNm;
|
||||
const fids = tableData.map((item) => item.fid);
|
||||
const core = GisApp.Module.layer as any;
|
||||
await core.showMultipleSearchResultsFromGeoJSON(layerNm, fids);
|
||||
await core.showMultipleSearchResultsFromGeoJSON(layerNm, fids, mapSearchNavigationOptions((GisApp.Module.core as any).map));
|
||||
setIsLoading(false);
|
||||
setIsSearchModalOpen(false);
|
||||
}
|
||||
@@ -222,7 +223,7 @@ export default function useSearchModal() {
|
||||
const layerNm = row.lyrTypeNm;
|
||||
const fids = detailTableData.map((item) => item.fid);
|
||||
const core = GisApp.Module.layer as any;
|
||||
await core.showMultipleSearchResultsFromGeoJSON(layerNm, fids);
|
||||
await core.showMultipleSearchResultsFromGeoJSON(layerNm, fids, mapSearchNavigationOptions((GisApp.Module.core as any).map));
|
||||
setIsLoading(false);
|
||||
setIsSearchModalOpen(false);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
*/
|
||||
|
||||
"use client";
|
||||
import { fitMapSelection, mapSearchNavigationOptions, beginMapNavigation } from "@/features/map-search/lib/map-navigation";
|
||||
import { useFloodPredictionMap } from "@/features/fldng-predc-prst/lib/use-flood-prediction-map";
|
||||
import {
|
||||
FLOOD_DNGR_MAP_LAYER_NAME,
|
||||
@@ -627,7 +628,7 @@ const MapPageClient = () => {
|
||||
const currentZoom = Number(view?.getZoom?.());
|
||||
const maxZoom =
|
||||
Number.isFinite(currentZoom) && currentZoom > 18 ? currentZoom : 18;
|
||||
view?.fit?.(extent, {
|
||||
fitMapSelection(map, extent, {
|
||||
duration: 350,
|
||||
padding: [80, 420, 80, 80],
|
||||
maxZoom,
|
||||
@@ -1292,6 +1293,7 @@ const MapPageClient = () => {
|
||||
void core?.showSearchResultsFromGeoJSON?.(
|
||||
layerTypeName,
|
||||
`${layerTypeName}.${featureIdentifier}`,
|
||||
mapSearchNavigationOptions(gisMaps.current),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1384,7 +1386,9 @@ const MapPageClient = () => {
|
||||
focusMap = true,
|
||||
fallbackPoint?: { x: number; y: number } | null,
|
||||
fallbackDetail?: Partial<SinkholeDetail> | null,
|
||||
resetSearch = false,
|
||||
) => {
|
||||
const navigation = resetSearch ? beginMapNavigation(gisMaps.current) : null;
|
||||
if (!acdntNo) return;
|
||||
|
||||
try {
|
||||
@@ -1540,6 +1544,7 @@ const MapPageClient = () => {
|
||||
}
|
||||
: detail;
|
||||
|
||||
if (navigation && !navigation.isCurrent()) return;
|
||||
setSelectedSinkholeDetail(normalizedDetail);
|
||||
setSelectedSinkholeNearbyPipes(nearbyPipes);
|
||||
renderSelectedSinkhole(normalizedDetail, sinkholeRadiusMeters);
|
||||
@@ -1555,6 +1560,9 @@ const MapPageClient = () => {
|
||||
normalizedDetail.x != null &&
|
||||
normalizedDetail.y != null
|
||||
) {
|
||||
const x = Number(normalizedDetail.x);
|
||||
const y = Number(normalizedDetail.y);
|
||||
if (navigation && !navigation.commit([x, y, x, y])) return;
|
||||
focusSinkholeOnMap(normalizedDetail, sinkholeRadiusMeters);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -1615,6 +1623,7 @@ const MapPageClient = () => {
|
||||
|
||||
const focusSinkholeNearbyPipe = useCallback(
|
||||
async (pipe: SinkholeNearbyPipe) => {
|
||||
const navigation = beginMapNavigation(gisMaps.current);
|
||||
const fid = String(pipe?.fid ?? "").trim();
|
||||
if (!fid) {
|
||||
alert("표시할 관로 식별값이 없습니다.");
|
||||
@@ -1654,7 +1663,7 @@ const MapPageClient = () => {
|
||||
});
|
||||
|
||||
const firstFeature = features[0];
|
||||
if (firstFeature) {
|
||||
if (firstFeature && navigation.commit(firstFeature.getGeometry()?.getExtent())) {
|
||||
syncRightPanelFeatureSelectionOnMap(firstFeature);
|
||||
setFeatureData(firstFeature);
|
||||
setIsOpen(true);
|
||||
@@ -2005,7 +2014,7 @@ const MapPageClient = () => {
|
||||
try {
|
||||
shouldFocusAgedPipeOnNextSelectRef.current = true;
|
||||
const core = GisApp.Module.layer as any;
|
||||
await core.showSearchResultsFromGeoJSON(AGED_PIPE_LAYER_TYPE, row.fid);
|
||||
await core.showSearchResultsFromGeoJSON(AGED_PIPE_LAYER_TYPE, row.fid, mapSearchNavigationOptions(gisMaps.current));
|
||||
} catch (error) {
|
||||
shouldFocusAgedPipeOnNextSelectRef.current = false;
|
||||
console.error("노후관로 위치 이동 중 오류:", error);
|
||||
@@ -2025,7 +2034,7 @@ const MapPageClient = () => {
|
||||
try {
|
||||
shouldFocusAgedPipeOnNextSelectRef.current = true;
|
||||
const core = GisApp.Module.layer as any;
|
||||
await core.showSearchResultsFromGeoJSON(AGED_PIPE_LAYER_TYPE, row.fid);
|
||||
await core.showSearchResultsFromGeoJSON(AGED_PIPE_LAYER_TYPE, row.fid, mapSearchNavigationOptions(gisMaps.current));
|
||||
} catch (error) {
|
||||
shouldFocusAgedPipeOnNextSelectRef.current = false;
|
||||
console.error("관로 위치 이동 중 오류:", error);
|
||||
@@ -2149,6 +2158,7 @@ const MapPageClient = () => {
|
||||
true,
|
||||
fallbackPoint,
|
||||
row,
|
||||
true,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, type ComponentProps } from "react";
|
||||
import { useCallback, useEffect, useState, useSyncExternalStore, type ComponentProps } from "react";
|
||||
import { getMapSearchRevision, invalidateMapNavigation, subscribeMapSearch } from "@/features/map-search/lib/map-navigation";
|
||||
import { ProcSearchBar } from "@common/business_components/ui/map/searchBar";
|
||||
|
||||
/** 지도 검색바의 Radix Select는 브라우저 마운트 이후 생성한다.
|
||||
@@ -8,6 +9,12 @@ import { ProcSearchBar } from "@common/business_components/ui/map/searchBar";
|
||||
* hydration 불일치를 피한다. 공통 Select 및 다른 앱의 렌더링 방식은 변경하지 않는다.
|
||||
*/
|
||||
export default function MapSearchBar(props: ComponentProps<typeof ProcSearchBar>) {
|
||||
/** 부모의 지도 준비 렌더에 맞춰 해당 인스턴스만 구독한다. */
|
||||
const map = props.gisMaps.current;
|
||||
const subscribe = useCallback((listener: () => void) => subscribeMapSearch(map, listener), [map]);
|
||||
const getSnapshot = useCallback(() => getMapSearchRevision(map), [map]);
|
||||
const resetRevision = useSyncExternalStore(subscribe, getSnapshot, () => 0);
|
||||
useEffect(() => () => invalidateMapNavigation(map), [map]);
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -25,5 +32,5 @@ export default function MapSearchBar(props: ComponentProps<typeof ProcSearchBar>
|
||||
);
|
||||
}
|
||||
|
||||
return <ProcSearchBar {...props} />;
|
||||
return <ProcSearchBar {...props} resetRevision={resetRevision} onSearchInteraction={() => invalidateMapNavigation(map)} />;
|
||||
}
|
||||
|
||||
@@ -198,11 +198,8 @@ export function VerticalTable({
|
||||
head.type === "select" ? (
|
||||
<select
|
||||
id={head.id}
|
||||
value={
|
||||
formData[head.id] !== undefined
|
||||
? String(formData[head.id])
|
||||
: ""
|
||||
}
|
||||
// DB 값이 null 이면 "null" 문자열이 선택값으로 들어가 목록에 없는 값이 되므로 빈 값으로 맞춘다.
|
||||
value={String(formData[head.id] ?? "")}
|
||||
onChange={(e) =>
|
||||
handleInputChange(head.id, e.target.value)
|
||||
}
|
||||
@@ -323,11 +320,8 @@ export function VerticalTable({
|
||||
head.type === "select" ? (
|
||||
<select
|
||||
id={head.id}
|
||||
value={
|
||||
formData[head.id] !== undefined
|
||||
? String(formData[head.id])
|
||||
: ""
|
||||
}
|
||||
// DB 값이 null 이면 "null" 문자열이 선택값으로 들어가 목록에 없는 값이 되므로 빈 값으로 맞춘다.
|
||||
value={String(formData[head.id] ?? "")}
|
||||
onChange={(e) =>
|
||||
handleInputChange(head.id, e.target.value)
|
||||
}
|
||||
@@ -357,11 +351,8 @@ export function VerticalTable({
|
||||
? "number"
|
||||
: "text"
|
||||
}
|
||||
value={
|
||||
formData[head.id] !== undefined
|
||||
? String(formData[head.id])
|
||||
: ""
|
||||
}
|
||||
// DB 값이 null 이면 "null" 글자가 입력칸에 보이므로 빈칸으로 맞춘다.
|
||||
value={String(formData[head.id] ?? "")}
|
||||
onChange={(e) => {
|
||||
if (head.isNumber) {
|
||||
const newValue = e.target.value;
|
||||
|
||||
@@ -669,3 +669,12 @@
|
||||
- 저장 API는 실제 시트명 등록을 계속 요구하며 미준비면 기존 결과를 변경하기 전에 차단한다. 프론트는 첨부 업로드 전에 최신 상태를 확인하며 서버에서 같은 오류가 반환돼도 alert로 안내한다.
|
||||
- 검증: ExmnTemplateGateTest 7건 통과(Excel/PPL PDF/MNHL 검토 독립, 미준비 commit 무쓰기 차단, 기존 시트명 보존, 준비 완료 commit, 해시 불일치 차단), FE 타입 검사 통과. 테스트는 mock 기반이며 실제 파일 저장·DB 변경 및 브라우저 끝단 저장은 수행하지 않았다.
|
||||
- 임시 테스트 소스·산출물은 요약 후 정리한다. 이전 ‘검토 전 양식 확인’ 기록은 이 지시로 대체한다.
|
||||
|
||||
## 2026-10-02 quick_vibe 등록 입력 필수 표시 보완
|
||||
|
||||
- 사업대상 처리분구 제목에 기존 폼과 같은 붉은 별표와 접근성용 필수 문구를 추가했다. 기존 1개 이상 선택 검증은 유지한다.
|
||||
- 정밀조사연장은 숫자·소수점 하나·입력 삭제만 허용한다. 문자/한글/부호/지수/콤마/복수 소수점 입력 및 붙여넣기는 거부한다. km 소수 입력과 기존 저장 검증은 유지한다.
|
||||
- 원본: 요구사항 기능 시트76행 SFR-010-004, PPT 실제39슬라이드 SN_MN040105 사업정보 등록/처리분구 선택/정밀조사연장(km).
|
||||
- 검증: SN 전체 타입검사 exit0, 숫자/소수/삭제 및 거부 문자열 Node 검증 통과, git diff --check 통과. 실브라우저 입력 검증은 미수행.
|
||||
- 수정 소스: `frontend/apps/sn/src/features/exmn/ui/exmn-business-register-popup-page.tsx`. API/백엔드 코드·저장 계약 변경 없음. 기존 GIS 작업은 변경하지 않았다.
|
||||
- 롤백: 해당 소스의 이번 필수 표시/입력 제한만 복원한다.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user