[st] feat: 통계검증리포트 화면 레이아웃 구현
This commit is contained in:
@@ -0,0 +1,361 @@
|
||||
"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;
|
||||
@@ -6,6 +6,7 @@ export interface StatsInputStatusListRequestParams {
|
||||
sewerBzmnCd?: string;
|
||||
statsYr?: string;
|
||||
mode?: string;
|
||||
statsCd?: string;
|
||||
}
|
||||
|
||||
export interface StatsInputStatusList {
|
||||
@@ -25,10 +26,14 @@ export interface StatsInputStatusList {
|
||||
rjctOpnnCn?: string;
|
||||
}
|
||||
|
||||
export type StatsInputStatusListApiResponse = ApiResponse<StatsInputStatusList[]> & {
|
||||
export type StatsInputStatusListApiResponse = ApiResponse<
|
||||
StatsInputStatusList[]
|
||||
> & {
|
||||
message: string;
|
||||
}
|
||||
};
|
||||
|
||||
export interface StatisticsInputStatusService {
|
||||
getStatisticsInputStatusList(param: StatsInputStatusListRequestParams): Promise<StatsInputStatusListApiResponse>;
|
||||
getStatisticsInputStatusList(
|
||||
param: StatsInputStatusListRequestParams,
|
||||
): Promise<StatsInputStatusListApiResponse>;
|
||||
}
|
||||
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import type { ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
|
||||
const READ_ONLY_STYLE = "rg-cell-readonly";
|
||||
|
||||
export const statisticsVerificationSummaryColumnInfo: ColumnInfo = {
|
||||
columns: [
|
||||
{ name: "summaryId", fieldName: "summaryId", header: { text: "summaryId" }, visible: false },
|
||||
{ name: "sido", fieldName: "sido", header: { text: "시도" }, styleName: READ_ONLY_STYLE },
|
||||
{ name: "sigungu", fieldName: "sigungu", header: { text: "시군구" }, styleName: READ_ONLY_STYLE },
|
||||
{ name: "reportType", fieldName: "reportType", header: { text: "구분" }, styleName: READ_ONLY_STYLE },
|
||||
{ name: "progressStatus", fieldName: "progressStatus", header: { text: "진행상태" }, width: 120, styleName: READ_ONLY_STYLE },
|
||||
{ name: "writerName", fieldName: "writerName", header: { text: "작성자" }, styleName: READ_ONLY_STYLE },
|
||||
{ name: "writerContact", fieldName: "writerContact", header: { text: "작성자연락처" }, width: 130, styleName: READ_ONLY_STYLE },
|
||||
{ name: "writerDept", fieldName: "writerDept", header: { text: "부서" }, styleName: READ_ONLY_STYLE },
|
||||
{ name: "writerPosition", fieldName: "writerPosition", header: { text: "직급" }, styleName: READ_ONLY_STYLE },
|
||||
{ name: "approverName", fieldName: "approverName", header: { text: "승인자" }, styleName: READ_ONLY_STYLE },
|
||||
{ name: "approverDept", fieldName: "approverDept", header: { text: "부서" }, styleName: READ_ONLY_STYLE },
|
||||
{ name: "approverPosition", fieldName: "approverPosition", header: { text: "직급" }, styleName: READ_ONLY_STYLE },
|
||||
{ name: "returnReason", fieldName: "returnReason", header: { text: "반려사유" }, width: 220, styleName: READ_ONLY_STYLE },
|
||||
],
|
||||
columnLayout: [
|
||||
"summaryId",
|
||||
"sido",
|
||||
"sigungu",
|
||||
"reportType",
|
||||
"progressStatus",
|
||||
"writerName",
|
||||
"writerContact",
|
||||
"writerDept",
|
||||
"writerPosition",
|
||||
"approverName",
|
||||
"approverDept",
|
||||
"approverPosition",
|
||||
"returnReason",
|
||||
],
|
||||
};
|
||||
|
||||
export const statisticsVerificationDetailColumnInfo: ColumnInfo = {
|
||||
columns: [
|
||||
{ name: "detailId", fieldName: "detailId", header: { text: "detailId" }, visible: false },
|
||||
{ name: "summaryId", fieldName: "summaryId", header: { text: "summaryId" }, visible: false },
|
||||
{ name: "sido", fieldName: "sido", header: { text: "시도" }, styleName: READ_ONLY_STYLE },
|
||||
{ name: "sigungu", fieldName: "sigungu", header: { text: "시군구" }, styleName: READ_ONLY_STYLE },
|
||||
{ name: "indicator", fieldName: "indicator", header: { text: "지표" }, width: 140, styleName: READ_ONLY_STYLE },
|
||||
{ name: "detailItem", fieldName: "detailItem", header: { text: "세부항목" }, width: 180, styleName: READ_ONLY_STYLE },
|
||||
{ name: "verifyDate", fieldName: "verifyDate", header: { text: "검증날짜" }, width: 110, styleName: READ_ONLY_STYLE },
|
||||
{ name: "errorDetail", fieldName: "errorDetail", header: { text: "오류내역" }, width: 200, styleName: READ_ONLY_STYLE },
|
||||
{ name: "inputValue", fieldName: "inputValue", header: { text: "입력값" }, width: 90, styleName: READ_ONLY_STYLE },
|
||||
{ name: "compareValue", fieldName: "compareValue", header: { text: "비교값" }, width: 90, styleName: READ_ONLY_STYLE },
|
||||
{ name: "status", fieldName: "status", header: { text: "상태" }, width: 90, styleName: READ_ONLY_STYLE },
|
||||
{ name: "exceptionReason", fieldName: "exceptionReason", header: { text: "예외사유" }, width: 170, styleName: READ_ONLY_STYLE },
|
||||
{ name: "returnOpinion", fieldName: "returnOpinion", header: { text: "반려의견" }, width: 170, styleName: READ_ONLY_STYLE },
|
||||
{ name: "approvalYn", fieldName: "approvalYn", header: { text: "승인여부" }, width: 90, styleName: READ_ONLY_STYLE },
|
||||
],
|
||||
columnLayout: [
|
||||
"detailId",
|
||||
"summaryId",
|
||||
"sido",
|
||||
"sigungu",
|
||||
"indicator",
|
||||
"detailItem",
|
||||
"verifyDate",
|
||||
"errorDetail",
|
||||
"inputValue",
|
||||
"compareValue",
|
||||
"status",
|
||||
"exceptionReason",
|
||||
"returnOpinion",
|
||||
"approvalYn",
|
||||
],
|
||||
};
|
||||
+288
@@ -0,0 +1,288 @@
|
||||
import { fakerKO as faker } from "@faker-js/faker";
|
||||
|
||||
import type {
|
||||
StatisticsVerificationDetailRow,
|
||||
StatisticsVerificationFilterState,
|
||||
StatisticsVerificationOption,
|
||||
StatisticsVerificationSubFilterState,
|
||||
StatisticsVerificationSummaryRow,
|
||||
} from "./statistics-verification-report-types";
|
||||
|
||||
const NETWORK_DELAY_MS = 300;
|
||||
|
||||
export const SIDO_OPTIONS: StatisticsVerificationOption[] = [
|
||||
{ label: "전체", value: "ALL" },
|
||||
{ label: "서울특별시", value: "SEOUL" },
|
||||
{ label: "부산광역시", value: "BUSAN" },
|
||||
{ label: "대구광역시", value: "DAEGU" },
|
||||
{ label: "경기도", value: "GYEONGGI" },
|
||||
];
|
||||
|
||||
const SIGUNGU_OPTIONS_BY_SIDO: Record<string, StatisticsVerificationOption[]> =
|
||||
{
|
||||
ALL: [{ label: "전체", value: "ALL" }],
|
||||
SEOUL: [
|
||||
{ label: "전체", value: "ALL" },
|
||||
{ label: "중구", value: "SEOUL_JUNGGU" },
|
||||
{ label: "강남구", value: "SEOUL_GANGNAM" },
|
||||
],
|
||||
BUSAN: [
|
||||
{ label: "전체", value: "ALL" },
|
||||
{ label: "해운대구", value: "BUSAN_HAEUNDAE" },
|
||||
{ label: "수영구", value: "BUSAN_SUYEONG" },
|
||||
],
|
||||
DAEGU: [
|
||||
{ label: "전체", value: "ALL" },
|
||||
{ label: "중구", value: "DAEGU_JUNGGU" },
|
||||
{ label: "달서구", value: "DAEGU_DALSEO" },
|
||||
],
|
||||
GYEONGGI: [
|
||||
{ label: "전체", value: "ALL" },
|
||||
{ label: "수원시", value: "GYEONGGI_SUWON" },
|
||||
{ label: "성남시", value: "GYEONGGI_SEONGNAM" },
|
||||
],
|
||||
};
|
||||
|
||||
export const STATS_INDICATOR_OPTIONS: StatisticsVerificationOption[] = [
|
||||
{ label: "통계항목", value: "ALL" },
|
||||
{ label: "하수도 보급률", value: "SEWERAGE_SUPPLY" },
|
||||
{ label: "관로 보급 현황", value: "PIPE_STATUS" },
|
||||
{ label: "분류식 비율", value: "SEPARATED_RATIO" },
|
||||
];
|
||||
|
||||
export const STATUS_OPTIONS: StatisticsVerificationOption[] = [
|
||||
{ label: "수정완료", value: "DONE" },
|
||||
{ label: "검토중", value: "REVIEW" },
|
||||
{ label: "오류", value: "ERROR" },
|
||||
];
|
||||
|
||||
export const APPROVAL_STATUS_OPTIONS: StatisticsVerificationOption[] = [
|
||||
{ label: "전체", value: "ALL" },
|
||||
{ label: "승인완료", value: "APPROVED" },
|
||||
{ label: "반려", value: "REJECTED" },
|
||||
{ label: "대기", value: "PENDING" },
|
||||
];
|
||||
|
||||
export const DEFAULT_FILTERS: StatisticsVerificationFilterState = {
|
||||
sido: "ALL",
|
||||
sigungu: "ALL",
|
||||
year: "ALL",
|
||||
statsIndicator: "ALL",
|
||||
};
|
||||
|
||||
export const DEFAULT_SUB_FILTERS: StatisticsVerificationSubFilterState = {
|
||||
status: "DONE",
|
||||
approvalStatus: "ALL",
|
||||
};
|
||||
|
||||
export function getYearOptions(): StatisticsVerificationOption[] {
|
||||
const thisYear = new Date().getFullYear();
|
||||
return [
|
||||
{ label: "전체", value: "ALL" },
|
||||
...Array.from({ length: 5 }, (_, index) => {
|
||||
const year = String(thisYear - index - 1);
|
||||
return { label: year, value: year };
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
export function getSigunguOptions(
|
||||
sido: string,
|
||||
): StatisticsVerificationOption[] {
|
||||
return SIGUNGU_OPTIONS_BY_SIDO[sido] ?? [];
|
||||
}
|
||||
|
||||
export async function fetchStatisticsVerificationSummaryMock(
|
||||
filters: StatisticsVerificationFilterState,
|
||||
): Promise<StatisticsVerificationSummaryRow[]> {
|
||||
await wait();
|
||||
faker.seed(20260416);
|
||||
|
||||
const rows = Array.from({ length: 12 }, (_, index) => {
|
||||
const sido = pickSidoName(filters.sido);
|
||||
const sigungu = pickSigunguName(filters.sido, filters.sigungu);
|
||||
const progressStatus = faker.helpers.arrayElement([
|
||||
"승인대기",
|
||||
"승인완료",
|
||||
"반려",
|
||||
]);
|
||||
return {
|
||||
summaryId: index + 1,
|
||||
sido,
|
||||
sigungu,
|
||||
reportType: faker.helpers.arrayElement([
|
||||
"정기보고",
|
||||
"수정보고",
|
||||
"보완보고",
|
||||
]),
|
||||
progressStatus,
|
||||
writerName: faker.person.lastName() + faker.person.firstName(),
|
||||
writerContact: faker.phone.number({ style: "national" }),
|
||||
writerDept: faker.helpers.arrayElement([
|
||||
"통계팀",
|
||||
"하수행정과",
|
||||
"정책기획과",
|
||||
]),
|
||||
writerPosition: faker.helpers.arrayElement([
|
||||
"주무관",
|
||||
"팀장",
|
||||
"과장",
|
||||
]),
|
||||
approverName:
|
||||
progressStatus === "승인대기"
|
||||
? "-"
|
||||
: faker.person.lastName() + faker.person.firstName(),
|
||||
approverDept:
|
||||
progressStatus === "승인대기"
|
||||
? "-"
|
||||
: faker.helpers.arrayElement(["검증운영팀", "하수행정과"]),
|
||||
approverPosition:
|
||||
progressStatus === "승인대기"
|
||||
? "-"
|
||||
: faker.helpers.arrayElement(["팀장", "과장"]),
|
||||
returnReason:
|
||||
progressStatus === "반려"
|
||||
? faker.helpers.arrayElement([
|
||||
"오류내역 확인 필요",
|
||||
"예외사유 보강 필요",
|
||||
"입력값 검증 재요청",
|
||||
])
|
||||
: "-",
|
||||
};
|
||||
});
|
||||
|
||||
return rows.filter((row) => {
|
||||
if (filters.sido !== "ALL" && row.sido !== pickSidoName(filters.sido))
|
||||
return false;
|
||||
if (filters.sigungu !== "ALL") {
|
||||
const currentSigungu = pickSigunguName(
|
||||
filters.sido,
|
||||
filters.sigungu,
|
||||
);
|
||||
if (row.sigungu !== currentSigungu) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchStatisticsVerificationDetailMock(
|
||||
filters: StatisticsVerificationFilterState,
|
||||
subFilters: StatisticsVerificationSubFilterState,
|
||||
selectedSummaryId?: number | null,
|
||||
): Promise<StatisticsVerificationDetailRow[]> {
|
||||
await wait();
|
||||
faker.seed(20260417);
|
||||
|
||||
const rows = Array.from({ length: 24 }, (_, index) => {
|
||||
const approvalYn = faker.helpers.arrayElement(["승인", "미승인"]);
|
||||
const status = faker.helpers.arrayElement([
|
||||
"수정완료",
|
||||
"검토중",
|
||||
"오류",
|
||||
]);
|
||||
const summaryId = faker.number.int({ min: 1, max: 12 });
|
||||
return {
|
||||
detailId: index + 1,
|
||||
summaryId,
|
||||
sido: pickSidoName(filters.sido),
|
||||
sigungu: pickSigunguName(filters.sido, filters.sigungu),
|
||||
indicator: pickIndicator(filters.statsIndicator),
|
||||
detailItem: faker.helpers.arrayElement([
|
||||
"처리구역면적",
|
||||
"처리인구",
|
||||
"분류식관거연장",
|
||||
"하수관로보급률",
|
||||
]),
|
||||
verifyDate: faker.date
|
||||
.recent({ days: 20 })
|
||||
.toISOString()
|
||||
.slice(0, 10),
|
||||
errorDetail: faker.helpers.arrayElement([
|
||||
"전년 대비 편차 초과",
|
||||
"입력값 누락",
|
||||
"상호검증 불일치",
|
||||
]),
|
||||
inputValue: faker.number.int({ min: 100, max: 5000 }),
|
||||
compareValue: faker.number.int({ min: 100, max: 5000 }),
|
||||
status,
|
||||
exceptionReason:
|
||||
status === "오류"
|
||||
? faker.lorem.sentence({ min: 1, max: 2 })
|
||||
: "-",
|
||||
returnOpinion:
|
||||
approvalYn === "미승인"
|
||||
? faker.helpers.arrayElement([
|
||||
"산식 재검토 필요",
|
||||
"근거자료 보강 요청",
|
||||
"담당자 확인 필요",
|
||||
])
|
||||
: "-",
|
||||
approvalYn,
|
||||
};
|
||||
});
|
||||
|
||||
return rows.filter((row) => {
|
||||
if (selectedSummaryId && row.summaryId !== selectedSummaryId)
|
||||
return false;
|
||||
if (subFilters.status === "DONE" && row.status !== "수정완료")
|
||||
return false;
|
||||
if (subFilters.status === "REVIEW" && row.status !== "검토중")
|
||||
return false;
|
||||
if (subFilters.status === "ERROR" && row.status !== "오류")
|
||||
return false;
|
||||
if (
|
||||
subFilters.approvalStatus === "APPROVED" &&
|
||||
row.approvalYn !== "승인"
|
||||
)
|
||||
return false;
|
||||
if (
|
||||
subFilters.approvalStatus === "REJECTED" &&
|
||||
row.approvalYn !== "미승인"
|
||||
)
|
||||
return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function wait() {
|
||||
return new Promise((resolve) => setTimeout(resolve, NETWORK_DELAY_MS));
|
||||
}
|
||||
|
||||
function pickSidoName(value: string): string {
|
||||
if (value === "ALL")
|
||||
return faker.helpers.arrayElement([
|
||||
"서울특별시",
|
||||
"부산광역시",
|
||||
"대구광역시",
|
||||
"경기도",
|
||||
]);
|
||||
|
||||
const matched = SIDO_OPTIONS.find((option) => option.value === value);
|
||||
return matched?.label ?? "서울특별시";
|
||||
}
|
||||
|
||||
function pickSigunguName(sido: string, sigungu: string): string {
|
||||
if (sigungu !== "ALL") {
|
||||
const options = getSigunguOptions(sido);
|
||||
const matched = options.find((option) => option.value === sigungu);
|
||||
if (matched) return matched.label;
|
||||
}
|
||||
|
||||
const options = getSigunguOptions(sido).filter(
|
||||
(option) => option.value !== "ALL",
|
||||
);
|
||||
if (options.length === 0) return "전체";
|
||||
return faker.helpers.arrayElement(options).label;
|
||||
}
|
||||
|
||||
function pickIndicator(indicator: string): string {
|
||||
if (indicator === "ALL")
|
||||
return faker.helpers.arrayElement(
|
||||
STATS_INDICATOR_OPTIONS.filter((item) => item.value !== "ALL"),
|
||||
).label;
|
||||
|
||||
const matched = STATS_INDICATOR_OPTIONS.find(
|
||||
(option) => option.value === indicator,
|
||||
);
|
||||
return matched?.label ?? "통계항목";
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
export interface StatisticsVerificationFilterState {
|
||||
sido: string;
|
||||
sigungu: string;
|
||||
year: string;
|
||||
statsIndicator: string;
|
||||
}
|
||||
|
||||
export interface StatisticsVerificationSubFilterState {
|
||||
status: string;
|
||||
approvalStatus: string;
|
||||
}
|
||||
|
||||
export interface StatisticsVerificationOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface StatisticsVerificationSummaryRow {
|
||||
summaryId: number;
|
||||
sido: string;
|
||||
sigungu: string;
|
||||
reportType: string;
|
||||
progressStatus: string;
|
||||
writerName: string;
|
||||
writerContact: string;
|
||||
writerDept: string;
|
||||
writerPosition: string;
|
||||
approverName: string;
|
||||
approverDept: string;
|
||||
approverPosition: string;
|
||||
returnReason: string;
|
||||
}
|
||||
|
||||
export interface StatisticsVerificationDetailRow {
|
||||
detailId: number;
|
||||
summaryId: number;
|
||||
sido: string;
|
||||
sigungu: string;
|
||||
indicator: string;
|
||||
detailItem: string;
|
||||
verifyDate: string;
|
||||
errorDetail: string;
|
||||
inputValue: number;
|
||||
compareValue: number;
|
||||
status: string;
|
||||
exceptionReason: string;
|
||||
returnOpinion: string;
|
||||
approvalYn: string;
|
||||
}
|
||||
@@ -13,4 +13,4 @@ export const REFRESH_TOKEN =
|
||||
export const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id";
|
||||
export const ST_SERVICE_URL =
|
||||
process.env.NEXT_PUBLIC_ST_SERVICE_URL || "http://localhost:8080";
|
||||
export const COOKIE_DOMAIN = process.env.NEXT_PUBLIC_COOKIE_DOMAIN;
|
||||
export const COOKIE_DOMAIN = process.env.NEXT_PUBLIC_COOKIE_DOMAIN;
|
||||
|
||||
Reference in New Issue
Block a user