Merge branch 'iipbs/dm/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/dm/dev

This commit is contained in:
2026-04-01 17:37:46 +09:00
190 changed files with 16142 additions and 6886 deletions
@@ -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>
+82
View File
@@ -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;
}
+5
View File
@@ -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}` : ""}`;
}
+2 -2
View File
@@ -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"
}