Merge branch 'iipbs/dm/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/dm/dev
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import SubPageHeader from "../../_components/SubPageHeader";
|
||||
//import { useDmStore } from "@/app/store";
|
||||
|
||||
type WaterDetail = {
|
||||
toc: string;
|
||||
@@ -114,12 +115,17 @@ type PairField = { label: string; value: string };
|
||||
export default function DemandStatusPage() {
|
||||
const rows = useMemo(() => MOCK, []);
|
||||
|
||||
// ✅ rows가 빈 경우도 대비 (런타임 오류 방지)
|
||||
// rows가 빈 경우도 대비 (런타임 오류 방지)
|
||||
const [selectedId, setSelectedId] = useState<number | null>(rows[0]?.id ?? null);
|
||||
|
||||
// ✅ 상세영역 스크롤 이동용
|
||||
// 상세영역 스크롤 이동용
|
||||
const detailRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
// update
|
||||
/* const updateSdmInfoData = useDmStore(
|
||||
(state) => state.updateSdmInfoData
|
||||
); */
|
||||
|
||||
// ✅ selected 계산 안정화
|
||||
const selected = useMemo(() => {
|
||||
if (!rows.length) return null;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Gis from "@common/business_components/ui/map/Gis";
|
||||
import "@/app/commonOrigin.css";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { mapService } from "../service";
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
|
||||
@@ -13,9 +13,10 @@ import { useRouter } from "next/navigation";
|
||||
import SubPageHeader from "@/components/SubPageHeader";
|
||||
import WaterQualityRefModal from "../modal/WaterQualityRefModal";
|
||||
import CandidateSiteModal, { type CandidateSite } from "@/components/CandidateSiteModal";
|
||||
import { useDmStore} from "@/app/store";
|
||||
import { useDmStore } from "@/app/store";
|
||||
import type { SdmInfoFormData } from "@/app/store/sdmMng";
|
||||
import SimilarCaseModal, { type SimilarCaseDetail, type SimilarCaseRow } from "@/app/(landing)/reuse/apply/modal/SimilarCaseModal";
|
||||
import type { SubsInfo } from "@/app/store/map/type";
|
||||
|
||||
type PHRange = { min: string; max: string };
|
||||
|
||||
@@ -60,6 +61,7 @@ export default function DemandRegisterPage() {
|
||||
const [cand1, setCand1] = useState("");
|
||||
const [cand2, setCand2] = useState("");
|
||||
const [cand3, setCand3] = useState("");
|
||||
const [candidateList, setCandidateList] = useState<SubsInfo[]>([]);
|
||||
|
||||
const useTypesData = useDmStore((state) => state.Sdm.useTypesData);
|
||||
const getUseTypesData = useDmStore((state) => state.getUseTypesData);
|
||||
@@ -121,16 +123,38 @@ export default function DemandRegisterPage() {
|
||||
getUseTypesData();
|
||||
}, [getUseTypesData]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (event: MessageEvent) => {
|
||||
if (!event.data) return;
|
||||
|
||||
if (event.data.type === "SET_SUBS_INFO") {
|
||||
const list = event.data.payload || [];
|
||||
|
||||
// 최대 3개
|
||||
setCand1(list[0]?.ruseUtztnFcltNm || "");
|
||||
setCand2(list[1]?.ruseUtztnFcltNm || "");
|
||||
setCand3(list[2]?.ruseUtztnFcltNm || "");
|
||||
setCandidateList(list);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", handler);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("message", handler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// option
|
||||
// 'req':필수 / 'num' : 숫자 / 'num+[숫자] : 자릿수
|
||||
const validation = (val, text, option :string[]=[] ) => {
|
||||
if(option.includes('req')){
|
||||
if(val.trim() === "" || val ===undefined) {
|
||||
alert(`${text} 항목은 필수 입력값입니다.`);
|
||||
const validation = (val, text, option: string[] = []) => {
|
||||
if (option.includes('req')) {
|
||||
if (val.trim() === "" || val === undefined) {
|
||||
alert(`${text} 항목은 필수 입력값입니다.`);
|
||||
return false;
|
||||
}
|
||||
}else{
|
||||
if(val.trim()===""){
|
||||
} else {
|
||||
if (val.trim() === "") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -144,8 +168,7 @@ export default function DemandRegisterPage() {
|
||||
|
||||
if (!regex.test(val)) {
|
||||
alert(
|
||||
`${text}은(는) ${
|
||||
isDot ? "숫자 또는 소수" : "숫자"
|
||||
`${text}은(는) ${isDot ? "숫자 또는 소수" : "숫자"
|
||||
}만 입력 가능합니다.`
|
||||
);
|
||||
return false;
|
||||
@@ -230,29 +253,29 @@ export default function DemandRegisterPage() {
|
||||
});
|
||||
|
||||
// 데이터 저장
|
||||
const saveInfo = async() => {
|
||||
if(!validation(reqQty, '요구수량', ['req', 'numDot'])) return false;
|
||||
if(!validation(usePurpose, '사용용도', ['req'])) return false;
|
||||
if(!validation(hopePrice, '희망단가', ['req', 'numDot'])) return false;
|
||||
/* if(!noDetect && !validation(coliform, '총대장균군수', ['req','numDot'])) return false;
|
||||
if(!validation(residualCl, ['req', '결합잔류염소', 'numDot'])) return false;
|
||||
if(!validation(turbidity, '탁도', ['req', 'numDot'])) return false;
|
||||
if(!validation(bod, '생물화학적산소요구량', ['req', 'numDot'])) return false;
|
||||
if(!validation(odor, '냄새', ['req'])) return false; */
|
||||
|
||||
const saveInfo = async () => {
|
||||
if (!validation(reqQty, '요구수량', ['req', 'numDot'])) return false;
|
||||
if (!validation(usePurpose, '사용용도', ['req'])) return false;
|
||||
if (!validation(hopePrice, '희망단가', ['req', 'numDot'])) return false;
|
||||
/* if(!noDetect && !validation(coliform, '총대장균군수', ['req','numDot'])) return false;
|
||||
if(!validation(residualCl, ['req', '결합잔류염소', 'numDot'])) return false;
|
||||
if(!validation(turbidity, '탁도', ['req', 'numDot'])) return false;
|
||||
if(!validation(bod, '생물화학적산소요구량', ['req', 'numDot'])) return false;
|
||||
if(!validation(odor, '냄새', ['req'])) return false; */
|
||||
|
||||
// 필수는 아니지만 숫자로는 들어와야함.
|
||||
/* if(!validation(tn, '총질소', ['numDot'])) return false;
|
||||
if(!validation(residualCl, '결합잔류염소', ['numDot'])) return false;
|
||||
if(!validation(tp, '총인', ['numDot'])) return false;
|
||||
if(!validation(ph.min, 'ph', ['numDot'])) return false;
|
||||
if(!validation(ph.max, 'ph', ['numDot'])) return false;
|
||||
if(!validation(chloride, '염화물', ['numDot'])) return false;
|
||||
if(!validation(ec, '전기전도도', ['numDot'])) return false; */
|
||||
|
||||
/* if(!validation(tn, '총질소', ['numDot'])) return false;
|
||||
if(!validation(residualCl, '결합잔류염소', ['numDot'])) return false;
|
||||
if(!validation(tp, '총인', ['numDot'])) return false;
|
||||
if(!validation(ph.min, 'ph', ['numDot'])) return false;
|
||||
if(!validation(ph.max, 'ph', ['numDot'])) return false;
|
||||
if(!validation(chloride, '염화물', ['numDot'])) return false;
|
||||
if(!validation(ec, '전기전도도', ['numDot'])) return false; */
|
||||
|
||||
const userStr = sessionStorage.getItem("user");
|
||||
const user = userStr ? JSON.parse(userStr) : null;
|
||||
|
||||
if(user){
|
||||
if (user) {
|
||||
// 후보지 목록 (candsRef에 코드까지 보관됨)
|
||||
const splyCndtList = candsRef.current
|
||||
.filter((x) => x.name)
|
||||
@@ -261,25 +284,25 @@ export default function DemandRegisterPage() {
|
||||
ruseUtztnFcltCd: x.code,
|
||||
}));
|
||||
|
||||
const payload : SdmInfoFormData = {
|
||||
dmndTmng : new Date(reqDate),
|
||||
dmdInfoId : user.dmdInfoId,
|
||||
dmndQty : Number(reqQty),
|
||||
wntUntprc : Number(hopePrice),
|
||||
useUsgCd : usePurpose,
|
||||
const payload: SdmInfoFormData = {
|
||||
dmndTmng: new Date(reqDate),
|
||||
dmdInfoId: user.dmdInfoId,
|
||||
dmndQty: Number(reqQty),
|
||||
wntUntprc: Number(hopePrice),
|
||||
useUsgCd: usePurpose,
|
||||
// 수질
|
||||
grssClfg : Number(coliform),
|
||||
tn : Number(tn)||null,
|
||||
combResdch: Number(residualCl)||null,
|
||||
tph : Number(tp)||null,
|
||||
trbt : Number(turbidity),
|
||||
ph : ph.min||''+((ph.min||ph.max)?'~':'')+ph.max||'',
|
||||
blgcOxygDmndQnt : Number(bod),
|
||||
clIonQnt : Number(chloride)||null,
|
||||
smll : odor,
|
||||
cdtnv : Number(ec)||null,
|
||||
crmty : Number(color)||null,
|
||||
rmrk : remark,
|
||||
grssClfg: Number(coliform),
|
||||
tn: Number(tn) || null,
|
||||
combResdch: Number(residualCl) || null,
|
||||
tph: Number(tp) || null,
|
||||
trbt: Number(turbidity),
|
||||
ph: ph.min || '' + ((ph.min || ph.max) ? '~' : '') + ph.max || '',
|
||||
blgcOxygDmndQnt: Number(bod),
|
||||
clIonQnt: Number(chloride) || null,
|
||||
smll: odor,
|
||||
cdtnv: Number(ec) || null,
|
||||
crmty: Number(color) || null,
|
||||
rmrk: remark,
|
||||
splyCndtList, // 후보지 목록 추가
|
||||
}
|
||||
const success = await insertSdmInfoData(payload);
|
||||
@@ -344,13 +367,13 @@ export default function DemandRegisterPage() {
|
||||
const fncSetOptionValue = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const value = e.target.value;
|
||||
initQualityForm();
|
||||
|
||||
|
||||
switch (value) {
|
||||
// 청소화장실용도
|
||||
case '01':
|
||||
setTp('0.2');
|
||||
setTurbidity('2');
|
||||
setPh({min:'5.8', max:'8.5'});
|
||||
setPh({ min: '5.8', max: '8.5' });
|
||||
setBod('5');
|
||||
setColor('20');
|
||||
break;
|
||||
@@ -358,14 +381,14 @@ export default function DemandRegisterPage() {
|
||||
case '02':
|
||||
setTp('0.2');
|
||||
setTurbidity('2');
|
||||
setPh({min:'5.8', max:'8.5'});
|
||||
setPh({ min: '5.8', max: '8.5' });
|
||||
setBod('5');
|
||||
break;
|
||||
// 조경용도
|
||||
case '03':
|
||||
setTp('0.2');
|
||||
setTurbidity('2');
|
||||
setPh({min:'5.8', max:'8.5'});
|
||||
setPh({ min: '5.8', max: '8.5' });
|
||||
setBod('5');
|
||||
setChloride('250');
|
||||
break;
|
||||
@@ -373,318 +396,318 @@ export default function DemandRegisterPage() {
|
||||
case '05':
|
||||
break;
|
||||
// 농업용도
|
||||
case '06' :
|
||||
case '06':
|
||||
setTurbidity('5');
|
||||
setPh({min:'5.8', max:'8.5'});
|
||||
setPh({ min: '5.8', max: '8.5' });
|
||||
setBod('8');
|
||||
setEc('2000');
|
||||
setFarmText("이하 (** 간접:2000,직접:700 )");
|
||||
break;
|
||||
// 하천 등 유지보수
|
||||
case '07' :
|
||||
case '07':
|
||||
setColiform('1000');
|
||||
setTn('10');
|
||||
setTp('0.5');
|
||||
setPh({min:'5.8', max:'8.5'});
|
||||
setPh({ min: '5.8', max: '8.5' });
|
||||
setBod('5');
|
||||
break;
|
||||
// 공업
|
||||
case '08':
|
||||
setPh({min:'5.8', max:'8.5'});
|
||||
setPh({ min: '5.8', max: '8.5' });
|
||||
break;
|
||||
default:
|
||||
console.log('잘못된 선택입니다.');
|
||||
}
|
||||
|
||||
|
||||
setUsePurpose(value);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<main className="bg-white">
|
||||
|
||||
{/* 서브 타이틀 + 브레드크럼 */}
|
||||
<SubPageHeader
|
||||
title="수요정보등록"
|
||||
crumbs={[
|
||||
{ label: "Home", href: "/reuse" },
|
||||
{ label: "수요신청", href: "/reuse/apply" },
|
||||
{ label: "수요정보등록" },
|
||||
]}
|
||||
/>
|
||||
<main className="bg-white">
|
||||
|
||||
{/* 컨텐츠 영역 */}
|
||||
<div>
|
||||
{/* 서브 타이틀 + 브레드크럼 */}
|
||||
<SubPageHeader
|
||||
title="수요정보등록"
|
||||
crumbs={[
|
||||
{ label: "Home", href: "/reuse" },
|
||||
{ label: "수요신청", href: "/reuse/apply" },
|
||||
{ label: "수요정보등록" },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* 기본정보 테이블 */}
|
||||
<div className="border border-neutral-200 bg-white">
|
||||
<FormRow label="요구시기" required>
|
||||
<DateInput value={reqDate} onChange={setReqDate} />
|
||||
</FormRow>
|
||||
{/* 컨텐츠 영역 */}
|
||||
<div>
|
||||
|
||||
<Divider />
|
||||
{/* 기본정보 테이블 */}
|
||||
<div className="border border-neutral-200 bg-white">
|
||||
<FormRow label="요구시기" required>
|
||||
<DateInput value={reqDate} onChange={setReqDate} />
|
||||
</FormRow>
|
||||
|
||||
<FormRow label="요구수량(m²/d)" required>
|
||||
<TextInput value={reqQty} onChange={setReqQty} placeholder="" />
|
||||
</FormRow>
|
||||
<Divider />
|
||||
|
||||
<Divider />
|
||||
<FormRow label="요구수량(m²/d)" required>
|
||||
<TextInput value={reqQty} onChange={setReqQty} placeholder="" />
|
||||
</FormRow>
|
||||
|
||||
<FormRow label="사용용도" required>
|
||||
<select
|
||||
value={usePurpose}
|
||||
onChange={(e) => fncSetOptionValue(e)}
|
||||
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 value="">선택하세요</option>
|
||||
{useTypesData.map((item) => (
|
||||
<option key={item.val} value={item.val}>
|
||||
{item.text}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</FormRow>
|
||||
<Divider />
|
||||
|
||||
<Divider />
|
||||
<FormRow label="사용용도" required>
|
||||
<select
|
||||
value={usePurpose}
|
||||
onChange={(e) => fncSetOptionValue(e)}
|
||||
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 value="">선택하세요</option>
|
||||
{useTypesData.map((item) => (
|
||||
<option key={item.val} value={item.val}>
|
||||
{item.text}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</FormRow>
|
||||
|
||||
<FormRow label="희망단가(원)" required>
|
||||
<TextInput value={hopePrice} onChange={setHopePrice} />
|
||||
</FormRow>
|
||||
</div>
|
||||
<Divider />
|
||||
|
||||
{/* 요구수질 섹션 타이틀 바 */}
|
||||
<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>
|
||||
<FormRow label="희망단가(원)" required>
|
||||
<TextInput value={hopePrice} onChange={setHopePrice} />
|
||||
</FormRow>
|
||||
</div>
|
||||
|
||||
{/* 요구수질 테이블 */}
|
||||
<div className="border border-neutral-200 bg-white">
|
||||
{/* 1행 */}
|
||||
<QualityRow
|
||||
leftLabel="총대장균군수(개/mg)"
|
||||
|
||||
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={farmText} />}
|
||||
/>
|
||||
<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={(e) => {
|
||||
searchCandidate(e);
|
||||
|
||||
//splyList => 해당객체가 대민에서 선택한 후보처 리스트
|
||||
//setLastSub => 해당 스테이트가 선택한 마지막 후보처를 넣어야하는 스테이트
|
||||
}}>
|
||||
후보지 조회
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="h-11 rounded-md bg-[#888] px-5 text-base text-white hover:bg-neutral-600"
|
||||
onClick={(e) => {
|
||||
// TODO: 유사사례 연결
|
||||
searchSimilarCase(e);
|
||||
}}
|
||||
>
|
||||
유사사례
|
||||
</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: 저장 처리
|
||||
saveInfo();
|
||||
}}
|
||||
>
|
||||
수요정보 등록
|
||||
</button>
|
||||
</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>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<SimilarCaseModal
|
||||
open={openSimilar}
|
||||
onClose={() => setOpenSimilar(false)}
|
||||
rows={rows} // 실제 데이터
|
||||
detailsByCaseId={detailsByCaseId}
|
||||
onSearch={async (criterion) => {
|
||||
const code = getCriterionCode(criterion);
|
||||
if (!code) return;
|
||||
// 값은 상황에 맞게 (예시: usePurpose)
|
||||
await getSimilarCaseList(code, code=='S'?reqQty:code=='U'?usePurpose:'');
|
||||
}}
|
||||
/>
|
||||
{/* 요구수질 테이블 */}
|
||||
<div className="border border-neutral-200 bg-white">
|
||||
{/* 1행 */}
|
||||
<QualityRow
|
||||
leftLabel="총대장균군수(개/mg)"
|
||||
|
||||
<CandidateSiteModal
|
||||
open={openCand}
|
||||
onClose={() => setOpenCand(false)}
|
||||
initialSelected={selectedCands}
|
||||
onApply={(list) => {
|
||||
setSelectedCands(list);
|
||||
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 />
|
||||
|
||||
// ✅ cand1~3 입력칸에 반영(시설명만 넣는 버전)
|
||||
setCand1(list[0]?.name ?? "");
|
||||
setCand2(list[1]?.name ?? "");
|
||||
setCand3(list[2]?.name ?? "");
|
||||
{/* 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 />
|
||||
|
||||
setOpenCand(false);
|
||||
}}
|
||||
/>
|
||||
{/* 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={farmText} />}
|
||||
/>
|
||||
<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={(e) => {
|
||||
searchCandidate(e);
|
||||
|
||||
//splyList => 해당객체가 대민에서 선택한 후보처 리스트
|
||||
//setLastSub => 해당 스테이트가 선택한 마지막 후보처를 넣어야하는 스테이트
|
||||
}}>
|
||||
후보지 조회
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="h-11 rounded-md bg-[#888] px-5 text-base text-white hover:bg-neutral-600"
|
||||
onClick={(e) => {
|
||||
// TODO: 유사사례 연결
|
||||
searchSimilarCase(e);
|
||||
}}
|
||||
>
|
||||
유사사례
|
||||
</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: 저장 처리
|
||||
saveInfo();
|
||||
}}
|
||||
>
|
||||
수요정보 등록
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<SimilarCaseModal
|
||||
open={openSimilar}
|
||||
onClose={() => setOpenSimilar(false)}
|
||||
rows={rows} // 실제 데이터
|
||||
detailsByCaseId={detailsByCaseId}
|
||||
onSearch={async (criterion) => {
|
||||
const code = getCriterionCode(criterion);
|
||||
if (!code) return;
|
||||
// 값은 상황에 맞게 (예시: usePurpose)
|
||||
await getSimilarCaseList(code, code == 'S' ? reqQty : code == 'U' ? usePurpose : '');
|
||||
}}
|
||||
/>
|
||||
|
||||
<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);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -819,7 +842,7 @@ function DateInput({
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
}) {
|
||||
|
||||
|
||||
const ref = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const openPicker = () => {
|
||||
@@ -863,7 +886,7 @@ 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>
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
@import "../../../packages/common/components/src/base.css";
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
--radius: 0.5rem
|
||||
}
|
||||
.dark {
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "NotoSansKR";
|
||||
src: url("/fonts/NotoSansKR-Regular.woff2") format("woff2");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "NotoSansKR", sans-serif;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ const serverApiUrl = process.env.NEXT_PUBLIC_CS_SERVER_API_URL;
|
||||
const GEOSERVER_URL = process.env.NEXT_PUBLIC_GEOSERVER_URL || "";
|
||||
const LX_GEOSERVER_URL = process.env.NEXT_PUBLIC_LX_GEOSERVER_URL || "";
|
||||
const GATEWAY_URL = process.env.NEXT_PUBLIC_GATEWAY_URL || "";
|
||||
const SEWERAGE_API_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || "";
|
||||
const BUSINESS_JUSO_API_URL = process.env.NEXT_PUBLIC_FINDADRESS_URL || "";
|
||||
const AdressKey = process.env.NEXT_PRIVATE_WR_ADDRESS_KEY || "";
|
||||
|
||||
@@ -72,6 +73,8 @@ export function middleware(req: NextRequest) {
|
||||
if (pathname.startsWith("/geoserver")) {
|
||||
let queryParams = searchParams.toString();
|
||||
const typename = searchParams.get("TYPENAME");
|
||||
const startYmd = searchParams.get("START_YMD") ?? null;
|
||||
const endYmd = searchParams.get("END_YMD") ?? null;
|
||||
console.log("typename ", typename);
|
||||
|
||||
let newUrl = `${GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`;
|
||||
@@ -102,6 +105,8 @@ export function middleware(req: NextRequest) {
|
||||
) {
|
||||
const url = req.nextUrl.clone();
|
||||
url.searchParams.set("TYPENAME", typename.replace("hasudo:", ""));
|
||||
if (startYmd) url.searchParams.set("startYmd", startYmd);
|
||||
if (endYmd) url.searchParams.set("endYmd", endYmd);
|
||||
queryParams = url.searchParams.toString();
|
||||
newUrl = `${GATEWAY_URL}/sewerage-service/api/v1/map/geometry${queryParams ? `?${queryParams}` : ""}`;
|
||||
}
|
||||
|
||||
@@ -29,8 +29,8 @@
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@types/webpack": "^5.28.5",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.1.7",
|
||||
"postcss": "^8",
|
||||
"eslint-config-next": "15.3.8",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user