물 재이용 대민서비스 수요신청 > 수요정보등록 > 후보지조회 / 유사사례조회 모달팝업 수정 및 신규 추가
This commit is contained in:
@@ -133,7 +133,7 @@ export default function CandidateSiteModal({
|
||||
/>
|
||||
|
||||
{/* panel */}
|
||||
<div className="relative w-[94vw] max-w-[1400px] rounded-xl bg-white shadow-xl">
|
||||
<div className="relative w-[94vw] max-w-[1700px] rounded-xl bg-white shadow-xl">
|
||||
{/* header */}
|
||||
<div className="flex items-center justify-between px-8 py-6">
|
||||
<h2 className="text-2xl font-extrabold text-neutral-900">후보지 조회</h2>
|
||||
|
||||
@@ -0,0 +1,606 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
type Criterion = "" | "supplyAmt" | "treatment" | "usePurpose";
|
||||
|
||||
export type SimilarCaseRow = {
|
||||
caseId: string;
|
||||
projectName: string;
|
||||
period: string;
|
||||
|
||||
supplyFacilityType?: string;
|
||||
supplyName?: string;
|
||||
supplyCapacity?: number;
|
||||
|
||||
demandName?: string;
|
||||
demandAddress?: string;
|
||||
|
||||
facilityCapacity?: number;
|
||||
pipeKm?: number;
|
||||
purpose?: string;
|
||||
|
||||
// 유사기준용
|
||||
supplyAmt?: number; // 공급량(m3/일)
|
||||
treatment?: string; // 처리공법
|
||||
usePurpose?: string; // 이용용도
|
||||
};
|
||||
|
||||
export type SimilarCaseDetail = {
|
||||
// 보증수질
|
||||
coliform?: string;
|
||||
residualCl?: string;
|
||||
turbidity?: string;
|
||||
bod?: string;
|
||||
odor?: string;
|
||||
color?: string;
|
||||
tn?: string;
|
||||
tp?: string;
|
||||
ph?: string;
|
||||
chloride?: string;
|
||||
ec?: string;
|
||||
note?: string;
|
||||
|
||||
// 소요사업비(백만원) - 현재 타입에 있는 항목만 매핑
|
||||
costTotal?: number;
|
||||
costFacility?: number;
|
||||
costPipe?: number;
|
||||
costDesign?: number;
|
||||
costSupervision?: number;
|
||||
costOther?: number;
|
||||
|
||||
// 재원조달계획(백만원)
|
||||
fundingTotal?: number;
|
||||
fundingNational?: number;
|
||||
fundingLocal?: number;
|
||||
fundingSelf?: number;
|
||||
fundingRate?: number; // %
|
||||
};
|
||||
|
||||
function formatNumber(v: any) {
|
||||
if (v === null || v === undefined || v === "" || Number.isNaN(Number(v)))
|
||||
return "-";
|
||||
const n = typeof v === "number" ? v : Number(String(v).replaceAll(",", ""));
|
||||
if (!Number.isFinite(n)) return "-";
|
||||
return n.toLocaleString("ko-KR");
|
||||
}
|
||||
|
||||
export default function SimilarCaseModal({
|
||||
open,
|
||||
onClose,
|
||||
rows,
|
||||
detailsByCaseId,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
rows: SimilarCaseRow[];
|
||||
detailsByCaseId: Record<string, SimilarCaseDetail | undefined>;
|
||||
}) {
|
||||
// open 타입 방어
|
||||
if (typeof open !== "boolean") {
|
||||
console.error("SimilarCaseModal: open prop is not boolean:", open);
|
||||
return null;
|
||||
}
|
||||
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [criterion, setCriterion] = useState<Criterion>("");
|
||||
const [selectedCaseId, setSelectedCaseId] = useState<string>("");
|
||||
|
||||
useEffect(() => setMounted(true), []);
|
||||
|
||||
// 모달 열릴 때 초기화
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setCriterion("");
|
||||
setSelectedCaseId("");
|
||||
}, [open]);
|
||||
|
||||
// ESC 닫기 + 배경 스크롤 잠금
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") onClose();
|
||||
};
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
|
||||
const prevOverflow = document.body.style.overflow;
|
||||
document.body.style.overflow = "hidden";
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
document.body.style.overflow = prevOverflow;
|
||||
};
|
||||
}, [open, onClose]);
|
||||
|
||||
const groupInfo = useMemo(() => {
|
||||
const counts = new Map<string, number>();
|
||||
rows.forEach((r) => counts.set(r.caseId, (counts.get(r.caseId) ?? 0) + 1));
|
||||
|
||||
const firstIndex = new Map<string, number>();
|
||||
rows.forEach((r, idx) => {
|
||||
if (!firstIndex.has(r.caseId)) firstIndex.set(r.caseId, idx);
|
||||
});
|
||||
|
||||
return { counts, firstIndex };
|
||||
}, [rows]);
|
||||
|
||||
const selectedDetail = selectedCaseId ? detailsByCaseId[selectedCaseId] : undefined;
|
||||
|
||||
if (!open || !mounted) return null;
|
||||
|
||||
const tdBase =
|
||||
"border border-[#C9C9C9] px-2 py-2 text-sm text-neutral-800 align-middle";
|
||||
const thBase =
|
||||
"border border-[#C9C9C9] bg-[#E7F4E2] px-2 py-3 text-sm font-semibold text-neutral-800 text-center align-middle";
|
||||
const highlightTd = "bg-neutral-200";
|
||||
|
||||
const showList = criterion !== "";
|
||||
const hasRows = rows.length > 0;
|
||||
|
||||
const ui = (
|
||||
<div className="fixed inset-0 z-[1000] flex items-center justify-center">
|
||||
{/* overlay */}
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-0 bg-black/30"
|
||||
aria-label="모달 닫기"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* panel */}
|
||||
<div className="relative w-[94vw] max-w-[1700px] rounded-xl bg-white shadow-xl">
|
||||
{/* header */}
|
||||
<div className="flex items-center justify-between px-8 py-6">
|
||||
<h2 className="text-2xl font-extrabold text-neutral-900">유사사례 조회</h2>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="닫기"
|
||||
onClick={onClose}
|
||||
className="grid h-10 w-10 place-items-center rounded-sm hover:bg-neutral-100"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="h-px w-full bg-neutral-200" />
|
||||
|
||||
{/* body */}
|
||||
<div className="px-8 py-6">
|
||||
{/* 상단 타이틀 + 유사기준 */}
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="text-lg font-semibold text-neutral-900">
|
||||
물 재이용 사업 유사 사례
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-semibold text-neutral-800">
|
||||
유사기준
|
||||
</span>
|
||||
<select
|
||||
value={criterion}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value as Criterion;
|
||||
setCriterion(v);
|
||||
setSelectedCaseId("");
|
||||
}}
|
||||
className="h-10 w-[180px] rounded-md border border-neutral-300 bg-white px-3 text-sm outline-none focus:border-neutral-500"
|
||||
>
|
||||
<option value="">유사기준선택</option>
|
||||
<option value="supplyAmt">공급량</option>
|
||||
<option value="treatment">처리공법</option>
|
||||
<option value="usePurpose">이용용도</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 리스트 영역 */}
|
||||
<div className="border border-[#C9C9C9] bg-white">
|
||||
{!showList ? (
|
||||
<div className="grid min-h-[260px] place-items-center bg-emerald-50/30">
|
||||
<div className="text-neutral-500">
|
||||
유사기준을 선택해주세요.
|
||||
</div>
|
||||
</div>
|
||||
) : !hasRows ? (
|
||||
<div className="grid min-h-[260px] place-items-center bg-emerald-50/30">
|
||||
<div className="text-neutral-500">조회 결과가 없습니다.</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-h-[300px] overflow-auto">
|
||||
<table className="w-full border-collapse">
|
||||
<thead className="sticky top-0 z-10">
|
||||
<tr>
|
||||
<th rowSpan={2} className={thBase}>
|
||||
선택
|
||||
</th>
|
||||
<th rowSpan={2} className={thBase}>
|
||||
사업명
|
||||
</th>
|
||||
<th rowSpan={2} className={thBase}>
|
||||
사업기간
|
||||
</th>
|
||||
|
||||
<th colSpan={3} className={thBase}>
|
||||
공급처
|
||||
</th>
|
||||
<th colSpan={2} className={thBase}>
|
||||
수요처
|
||||
</th>
|
||||
<th colSpan={6} className={thBase}>
|
||||
재이용시설
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className={thBase}>시설종류</th>
|
||||
<th className={thBase}>공급처명</th>
|
||||
<th className={thBase}>
|
||||
시설용량
|
||||
<br />
|
||||
(m³/일)
|
||||
</th>
|
||||
|
||||
<th className={thBase}>수요처명</th>
|
||||
<th className={thBase}>주소</th>
|
||||
|
||||
<th className={thBase}>
|
||||
시설용량
|
||||
<br />
|
||||
(m³/일)
|
||||
</th>
|
||||
<th className={thBase}>
|
||||
관로
|
||||
<br />
|
||||
(km)
|
||||
</th>
|
||||
<th className={thBase}>용도</th>
|
||||
|
||||
<th className={thBase}>
|
||||
공급량
|
||||
<br />
|
||||
(m³/일)
|
||||
</th>
|
||||
<th className={thBase}>처리공법</th>
|
||||
<th className={thBase}>이용용도</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{rows.map((r, idx) => {
|
||||
const isFirst = groupInfo.firstIndex.get(r.caseId) === idx;
|
||||
const rowSpan = groupInfo.counts.get(r.caseId) ?? 1;
|
||||
const selected = selectedCaseId === r.caseId;
|
||||
|
||||
const supplyAmtCls = criterion === "supplyAmt" ? highlightTd : "";
|
||||
const treatmentCls = criterion === "treatment" ? highlightTd : "";
|
||||
const usePurposeCls = criterion === "usePurpose" ? highlightTd : "";
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={`${r.caseId}-${idx}`}
|
||||
className={[
|
||||
"cursor-pointer hover:bg-neutral-50",
|
||||
selected ? "bg-sky-50" : "",
|
||||
].join(" ")}
|
||||
onClick={() => setSelectedCaseId(r.caseId)}
|
||||
>
|
||||
{isFirst ? (
|
||||
<>
|
||||
<td className={`${tdBase} text-center`} rowSpan={rowSpan}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selected}
|
||||
readOnly
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
</td>
|
||||
<td className={`${tdBase} text-center`} rowSpan={rowSpan}>
|
||||
{r.projectName ?? "-"}
|
||||
</td>
|
||||
<td className={`${tdBase} text-center`} rowSpan={rowSpan}>
|
||||
{r.period ?? "-"}
|
||||
</td>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<td className={`${tdBase} text-center`}>
|
||||
{r.supplyFacilityType ?? "-"}
|
||||
</td>
|
||||
<td className={`${tdBase} text-center`}>{r.supplyName ?? "-"}</td>
|
||||
<td className={`${tdBase} text-center`}>
|
||||
{formatNumber(r.supplyCapacity)}
|
||||
</td>
|
||||
|
||||
<td className={`${tdBase} text-center`}>{r.demandName ?? "-"}</td>
|
||||
<td className={`${tdBase} text-center`}>{r.demandAddress ?? "-"}</td>
|
||||
|
||||
<td className={`${tdBase} text-center`}>
|
||||
{formatNumber(r.facilityCapacity)}
|
||||
</td>
|
||||
<td className={`${tdBase} text-center`}>
|
||||
{formatNumber(r.pipeKm)}
|
||||
</td>
|
||||
<td className={tdBase}>{r.purpose ?? "-"}</td>
|
||||
|
||||
<td className={`${tdBase} text-center ${supplyAmtCls}`}>
|
||||
{formatNumber(r.supplyAmt)}
|
||||
</td>
|
||||
<td className={`${tdBase} text-center ${treatmentCls}`}>
|
||||
{r.treatment ?? "-"}
|
||||
</td>
|
||||
<td className={`${tdBase} text-center ${usePurposeCls}`}>
|
||||
{r.usePurpose ?? "-"}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 상세정보 */}
|
||||
<div className="mt-6">
|
||||
<div className="mb-2 text-lg font-semibold text-neutral-900">
|
||||
상세정보
|
||||
</div>
|
||||
|
||||
{/* 1) 보증수질 */}
|
||||
<div className="overflow-hidden border border-[#C9C9C9]">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colSpan={13} className={thBase}>
|
||||
보증수질
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th rowSpan={2} className={thBase}>
|
||||
총대장균군수<br />(개/100ml)
|
||||
</th>
|
||||
<th rowSpan={2} className={thBase}>
|
||||
용도별 결합 잔류염소<br />(mg/L)
|
||||
</th>
|
||||
<th rowSpan={2} className={thBase}>
|
||||
탁도<br />(NTU)
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className={thBase}>
|
||||
생물학적<br />산소요구량(BOD)(mg/L)
|
||||
</th>
|
||||
<th className={thBase}>냄새</th>
|
||||
<th className={thBase}>색도(도)</th>
|
||||
<th className={thBase}>
|
||||
총질소<br />(T-N)(mg/L)
|
||||
</th>
|
||||
<th className={thBase}>
|
||||
총인<br />(T-P)(mg/L)
|
||||
</th>
|
||||
<th className={thBase}>
|
||||
수소이온<br />농도(pH)
|
||||
</th>
|
||||
<th className={thBase}>
|
||||
염화물<br />(mgCL/L)
|
||||
</th>
|
||||
<th className={thBase}>
|
||||
전기전도도<br />(μs/cm)
|
||||
</th>
|
||||
<th className={thBase}>
|
||||
비고
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className={`${tdBase} text-center`}>
|
||||
{selectedDetail?.coliform ?? "-"}
|
||||
</td>
|
||||
<td className={`${tdBase} text-center`}>
|
||||
{selectedDetail?.residualCl ?? "-"}
|
||||
</td>
|
||||
<td className={`${tdBase} text-center`}>
|
||||
{selectedDetail?.turbidity ?? "-"}
|
||||
</td>
|
||||
|
||||
<td className={`${tdBase} text-center`}>
|
||||
{selectedDetail?.bod ?? "-"}
|
||||
</td>
|
||||
<td className={`${tdBase} text-center`}>
|
||||
{selectedDetail?.odor ?? "-"}
|
||||
</td>
|
||||
<td className={`${tdBase} text-center`}>
|
||||
{selectedDetail?.color ?? "-"}
|
||||
</td>
|
||||
<td className={`${tdBase} text-center`}>
|
||||
{selectedDetail?.tn ?? "-"}
|
||||
</td>
|
||||
<td className={`${tdBase} text-center`}>
|
||||
{selectedDetail?.tp ?? "-"}
|
||||
</td>
|
||||
<td className={`${tdBase} text-center`}>
|
||||
{selectedDetail?.ph ?? "-"}
|
||||
</td>
|
||||
<td className={`${tdBase} text-center`}>
|
||||
{selectedDetail?.chloride ?? "-"}
|
||||
</td>
|
||||
<td className={`${tdBase} text-center`}>
|
||||
{selectedDetail?.ec ?? "-"}
|
||||
</td>
|
||||
|
||||
<td className={`${tdBase} text-center`}>
|
||||
{selectedDetail?.note ?? "-"}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* 2) 사업비 */}
|
||||
<div className="mt-4 overflow-hidden border border-[#C9C9C9]">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colSpan={14} className={thBase}>
|
||||
소요사업비(백만원)
|
||||
</th>
|
||||
<th colSpan={5} className={thBase}>
|
||||
재원조달계획(백만원)
|
||||
</th>
|
||||
<th colSpan={5} className={thBase}>
|
||||
사업비 정산(백만원)
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
{/* 소요사업비 */}
|
||||
<th rowSpan={2} className={thBase}>총</th>
|
||||
<th colSpan={2} className={thBase}>토목</th>
|
||||
<th rowSpan={2} className={thBase}>건축</th>
|
||||
<th rowSpan={2} className={thBase}>기계</th>
|
||||
<th rowSpan={2} className={thBase}>전기<br />·계측</th>
|
||||
<th rowSpan={2} className={thBase}>GIS<br />DB</th>
|
||||
<th rowSpan={2} className={thBase}>CCTV</th>
|
||||
<th rowSpan={2} className={thBase}>시공 및 준공</th>
|
||||
<th rowSpan={2} className={thBase}>폐기물</th>
|
||||
<th rowSpan={2} className={thBase}>설계비</th>
|
||||
<th rowSpan={2} className={thBase}>감리비</th>
|
||||
<th rowSpan={2} className={thBase}>시설부대</th>
|
||||
<th rowSpan={2} className={thBase}>기타</th>
|
||||
|
||||
{/* 재원조달 */}
|
||||
<th rowSpan={2} className={thBase}>총</th>
|
||||
<th rowSpan={2} className={thBase}>국비</th>
|
||||
<th rowSpan={2} className={thBase}>지방비</th>
|
||||
<th rowSpan={2} className={thBase}>원인자부담</th>
|
||||
<th rowSpan={2} className={thBase}>국고비율</th>
|
||||
|
||||
{/* 정산 */}
|
||||
<th rowSpan={2} className={thBase}>총</th>
|
||||
<th rowSpan={2} className={thBase}>국비</th>
|
||||
<th rowSpan={2} className={thBase}>지방비</th>
|
||||
<th rowSpan={2} className={thBase}>원인자부담</th>
|
||||
<th rowSpan={2} className={thBase}>국고비율</th>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th className={thBase}>재이용<br />시설</th>
|
||||
<th className={thBase}>관로</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
{/* 소요사업비 */}
|
||||
<td className={`${tdBase} text-right`}>
|
||||
{selectedDetail?.costTotal != null
|
||||
? formatNumber(selectedDetail.costTotal)
|
||||
: "-"}
|
||||
</td>
|
||||
<td className={`${tdBase} text-right`}>
|
||||
{selectedDetail?.costFacility != null
|
||||
? formatNumber(selectedDetail.costFacility)
|
||||
: "-"}
|
||||
</td>
|
||||
<td className={`${tdBase} text-right`}>
|
||||
{selectedDetail?.costPipe != null
|
||||
? formatNumber(selectedDetail.costPipe)
|
||||
: "-"}
|
||||
</td>
|
||||
<td className={`${tdBase} text-center`}>-</td>
|
||||
<td className={`${tdBase} text-center`}>-</td>
|
||||
<td className={`${tdBase} text-center`}>-</td>
|
||||
<td className={`${tdBase} text-center`}>-</td>
|
||||
<td className={`${tdBase} text-center`}>-</td>
|
||||
<td className={`${tdBase} text-center`}>-</td>
|
||||
<td className={`${tdBase} text-center`}>-</td>
|
||||
<td className={`${tdBase} text-right`}>
|
||||
{selectedDetail?.costDesign != null
|
||||
? formatNumber(selectedDetail.costDesign)
|
||||
: "-"}
|
||||
</td>
|
||||
<td className={`${tdBase} text-right`}>
|
||||
{selectedDetail?.costSupervision != null
|
||||
? formatNumber(selectedDetail.costSupervision)
|
||||
: "-"}
|
||||
</td>
|
||||
<td className={`${tdBase} text-center`}>-</td>
|
||||
<td className={`${tdBase} text-right`}>
|
||||
{selectedDetail?.costOther != null
|
||||
? formatNumber(selectedDetail.costOther)
|
||||
: "-"}
|
||||
</td>
|
||||
|
||||
{/* 재원조달 */}
|
||||
<td className={`${tdBase} text-right`}>
|
||||
{selectedDetail?.fundingTotal != null
|
||||
? formatNumber(selectedDetail.fundingTotal)
|
||||
: "-"}
|
||||
</td>
|
||||
<td className={`${tdBase} text-right`}>
|
||||
{selectedDetail?.fundingNational != null
|
||||
? formatNumber(selectedDetail.fundingNational)
|
||||
: "-"}
|
||||
</td>
|
||||
<td className={`${tdBase} text-right`}>
|
||||
{selectedDetail?.fundingLocal != null
|
||||
? formatNumber(selectedDetail.fundingLocal)
|
||||
: "-"}
|
||||
</td>
|
||||
<td className={`${tdBase} text-right`}>
|
||||
{selectedDetail?.fundingSelf != null
|
||||
? formatNumber(selectedDetail.fundingSelf)
|
||||
: "-"}
|
||||
</td>
|
||||
<td className={`${tdBase} text-center`}>
|
||||
{typeof selectedDetail?.fundingRate === "number"
|
||||
? `${selectedDetail.fundingRate}%`
|
||||
: "-"}
|
||||
</td>
|
||||
|
||||
{/* 정산(필드 없으면 동일 값 표시) */}
|
||||
<td className={`${tdBase} text-right`}>
|
||||
{selectedDetail?.fundingTotal != null
|
||||
? formatNumber(selectedDetail.fundingTotal)
|
||||
: "-"}
|
||||
</td>
|
||||
<td className={`${tdBase} text-right`}>
|
||||
{selectedDetail?.fundingNational != null
|
||||
? formatNumber(selectedDetail.fundingNational)
|
||||
: "-"}
|
||||
</td>
|
||||
<td className={`${tdBase} text-right`}>
|
||||
{selectedDetail?.fundingLocal != null
|
||||
? formatNumber(selectedDetail.fundingLocal)
|
||||
: "-"}
|
||||
</td>
|
||||
<td className={`${tdBase} text-right`}>
|
||||
{selectedDetail?.fundingSelf != null
|
||||
? formatNumber(selectedDetail.fundingSelf)
|
||||
: "-"}
|
||||
</td>
|
||||
<td className={`${tdBase} text-center`}>
|
||||
{typeof selectedDetail?.fundingRate === "number"
|
||||
? `${selectedDetail.fundingRate}%`
|
||||
: "-"}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{!selectedCaseId && showList ? (
|
||||
<div className="mt-3 text-sm text-neutral-500">
|
||||
리스트에서 사업을 선택하면 상세정보가 표시됩니다.
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return createPortal(ui, document.body);
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
// apps/dm/app/(landing)/reuse/apply/register/page.tsx
|
||||
// apps/dm/app/(landing)/reuse/apply/register/page.tsx (수요정보등록 페이지)
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState, useRef } from "react";
|
||||
import { useMemo, useState, useRef, useEffect } from "react";
|
||||
import SubPageHeader from "../../_components/SubPageHeader";
|
||||
import WaterQualityRefModal from "../modal/WaterQualityRefModal";
|
||||
import CandidateSiteModal, { type CandidateSite } from "../modal/CandidateSiteModal";
|
||||
import SimilarCaseModal, { type SimilarCaseRow, type SimilarCaseDetail } from "../modal/SimilarCaseModal";
|
||||
|
||||
type PHRange = { min: string; max: string };
|
||||
|
||||
@@ -22,6 +23,141 @@ export default function DemandRegisterPage() {
|
||||
const [openCand, setOpenCand] = useState(false);
|
||||
const [selectedCands, setSelectedCands] = useState<CandidateSite[]>([]);
|
||||
|
||||
// 유사사례 모달
|
||||
const [openSimilar, setOpenSimilar] = useState(false);
|
||||
|
||||
// 유사사례 rows (목업) - API 연동 시 여기만 교체
|
||||
const rows: SimilarCaseRow[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
caseId: "C-001",
|
||||
projectName: "OO시 물 재이용(빗물, 중수도) 시설 설치사업",
|
||||
period: "2020.01. ~ 2025.05.",
|
||||
supplyFacilityType: "중수도",
|
||||
supplyName: "OO새교복지타운",
|
||||
supplyCapacity: 0,
|
||||
demandName: "OO새교복지타운",
|
||||
demandAddress: "OO시 수청로 192",
|
||||
facilityCapacity: 50,
|
||||
pipeKm: 0,
|
||||
purpose: "청소, 화장실, 조경",
|
||||
supplyAmt: 50,
|
||||
treatment: "MBLM",
|
||||
usePurpose: "청소,화장실",
|
||||
},
|
||||
{
|
||||
caseId: "C-001",
|
||||
projectName: "OO시 물 재이용(빗물, 중수도) 시설 설치사업",
|
||||
period: "2020.01. ~ 2025.05.",
|
||||
supplyFacilityType: "중수도",
|
||||
supplyName: "OO스포츠센터",
|
||||
supplyCapacity: 0,
|
||||
demandName: "OO스포츠센터",
|
||||
demandAddress: "OO시 경기동로 33",
|
||||
facilityCapacity: 50,
|
||||
pipeKm: 0,
|
||||
purpose: "청소, 화장실, 조경",
|
||||
supplyAmt: 50,
|
||||
treatment: "MBLM",
|
||||
usePurpose: "청소,화장실",
|
||||
},
|
||||
{
|
||||
caseId: "C-002",
|
||||
projectName: "OO 엄성저수지 하수처리수 재이용사업",
|
||||
period: "2019.12 ~ 2026.01",
|
||||
supplyFacilityType: "하수",
|
||||
supplyName: "OO 3,4단계 공공하수처리시설",
|
||||
supplyCapacity: 70000,
|
||||
demandName: "OO천",
|
||||
demandAddress: "-",
|
||||
facilityCapacity: 0,
|
||||
pipeKm: 3,
|
||||
purpose: "하천 등 유지용수",
|
||||
supplyAmt: 2300,
|
||||
treatment: "탈황,총인, 생물산화과,소독",
|
||||
usePurpose: "하천 등 유지용수",
|
||||
},
|
||||
{
|
||||
caseId: "C-002",
|
||||
projectName: "OO 엄성저수지 하수처리수 재이용사업",
|
||||
period: "2019.12 ~ 2026.01",
|
||||
supplyFacilityType: "하수",
|
||||
supplyName: "OO 3,4단계 공공하수처리시설",
|
||||
supplyCapacity: 70000,
|
||||
demandName: "OO저수지",
|
||||
demandAddress: "-",
|
||||
facilityCapacity: 0,
|
||||
pipeKm: 5,
|
||||
purpose: "하천 등 유지용수",
|
||||
supplyAmt: 7700,
|
||||
treatment: "탈황,총인, 생물산화과,소독",
|
||||
usePurpose: "하천 등 유지용수",
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
// 유사사례 상세(목업) - caseId로 매핑
|
||||
const detailsByCaseId: Record<string, SimilarCaseDetail> = useMemo(
|
||||
() => ({
|
||||
"C-001": {
|
||||
coliform: "불검출",
|
||||
residualCl: "0.2이상",
|
||||
turbidity: "1.5이하",
|
||||
bod: "4.0이하",
|
||||
odor: "불쾌하지않을것",
|
||||
color: "10이하",
|
||||
tn: "-",
|
||||
tp: "-",
|
||||
ph: "5.8~8.5",
|
||||
chloride: "-",
|
||||
ec: "-",
|
||||
note: "-",
|
||||
|
||||
costTotal: 4305,
|
||||
costFacility: 3518,
|
||||
costPipe: undefined,
|
||||
costDesign: 350,
|
||||
costSupervision: 417,
|
||||
costOther: 12,
|
||||
|
||||
fundingTotal: 4305,
|
||||
fundingNational: 3104,
|
||||
fundingLocal: 1291,
|
||||
fundingSelf: undefined,
|
||||
fundingRate: 70.0,
|
||||
},
|
||||
"C-002": {
|
||||
coliform: "불검출",
|
||||
residualCl: "0.15이상",
|
||||
turbidity: "2.0이하",
|
||||
bod: "5.0이하",
|
||||
odor: "무",
|
||||
color: "10이하",
|
||||
tn: "-",
|
||||
tp: "-",
|
||||
ph: "6.0~8.5",
|
||||
chloride: "-",
|
||||
ec: "-",
|
||||
note: "-",
|
||||
|
||||
costTotal: 9800,
|
||||
costFacility: 2100,
|
||||
costPipe: 1800,
|
||||
costDesign: 300,
|
||||
costSupervision: 280,
|
||||
costOther: 1200,
|
||||
|
||||
fundingTotal: 9800,
|
||||
fundingNational: 4900,
|
||||
fundingLocal: 3000,
|
||||
fundingSelf: 1900,
|
||||
fundingRate: 50,
|
||||
},
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
// 기본정보
|
||||
const [reqDate, setReqDate] = useState<string>(today);
|
||||
const [reqQty, setReqQty] = useState<string>("");
|
||||
@@ -49,264 +185,352 @@ export default function DemandRegisterPage() {
|
||||
const [cand2, setCand2] = useState("");
|
||||
const [cand3, setCand3] = useState("");
|
||||
|
||||
// 샘플 데이터
|
||||
const similarRows: SimilarCaseRow[] = [
|
||||
{
|
||||
caseId: "A",
|
||||
projectName: "OO시 물 재이용(빗물, 중수도)\n시설 설치사업",
|
||||
period: "2020.01.\n~2025.05.",
|
||||
supplyFacilityType: "중수도",
|
||||
supplyName: "OO에코복지타운",
|
||||
supplyCapacity: 50,
|
||||
demandName: "OO에코복지타운",
|
||||
demandAddress: "OO시 수성로 192",
|
||||
facilityCapacity: 50,
|
||||
pipeKm: 0,
|
||||
purpose: "청소, 화장실,\n조경",
|
||||
supplyAmt: 50,
|
||||
treatment: "MBLM",
|
||||
usePurpose: "청소,화장실",
|
||||
},
|
||||
{
|
||||
caseId: "A",
|
||||
projectName: "OO시 물 재이용(빗물, 중수도)\n시설 설치사업",
|
||||
period: "2020.01.\n~2025.05.",
|
||||
supplyFacilityType: "중수도",
|
||||
supplyName: "OO스포츠센터",
|
||||
supplyCapacity: 50,
|
||||
demandName: "OO스포츠센터",
|
||||
demandAddress: "OO시 경기둥로 33",
|
||||
facilityCapacity: 50,
|
||||
pipeKm: 0,
|
||||
purpose: "청소, 화장실,\n조경",
|
||||
supplyAmt: 50,
|
||||
treatment: "MBLM",
|
||||
usePurpose: "청소,화장실",
|
||||
},
|
||||
{
|
||||
caseId: "B",
|
||||
projectName: "OO 염성저수지 하수처리수\n재이용사업",
|
||||
period: "2019.12\n~2026.1",
|
||||
supplyFacilityType: "하수",
|
||||
supplyName: "OO 3.4단계 공공하수처리시설",
|
||||
supplyCapacity: 70000,
|
||||
demandName: "OO천",
|
||||
demandAddress: "-",
|
||||
facilityCapacity: 0,
|
||||
pipeKm: 3,
|
||||
purpose: "하천 등\n유지용수",
|
||||
supplyAmt: 2300,
|
||||
treatment: "탈황,총인,\n생물여과,소독",
|
||||
usePurpose: "하천 등\n유지용수",
|
||||
},
|
||||
{
|
||||
caseId: "B",
|
||||
projectName: "OO 염성저수지 하수처리수\n재이용사업",
|
||||
period: "2019.12\n~2026.1",
|
||||
supplyFacilityType: "하수",
|
||||
supplyName: "OO 3.4단계 공공하수처리시설",
|
||||
supplyCapacity: 70000,
|
||||
demandName: "OO저수지",
|
||||
demandAddress: "-",
|
||||
facilityCapacity: 0,
|
||||
pipeKm: 5,
|
||||
purpose: "하천 등\n유지용수",
|
||||
supplyAmt: 7700,
|
||||
treatment: "탈황,총인,\n생물여과,소독",
|
||||
usePurpose: "하천 등\n유지용수",
|
||||
},
|
||||
];
|
||||
|
||||
const similarDetailsByCaseId: Record<string, SimilarCaseDetail> = {
|
||||
A: {
|
||||
coliform: "불검출",
|
||||
residualCl: "0.2이상",
|
||||
turbidity: "1.5이하",
|
||||
bod: "4.0이하",
|
||||
odor: "불쾌하지않을것",
|
||||
color: "10이하",
|
||||
tn: "-",
|
||||
tp: "-",
|
||||
ph: "5.8~8.5",
|
||||
chloride: "-",
|
||||
ec: "-",
|
||||
note: "-",
|
||||
costTotal: 4305,
|
||||
costFacility: 3518,
|
||||
costPipe: 0,
|
||||
costDesign: 350,
|
||||
costSupervision: 417,
|
||||
costOther: 12,
|
||||
fundingTotal: 4305,
|
||||
fundingNational: 3104,
|
||||
fundingLocal: 1291,
|
||||
fundingSelf: 0,
|
||||
fundingRate: 70.0,
|
||||
},
|
||||
B: {
|
||||
coliform: "-",
|
||||
residualCl: "-",
|
||||
turbidity: "-",
|
||||
bod: "-",
|
||||
odor: "-",
|
||||
color: "-",
|
||||
tn: "-",
|
||||
tp: "-",
|
||||
ph: "-",
|
||||
chloride: "-",
|
||||
ec: "-",
|
||||
note: "-",
|
||||
costTotal: 0,
|
||||
costFacility: 0,
|
||||
costPipe: 0,
|
||||
costDesign: 0,
|
||||
costSupervision: 0,
|
||||
costOther: 0,
|
||||
fundingTotal: 0,
|
||||
fundingNational: 0,
|
||||
fundingLocal: 0,
|
||||
fundingSelf: 0,
|
||||
fundingRate: 0,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<main className="bg-white">
|
||||
|
||||
{/* 서브 타이틀 + 브레드크럼 */}
|
||||
<SubPageHeader
|
||||
title="수요정보등록"
|
||||
crumbs={[
|
||||
{ label: "Home", href: "/reuse" },
|
||||
{ label: "수요신청", href: "/reuse/apply" },
|
||||
{ label: "수요정보등록" },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* 컨텐츠 영역 */}
|
||||
<div>
|
||||
|
||||
{/* 기본정보 테이블 */}
|
||||
<div className="border border-neutral-200 bg-white">
|
||||
<FormRow label="요구시기" required>
|
||||
<DateInput value={reqDate} onChange={setReqDate} />
|
||||
</FormRow>
|
||||
|
||||
<Divider />
|
||||
|
||||
<FormRow label="요구수량(m²/d)" required>
|
||||
<TextInput value={reqQty} onChange={setReqQty} placeholder="" />
|
||||
</FormRow>
|
||||
|
||||
<Divider />
|
||||
|
||||
<FormRow label="사용용도" required>
|
||||
<TextInput value={usePurpose} onChange={setUsePurpose} />
|
||||
</FormRow>
|
||||
|
||||
<Divider />
|
||||
|
||||
<FormRow label="희망단가(원)" required>
|
||||
<TextInput value={hopePrice} onChange={setHopePrice} />
|
||||
</FormRow>
|
||||
</div>
|
||||
|
||||
{/* 요구수질 섹션 타이틀 바 */}
|
||||
<div className="grid grid-cols-12">
|
||||
<div className="col-span-12 lg:col-span-2 inline-flex justify-center items-center gap-3 px-6 py-6 text-center">
|
||||
<span className="text-lg font-semibold text-neutral-800">요구수질</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpenRef(true)}
|
||||
className="rounded border-[2px] border-[#DE3412] px-2 py-3 text-[15px] font-medium text-[#4F4F4F] hover:bg-orange-50"
|
||||
>
|
||||
수질기준 참고
|
||||
</button>
|
||||
{/* 수질기준참고 모달 */}
|
||||
<WaterQualityRefModal
|
||||
open={openRef}
|
||||
onClose={() => setOpenRef(false)}
|
||||
imageSrc="/images/landing/water_quality_ref_1.png"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 요구수질 테이블 */}
|
||||
<div className="border border-neutral-200 bg-white">
|
||||
{/* 1행 */}
|
||||
<QualityRow
|
||||
leftLabel="총대장균군수(개/mg)"
|
||||
leftRequired
|
||||
leftControl={
|
||||
<div className="flex items-center gap-4">
|
||||
<TextInput
|
||||
value={coliform}
|
||||
onChange={setColiform}
|
||||
disabled={noDetect}
|
||||
className={noDetect ? "bg-neutral-100 text-neutral-400" : ""}
|
||||
/>
|
||||
<label className="flex items-center gap-2 text-sm text-neutral-700">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={noDetect}
|
||||
onChange={(e) => {
|
||||
setNoDetect(e.target.checked);
|
||||
if (e.target.checked) setColiform("");
|
||||
}}
|
||||
className="h-4 w-4 rounded border-neutral-300"
|
||||
/>
|
||||
불검출
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
rightLabel="총질소(T-N)(mg/L)"
|
||||
rightControl={
|
||||
<InlineSuffixInput value={tn} onChange={setTn} suffix="이하" />
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
{/* 2행 */}
|
||||
<QualityRow
|
||||
leftLabel="결합잔류염소(mg/L)"
|
||||
leftRequired
|
||||
leftControl={<TextInput value={residualCl} onChange={setResidualCl} />}
|
||||
rightLabel="총인(T-P)(mg/L)"
|
||||
rightControl={
|
||||
<InlineSuffixInput value={tp} onChange={setTp} suffix="이하" />
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
{/* 3행 */}
|
||||
<QualityRow
|
||||
leftLabel="탁도(NTU)"
|
||||
leftRequired
|
||||
leftControl={
|
||||
<InlineSuffixInput value={turbidity} onChange={setTurbidity} suffix="이하" />
|
||||
}
|
||||
rightLabel="수소이온농도(pH)"
|
||||
rightControl={
|
||||
<div className="flex items-center gap-2">
|
||||
<TextInput
|
||||
value={ph.min}
|
||||
onChange={(v) => setPh((p) => ({ ...p, min: v }))}
|
||||
className="w-32"
|
||||
/>
|
||||
<span className="text-neutral-500">~</span>
|
||||
<TextInput
|
||||
value={ph.max}
|
||||
onChange={(v) => setPh((p) => ({ ...p, max: v }))}
|
||||
className="w-32"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
{/* 4행 */}
|
||||
<QualityRow
|
||||
leftLabel={
|
||||
<>
|
||||
생물화학적산소요구량
|
||||
<br />
|
||||
(BOD)(mg/L)
|
||||
</>
|
||||
}
|
||||
leftRequired
|
||||
leftControl={<InlineSuffixInput value={bod} onChange={setBod} suffix="이하" />}
|
||||
rightLabel="염화물(mgCL/L)"
|
||||
rightControl={<InlineSuffixInput value={chloride} onChange={setChloride} suffix="이하" />}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
{/* 5행 */}
|
||||
<QualityRow
|
||||
leftLabel="냄새"
|
||||
leftRequired
|
||||
leftControl={
|
||||
<select
|
||||
value={odor}
|
||||
onChange={(e) => setOdor(e.target.value)}
|
||||
className="h-11 w-full max-w-[200px] rounded-md border border-neutral-300 bg-white px-3 text-sm outline-none focus:border-neutral-500"
|
||||
>
|
||||
<option>불쾌하지 않을 것</option>
|
||||
<option>약간 있음</option>
|
||||
<option>있음</option>
|
||||
</select>
|
||||
}
|
||||
rightLabel="전기전도도(µs/cm)"
|
||||
rightControl={<InlineSuffixInput value={ec} onChange={setEc} suffix="이하" />}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
{/* 6행 */}
|
||||
<QualityRow
|
||||
leftLabel="색도(도)"
|
||||
leftRequired
|
||||
leftControl={<InlineSuffixInput value={color} onChange={setColor} suffix="이하" />}
|
||||
rightLabel="비고"
|
||||
rightControl={
|
||||
<textarea
|
||||
value={remark}
|
||||
onChange={(e) => setRemark(e.target.value)}
|
||||
className="h-11 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm outline-none focus:border-neutral-500"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
{/* 후보지 선택 */}
|
||||
<div className="grid grid-cols-12">
|
||||
<LabelCell>
|
||||
공급처 후보지 선택
|
||||
<br />
|
||||
(필수아님)
|
||||
</LabelCell>
|
||||
|
||||
<div className="col-span-12 md:col-span-10 p-6">
|
||||
<div className="flex flex-col gap-3 xl:flex-row xl:items-center xl:gap-4">
|
||||
<TextInput value={cand1} onChange={setCand1} className="max-w-[220px]" />
|
||||
<TextInput value={cand2} onChange={setCand2} className="max-w-[220px]" />
|
||||
<TextInput value={cand3} onChange={setCand3} className="max-w-[220px]" />
|
||||
|
||||
<div className="flex items-center gap-3 xl:ml-2">
|
||||
<button
|
||||
type="button"
|
||||
className="h-11 rounded-md bg-[#888] px-5 text-base text-white hover:bg-neutral-700"
|
||||
onClick={() => setOpenCand(true)}
|
||||
>
|
||||
후보지 조회
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="h-11 rounded-md bg-[#888] px-5 text-base text-white hover:bg-neutral-600"
|
||||
onClick={() => {
|
||||
// TODO: 유사사례 연결
|
||||
console.log("유사사례");
|
||||
}}
|
||||
>
|
||||
유사사례
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 하단 버튼 */}
|
||||
<div className="flex justify-center py-12">
|
||||
<button
|
||||
type="button"
|
||||
className="h-11 rounded-md bg-[#0B4EA2] px-10 text-lg text-white hover:opacity-90"
|
||||
onClick={() => {
|
||||
// TODO: 저장 처리
|
||||
console.log("수요정보 등록");
|
||||
}}
|
||||
>
|
||||
수요정보 등록
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<CandidateSiteModal
|
||||
open={openCand}
|
||||
onClose={() => setOpenCand(false)}
|
||||
initialSelected={selectedCands}
|
||||
onApply={(list) => {
|
||||
setSelectedCands(list);
|
||||
|
||||
// ✅ cand1~3 입력칸에 반영(시설명만 넣는 버전)
|
||||
setCand1(list[0]?.name ?? "");
|
||||
setCand2(list[1]?.name ?? "");
|
||||
setCand3(list[2]?.name ?? "");
|
||||
|
||||
setOpenCand(false);
|
||||
}}
|
||||
<main className="bg-white">
|
||||
<SubPageHeader
|
||||
title="수요정보등록"
|
||||
crumbs={[
|
||||
{ label: "Home", href: "/reuse" },
|
||||
{ label: "수요신청", href: "/reuse/apply" },
|
||||
{ label: "수요정보등록" },
|
||||
]}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<div className="border border-neutral-200 bg-white">
|
||||
<FormRow label="요구시기" required>
|
||||
<DateInput value={reqDate} onChange={setReqDate} />
|
||||
</FormRow>
|
||||
<Divider />
|
||||
|
||||
<FormRow label="요구수량(m²/d)" required>
|
||||
<TextInput value={reqQty} onChange={setReqQty} />
|
||||
</FormRow>
|
||||
<Divider />
|
||||
|
||||
<FormRow label="사용용도" required>
|
||||
<TextInput value={usePurpose} onChange={setUsePurpose} />
|
||||
</FormRow>
|
||||
<Divider />
|
||||
|
||||
<FormRow label="희망단가(원)" required>
|
||||
<TextInput value={hopePrice} onChange={setHopePrice} />
|
||||
</FormRow>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-12">
|
||||
<div className="col-span-12 lg:col-span-2 inline-flex items-center justify-center gap-3 px-6 py-6 text-center">
|
||||
<span className="text-lg font-semibold text-neutral-800">요구수질</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpenRef(true)}
|
||||
className="rounded border-[2px] border-[#DE3412] px-2 py-3 text-[15px] font-medium text-[#4F4F4F] hover:bg-orange-50"
|
||||
>
|
||||
수질기준 참고
|
||||
</button>
|
||||
|
||||
<WaterQualityRefModal
|
||||
open={openRef}
|
||||
onClose={() => setOpenRef(false)}
|
||||
imageSrc="/images/landing/water_quality_ref_1.png"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border border-neutral-200 bg-white">
|
||||
<QualityRow
|
||||
leftLabel="총대장균군수(개/mg)"
|
||||
leftRequired
|
||||
leftControl={
|
||||
<div className="flex items-center gap-4">
|
||||
<TextInput
|
||||
value={coliform}
|
||||
onChange={setColiform}
|
||||
disabled={noDetect}
|
||||
className={noDetect ? "bg-neutral-100 text-neutral-400" : ""}
|
||||
/>
|
||||
<label className="flex items-center gap-2 text-sm text-neutral-700">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={noDetect}
|
||||
onChange={(e) => {
|
||||
setNoDetect(e.target.checked);
|
||||
if (e.target.checked) setColiform("");
|
||||
}}
|
||||
className="h-4 w-4 rounded border-neutral-300"
|
||||
/>
|
||||
불검출
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
rightLabel="총질소(T-N)(mg/L)"
|
||||
rightControl={<InlineSuffixInput value={tn} onChange={setTn} suffix="이하" />}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
<QualityRow
|
||||
leftLabel="결합잔류염소(mg/L)"
|
||||
leftRequired
|
||||
leftControl={<TextInput value={residualCl} onChange={setResidualCl} />}
|
||||
rightLabel="총인(T-P)(mg/L)"
|
||||
rightControl={<InlineSuffixInput value={tp} onChange={setTp} suffix="이하" />}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
<QualityRow
|
||||
leftLabel="탁도(NTU)"
|
||||
leftRequired
|
||||
leftControl={<InlineSuffixInput value={turbidity} onChange={setTurbidity} suffix="이하" />}
|
||||
rightLabel="수소이온농도(pH)"
|
||||
rightControl={
|
||||
<div className="flex items-center gap-2">
|
||||
<TextInput value={ph.min} onChange={(v) => setPh((p) => ({ ...p, min: v }))} className="w-32" />
|
||||
<span className="text-neutral-500">~</span>
|
||||
<TextInput value={ph.max} onChange={(v) => setPh((p) => ({ ...p, max: v }))} className="w-32" />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
<QualityRow
|
||||
leftLabel={
|
||||
<>
|
||||
생물화학적산소요구량
|
||||
<br />
|
||||
(BOD)(mg/L)
|
||||
</>
|
||||
}
|
||||
leftRequired
|
||||
leftControl={<InlineSuffixInput value={bod} onChange={setBod} suffix="이하" />}
|
||||
rightLabel="염화물(mgCL/L)"
|
||||
rightControl={<InlineSuffixInput value={chloride} onChange={setChloride} suffix="이하" />}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
<QualityRow
|
||||
leftLabel="냄새"
|
||||
leftRequired
|
||||
leftControl={
|
||||
<select
|
||||
value={odor}
|
||||
onChange={(e) => setOdor(e.target.value)}
|
||||
className="h-11 w-full max-w-[200px] rounded-md border border-neutral-300 bg-white px-3 text-sm outline-none focus:border-neutral-500"
|
||||
>
|
||||
<option>불쾌하지 않을 것</option>
|
||||
<option>약간 있음</option>
|
||||
<option>있음</option>
|
||||
</select>
|
||||
}
|
||||
rightLabel="전기전도도(µs/cm)"
|
||||
rightControl={<InlineSuffixInput value={ec} onChange={setEc} suffix="이하" />}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
<QualityRow
|
||||
leftLabel="색도(도)"
|
||||
leftRequired
|
||||
leftControl={<InlineSuffixInput value={color} onChange={setColor} suffix="이하" />}
|
||||
rightLabel="비고"
|
||||
rightControl={
|
||||
<textarea
|
||||
value={remark}
|
||||
onChange={(e) => setRemark(e.target.value)}
|
||||
className="h-11 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm outline-none focus:border-neutral-500"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
<div className="grid grid-cols-12">
|
||||
<LabelCell>
|
||||
공급처 후보지 선택
|
||||
<br />
|
||||
(필수아님)
|
||||
</LabelCell>
|
||||
|
||||
<div className="col-span-12 md:col-span-10 p-6">
|
||||
<div className="flex flex-col gap-3 xl:flex-row xl:items-center xl:gap-4">
|
||||
<TextInput value={cand1} onChange={setCand1} className="max-w-[220px]" />
|
||||
<TextInput value={cand2} onChange={setCand2} className="max-w-[220px]" />
|
||||
<TextInput value={cand3} onChange={setCand3} className="max-w-[220px]" />
|
||||
|
||||
<div className="flex items-center gap-3 xl:ml-2">
|
||||
<button
|
||||
type="button"
|
||||
className="h-11 rounded-md bg-[#888] px-5 text-base text-white hover:bg-neutral-700"
|
||||
onClick={() => setOpenCand(true)}
|
||||
>
|
||||
후보지 조회
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="h-11 rounded-md bg-[#888] px-5 text-base text-white hover:bg-neutral-700"
|
||||
onClick={() => setOpenSimilar(true)}
|
||||
>
|
||||
유사사례 조회
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center py-12">
|
||||
<button
|
||||
type="button"
|
||||
className="h-11 rounded-md bg-[#0B4EA2] px-10 text-lg text-white hover:opacity-90"
|
||||
onClick={() => console.log("수요정보 등록")}
|
||||
>
|
||||
수요정보 등록
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* 후보지 조회 모달 */}
|
||||
<CandidateSiteModal
|
||||
open={openCand}
|
||||
onClose={() => setOpenCand(false)}
|
||||
initialSelected={selectedCands}
|
||||
onApply={(list) => {
|
||||
setSelectedCands(list);
|
||||
setCand1(list[0]?.name ?? "");
|
||||
setCand2(list[1]?.name ?? "");
|
||||
setCand3(list[2]?.name ?? "");
|
||||
setOpenCand(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 유사사례 모달 */}
|
||||
<SimilarCaseModal
|
||||
open={openSimilar}
|
||||
onClose={() => setOpenSimilar(false)}
|
||||
rows={rows}
|
||||
detailsByCaseId={detailsByCaseId}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -325,7 +549,7 @@ function LabelCell({
|
||||
required?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="col-span-12 md:col-span-2 flex items-center justify-center text-center bg-emerald-50/70 px-6 py-6 text-lg font-semibold text-neutral-800">
|
||||
<div className="col-span-12 md:col-span-2 flex items-center justify-center bg-emerald-50/70 px-6 py-6 text-center text-lg font-semibold text-neutral-800">
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{children}
|
||||
{required ? <span className="text-red-500">*</span> : null}
|
||||
@@ -366,23 +590,19 @@ function QualityRow({
|
||||
}) {
|
||||
return (
|
||||
<div className="grid grid-cols-12">
|
||||
{/* 좌측 라벨 */}
|
||||
<div className="col-span-12 lg:col-span-2 flex items-center justify-center text-center bg-emerald-50/70 px-6 py-6 text-lg font-semibold text-neutral-800">
|
||||
<div className="col-span-12 lg:col-span-2 flex items-center justify-center bg-emerald-50/70 px-6 py-6 text-center text-lg font-semibold text-neutral-800">
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<span className="leading-6">{leftLabel}</span>
|
||||
{leftRequired ? <span className="text-red-500">*</span> : null}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 좌측 입력 */}
|
||||
<div className="col-span-12 lg:col-span-4 p-6">{leftControl}</div>
|
||||
|
||||
{/* 우측 라벨 */}
|
||||
<div className="col-span-12 lg:col-span-2 flex items-center justify-center text-center bg-emerald-50/70 px-6 py-6 text-lg font-semibold text-neutral-800">
|
||||
<div className="col-span-12 lg:col-span-2 flex items-center justify-center bg-emerald-50/70 px-6 py-6 text-center text-lg font-semibold text-neutral-800">
|
||||
<span className="leading-6">{rightLabel}</span>
|
||||
</div>
|
||||
|
||||
{/* 우측 입력 */}
|
||||
<div className="col-span-12 lg:col-span-4 p-6">{rightControl}</div>
|
||||
</div>
|
||||
);
|
||||
@@ -441,23 +661,21 @@ function DateInput({
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
}) {
|
||||
|
||||
const ref = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const openPicker = () => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
|
||||
// Chrome/Edge 등 지원
|
||||
if (typeof (el as any).showPicker === "function") {
|
||||
(el as any).showPicker();
|
||||
return;
|
||||
}
|
||||
|
||||
// fallback (일부 브라우저)
|
||||
el.focus();
|
||||
el.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative w-full max-w-[200px]">
|
||||
<input
|
||||
@@ -467,7 +685,6 @@ function DateInput({
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className={[
|
||||
"h-11 w-full rounded-md border border-neutral-300 bg-white px-3 pr-10 text-sm outline-none focus:border-neutral-500",
|
||||
// ✅ 기본 달력 아이콘 숨김 (Tailwind arbitrary selector)
|
||||
"[&::-webkit-calendar-picker-indicator]:opacity-0",
|
||||
"[&::-webkit-calendar-picker-indicator]:absolute",
|
||||
"[&::-webkit-calendar-picker-indicator]:right-0",
|
||||
@@ -477,7 +694,6 @@ function DateInput({
|
||||
].join(" ")}
|
||||
/>
|
||||
|
||||
{/* ✅ 클릭 가능한 아이콘 버튼 */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={openPicker}
|
||||
@@ -485,9 +701,15 @@ function DateInput({
|
||||
aria-label="달력 열기"
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M7.14286 7.99104H16.7812M6.48512 2.5625V4.19126M17.3125 2.5625V4.19106M20.5 7.19106L20.5 18.5625C20.5 20.2194 19.1569 21.5625 17.5 21.5625H6.5C4.84315 21.5625 3.5 20.2194 3.5 18.5625V7.19106C3.5 5.53421 4.84315 4.19106 6.5 4.19106H17.5C19.1569 4.19106 20.5 5.53421 20.5 7.19106Z" stroke="#1A1A1A" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path
|
||||
d="M7.14286 7.99104H16.7812M6.48512 2.5625V4.19126M17.3125 2.5625V4.19106M20.5 7.19106L20.5 18.5625C20.5 20.2194 19.1569 21.5625 17.5 21.5625H6.5C4.84315 21.5625 3.5 20.2194 3.5 18.5625V7.19106C3.5 5.53421 4.84315 4.19106 6.5 4.19106H17.5C19.1569 4.19106 20.5 5.53421 20.5 7.19106Z"
|
||||
stroke="#1A1A1A"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user