362 lines
13 KiB
TypeScript
362 lines
13 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useMemo, useState } from "react";
|
|
|
|
import { useAlertStore } from "@/shared/lib";
|
|
import {
|
|
Button,
|
|
Label,
|
|
LoadingWithAnimation,
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@common/components";
|
|
|
|
import {
|
|
statisticsVerificationDetailColumnInfo,
|
|
statisticsVerificationSummaryColumnInfo,
|
|
} from "../../../entities/statistics/model/statistics-verification-report/statistics-verification-report-columns";
|
|
import {
|
|
APPROVAL_STATUS_OPTIONS,
|
|
DEFAULT_FILTERS,
|
|
DEFAULT_SUB_FILTERS,
|
|
fetchStatisticsVerificationDetailMock,
|
|
fetchStatisticsVerificationSummaryMock,
|
|
getSigunguOptions,
|
|
getYearOptions,
|
|
SIDO_OPTIONS,
|
|
STATS_INDICATOR_OPTIONS,
|
|
STATUS_OPTIONS,
|
|
} from "../../../entities/statistics/model/statistics-verification-report/statistics-verification-report-mock";
|
|
import type {
|
|
StatisticsVerificationDetailRow,
|
|
StatisticsVerificationFilterState,
|
|
StatisticsVerificationSubFilterState,
|
|
StatisticsVerificationSummaryRow,
|
|
} from "../../../entities/statistics/model/statistics-verification-report/statistics-verification-report-types";
|
|
|
|
import ApproveButton from "@/components/button/ApproveButton";
|
|
import ReturnButton from "@/components/button/ReturnButton";
|
|
import DataGrid from "@/components/module/DataGrid";
|
|
import { toGridRows } from "@/components/util/grid/gridFunction";
|
|
|
|
function StatisticsVerificationReport() {
|
|
const { setMessage: alert } = useAlertStore((state) => state);
|
|
const yearOptions = useMemo(() => getYearOptions(), []);
|
|
const [filters, setFilters] =
|
|
useState<StatisticsVerificationFilterState>(DEFAULT_FILTERS);
|
|
const [subFilters, setSubFilters] =
|
|
useState<StatisticsVerificationSubFilterState>(DEFAULT_SUB_FILTERS);
|
|
const [summaryRows, setSummaryRows] = useState<
|
|
StatisticsVerificationSummaryRow[]
|
|
>([]);
|
|
const [detailRows, setDetailRows] = useState<
|
|
StatisticsVerificationDetailRow[]
|
|
>([]);
|
|
const [summaryGridRows, setSummaryGridRows] = useState<
|
|
Record<string, string | number>[]
|
|
>([]);
|
|
const [detailGridRows, setDetailGridRows] = useState<
|
|
Record<string, string | number>[]
|
|
>([]);
|
|
const [selectedSummaryRow, setSelectedSummaryRow] =
|
|
useState<StatisticsVerificationSummaryRow | null>(null);
|
|
const [selectedDetailRow, setSelectedDetailRow] =
|
|
useState<StatisticsVerificationDetailRow | null>(null);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
const sigunguOptions = useMemo(
|
|
() => getSigunguOptions(filters.sido),
|
|
[filters.sido],
|
|
);
|
|
|
|
useEffect(() => {
|
|
void handleSearch(filters, subFilters);
|
|
}, []);
|
|
|
|
async function handleSearch(
|
|
nextFilters: StatisticsVerificationFilterState,
|
|
nextSubFilters: StatisticsVerificationSubFilterState,
|
|
summaryId?: number | null,
|
|
) {
|
|
try {
|
|
setIsLoading(true);
|
|
const [summaryData, detailData] = await Promise.all([
|
|
fetchStatisticsVerificationSummaryMock(nextFilters),
|
|
fetchStatisticsVerificationDetailMock(
|
|
nextFilters,
|
|
nextSubFilters,
|
|
summaryId,
|
|
),
|
|
]);
|
|
setSummaryRows(summaryData);
|
|
setDetailRows(detailData);
|
|
setSummaryGridRows(toGridRows(summaryData));
|
|
setDetailGridRows(toGridRows(detailData));
|
|
if (summaryData.length === 0)
|
|
alert("조회된 상단 목록 데이터가 없습니다.");
|
|
if (detailData.length === 0)
|
|
alert("조회된 상세 목록 데이터가 없습니다.");
|
|
} catch {
|
|
alert("통계검증리포트 조회 중 오류가 발생했습니다.");
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}
|
|
|
|
function handleFilterChange<
|
|
K extends keyof StatisticsVerificationFilterState,
|
|
>(key: K, value: StatisticsVerificationFilterState[K]) {
|
|
if (key === "sido") {
|
|
setFilters((prev) => ({ ...prev, sido: value, sigungu: "ALL" }));
|
|
return;
|
|
}
|
|
setFilters((prev) => ({ ...prev, [key]: value }));
|
|
}
|
|
|
|
function handleSubFilterChange<
|
|
K extends keyof StatisticsVerificationSubFilterState,
|
|
>(key: K, value: StatisticsVerificationSubFilterState[K]) {
|
|
setSubFilters((prev) => ({ ...prev, [key]: value }));
|
|
}
|
|
|
|
function handleReset() {
|
|
setFilters(DEFAULT_FILTERS);
|
|
setSubFilters(DEFAULT_SUB_FILTERS);
|
|
setSelectedSummaryRow(null);
|
|
setSelectedDetailRow(null);
|
|
void handleSearch(DEFAULT_FILTERS, DEFAULT_SUB_FILTERS);
|
|
}
|
|
|
|
function handleApprove() {
|
|
if (!selectedSummaryRow) {
|
|
alert("승인할 상단 목록 행을 먼저 선택해주세요.");
|
|
return;
|
|
}
|
|
alert(
|
|
`승인 API TBD: ${selectedSummaryRow.sido} ${selectedSummaryRow.sigungu} (${selectedSummaryRow.reportType})`,
|
|
);
|
|
}
|
|
|
|
function handleReject() {
|
|
if (!selectedDetailRow) {
|
|
alert("반려할 상세 항목 행을 먼저 선택해주세요.");
|
|
return;
|
|
}
|
|
alert(
|
|
`반려 API TBD: ${selectedDetailRow.sido} ${selectedDetailRow.sigungu} ${selectedDetailRow.detailItem}`,
|
|
);
|
|
}
|
|
|
|
function handleDownload() {
|
|
if (detailRows.length === 0) {
|
|
alert("다운로드할 상세 목록이 없습니다.");
|
|
return;
|
|
}
|
|
|
|
const headers = [
|
|
"시도",
|
|
"시군구",
|
|
"지표",
|
|
"세부항목",
|
|
"검증날짜",
|
|
"오류내역",
|
|
"입력값",
|
|
"비교값",
|
|
"상태",
|
|
"예외사유",
|
|
"반려의견",
|
|
"승인여부",
|
|
];
|
|
const rows = detailRows.map((row) =>
|
|
[
|
|
row.sido,
|
|
row.sigungu,
|
|
row.indicator,
|
|
row.detailItem,
|
|
row.verifyDate,
|
|
row.errorDetail,
|
|
String(row.inputValue),
|
|
String(row.compareValue),
|
|
row.status,
|
|
row.exceptionReason,
|
|
row.returnOpinion,
|
|
row.approvalYn,
|
|
].join(","),
|
|
);
|
|
const csv = [headers.join(","), ...rows].join("\n");
|
|
const blob = new Blob([`\uFEFF${csv}`], {
|
|
type: "text/csv;charset=utf-8;",
|
|
});
|
|
const url = URL.createObjectURL(blob);
|
|
const link = document.createElement("a");
|
|
link.href = url;
|
|
link.download = "statistics-verification-report-detail.csv";
|
|
link.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
function handleSummaryRowClick(row: Record<string, string | number>) {
|
|
const summaryId = Number(row.summaryId);
|
|
const selected = summaryRows.find(
|
|
(item) => item.summaryId === summaryId,
|
|
);
|
|
setSelectedSummaryRow(selected ?? null);
|
|
if (!summaryId) return;
|
|
void handleSearch(filters, subFilters, summaryId);
|
|
}
|
|
|
|
function handleDetailRowClick(row: Record<string, string | number>) {
|
|
const detailId = Number(row.detailId);
|
|
const selected = detailRows.find((item) => item.detailId === detailId);
|
|
setSelectedDetailRow(selected ?? null);
|
|
}
|
|
|
|
return (
|
|
<div className="relative">
|
|
<div className="card !py-0">
|
|
<div className="grid grid-cols-1 gap-[0.8rem] p-[1.2rem] md:grid-cols-[repeat(4,minmax(0,1fr))_auto]">
|
|
<FilterSelect
|
|
label="시도"
|
|
value={filters.sido}
|
|
onChange={(value) => handleFilterChange("sido", value)}
|
|
options={SIDO_OPTIONS}
|
|
/>
|
|
<FilterSelect
|
|
label="시군구"
|
|
value={filters.sigungu}
|
|
onChange={(value) =>
|
|
handleFilterChange("sigungu", value)
|
|
}
|
|
options={sigunguOptions}
|
|
/>
|
|
<FilterSelect
|
|
label="년도"
|
|
value={filters.year}
|
|
onChange={(value) => handleFilterChange("year", value)}
|
|
options={yearOptions}
|
|
/>
|
|
<FilterSelect
|
|
label="통계지표"
|
|
value={filters.statsIndicator}
|
|
onChange={(value) =>
|
|
handleFilterChange("statsIndicator", value)
|
|
}
|
|
options={STATS_INDICATOR_OPTIONS}
|
|
/>
|
|
|
|
<div className="flex items-end justify-end gap-[0.6rem]">
|
|
<Button color="green" onClick={handleReset}>
|
|
초기화
|
|
</Button>
|
|
<Button
|
|
onClick={() =>
|
|
void handleSearch(filters, subFilters)
|
|
}
|
|
>
|
|
조회
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card !py-0">
|
|
<div className="m-[0.6rem] flex items-center justify-end">
|
|
<ApproveButton approveFunc={handleApprove} />
|
|
</div>
|
|
</div>
|
|
|
|
<DataGrid
|
|
columnInfo={statisticsVerificationSummaryColumnInfo}
|
|
fixedColumns={3}
|
|
isExcelImport={false}
|
|
onRowClick={handleSummaryRowClick}
|
|
rowData={summaryGridRows}
|
|
rowStartNum={2}
|
|
setRowData={setSummaryGridRows}
|
|
showHeaderCheckbox={true}
|
|
/>
|
|
|
|
<div className="card !py-0">
|
|
<div className="m-[0.6rem] flex flex-wrap items-end justify-between gap-[0.8rem]">
|
|
<div className="grid w-full grid-cols-1 gap-[0.8rem] md:w-auto md:grid-cols-2">
|
|
<FilterSelect
|
|
label="상태"
|
|
value={subFilters.status}
|
|
onChange={(value) =>
|
|
handleSubFilterChange("status", value)
|
|
}
|
|
options={STATUS_OPTIONS}
|
|
/>
|
|
<FilterSelect
|
|
label="승인상태"
|
|
value={subFilters.approvalStatus}
|
|
onChange={(value) =>
|
|
handleSubFilterChange("approvalStatus", value)
|
|
}
|
|
options={APPROVAL_STATUS_OPTIONS}
|
|
/>
|
|
</div>
|
|
<div className="flex items-end justify-end gap-[0.6rem]">
|
|
<Button color="gray" onClick={handleDownload}>
|
|
다운로드
|
|
</Button>
|
|
<ReturnButton returnFunc={handleReject} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<DataGrid
|
|
columnInfo={statisticsVerificationDetailColumnInfo}
|
|
fixedColumns={4}
|
|
isExcelImport={false}
|
|
onRowClick={handleDetailRowClick}
|
|
rowData={detailGridRows}
|
|
rowStartNum={2}
|
|
setRowData={setDetailGridRows}
|
|
showHeaderCheckbox={true}
|
|
/>
|
|
|
|
{isLoading && (
|
|
<div className="[data-theme='black']:bg-transparent absolute inset-0 z-50 flex items-center justify-center rounded-lg p-10">
|
|
<div className="relative size-[5rem]">
|
|
<LoadingWithAnimation color="primary" />
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface FilterSelectProps {
|
|
label: string;
|
|
onChange: (value: string) => void;
|
|
options: Array<{ label: string; value: string }>;
|
|
value: string;
|
|
}
|
|
|
|
function FilterSelect({ label, onChange, options, value }: FilterSelectProps) {
|
|
return (
|
|
<div className="flex items-center gap-[0.8rem]">
|
|
<Label className="min-w-[4rem] text-[1.3rem]">{label}</Label>
|
|
<Select value={value} onValueChange={onChange}>
|
|
<SelectTrigger className="h-[3.2rem] min-w-[14rem]">
|
|
<SelectValue placeholder="전체" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{options.map((option) => (
|
|
<SelectItem key={option.value} value={option.value}>
|
|
{option.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export { StatisticsVerificationReport };
|
|
export default StatisticsVerificationReport;
|