370 lines
14 KiB
TypeScript
370 lines
14 KiB
TypeScript
// apps/dm/app/(landing)/reuse/apply/status/page.tsx
|
||
"use client";
|
||
|
||
import { useEffect, useMemo, useRef, useState } from "react";
|
||
import SubPageHeader from "../../_components/SubPageHeader";
|
||
|
||
type WaterDetail = {
|
||
toc: string;
|
||
bdo: string;
|
||
tn: string;
|
||
tp: string;
|
||
cl: string;
|
||
conductivity: string;
|
||
coliform: string;
|
||
turbidity: string;
|
||
odor: string;
|
||
color: string;
|
||
ph: string;
|
||
note: string;
|
||
};
|
||
|
||
type Row = {
|
||
id: number;
|
||
sido: string;
|
||
sigungu: string;
|
||
company: string;
|
||
address: string;
|
||
phone: string;
|
||
date: string;
|
||
status: "진행중" | "진행완료";
|
||
detail: {
|
||
reqDate: string;
|
||
reqQty: string;
|
||
usePurpose: string;
|
||
price: string;
|
||
water: WaterDetail;
|
||
candidates: [string, string, string];
|
||
};
|
||
};
|
||
|
||
const MOCK: Row[] = [
|
||
{
|
||
id: 1,
|
||
sido: "서울특별시",
|
||
sigungu: "금천구",
|
||
company: "서울워터솔루션(주)",
|
||
address: "서울특별시 금천구 가산디지털2로 119, 24층(가산동)",
|
||
phone: "010-0000-0000",
|
||
date: "2025-01-14",
|
||
status: "진행중",
|
||
detail: {
|
||
reqDate: "2025-02",
|
||
reqQty: "1,200",
|
||
usePurpose: "공업용수",
|
||
price: "350",
|
||
water: {
|
||
toc: "4",
|
||
bdo: "3",
|
||
tn: "10",
|
||
tp: "1.0",
|
||
cl: "120",
|
||
conductivity: "800",
|
||
coliform: "0",
|
||
turbidity: "2",
|
||
odor: "무",
|
||
color: "무",
|
||
ph: "7.2",
|
||
note: "-",
|
||
},
|
||
candidates: ["후보지1", "후보지2", "후보지3"],
|
||
},
|
||
},
|
||
{
|
||
id: 2,
|
||
sido: "제주특별자치도",
|
||
sigungu: "서귀포",
|
||
company: "회사명이 길어도괜찮지",
|
||
address: "제주특별자치도 서귀포시 천제연로 156, 3층 2호(색달동 1077호)",
|
||
phone: "010-0000-0000",
|
||
date: "2025-01-14",
|
||
status: "진행완료",
|
||
detail: {
|
||
reqDate: "2025-03",
|
||
reqQty: "800",
|
||
usePurpose: "조경용수",
|
||
price: "300",
|
||
water: {
|
||
toc: "5",
|
||
bdo: "4",
|
||
tn: "12",
|
||
tp: "1.2",
|
||
cl: "140",
|
||
conductivity: "900",
|
||
coliform: "0",
|
||
turbidity: "3",
|
||
odor: "무",
|
||
color: "무",
|
||
ph: "7.0",
|
||
note: "현장 협의",
|
||
},
|
||
candidates: ["후보지1", "후보지2", "후보지3"],
|
||
},
|
||
},
|
||
];
|
||
|
||
function StatusBadge({ v }: { v: Row["status"] }) {
|
||
const cls = v === "진행중" ? "text-red-600" : "text-lime-600";
|
||
return <span className={["font-medium", cls].join(" ")}>{v}</span>;
|
||
}
|
||
|
||
type TopField = { label: string; value: string; required?: boolean };
|
||
type PairField = { label: string; value: string };
|
||
|
||
export default function DemandStatusPage() {
|
||
const rows = useMemo(() => MOCK, []);
|
||
|
||
// ✅ rows가 빈 경우도 대비 (런타임 오류 방지)
|
||
const [selectedId, setSelectedId] = useState<number | null>(rows[0]?.id ?? null);
|
||
|
||
// ✅ 상세영역 스크롤 이동용
|
||
const detailRef = useRef<HTMLDivElement | null>(null);
|
||
|
||
// ✅ selected 계산 안정화
|
||
const selected = useMemo(() => {
|
||
if (!rows.length) return null;
|
||
const found = rows.find((r) => r.id === selectedId);
|
||
return found ?? rows[0];
|
||
}, [rows, selectedId]);
|
||
|
||
// ✅ rows가 바뀌었는데 selectedId가 null이면 자동 첫번째 선택
|
||
useEffect(() => {
|
||
const first = rows[0];
|
||
if (!first) return; // ✅ rows가 비어있으면 종료 (TS + 런타임 안전)
|
||
|
||
if (selectedId == null) setSelectedId(first.id);
|
||
}, [rows, selectedId]);
|
||
|
||
// ✅ 상단 2행(요구시기/요구수량/사용용도/희망단가) - TS 안전하게 object로 구성
|
||
const topFields: TopField[] = useMemo(() => {
|
||
if (!selected) return [];
|
||
return [
|
||
{ label: "요구시기", value: selected.detail.reqDate, required: true },
|
||
{ label: "요구수량(㎡/d)", value: selected.detail.reqQty },
|
||
{ label: "사용용도", value: selected.detail.usePurpose },
|
||
{ label: "희망단가(원)", value: selected.detail.price },
|
||
];
|
||
}, [selected]);
|
||
|
||
// ✅ 수질 항목 - TS 안전
|
||
const waterFields: PairField[] = useMemo(() => {
|
||
if (!selected) return [];
|
||
const w = selected.detail.water;
|
||
return [
|
||
{ label: "총대장균군수(개/100mℓ)", value: w.coliform },
|
||
{ label: "결합잔류염소(mg/L)", value: "-" },
|
||
{ label: "탁도(NTU)", value: w.turbidity },
|
||
|
||
{ label: "생물화학적산소요구량(BOD)(mg/L)", value: w.bdo },
|
||
{ label: "냄새", value: w.odor },
|
||
{ label: "색도(도)", value: w.color },
|
||
|
||
{ label: "총질소(T-N)(mg/L)", value: w.tn },
|
||
{ label: "총인(T-P)(mg/L)", value: w.tp },
|
||
{ label: "수소이온농도(pH)", value: w.ph },
|
||
|
||
{ label: "염화물(mgCL/L)", value: w.cl },
|
||
{ label: "전기전도도(µs/cm)", value: w.conductivity },
|
||
{ label: "비고", value: w.note },
|
||
];
|
||
}, [selected]);
|
||
|
||
const onSelectRow = (id: number) => {
|
||
setSelectedId(id);
|
||
|
||
// ✅ “행 클릭 → 아래 상세로 보여짐” 체감 확실히: 상세영역으로 이동
|
||
// (사용자가 이미 아래를 보고 있다면 거의 움직이지 않음)
|
||
requestAnimationFrame(() => {
|
||
detailRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||
});
|
||
};
|
||
|
||
return (
|
||
<main className="bg-white">
|
||
{/* 서브 타이틀 + 브레드크럼 */}
|
||
<SubPageHeader
|
||
title="수요신청현황"
|
||
crumbs={[
|
||
{ label: "Home", href: "/reuse" },
|
||
{ label: "수요신청", href: "/reuse/apply" },
|
||
{ label: "수요신청현황" },
|
||
]}
|
||
hideFooterLine
|
||
hideGuideText
|
||
/>
|
||
|
||
{/* 컨텐츠 영역 */}
|
||
<div>
|
||
{/* 리스트 테이블 */}
|
||
<section className="mt-6">
|
||
<div className="text-2xl font-bold text-slate-700">수요정보 신청현황</div>
|
||
|
||
<div className="mt-3 border border-neutral-200">
|
||
<table className="w-full table-fixed">
|
||
<thead className="bg-[#E7F4E2]">
|
||
<tr className="text-sm text-slate-700">
|
||
<th className="w-[60px] px-3 py-3 text-center">NO.</th>
|
||
<th className="w-[120px] px-3 py-3 text-center">시도</th>
|
||
<th className="w-[120px] px-3 py-3 text-center">시군구</th>
|
||
<th className="w-[180px] px-3 py-3 text-center">회사명</th>
|
||
<th className="px-3 py-3 text-center">공장주소</th>
|
||
<th className="w-[140px] px-3 py-3 text-center">전화번호</th>
|
||
<th className="w-[120px] px-3 py-3 text-center">신청일자</th>
|
||
<th className="w-[110px] px-3 py-3 text-center">진행상태</th>
|
||
</tr>
|
||
</thead>
|
||
|
||
<tbody className="bg-white text-base text-slate-700">
|
||
{rows.length === 0 ? (
|
||
<tr>
|
||
<td colSpan={8} className="px-3 py-10 text-center text-slate-400">
|
||
조회된 신청내역이 없습니다.
|
||
</td>
|
||
</tr>
|
||
) : (
|
||
rows.map((r) => {
|
||
const active = r.id === selected?.id;
|
||
return (
|
||
<tr
|
||
key={r.id}
|
||
role="row"
|
||
tabIndex={0}
|
||
onClick={() => onSelectRow(r.id)}
|
||
onKeyDown={(e) => {
|
||
if (e.key === "Enter" || e.key === " ") onSelectRow(r.id);
|
||
}}
|
||
className={[
|
||
"border-t border-neutral-200 outline-none",
|
||
"cursor-pointer",
|
||
active ? "bg-slate-50" : "hover:bg-slate-50",
|
||
"focus-visible:ring-2 focus-visible:ring-slate-300",
|
||
].join(" ")}
|
||
aria-selected={active}
|
||
>
|
||
<td className="px-3 py-3 text-center">{r.id}</td>
|
||
<td className="px-3 py-3 text-center">{r.sido}</td>
|
||
<td className="px-3 py-3 text-center">{r.sigungu}</td>
|
||
<td className="px-3 py-3 text-center">{r.company}</td>
|
||
<td className="px-3 py-3 text-center">{r.address}</td>
|
||
<td className="px-3 py-3 text-center">{r.phone}</td>
|
||
<td className="px-3 py-3 text-center">{r.date}</td>
|
||
<td className="px-3 py-3 text-center">
|
||
<StatusBadge v={r.status} />
|
||
</td>
|
||
</tr>
|
||
);
|
||
})
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
{/* 상세현황 */}
|
||
<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>
|
||
</div>
|
||
|
||
{!selected ? (
|
||
<div className="mt-3 border border-neutral-200 px-6 py-10 text-center text-sm text-slate-400">
|
||
상세 정보를 표시할 신청건이 없습니다.
|
||
</div>
|
||
) : (
|
||
<div className="mt-3 border border-neutral-200">
|
||
<div className="bg-sky-50 px-4 py-3 text-sm font-semibold text-slate-700 border-b border-neutral-200">
|
||
요구수량
|
||
</div>
|
||
|
||
<div className="grid grid-cols-12 text-sm">
|
||
{/* 상단 2행 */}
|
||
{topFields.map((f) => (
|
||
<div
|
||
key={f.label}
|
||
className="col-span-12 md:col-span-6 grid grid-cols-12 border-b border-neutral-200"
|
||
>
|
||
<div className="col-span-4 bg-sky-50 px-4 py-4 flex items-center justify-center text-center font-semibold text-slate-700">
|
||
{f.label}
|
||
{f.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">
|
||
{f.value}
|
||
</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">
|
||
요구수질
|
||
</div>
|
||
|
||
{waterFields.map((f) => (
|
||
<div
|
||
key={f.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">
|
||
{f.label}
|
||
</div>
|
||
<div className="col-span-7 px-3 py-4 flex items-center justify-center text-center">
|
||
{f.value}
|
||
</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">
|
||
공급처 후보지
|
||
</div>
|
||
|
||
{selected.detail.candidates.map((v, i) => (
|
||
<div
|
||
key={`candidate-${i}`}
|
||
className="col-span-12 md:col-span-4 grid grid-cols-12 border-b md:border-b-0 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">
|
||
후보지{i + 1}
|
||
</div>
|
||
<div className="col-span-7 px-3 py-4 flex items-center justify-center text-center">
|
||
{v}
|
||
</div>
|
||
</div>
|
||
))}
|
||
|
||
{/* 수요신청 진행상태 */}
|
||
<div className="col-span-12 bg-sky-50 px-4 py-3 text-sm font-semibold text-slate-700 border-t border-b border-neutral-200">
|
||
수요신청 진행상태
|
||
</div>
|
||
|
||
<div className="col-span-12 md:col-span-4 grid grid-cols-12 border-b md:border-b-0 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">
|
||
진행상태
|
||
</div>
|
||
<div className="col-span-7 px-3 py-4 flex items-center justify-center text-center">
|
||
{selected.status}
|
||
</div>
|
||
</div>
|
||
<div className="col-span-12 md:col-span-4 grid grid-cols-12 border-b md:border-b-0 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">
|
||
공급처매칭결과
|
||
</div>
|
||
<div className="col-span-7 px-3 py-4 flex items-center justify-center text-center">
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</section>
|
||
</div>
|
||
</main>
|
||
);
|
||
}
|