수요정보등록 validation 제거(임시), 수요신청현황 수정버튼 임시비활성화

This commit is contained in:
jsyang
2026-03-31 20:18:45 +09:00
parent e566bc087b
commit fc03a19da1
2 changed files with 327 additions and 42 deletions
@@ -189,20 +189,20 @@ export default function DemandRegisterPage() {
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(!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(odor, '냄새', ['req'])) return false; */
// 필수는 아니지만 숫자로는 들어와야함.
if(!validation(tn, '총질소', ['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;
if(!validation(ec, '전기전도도', ['numDot'])) return false; */
const userStr = sessionStorage.getItem("user");
const user = userStr ? JSON.parse(userStr) : null;
@@ -421,7 +421,7 @@ export default function DemandRegisterPage() {
{/* 1행 */}
<QualityRow
leftLabel="총대장균군수(개/mg)"
leftRequired
leftControl={
<div className="flex items-center gap-4">
<TextInput
@@ -454,7 +454,7 @@ export default function DemandRegisterPage() {
{/* 2행 */}
<QualityRow
leftLabel="결합잔류염소(mg/L)"
leftRequired
/* leftRequired */
leftControl={<TextInput value={residualCl} onChange={setResidualCl} />}
rightLabel="총인(T-P)(mg/L)"
rightControl={
@@ -466,7 +466,7 @@ export default function DemandRegisterPage() {
{/* 3행 */}
<QualityRow
leftLabel="탁도(NTU)"
leftRequired
/* leftRequired */
leftControl={
<InlineSuffixInput value={turbidity} onChange={setTurbidity} suffix="이하" />
}
@@ -498,7 +498,7 @@ export default function DemandRegisterPage() {
(BOD)(mg/L)
</>
}
leftRequired
/* leftRequired */
leftControl={<InlineSuffixInput value={bod} onChange={setBod} suffix="이하" />}
rightLabel="염화물(mgCL/L)"
rightControl={<InlineSuffixInput value={chloride} onChange={setChloride} suffix="이하" />}
@@ -508,7 +508,7 @@ export default function DemandRegisterPage() {
{/* 5행 */}
<QualityRow
leftLabel="냄새"
leftRequired
/* leftRequired */
leftControl={
<select
value={odor}
@@ -528,7 +528,7 @@ export default function DemandRegisterPage() {
{/* 6행 */}
<QualityRow
leftLabel="색도(도)"
leftRequired
/* leftRequired */
leftControl={<InlineSuffixInput value={color} onChange={setColor} suffix="이하" />}
rightLabel="비고"
rightControl={
@@ -4,9 +4,11 @@ import { useEffect, useMemo, useRef, useState } from "react";
import SubPageHeader from "@/components/SubPageHeader";
import { useDmStore } from "@/app/store";
import type { DemandCompBizType } from "@/app/store/sdmMng";
import { Textarea } from "@common/components/ui";
import type { SelectBox } from "@common/business_components/ui";
type TopField = { label: string; value: string; required?: boolean };
type PairField = { label: string; value: string };
type TopField = { label: string; key: string, value: string; required?: boolean };
type PairField = { label: string; key: string, value: string };
type SessionUser = { dmdInfoId?: string };
const formatDate = (value: string | Date | "" | undefined) => {
@@ -35,6 +37,93 @@ function StatusBadge({ v }: { v: string }) {
return <span className={["font-medium", cls].join(" ")}>{v || "-"}</span>;
}
function TextInput({
value,
onChange,
placeholder,
disabled,
className,
}: {
value: string;
onChange: (v: string) => void;
placeholder?: string;
disabled?: boolean;
className?: string;
}) {
return (
<input
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
disabled={disabled}
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",
disabled ? "cursor-not-allowed" : "",
className ?? "",
].join(" ")}
/>
);
}
function DateInput({
value,
onChange,
}: {
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
ref={ref}
type="date"
value={value}
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",
"[&::-webkit-calendar-picker-indicator]:w-10",
"[&::-webkit-calendar-picker-indicator]:h-full",
"[&::-webkit-calendar-picker-indicator]:cursor-pointer",
].join(" ")}
/>
{/* ✅ 클릭 가능한 아이콘 버튼 */}
<button
type="button"
onClick={openPicker}
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 text-neutral-500 hover:text-neutral-700"
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" />
</svg>
</button>
</div>
);
}
export default function DemandStatusPage() {
const { getSdmInfoStatusList, Sdm, Common } = useDmStore((state) => state);
const { successLogin, sdmInfoStatusList } = Sdm;
@@ -46,6 +135,16 @@ export default function DemandStatusPage() {
const [selectedDmdInfoSn, setSelectedDmdInfoSn] = useState<number | "" | null>(null);
const [transformedSdmInfoStatusList, setTransformedSdmInfoStatusList] = useState<DemandCompBizType[]>([]);
// 수정 유무
const [ isEditMode, setIsEditMode ] = useState(false);
// 수정용 (UI에서 바뀌는 값)
const [editData, setEditData] = useState<any>(null);
// 원본 백업 (취소용)
const [originData, setOriginData] = useState<any>(null);
// 사용용도
const useTypesData = useDmStore((state) => state.Sdm.useTypesData);
const getUseTypesData = useDmStore((state) => state.getUseTypesData);
useEffect(() => {
const user = sessionStorage.getItem("user");
if (user) {
@@ -53,6 +152,10 @@ export default function DemandStatusPage() {
}
}, []);
useEffect(() => {
getUseTypesData();
}, [getUseTypesData]);
useEffect(() => {
if (!successLogin && !isLoginStorageInfo) return;
@@ -112,10 +215,10 @@ export default function DemandStatusPage() {
if (!selectedSdmInfoStatus) return [];
return [
{ label: "요구시기", value: formatDate(selectedSdmInfoStatus.dmndTmng), required: true },
{ label: "요구수량(㎥/d)", value: formatValue(selectedSdmInfoStatus.dmndQty) },
{ label: "사용용도", value: formatValue(selectedSdmInfoStatus.useUsgCd) },
{ label: "희망단가(원/㎥)", value: formatValue(selectedSdmInfoStatus.wntUntprc) },
{ label: "요구시기", key: "dmndTmng", value: formatDate(selectedSdmInfoStatus.dmndTmng), required: true },
{ label: "요구수량(㎥/d)", key: "dmndQty", value: formatValue(selectedSdmInfoStatus.dmndQty) },
{ label: "사용용도", key: "useUsgCd", value: formatValue(selectedSdmInfoStatus.useUsgCd) },
{ label: "희망단가(원/㎥)", key: "wntUntprc", value: formatValue(selectedSdmInfoStatus.wntUntprc) },
];
}, [selectedSdmInfoStatus]);
@@ -123,21 +226,21 @@ export default function DemandStatusPage() {
if (!selectedSdmInfoStatus) return [];
return [
{ label: "총대장균군수(개/100mL)", value: formatBelow(selectedSdmInfoStatus.grssClfg) },
{ label: "결합잔류염소(mg/L)", value: formatBelow(selectedSdmInfoStatus.combResdch) },
{ label: "탁도(NTU)", value: formatBelow(selectedSdmInfoStatus.trbt) },
{ label: "총대장균군수(개/100mL)", key: "grssClfg", value: formatBelow(selectedSdmInfoStatus.grssClfg) },
{ label: "결합잔류염소(mg/L)", key:"combResdch", value: formatBelow(selectedSdmInfoStatus.combResdch) },
{ label: "탁도(NTU)", key:"trbt", value: formatBelow(selectedSdmInfoStatus.trbt) },
{ label: "생물학적산소요구량(BOD)(mg/L)", value: formatBelow(selectedSdmInfoStatus.blgcOxygDmndQnt) },
{ label: "냄새", value: formatValue(selectedSdmInfoStatus.smll) },
{ label: "색도(도)", value: formatBelow(selectedSdmInfoStatus.crmty) },
{ label: "생물학적산소요구량(BOD)(mg/L)", key:"blgcOxygDmndQnt", value: formatBelow(selectedSdmInfoStatus.blgcOxygDmndQnt) },
{ label: "냄새", key:"smll", value: formatValue(selectedSdmInfoStatus.smll) },
{ label: "색도(도)", key:"crmty", value: formatBelow(selectedSdmInfoStatus.crmty) },
{ label: "총질소(T-N)(mg/L)", value: formatBelow(selectedSdmInfoStatus.tn) },
{ label: "총인(T-P)(mg/L)", value: formatBelow(selectedSdmInfoStatus.tph) },
{ label: "수소이온농도(pH)", value: formatValue(selectedSdmInfoStatus.ph) },
{ label: "총질소(T-N)(mg/L)", key:"tn", value: formatBelow(selectedSdmInfoStatus.tn) },
{ label: "총인(T-P)(mg/L)", key:"tph", value: formatBelow(selectedSdmInfoStatus.tph) },
{ label: "수소이온농도(pH)", key:"ph", value: formatValue(selectedSdmInfoStatus.ph) },
{ label: "염화물(mgCL/L)", value: formatBelow(selectedSdmInfoStatus.clIonQnt) },
{ label: "전기전도도(μS/cm)", value: formatBelow(selectedSdmInfoStatus.cdtnv) },
{ label: "비고", value: formatValue(selectedSdmInfoStatus.rmrk) },
{ label: "염화물(mgCL/L)", key:"clIonQnt", value: formatBelow(selectedSdmInfoStatus.clIonQnt) },
{ label: "전기전도도(μS/cm)", key:"cdtnv", value: formatBelow(selectedSdmInfoStatus.cdtnv) },
{ label: "비고", key:"rmrk", value: formatValue(selectedSdmInfoStatus.rmrk) },
];
}, [selectedSdmInfoStatus]);
@@ -151,13 +254,25 @@ export default function DemandStatusPage() {
}, [selectedSdmInfoStatus]);
const onSelectSdmInfoStatus = (dmdInfoSn: number | "") => {
if(isEditMode){
if(confirm('저장되지 않은 데이터는 사라집니다.\n진행하시겠습니까?')){
setIsEditMode(false);
}
}
setSelectedDmdInfoSn(dmdInfoSn);
requestAnimationFrame(() => {
detailRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
});
};
// ph 전후로 나눔
const getRange = (val: string) => {
let a = val.split('~')[0]||''; let b = val.split('~')[1]||'';
return { a, b };
};
return (
<main className="bg-white">
<SubPageHeader
title="수요신청현황"
@@ -240,12 +355,44 @@ export default function DemandStatusPage() {
<section className="mt-10 pb-16" ref={detailRef}>
<div className="flex justify-between items-center">
<div className="text-2xl font-bold text-slate-700">수요신청 상세현황</div>
<button
type="button"
className="h-8 rounded-md bg-[#888] px-4 text-base text-white hover:bg-neutral-700"
>
수정
</button>
{/* {
!isEditMode ? (
<button
type="button"
className="h-8 rounded-md bg-[#888] px-4 text-base text-white hover:bg-neutral-700"
onClick={() => {
setOriginData(selectedSdmInfoStatus);
setEditData({...selectedSdmInfoStatus});
setIsEditMode(true)}
}
>
수정
</button>
) : (
<div className="flex gap-2">
<button
type="button"
className="h-8 rounded-md bg-[#888] px-4 text-base text-white hover:bg-neutral-700"
onClick={() => {
setOriginData(editData);
setIsEditMode(false)}
}
>
저장
</button>
<button
type="button"
className="h-8 rounded-md bg-[#888] px-4 text-base text-white hover:bg-neutral-700"
onClick={() => {
setEditData(originData);
setIsEditMode(false)
}}
>
취소
</button>
</div>
)
} */}
</div>
{!selectedSdmInfoStatus ? (
@@ -268,9 +415,47 @@ export default function DemandStatusPage() {
{field.label}
{field.required ? <span className="ml-1 text-red-500">*</span> : null}
</div>
<div className="col-span-8 px-4 py-4 flex items-center justify-center text-center">
{field.value}
</div>
{
!isEditMode ?
<div className="col-span-8 px-4 py-4 flex items-center justify-center text-center">
{field.key==='useUsgCd'? useTypesData.filter(item=>item.val===field.value)[0]?.text: field.value}
</div>
: <div className="col-span-8 px-4 py-4 flex items-center justify-center text-center">
{field.key ==='dmndTmng'?
<DateInput value={editData?.[field.key] ?? ""} onChange={(v) =>
setEditData((prev) => ({
...prev,
[field.key]: v,
}))
} />
: field.key ==='useUsgCd'?
<select
value={editData?.[field.key] ?? ""}
onChange={(e) =>
setEditData((prev) => ({
...prev,
[field.key]: 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 value="">선택하세요</option>
{useTypesData.map((item) => (
<option key={item.val} value={item.val}>
{item.text}
</option>
))}
</select>
: <TextInput value={editData?.[field.key] ?? ""} onChange={(v) =>
setEditData((prev) => ({
...prev,
[field.key]: v,
}))
}></TextInput>
}
</div>
}
</div>
))}
@@ -278,19 +463,119 @@ export default function DemandStatusPage() {
요구수질
</div>
{waterFields.map((field) => (
{waterFields.map((field) => {
const { a, b } = editData?.ph ? getRange(editData.ph) : { a: "", b: "" };
return (
<div
key={field.label}
className="col-span-12 md:col-span-4 grid grid-cols-12 border-b border-neutral-200"
key={field.label}
className="col-span-12 md:col-span-4 grid grid-cols-12 border-b border-neutral-200"
>
<div className="col-span-5 bg-sky-50 px-3 py-4 flex items-center justify-center text-center font-semibold text-slate-700">
{field.label}
</div>
<div className="col-span-7 px-3 py-4 flex items-center justify-center text-center">
{field.value}
{
!isEditMode ? field.value
:
field.key === "grssClfg" ? (
<div className="flex items-center gap-4">
<TextInput
value={
editData?.grssClfg === -1
? ""
: editData?.grssClfg ?? ""
}
onChange={(v) =>
setEditData((prev) => ({
...prev,
grssClfg: v,
}))
}
disabled={editData?.grssClfg === -1}
className={
editData?.grssClfg === -1
? "bg-neutral-100 text-neutral-400"
: ""
}
/>
<label className="flex items-center gap-2 w-3/4 text-sm text-neutral-700">
<input
type="checkbox"
checked={editData?.grssClfg === -1}
onChange={(e) => {
const checked = e.target.checked;
setEditData((prev) => ({
...prev,
grssClfg: checked ? -1 : "",
}));
}}
className="h-4 w-4 rounded border-neutral-300"
/>
불검출
</label>
</div>
) : field.key === 'smll' ? (
<select
value={
editData?.smll === -1
? ""
: editData?.smll ?? ""
}
onChange={(v) =>
setEditData((prev) => ({
...prev,
grssClfg: v,
}))
}
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>
) : field.key === 'ph' ? (
<>
<TextInput
value={a}
onChange={(v) =>
setEditData((prev) => ({
...prev,
[field.key]: `${v}~${b}`,
}))
}
/>
~
<TextInput
value={b}
onChange={(v) =>
setEditData((prev) => ({
...prev,
[field.key]: `${a}~${v}`,
}))
}
/>
</>
) : (
<>
<TextInput
value={editData?.[field.key] ?? ""}
onChange={(v) =>
setEditData((prev) => ({
...prev,
[field.key]: v,
}))
}
/>
{
field.key==='tn'||field.key === 'tph'||field.key === 'trbt'||field.key==='blgcOxygDmndQnt'||field.key==='clIonQnt'||field.key==='cdtnv'|| field.key=='crmty'
?<>이하</>:<></>
}
</>
)
}
</div>
</div>
))}
)})}
<div className="col-span-12 bg-sky-50 px-4 py-3 text-sm font-semibold text-slate-700 border-b border-neutral-200">
공급처 후보지