Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend
This commit is contained in:
@@ -14,7 +14,9 @@
|
||||
"@common/components": "workspace:*",
|
||||
"@common/config": "workspace:*",
|
||||
"@common/network": "workspace:*",
|
||||
"@rsone/components": "workspace:*",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@faker-js/faker": "^9.9.0",
|
||||
"@fontsource-variable/noto-sans-kr": "^5.2.5",
|
||||
"@frontend-opensource/use-react-hooks": "^1.1.4",
|
||||
@@ -29,6 +31,7 @@
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@rsone/components": "workspace:*",
|
||||
"@tanstack/react-query": "^5.67.1",
|
||||
"@tanstack/react-query-devtools": "^5.75.5",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
|
||||
@@ -1,9 +1,96 @@
|
||||
const Dashboard = () => {
|
||||
return (
|
||||
<div>
|
||||
Dashboard
|
||||
</div>
|
||||
);
|
||||
};
|
||||
"use client";
|
||||
|
||||
export default Dashboard;
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { closestCenter, DndContext } from "@dnd-kit/core";
|
||||
import { rectSortingStrategy, SortableContext } from "@dnd-kit/sortable";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { useUserStore } from "@/shared/lib";
|
||||
import { Skeleton } from "@common/components";
|
||||
|
||||
import { useDndSensors } from "@/pages/statistics/lib/hooks/use-dnd-sensors";
|
||||
import { usePanelOrder } from "@/pages/statistics/lib/hooks/use-panel-order";
|
||||
import { getInitialPanels } from "@/pages/statistics/model/panel-config";
|
||||
import { SortablePanel } from "@/pages/statistics/ui/sortable-panel";
|
||||
|
||||
interface PanelItem {
|
||||
id: string;
|
||||
component: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function Dashboard() {
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const [initialPanels, setInitialPanels] = useState<PanelItem[]>([]);
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
const sensors = useDndSensors();
|
||||
|
||||
const { panels, handleDragEnd } = usePanelOrder({
|
||||
initialPanels,
|
||||
storageKey: "statistics-dashboard-panels-order",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!userInfo) return;
|
||||
const initialPanels = getInitialPanels({
|
||||
dashboardParams: {
|
||||
statsYr: String(dayjs().year() - 1),
|
||||
dtlInstCd: userInfo.dtlInstCd,
|
||||
enoCd: userInfo.enoCd,
|
||||
laeSeCd: userInfo.laeSeCd,
|
||||
},
|
||||
scheduleInfoParams: {
|
||||
statsYr: String(dayjs().year() - 1),
|
||||
},
|
||||
});
|
||||
setInitialPanels(initialPanels);
|
||||
}, [userInfo]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isMounted && initialPanels.length > 0 ? (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={panels.map((p) => p.id)}
|
||||
strategy={rectSortingStrategy}
|
||||
>
|
||||
{/* TBD: 그리드 레이아웃 비율 정의 필요
|
||||
현재: 3열 그리드, 반응형 브레이크포인트 적용
|
||||
SPEC 레이아웃에 따라 조정 필요 */}
|
||||
<div className="grid grid-cols-1 gap-4 p-2 md:grid-cols-2 lg:grid-cols-3">
|
||||
{panels.map((panel) => (
|
||||
<SortablePanel
|
||||
key={panel.id}
|
||||
id={panel.id}
|
||||
className={panel.className}
|
||||
>
|
||||
{panel.component}
|
||||
</SortablePanel>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-4 p-2 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Skeleton className="h-[20rem] rounded-xl" />
|
||||
<Skeleton className="h-[20rem] rounded-xl" />
|
||||
<Skeleton className="h-[20rem] rounded-xl" />
|
||||
<Skeleton className="h-[20rem] rounded-xl" />
|
||||
<Skeleton className="h-[20rem] rounded-xl" />
|
||||
<Skeleton className="h-[20rem] rounded-xl" />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Dashboard;
|
||||
|
||||
@@ -129,6 +129,7 @@ const DEFAULT_FILTERS: StatisticsVerificationFilterState = {
|
||||
statsYr: String(dayjs().year() - 1),
|
||||
sewerBzmnCd: null,
|
||||
statsCd: null,
|
||||
laeSbmsnSttsCd: "2",
|
||||
};
|
||||
|
||||
const DEFAULT_SUB_FILTERS: StatisticsVerificationSubFilterState = {
|
||||
@@ -156,15 +157,6 @@ function StatisticsVerificationReport() {
|
||||
useState<StatisticsVerificationFilterState>(DEFAULT_FILTERS);
|
||||
const [subFilters, setSubFilters] =
|
||||
useState<StatisticsVerificationSubFilterState>(DEFAULT_SUB_FILTERS);
|
||||
const [submittedFilters, setSubmittedFilters] =
|
||||
useState<StatisticsVerificationFilterState>(DEFAULT_FILTERS);
|
||||
const [submittedParams, setSubmittedParams] = useState<{
|
||||
main: StatisticsVerificationFilterState;
|
||||
sub: StatisticsVerificationSubFilterState;
|
||||
}>({
|
||||
main: DEFAULT_FILTERS,
|
||||
sub: DEFAULT_SUB_FILTERS,
|
||||
});
|
||||
const [selectedReportRows, setSelectedReportRows] = useState<
|
||||
StatsInputStatusList[]
|
||||
>([]);
|
||||
@@ -182,11 +174,30 @@ function StatisticsVerificationReport() {
|
||||
const { data: sigunguData } = useStAnlsSigunguCombo(
|
||||
filters.sewerBzmnCd ?? "",
|
||||
);
|
||||
const detailParams = useMemo(
|
||||
() => ({
|
||||
statsYr: filters.statsYr,
|
||||
ctpvNm:
|
||||
subFilters.ctpvNm !== "" ? subFilters.ctpvNm : filters.ctpvNm,
|
||||
sggNm: subFilters.sggNm !== "" ? subFilters.sggNm : filters.sggNm,
|
||||
sewerBzmnCd:
|
||||
subFilters.sewerBzmnCd !== ""
|
||||
? subFilters.sewerBzmnCd
|
||||
: filters.sewerBzmnCd,
|
||||
statsCd:
|
||||
subFilters.statsCd !== ""
|
||||
? subFilters.statsCd
|
||||
: filters.statsCd,
|
||||
prcsYnList: subFilters.prcsYnList,
|
||||
pbcpAprvYn: subFilters.pbcpAprvYn,
|
||||
laeSbmsnSttsCd: subFilters.laeSbmsnSttsCd,
|
||||
}),
|
||||
[filters, subFilters],
|
||||
);
|
||||
|
||||
const statisticsVerificationReportQuery =
|
||||
useStatsVrfcRptData(submittedFilters);
|
||||
const statisticsVerificationReportQuery = useStatsVrfcRptData(filters);
|
||||
const statisticsVerificationResultQuery =
|
||||
useStatsVrfcRsltData(submittedParams);
|
||||
useStatsVrfcRsltData(detailParams);
|
||||
|
||||
const statisticsVerificationReportData =
|
||||
statisticsVerificationReportQuery.data ?? [];
|
||||
@@ -253,11 +264,17 @@ function StatisticsVerificationReport() {
|
||||
const nextRows = statisticsVerificationResultGridRows;
|
||||
const prevLength = prevLengthRef.current;
|
||||
|
||||
if (!dataProvider || nextRows.length === 0) {
|
||||
if (!dataProvider) {
|
||||
prevLengthRef.current = nextRows.length;
|
||||
return;
|
||||
}
|
||||
|
||||
if (nextRows.length === 0) {
|
||||
dataProvider.setRows?.([]);
|
||||
prevLengthRef.current = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (nextRows.length > prevLength && prevLength > 0) {
|
||||
// 무한스크롤 시 데이터 처리
|
||||
const newRows = nextRows.slice(prevLength);
|
||||
@@ -327,17 +344,16 @@ function StatisticsVerificationReport() {
|
||||
statisticsVerificationReportQuery.isError,
|
||||
]);
|
||||
|
||||
function handleSearch(
|
||||
nextFilters: StatisticsVerificationFilterState,
|
||||
nextSubFilters: StatisticsVerificationSubFilterState,
|
||||
) {
|
||||
setSelectedReportRows([]);
|
||||
function resetResultState() {
|
||||
setSelectedResultRows([]);
|
||||
setSubmittedFilters({ ...nextFilters });
|
||||
setSubmittedParams({
|
||||
main: { ...nextFilters },
|
||||
sub: { ...nextSubFilters },
|
||||
});
|
||||
editedResultRowMapRef.current = new Map();
|
||||
prevLengthRef.current = 0;
|
||||
resultGridRef.current?.dataProvider?.setRows?.([]);
|
||||
}
|
||||
|
||||
function resetReportState() {
|
||||
setSelectedReportRows([]);
|
||||
resetResultState();
|
||||
}
|
||||
|
||||
async function handleLoadMoreDetail() {
|
||||
@@ -350,14 +366,6 @@ function StatisticsVerificationReport() {
|
||||
}
|
||||
}
|
||||
|
||||
function handleResultSearch() {
|
||||
setSelectedResultRows([]);
|
||||
setSubmittedParams({
|
||||
main: { ...filters },
|
||||
sub: { ...subFilters },
|
||||
});
|
||||
}
|
||||
|
||||
function handleFilterChange<
|
||||
K extends keyof StatisticsVerificationFilterState,
|
||||
>(key: K) {
|
||||
@@ -365,6 +373,7 @@ function StatisticsVerificationReport() {
|
||||
value?: StatisticsVerificationFilterState[K],
|
||||
optionData?: { label: string; value: string },
|
||||
) => {
|
||||
resetReportState();
|
||||
if (key === "sewerBzmnCd") {
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
@@ -372,8 +381,17 @@ function StatisticsVerificationReport() {
|
||||
sewerBzmnCd: optionData?.value,
|
||||
sggNm: "",
|
||||
}));
|
||||
setSubFilters((prev) => ({
|
||||
...prev,
|
||||
ctpvNm: "",
|
||||
sggNm: "",
|
||||
sewerBzmnCd: "",
|
||||
}));
|
||||
return;
|
||||
}
|
||||
if (key === "statsCd") {
|
||||
setSubFilters((prev) => ({ ...prev, statsCd: "" }));
|
||||
}
|
||||
setFilters((prev) => ({ ...prev, [key]: value }));
|
||||
};
|
||||
}
|
||||
@@ -382,10 +400,16 @@ function StatisticsVerificationReport() {
|
||||
K extends keyof StatisticsVerificationSubFilterState,
|
||||
>(key: K) {
|
||||
return (value?: StatisticsVerificationSubFilterState[K]) => {
|
||||
resetResultState();
|
||||
setSubFilters((prev) => ({ ...prev, [key]: value }));
|
||||
};
|
||||
}
|
||||
|
||||
function handleSubFilterReset() {
|
||||
setSubFilters(DEFAULT_SUB_FILTERS);
|
||||
resetResultState();
|
||||
}
|
||||
|
||||
function handleCellEdited(params: CellEditedCallbackParams) {
|
||||
const { itemIndex, fieldName, newValue } = params;
|
||||
const dataProvider = resultGridRef.current?.dataProvider as
|
||||
@@ -403,11 +427,7 @@ function StatisticsVerificationReport() {
|
||||
function handleReset() {
|
||||
setFilters(DEFAULT_FILTERS);
|
||||
setSubFilters(DEFAULT_SUB_FILTERS);
|
||||
setSelectedReportRows([]);
|
||||
setSelectedResultRows([]);
|
||||
editedResultRowMapRef.current = new Map();
|
||||
prevLengthRef.current = 0;
|
||||
handleSearch(DEFAULT_FILTERS, DEFAULT_SUB_FILTERS);
|
||||
resetReportState();
|
||||
}
|
||||
|
||||
function handleApprove() {
|
||||
@@ -532,9 +552,6 @@ function StatisticsVerificationReport() {
|
||||
<Button color="white" onClick={handleReset}>
|
||||
초기화
|
||||
</Button>
|
||||
<Button onClick={() => handleSearch(filters, subFilters)}>
|
||||
조회
|
||||
</Button>
|
||||
<Button
|
||||
color="green"
|
||||
onClick={handleApprove}
|
||||
@@ -557,6 +574,15 @@ function StatisticsVerificationReport() {
|
||||
setRowData={() => {}}
|
||||
showHeaderCheckbox={true}
|
||||
gridHeight={30}
|
||||
onRowClick={(row: StatsInputStatusList) => {
|
||||
setSubFilters((prev) => ({
|
||||
...prev,
|
||||
ctpvNm: row?.ctpvNm ?? "",
|
||||
sggNm: row?.sggNm ?? "",
|
||||
sewerBzmnCd: row?.sewerBzmnCd ?? "",
|
||||
statsCd: row?.statsCd ?? "",
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
|
||||
<h3 className="mt-10 px-2 text-[1.8rem] font-bold text-title">
|
||||
@@ -578,14 +604,15 @@ function StatisticsVerificationReport() {
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-end justify-end gap-2">
|
||||
<Button onClick={handleResultSearch}>조회</Button>
|
||||
<Button color="white" onClick={handleSubFilterReset}>
|
||||
초기화
|
||||
</Button>
|
||||
<Button
|
||||
color="red"
|
||||
onClick={handleReject}
|
||||
disabled={
|
||||
selectedResultRows.length === 0
|
||||
// ||
|
||||
// selectedResultRows.some((row) => row.prcsYn !== "2")
|
||||
selectedResultRows.length === 0 ||
|
||||
selectedResultRows.some((row) => row.prcsYn !== "2")
|
||||
}
|
||||
>
|
||||
반려
|
||||
|
||||
@@ -65,21 +65,15 @@ const Indicators: React.FC<IndicatorsProps> = ({ anlsSn, vrfcTrgtSn }) => {
|
||||
|
||||
return (
|
||||
<div className="card card-border !m-0 grid min-h-[10rem] grid-cols-4 gap-2">
|
||||
<IndicatorItem label="r 중위수" value={dtl?.rtCnterVl} />
|
||||
<IndicatorItem label="R 중위수" value={dtl?.rtCnterVl} />
|
||||
<IndicatorItem label="E 중위수" value={dtl?.lastTrsfScrCnterVl} />
|
||||
<IndicatorItem label="하위 기준값" value={dtl?.lastTrsfScrLlmtVl} />
|
||||
<IndicatorItem
|
||||
label="(E_m)E 중위수"
|
||||
value={dtl?.lastTrsfScrCnterVl}
|
||||
/>
|
||||
<IndicatorItem
|
||||
label="(E_l)E 하한값"
|
||||
value={dtl?.lastTrsfScrLlmtVl}
|
||||
/>
|
||||
<IndicatorItem
|
||||
label="(E_u)E 상한값"
|
||||
label="상위 기준값"
|
||||
value={dtl?.lastTrsfScrUplmtVl}
|
||||
/>
|
||||
<IndicatorItem label="E_D_L" value={dtl?.llmtVl} />
|
||||
<IndicatorItem label="E_D_U" value={dtl?.uplmtVl} />
|
||||
<IndicatorItem label="이상치 하한값" value={dtl?.llmtVl} />
|
||||
<IndicatorItem label="이상치 상한값" value={dtl?.uplmtVl} />
|
||||
<IndicatorItem label="이상치 하한선" value={dtl?.otlrLlmtLine} />
|
||||
<IndicatorItem label="이상치 상한선" value={dtl?.otlrUplmtLine} />
|
||||
</div>
|
||||
|
||||
@@ -46,11 +46,6 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
|
||||
selectedRowId,
|
||||
appliedFilter,
|
||||
}) => {
|
||||
const [selectedSido, setSelectedSido] = useState<string>("");
|
||||
const [selectedGugun, setSelectedGugun] = useState<string>("");
|
||||
const [selectedStatus, setSelectedStatus] = useState<string>("");
|
||||
const [showAllOnEmptyFilter, setShowAllOnEmptyFilter] =
|
||||
useState<boolean>(true);
|
||||
const [isDownloading, setIsDownloading] = useState<boolean>(false);
|
||||
|
||||
const [gridRowData, setGridRowData] = useState<
|
||||
@@ -58,7 +53,7 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
|
||||
>([]);
|
||||
|
||||
const { data: sidoData } = useStAnlsSidoCombo();
|
||||
const { data: gugunData } = useStAnlsSigunguCombo(selectedSido);
|
||||
const { data: gugunData } = useStAnlsSigunguCombo(appliedFilter.sido ?? "");
|
||||
const sidoOptions = sidoData ?? EMPTY_ADST_LIST;
|
||||
const gugunOptions = gugunData ?? EMPTY_ADST_LIST;
|
||||
|
||||
@@ -69,14 +64,7 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
|
||||
|
||||
const rowData = useMemo(() => {
|
||||
let data = (resultData?.content ?? []) as StAnlsRsltResponse[];
|
||||
|
||||
if (
|
||||
!appliedFilter.sido &&
|
||||
!appliedFilter.gugun &&
|
||||
!appliedFilter.status
|
||||
) {
|
||||
data = showAllOnEmptyFilter ? data : [];
|
||||
} else {
|
||||
{
|
||||
const sidoCd = appliedFilter.sido;
|
||||
if (sidoCd) {
|
||||
data = data.filter((item) =>
|
||||
@@ -116,32 +104,18 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
|
||||
}));
|
||||
|
||||
return mappedData;
|
||||
}, [resultData, appliedFilter, showAllOnEmptyFilter]);
|
||||
}, [resultData, appliedFilter]);
|
||||
|
||||
const handleSidoChange = (val: string) => {
|
||||
setSelectedSido(val);
|
||||
setSelectedGugun("");
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
onFilterChange({
|
||||
sido: selectedSido,
|
||||
gugun: selectedGugun,
|
||||
status: selectedStatus,
|
||||
});
|
||||
setShowAllOnEmptyFilter(false);
|
||||
onFilterChange({ ...appliedFilter, sido: val, gugun: "" });
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setSelectedSido("");
|
||||
setSelectedGugun("");
|
||||
setSelectedStatus("");
|
||||
onFilterChange({
|
||||
sido: "",
|
||||
gugun: "",
|
||||
status: "",
|
||||
});
|
||||
setShowAllOnEmptyFilter(true);
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
@@ -182,7 +156,7 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Select
|
||||
value={selectedSido || ""}
|
||||
value={appliedFilter.sido || ""}
|
||||
onValueChange={handleSidoChange}
|
||||
>
|
||||
<SelectTrigger className="w-[12rem] bg-white">
|
||||
@@ -197,9 +171,11 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
value={selectedGugun}
|
||||
onValueChange={setSelectedGugun}
|
||||
disabled={!selectedSido}
|
||||
value={appliedFilter.gugun || ""}
|
||||
onValueChange={(val) =>
|
||||
onFilterChange({ ...appliedFilter, gugun: val })
|
||||
}
|
||||
disabled={!appliedFilter.sido}
|
||||
>
|
||||
<SelectTrigger className="w-[12rem] bg-white">
|
||||
<SelectValue placeholder="시군구 선택" />
|
||||
@@ -213,8 +189,10 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
value={selectedStatus}
|
||||
onValueChange={setSelectedStatus}
|
||||
value={appliedFilter.status || ""}
|
||||
onValueChange={(val) =>
|
||||
onFilterChange({ ...appliedFilter, status: val })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[10rem] bg-white">
|
||||
<SelectValue placeholder="상태 선택" />
|
||||
@@ -231,7 +209,7 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
|
||||
<Button color="white" onClick={handleReset}>
|
||||
초기화
|
||||
</Button>
|
||||
<Button onClick={handleSearch}>조회</Button>
|
||||
{/* <Button onClick={handleSearch}>조회</Button> */}
|
||||
<Button
|
||||
color="green"
|
||||
onClick={handleDownload}
|
||||
|
||||
@@ -104,10 +104,12 @@ const ScatterChart: React.FC<ScatterChartProps> = ({
|
||||
anlsSn,
|
||||
vrfcTrgtSn,
|
||||
);
|
||||
console.log("resultData: ", resultData);
|
||||
const { data: chartData, isLoading: isLoadingChart } = useStAnlsChartData(
|
||||
anlsSn,
|
||||
vrfcTrgtSn,
|
||||
);
|
||||
console.log("chartData: ", chartData);
|
||||
|
||||
const isLoading = isLoadingGrid || isLoadingChart;
|
||||
|
||||
@@ -158,10 +160,12 @@ const ScatterChart: React.FC<ScatterChartProps> = ({
|
||||
item,
|
||||
};
|
||||
|
||||
if (item.otlrYnCd === "0") {
|
||||
normalPoints.push(pt);
|
||||
} else {
|
||||
if (item.otlrYnCd === "2") {
|
||||
return;
|
||||
} else if (item.otlrYnCd === "1") {
|
||||
outlierPoints.push(pt);
|
||||
} else {
|
||||
normalPoints.push(pt);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type { RecentPostSearchParams } from "../model/post-interface";
|
||||
|
||||
import { postApi } from "./post-service";
|
||||
|
||||
export function useRecentPostList(params?: RecentPostSearchParams) {
|
||||
return useQuery({
|
||||
queryKey: createRecentPostListQueryKey(params),
|
||||
queryFn: () => postApi.getRecentPostList(params),
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
|
||||
function createRecentPostListQueryKey(params?: RecentPostSearchParams) {
|
||||
return [
|
||||
ENDPOINT.BOARD_SERVICE.POSTS,
|
||||
"recentPosts",
|
||||
params?.page ?? "",
|
||||
params?.size ?? "",
|
||||
params?.sort ?? "",
|
||||
params?.keywordType ?? "",
|
||||
params?.keyword ?? "",
|
||||
params?.bbsId ?? "",
|
||||
params?.delYn ?? "",
|
||||
params?.frDateYmd ?? "",
|
||||
params?.toDateYmd ?? "",
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type {
|
||||
PostService,
|
||||
RecentPostListResponse,
|
||||
RecentPostSearchParams,
|
||||
} from "../model/post-interface";
|
||||
|
||||
async function getRecentPostList(
|
||||
params?: RecentPostSearchParams,
|
||||
): Promise<RecentPostListResponse> {
|
||||
const response = await client.get<RecentPostListResponse>(
|
||||
`${ENDPOINT.BOARD_SERVICE.POSTS}/recentPosts`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const postApi: PostService = {
|
||||
getRecentPostList,
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
export { useRecentPostList } from "./api/post-queries";
|
||||
export { postApi } from "./api/post-service";
|
||||
export {
|
||||
type PostService,
|
||||
type PstRecentListResponse,
|
||||
type RecentPostListResponse,
|
||||
type RecentPostSearchParams,
|
||||
} from "./model/post-interface";
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
export interface RecentPostSearchParams extends PageableParams {
|
||||
keywordType?: string;
|
||||
keyword?: string;
|
||||
bbsId?: number;
|
||||
delYn?: string;
|
||||
frDateYmd?: string;
|
||||
toDateYmd?: string;
|
||||
}
|
||||
|
||||
export interface PstRecentListResponse {
|
||||
rowNo: number;
|
||||
bbsId: number;
|
||||
pstNo: number;
|
||||
pstTtl: string;
|
||||
pstCn: string;
|
||||
inqNocs: number;
|
||||
rfrncUrl: string;
|
||||
popupYn: string;
|
||||
upndFixYn: string;
|
||||
prdPstgYn: string;
|
||||
rootPath: string;
|
||||
pstPath: string;
|
||||
ansPstn: number;
|
||||
wrtrNm: string;
|
||||
wrtrCplc: string;
|
||||
atflCd: string;
|
||||
regDt: string;
|
||||
rgtrNm: string;
|
||||
cmntCnt: number;
|
||||
bbsTypeCd: string;
|
||||
bbsNm: string;
|
||||
}
|
||||
|
||||
export type RecentPostListResponse = PageableList<PstRecentListResponse>;
|
||||
|
||||
export interface PostService {
|
||||
getRecentPostList(
|
||||
params?: RecentPostSearchParams,
|
||||
): Promise<RecentPostListResponse>;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type { ScheduleSearchParams } from "../model/schedule-interface";
|
||||
|
||||
import { scheduleApi } from "./schedule-service";
|
||||
|
||||
export function useSchedules(params?: ScheduleSearchParams) {
|
||||
return useQuery({
|
||||
queryKey: createSchedulesQueryKey(params),
|
||||
queryFn: () => scheduleApi.getSchedules(params),
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
|
||||
function createSchedulesQueryKey(params?: ScheduleSearchParams) {
|
||||
return [
|
||||
ENDPOINT.CMS_SERVICE.SCHEDULES,
|
||||
"list",
|
||||
params?.siteId ?? "",
|
||||
params?.schdlTtl ?? "",
|
||||
params?.schdlSeCd ?? "",
|
||||
params?.schdlBgngYmd ?? "",
|
||||
params?.schdlEndYmd ?? "",
|
||||
params?.reptYn ?? "",
|
||||
params?.commSchdlYn ?? "",
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type {
|
||||
Schedule,
|
||||
ScheduleSearchParams,
|
||||
ScheduleService,
|
||||
} from "../model/schedule-interface";
|
||||
|
||||
async function getSchedules(
|
||||
params?: ScheduleSearchParams,
|
||||
): Promise<Schedule[]> {
|
||||
const response = await client.get<Schedule[]>(
|
||||
ENDPOINT.CMS_SERVICE.SCHEDULES,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const scheduleApi: ScheduleService = {
|
||||
getSchedules,
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
export { useSchedules } from "./api/schedule-queries";
|
||||
export { scheduleApi } from "./api/schedule-service";
|
||||
export {
|
||||
type Schedule,
|
||||
type ScheduleSearchParams,
|
||||
type ScheduleService,
|
||||
} from "./model/schedule-interface";
|
||||
@@ -0,0 +1,27 @@
|
||||
export interface ScheduleSearchParams {
|
||||
siteId?: string;
|
||||
schdlTtl?: string;
|
||||
schdlSeCd?: string;
|
||||
schdlBgngYmd?: string;
|
||||
schdlEndYmd?: string;
|
||||
reptYn?: string;
|
||||
commSchdlYn?: string;
|
||||
}
|
||||
|
||||
export interface Schedule {
|
||||
schdlBgngDt: string;
|
||||
schdlEndDt: string;
|
||||
actualDate: string;
|
||||
schdlSn: number;
|
||||
siteId: string;
|
||||
schdlTtl: string;
|
||||
schdlSeCd: string;
|
||||
schdlCn: string;
|
||||
reptYn: string;
|
||||
commSchdlYn: string;
|
||||
atflCd: string;
|
||||
}
|
||||
|
||||
export interface ScheduleService {
|
||||
getSchedules(params?: ScheduleSearchParams): Promise<Schedule[]>;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type { DashboardSearchParams } from "../../model/dashboard/dashboard-interface";
|
||||
|
||||
import { dashboardApi } from "./dashboard-service";
|
||||
|
||||
// 이상치 선별보고서 조회
|
||||
export function useDashboardVrfcReport(params: DashboardSearchParams | null) {
|
||||
return useQuery({
|
||||
queryKey: createDashboardQueryKey("vrfc-report", params),
|
||||
queryFn: async () => {
|
||||
if (!params)
|
||||
throw new Error("Dashboard search params are required.");
|
||||
const response = await dashboardApi.getVrfcReport(params);
|
||||
return response.data;
|
||||
},
|
||||
enabled: hasRequiredDashboardParams(params),
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
|
||||
// 검증오류 수 조회
|
||||
export function useDashboardVrfcErrCount(params: DashboardSearchParams | null) {
|
||||
return useQuery({
|
||||
queryKey: createDashboardQueryKey("vrfc-err-count", params),
|
||||
queryFn: async () => {
|
||||
if (!params)
|
||||
throw new Error("Dashboard search params are required.");
|
||||
const response = await dashboardApi.getVrfcErrCount(params);
|
||||
return response.data;
|
||||
},
|
||||
enabled: hasRequiredDashboardParams(params),
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
|
||||
// 미제출 수 & 목록 조회
|
||||
export function useDashboardUnSubmitCount(
|
||||
params: DashboardSearchParams | null,
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: createDashboardQueryKey("un-submit-count", params),
|
||||
queryFn: async () => {
|
||||
if (!params)
|
||||
throw new Error("Dashboard search params are required.");
|
||||
const response = await dashboardApi.getUnSubmitCount(params);
|
||||
return response.data;
|
||||
},
|
||||
enabled: hasRequiredDashboardParams(params),
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
|
||||
// 통계입력현황 조회
|
||||
export function useDashboardStatsSubmissionRate(
|
||||
params: DashboardSearchParams | null,
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: createDashboardQueryKey("stats-submission-rate", params),
|
||||
queryFn: async () => {
|
||||
if (!params)
|
||||
throw new Error("Dashboard search params are required.");
|
||||
const response = await dashboardApi.getStatsSubmissionRate(params);
|
||||
return response.data;
|
||||
},
|
||||
enabled: hasRequiredDashboardParams(params),
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
|
||||
// 예외신청 수 조회
|
||||
export function useDashboardEcptReqCount(params: DashboardSearchParams | null) {
|
||||
return useQuery({
|
||||
queryKey: createDashboardQueryKey("ecpt-req-count", params),
|
||||
queryFn: async () => {
|
||||
if (!params)
|
||||
throw new Error("Dashboard search params are required.");
|
||||
const response = await dashboardApi.getEcptReqCount(params);
|
||||
return response.data;
|
||||
},
|
||||
enabled: hasRequiredDashboardParams(params),
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
|
||||
function createDashboardQueryKey(
|
||||
name: string,
|
||||
params: DashboardSearchParams | null,
|
||||
) {
|
||||
return [
|
||||
ENDPOINT.ST_SERVICE.DASHBOARD,
|
||||
name,
|
||||
params?.statsYr ?? "",
|
||||
params?.dtlInstCd ?? "",
|
||||
params?.enoCd ?? "",
|
||||
params?.laeSeCd ?? "",
|
||||
];
|
||||
}
|
||||
|
||||
function hasRequiredDashboardParams(
|
||||
params: DashboardSearchParams | null,
|
||||
): params is DashboardSearchParams {
|
||||
return Boolean(params?.statsYr && params.dtlInstCd);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { stClient } from "@/shared/api/client";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type {
|
||||
DashboardApiResponse,
|
||||
DashboardSearchParams,
|
||||
DashboardService,
|
||||
EcptReqCountResponse,
|
||||
StatsSubmissionResponse,
|
||||
UnSubmitCountResponse,
|
||||
VrfcErrCountResponse,
|
||||
VrfcReportResponse,
|
||||
} from "../../model/dashboard/dashboard-interface";
|
||||
|
||||
async function getVrfcReport(
|
||||
params: DashboardSearchParams,
|
||||
): Promise<DashboardApiResponse<VrfcReportResponse>> {
|
||||
const response = await stClient.get<
|
||||
DashboardApiResponse<VrfcReportResponse>
|
||||
>(`${ENDPOINT.ST_SERVICE.DASHBOARD}/vrfc-report`, { params });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function getVrfcErrCount(
|
||||
params: DashboardSearchParams,
|
||||
): Promise<DashboardApiResponse<VrfcErrCountResponse>> {
|
||||
const response = await stClient.get<
|
||||
DashboardApiResponse<VrfcErrCountResponse>
|
||||
>(`${ENDPOINT.ST_SERVICE.DASHBOARD}/vrfc-err-count`, { params });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function getUnSubmitCount(
|
||||
params: DashboardSearchParams,
|
||||
): Promise<DashboardApiResponse<UnSubmitCountResponse>> {
|
||||
const response = await stClient.get<
|
||||
DashboardApiResponse<UnSubmitCountResponse>
|
||||
>(`${ENDPOINT.ST_SERVICE.DASHBOARD}/un-submit-count`, { params });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function getStatsSubmissionRate(
|
||||
params: DashboardSearchParams,
|
||||
): Promise<DashboardApiResponse<StatsSubmissionResponse>> {
|
||||
const response = await stClient.get<
|
||||
DashboardApiResponse<StatsSubmissionResponse>
|
||||
>(`${ENDPOINT.ST_SERVICE.DASHBOARD}/stats-submission-rate`, { params });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function getEcptReqCount(
|
||||
params: DashboardSearchParams,
|
||||
): Promise<DashboardApiResponse<EcptReqCountResponse>> {
|
||||
const response = await stClient.get<
|
||||
DashboardApiResponse<EcptReqCountResponse>
|
||||
>(`${ENDPOINT.ST_SERVICE.DASHBOARD}/ecpt-req-count`, { params });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const dashboardApi: DashboardService = {
|
||||
getVrfcReport,
|
||||
getVrfcErrCount,
|
||||
getUnSubmitCount,
|
||||
getStatsSubmissionRate,
|
||||
getEcptReqCount,
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type { ScheduleInfoSearchParams } from "../../model/dashboard/schedule-info-interface";
|
||||
|
||||
import { scheduleInfoApi } from "./schedule-info-service";
|
||||
|
||||
export function useScheduleInfo(params: ScheduleInfoSearchParams | null) {
|
||||
return useQuery({
|
||||
queryKey: createScheduleInfoQueryKey(params),
|
||||
queryFn: async () => {
|
||||
if (!params)
|
||||
throw new Error("Schedule info search params are required.");
|
||||
const response = await scheduleInfoApi.getScheduleInfo(params);
|
||||
return response.data;
|
||||
},
|
||||
enabled: hasRequiredScheduleInfoParams(params),
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
|
||||
function createScheduleInfoQueryKey(params: ScheduleInfoSearchParams | null) {
|
||||
return [
|
||||
ENDPOINT.ST_SERVICE.DASHBOARD,
|
||||
"schedule-info",
|
||||
params?.statsYr ?? "",
|
||||
];
|
||||
}
|
||||
|
||||
function hasRequiredScheduleInfoParams(
|
||||
params: ScheduleInfoSearchParams | null,
|
||||
): params is ScheduleInfoSearchParams {
|
||||
return Boolean(params?.statsYr);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { stClient } from "@/shared/api/client";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type {
|
||||
ScheduleInfoApiResponse,
|
||||
ScheduleInfoResponse,
|
||||
ScheduleInfoSearchParams,
|
||||
ScheduleInfoService,
|
||||
} from "../../model/dashboard/schedule-info-interface";
|
||||
|
||||
async function getScheduleInfo(
|
||||
params: ScheduleInfoSearchParams,
|
||||
): Promise<ScheduleInfoApiResponse<ScheduleInfoResponse>> {
|
||||
const response = await stClient.get<
|
||||
ScheduleInfoApiResponse<ScheduleInfoResponse>
|
||||
>(`${ENDPOINT.ST_SERVICE.DASHBOARD}/schedule-info`, { params });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const scheduleInfoApi: ScheduleInfoService = {
|
||||
getScheduleInfo,
|
||||
};
|
||||
@@ -14,11 +14,6 @@ import { stAnlsRsltApi } from "./statistics-analysis-result-api";
|
||||
|
||||
const DETAIL_PAGE_SIZE = 100;
|
||||
|
||||
interface StatisticsVerificationDetailSubmittedParams {
|
||||
main: StatisticsVerificationFilterState;
|
||||
sub: StatisticsVerificationSubFilterState;
|
||||
}
|
||||
|
||||
export const useStAnlsSidoCombo = () => {
|
||||
return useQuery({
|
||||
queryKey: [ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST, "sido"],
|
||||
@@ -102,23 +97,24 @@ export const useStatsVrfcRptData = (
|
||||
};
|
||||
|
||||
export const useStatsVrfcRsltData = (
|
||||
submittedDetailParams: StatisticsVerificationDetailSubmittedParams | null,
|
||||
params: StatisticsVerificationSubFilterState | null,
|
||||
) => {
|
||||
return useInfiniteQuery({
|
||||
queryKey: [
|
||||
ENDPOINT.ST_SERVICE.INPUTVERIFY,
|
||||
"verify-results",
|
||||
submittedDetailParams?.main.statsYr ?? "",
|
||||
submittedDetailParams?.main.ctpvNm ?? "",
|
||||
submittedDetailParams?.main.sggNm ?? "",
|
||||
submittedDetailParams?.main.sewerBzmnCd ?? "",
|
||||
submittedDetailParams?.main.statsCd ?? "",
|
||||
submittedDetailParams?.sub.prcsYnList ?? "",
|
||||
submittedDetailParams?.sub.pbcpAprvYn ?? "",
|
||||
params?.statsYr ?? "",
|
||||
params?.ctpvNm ?? "",
|
||||
params?.sggNm ?? "",
|
||||
params?.sewerBzmnCd ?? "",
|
||||
params?.statsCd ?? "",
|
||||
params?.prcsYnList ?? "",
|
||||
params?.pbcpAprvYn ?? "",
|
||||
params?.laeSbmsnSttsCd ?? "",
|
||||
],
|
||||
initialPageParam: 1,
|
||||
queryFn: async ({ pageParam }) => {
|
||||
if (!submittedDetailParams)
|
||||
if (!params)
|
||||
return {
|
||||
verifyResultList: [],
|
||||
pageInfo: {
|
||||
@@ -134,13 +130,14 @@ export const useStatsVrfcRsltData = (
|
||||
page: pageParam,
|
||||
size: DETAIL_PAGE_SIZE,
|
||||
searchDto: {
|
||||
statsYr: submittedDetailParams.main.statsYr,
|
||||
ctpvNm: submittedDetailParams.main.ctpvNm,
|
||||
sggNm: submittedDetailParams.main.sggNm,
|
||||
sewerBzmnCd: submittedDetailParams.main.sewerBzmnCd,
|
||||
statsCd: submittedDetailParams.main.statsCd,
|
||||
prcsYnList: submittedDetailParams.sub.prcsYnList,
|
||||
pbcpAprvYn: submittedDetailParams.sub.pbcpAprvYn,
|
||||
statsYr: params.statsYr,
|
||||
ctpvNm: params.ctpvNm,
|
||||
sggNm: params.sggNm,
|
||||
sewerBzmnCd: params.sewerBzmnCd,
|
||||
statsCd: params.statsCd,
|
||||
prcsYnList: params.prcsYnList,
|
||||
pbcpAprvYn: params.pbcpAprvYn,
|
||||
laeSbmsnSttsCd: params.laeSbmsnSttsCd,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -153,7 +150,7 @@ export const useStatsVrfcRsltData = (
|
||||
if (currentPage + 1 <= totalPages) return currentPage + 1;
|
||||
return undefined;
|
||||
},
|
||||
enabled: Boolean(submittedDetailParams),
|
||||
enabled: Boolean(params),
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
export {
|
||||
useDashboardEcptReqCount,
|
||||
useDashboardStatsSubmissionRate,
|
||||
useDashboardUnSubmitCount,
|
||||
useDashboardVrfcErrCount,
|
||||
useDashboardVrfcReport,
|
||||
} from "./api/dashboard/dashboard-queries";
|
||||
export { dashboardApi } from "./api/dashboard/dashboard-service";
|
||||
export { useScheduleInfo } from "./api/dashboard/schedule-info-queries";
|
||||
export { scheduleInfoApi } from "./api/dashboard/schedule-info-service";
|
||||
export {
|
||||
useStAnlsSidoCombo,
|
||||
useStAnlsSigunguCombo,
|
||||
@@ -14,6 +24,24 @@ export {
|
||||
type JobStatusDto,
|
||||
type OutlierRecordDto,
|
||||
} from "./model/analysis/analysis-interface";
|
||||
export {
|
||||
type DashboardApiResponse,
|
||||
type DashboardSearchParams,
|
||||
type DashboardService,
|
||||
type EcptReqCountResponse,
|
||||
type StatsSubmissionResponse,
|
||||
type UnSubmitCountResponse,
|
||||
type UnSubmitStatsResponse,
|
||||
type VrfcErrCountResponse,
|
||||
type VrfcReportResponse,
|
||||
type VrfcReportSelect,
|
||||
} from "./model/dashboard/dashboard-interface";
|
||||
export {
|
||||
type ScheduleInfoApiResponse,
|
||||
type ScheduleInfoResponse,
|
||||
type ScheduleInfoSearchParams,
|
||||
type ScheduleInfoService,
|
||||
} from "./model/dashboard/schedule-info-interface";
|
||||
export { type VerifyResultItem } from "./model/general-verification-interface";
|
||||
export {
|
||||
IMR_DETAIL_VISIBLE_COLUMNS_BY_STATS_CD,
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
export interface DashboardSearchParams {
|
||||
statsYr: string;
|
||||
dtlInstCd: string;
|
||||
enoCd?: string;
|
||||
laeSeCd?: string;
|
||||
}
|
||||
|
||||
export interface DashboardApiResponse<T> {
|
||||
status: string;
|
||||
message: string;
|
||||
data: T;
|
||||
code: string;
|
||||
httpStatus: number;
|
||||
}
|
||||
|
||||
export interface VrfcReportResponse {
|
||||
typeCounts: Record<string, number>;
|
||||
list: VrfcReportSelect[];
|
||||
}
|
||||
|
||||
export interface VrfcReportSelect {
|
||||
vrfcCd: string;
|
||||
statsNm: string;
|
||||
rsnExpln: string;
|
||||
admdstNm: string;
|
||||
nowVlCn: string;
|
||||
compVlCn: string;
|
||||
}
|
||||
|
||||
export interface VrfcErrCountResponse {
|
||||
statsYr: string;
|
||||
vrfcErrCount: number;
|
||||
}
|
||||
|
||||
export interface UnSubmitCountResponse {
|
||||
statsYr: string;
|
||||
unSubmitCount: number;
|
||||
unSubmitList: UnSubmitStatsResponse[];
|
||||
}
|
||||
|
||||
export interface UnSubmitStatsResponse {
|
||||
statsCd: string;
|
||||
statsNm: string;
|
||||
}
|
||||
|
||||
export interface StatsSubmissionResponse {
|
||||
totalCount: number;
|
||||
submittedCount: number;
|
||||
submissionRate: number;
|
||||
statusLevel: string;
|
||||
}
|
||||
|
||||
export interface EcptReqCountResponse {
|
||||
statsYr: string;
|
||||
ecptReqCount: number;
|
||||
}
|
||||
|
||||
export interface DashboardService {
|
||||
getVrfcReport(
|
||||
params: DashboardSearchParams,
|
||||
): Promise<DashboardApiResponse<VrfcReportResponse>>;
|
||||
getVrfcErrCount(
|
||||
params: DashboardSearchParams,
|
||||
): Promise<DashboardApiResponse<VrfcErrCountResponse>>;
|
||||
getUnSubmitCount(
|
||||
params: DashboardSearchParams,
|
||||
): Promise<DashboardApiResponse<UnSubmitCountResponse>>;
|
||||
getStatsSubmissionRate(
|
||||
params: DashboardSearchParams,
|
||||
): Promise<DashboardApiResponse<StatsSubmissionResponse>>;
|
||||
getEcptReqCount(
|
||||
params: DashboardSearchParams,
|
||||
): Promise<DashboardApiResponse<EcptReqCountResponse>>;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
export interface ScheduleInfoSearchParams {
|
||||
statsYr: string;
|
||||
}
|
||||
|
||||
export interface ScheduleInfoApiResponse<T> {
|
||||
status: string;
|
||||
message: string;
|
||||
data: T;
|
||||
code: string;
|
||||
httpStatus: number;
|
||||
}
|
||||
|
||||
export interface ScheduleInfoResponse {
|
||||
statsYr: string;
|
||||
inptSeCd: number;
|
||||
inptBgngYmd: string;
|
||||
inptEndYmd: string;
|
||||
vrfcSeCd: number;
|
||||
vrfcBgngYmd: string;
|
||||
vrfcEndYmd: string;
|
||||
currentSe: string;
|
||||
dday: string;
|
||||
ddayLabel: string;
|
||||
}
|
||||
|
||||
export interface ScheduleInfoService {
|
||||
getScheduleInfo(
|
||||
params: ScheduleInfoSearchParams,
|
||||
): Promise<ScheduleInfoApiResponse<ScheduleInfoResponse>>;
|
||||
}
|
||||
@@ -196,11 +196,12 @@ export interface CopyVerifyDataRequest {
|
||||
}
|
||||
|
||||
export interface VerifyResultSearchRequest {
|
||||
sewerBzmnCd?: string | null;
|
||||
ctpvNm?: string | null;
|
||||
sggNm?: string | null;
|
||||
sewerBzmnCd?: string | null;
|
||||
statsYr: string;
|
||||
statsCd?: string | null;
|
||||
laeSbmsnSttsCd?: string | null;
|
||||
vrfcMthdCd?: string;
|
||||
prcsYnList?: string; // 검증 상태 리스트 (0: 미검증, 1: 수정완료, 2:예외통과, 3: 반려)
|
||||
pbcpAprvYn?: string; // 공단 승인 여부 (Y/N)
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface StatsInputStatusListRequestParams {
|
||||
sewerBzmnCd?: string | null;
|
||||
statsYr: string;
|
||||
statsCd?: string | null;
|
||||
laeSbmsnSttsCd?: string | null;
|
||||
}
|
||||
|
||||
export interface StatsInputStatusList {
|
||||
|
||||
@@ -47,7 +47,7 @@ export const regionalGridColumnInfo: ColumnInfo = {
|
||||
{
|
||||
name: "chgRt",
|
||||
fieldName: "chgRt",
|
||||
header: { text: "R" },
|
||||
header: { text: "전년대비 비율" },
|
||||
width: 60,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0.00",
|
||||
@@ -55,7 +55,7 @@ export const regionalGridColumnInfo: ColumnInfo = {
|
||||
{
|
||||
name: "trsfRt",
|
||||
fieldName: "trsfRt",
|
||||
header: { text: "S" },
|
||||
header: { text: "S_i" },
|
||||
width: 60,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0.00",
|
||||
@@ -63,7 +63,7 @@ export const regionalGridColumnInfo: ColumnInfo = {
|
||||
{
|
||||
name: "otlrTrsfRt",
|
||||
fieldName: "otlrTrsfRt",
|
||||
header: { text: "E" },
|
||||
header: { text: "기준값" },
|
||||
width: 60,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0.00",
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import {
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from "@dnd-kit/core";
|
||||
import { sortableKeyboardCoordinates } from "@dnd-kit/sortable";
|
||||
|
||||
export function useDndSensors() {
|
||||
return useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import type { DragEndEvent } from "@dnd-kit/core";
|
||||
import { arrayMove } from "@dnd-kit/sortable";
|
||||
|
||||
import type { PanelItem } from "../../model/types";
|
||||
|
||||
interface UsePanelOrderProps {
|
||||
initialPanels: PanelItem[];
|
||||
storageKey: string;
|
||||
}
|
||||
|
||||
export function usePanelOrder({
|
||||
initialPanels,
|
||||
storageKey,
|
||||
}: UsePanelOrderProps) {
|
||||
const [panels, setPanels] = useState<PanelItem[]>(initialPanels);
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialPanels.length === 0 || isInitialized) return;
|
||||
|
||||
const saved = localStorage.getItem(storageKey);
|
||||
if (saved) {
|
||||
try {
|
||||
const savedIds = JSON.parse(saved);
|
||||
const restoredPanels = savedIds
|
||||
.map((id: string) => initialPanels.find((p) => p.id === id))
|
||||
.filter(Boolean);
|
||||
if (restoredPanels.length === initialPanels.length) {
|
||||
setPanels(restoredPanels);
|
||||
}
|
||||
} catch {
|
||||
setPanels(initialPanels);
|
||||
}
|
||||
} else {
|
||||
setPanels(initialPanels);
|
||||
}
|
||||
setIsInitialized(true);
|
||||
}, [storageKey, initialPanels, isInitialized]);
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (over && active.id !== over.id) {
|
||||
setPanels((items) => {
|
||||
const oldIndex = items.findIndex(
|
||||
(item) => item.id === active.id,
|
||||
);
|
||||
const newIndex = items.findIndex((item) => item.id === over.id);
|
||||
const newItems = arrayMove(items, oldIndex, newIndex);
|
||||
|
||||
localStorage.setItem(
|
||||
storageKey,
|
||||
JSON.stringify(newItems.map((item) => item.id)),
|
||||
);
|
||||
|
||||
return newItems;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return { panels, handleDragEnd };
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { DashboardSearchParams } from "@/entities/statistics/model/dashboard/dashboard-interface";
|
||||
import type { ScheduleInfoSearchParams } from "@/entities/statistics/model/dashboard/schedule-info-interface";
|
||||
|
||||
import { NoticePanel } from "../ui/panels/notice-panel";
|
||||
import { ScheduleCalendarPanel } from "../ui/panels/schedule-calendar-panel";
|
||||
import { ScheduleInfoPanel } from "../ui/panels/schedule-info-panel";
|
||||
import { StatsSubmissionPanel } from "../ui/panels/stats-submission-panel";
|
||||
import { UnSubmitPanel } from "../ui/panels/unsubmit-panel";
|
||||
import { VrfcErrPanel } from "../ui/panels/vrfc-err-panel";
|
||||
import { VrfcReportPanel } from "../ui/panels/vrfc-report-panel";
|
||||
|
||||
import type { PanelItem } from "./types";
|
||||
|
||||
interface GetInitialPanelsParams {
|
||||
dashboardParams: DashboardSearchParams | null;
|
||||
scheduleInfoParams: ScheduleInfoSearchParams | null;
|
||||
}
|
||||
|
||||
export function getInitialPanels({
|
||||
dashboardParams,
|
||||
scheduleInfoParams,
|
||||
}: GetInitialPanelsParams): PanelItem[] {
|
||||
return [
|
||||
{
|
||||
id: "panel-a",
|
||||
component: <StatsSubmissionPanel params={dashboardParams} />,
|
||||
className: "",
|
||||
},
|
||||
{
|
||||
id: "panel-b",
|
||||
component: <ScheduleInfoPanel params={scheduleInfoParams} />,
|
||||
className: "",
|
||||
},
|
||||
{
|
||||
id: "panel-c",
|
||||
component: <NoticePanel />,
|
||||
className: "",
|
||||
},
|
||||
{
|
||||
id: "panel-d",
|
||||
component: <VrfcErrPanel params={dashboardParams} />,
|
||||
className: "",
|
||||
},
|
||||
{
|
||||
id: "panel-e",
|
||||
component: <ScheduleCalendarPanel />,
|
||||
className: "",
|
||||
},
|
||||
{
|
||||
id: "panel-f",
|
||||
component: <VrfcReportPanel params={dashboardParams} />,
|
||||
className: "",
|
||||
},
|
||||
{
|
||||
id: "panel-g",
|
||||
component: <UnSubmitPanel params={dashboardParams} />,
|
||||
className: "",
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface PanelItem {
|
||||
id: string;
|
||||
component: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { createColumnHelper, getCoreRowModel } from "@tanstack/react-table";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { type PstRecentListResponse, useRecentPostList } from "@/entities/post";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
import { useDragHandle } from "../sortable-panel";
|
||||
|
||||
export function NoticePanel() {
|
||||
const { dragHandleProps, isDragging } = useDragHandle();
|
||||
const router = useRouter();
|
||||
|
||||
const recentPostListQuery = useRecentPostList({
|
||||
page: 0,
|
||||
size: 3,
|
||||
keywordType: "bbsTypeCd",
|
||||
keyword: "BBSNOTICE",
|
||||
});
|
||||
|
||||
const handleRowClick = (post: PstRecentListResponse) => {
|
||||
router.push(
|
||||
`/cms/${post.bbsTypeCd.toLowerCase()}/${post.bbsId}/${post.pstNo}`,
|
||||
);
|
||||
};
|
||||
|
||||
const handleMoreClick = () => {
|
||||
router.push("/cms/bbsnotice/7");
|
||||
};
|
||||
|
||||
const { tableComponent } = useDataTable({
|
||||
query: recentPostListQuery,
|
||||
tableOptions: {
|
||||
columns: noticeColumns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
},
|
||||
dataTableOptions: {
|
||||
variant: "line",
|
||||
onRowClick: handleRowClick,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !m-0 h-full">
|
||||
<div
|
||||
className={`card-header cursor-grab active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
|
||||
{...dragHandleProps}
|
||||
title="헤더를 드래그하여 패널 순서 변경"
|
||||
>
|
||||
<h3 className="text-lg font-semibold">공지사항</h3>
|
||||
<Button onClick={handleMoreClick} className="h-8">
|
||||
더보기
|
||||
</Button>
|
||||
</div>
|
||||
{tableComponent}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<PstRecentListResponse>();
|
||||
|
||||
const noticeColumns = [
|
||||
columnHelper.accessor("pstTtl", {
|
||||
header: "제목",
|
||||
cell: ({ row }) => (
|
||||
<div className="py-2">
|
||||
<div className="truncate font-medium">
|
||||
{row.original.pstTtl}
|
||||
</div>
|
||||
<div className="mt-1 flex justify-between text-sm text-muted-foreground">
|
||||
<span>{row.original.wrtrNm}</span>
|
||||
<span>{formatDate(row.original.regDt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}),
|
||||
];
|
||||
|
||||
function formatDate(value: string) {
|
||||
return dayjs(value).format("YYYY-MM-DD");
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
"use client";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { SITE_ID } from "@/shared/config";
|
||||
import { useSchedules } from "@/entities/schedule";
|
||||
|
||||
import { useDragHandle } from "../sortable-panel";
|
||||
|
||||
// FullCalendar는 클라이언트 전용이므로 dynamic import
|
||||
const FullCalendar = dynamic(() => import("@fullcalendar/react"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export function ScheduleCalendarPanel() {
|
||||
const { dragHandleProps, isDragging } = useDragHandle();
|
||||
|
||||
// TBD: 초기 날짜 범위 설정 로직
|
||||
const today = dayjs();
|
||||
const startOfMonth = today.startOf("month").format("YYYY-MM-DD");
|
||||
const endOfMonth = today.endOf("month").format("YYYY-MM-DD");
|
||||
|
||||
const { data, isLoading } = useSchedules({
|
||||
siteId: SITE_ID,
|
||||
schdlBgngYmd: startOfMonth,
|
||||
schdlEndYmd: endOfMonth,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !m-0 h-full">
|
||||
<div
|
||||
className={`card-header cursor-grab active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
|
||||
{...dragHandleProps}
|
||||
title="헤더를 드래그하여 패널 순서 변경"
|
||||
>
|
||||
<h3 className="text-lg font-semibold">일정관리</h3>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{isLoading && (
|
||||
<div className="py-8 text-center">로딩 중...</div>
|
||||
)}
|
||||
{data && (
|
||||
<div className="h-[400px]">
|
||||
{/* TBD: FullCalendar 상세 설정 (locale, plugins, 이벤트 색상 등) */}
|
||||
<FullCalendar
|
||||
plugins={[dayGridPlugin, timeGridPlugin]}
|
||||
initialView="dayGridMonth"
|
||||
locale="ko"
|
||||
headerToolbar={{
|
||||
left: "prev,next",
|
||||
center: "title",
|
||||
right: "dayGridMonth,timeGridWeek",
|
||||
}}
|
||||
buttonText={{
|
||||
month: "월",
|
||||
week: "주",
|
||||
}}
|
||||
events={data.map((schedule) => ({
|
||||
id: schedule.schdlSn.toString(),
|
||||
title: schedule.schdlTtl,
|
||||
start: schedule.schdlBgngDt,
|
||||
end: schedule.schdlEndDt,
|
||||
// TBD: allDay 판단 기준 (1439분 이상?)
|
||||
allDay: true,
|
||||
}))}
|
||||
// TBD: 이벤트 클릭 핸들러 (현재 SPEC에서 제거됨)
|
||||
height="100%"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
|
||||
import type { ScheduleInfoSearchParams } from "@/entities/statistics";
|
||||
import { useScheduleInfo } from "@/entities/statistics";
|
||||
|
||||
import { useDragHandle } from "../sortable-panel";
|
||||
|
||||
interface ScheduleInfoPanelProps {
|
||||
params: ScheduleInfoSearchParams | null;
|
||||
}
|
||||
|
||||
export function ScheduleInfoPanel({ params }: ScheduleInfoPanelProps) {
|
||||
const { dragHandleProps, isDragging } = useDragHandle();
|
||||
const { data, isLoading, error } = useScheduleInfo(params);
|
||||
|
||||
return (
|
||||
<div className="card card-border !m-0 h-full">
|
||||
<div
|
||||
className={`card-header cursor-grab active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
|
||||
{...dragHandleProps}
|
||||
title="헤더를 드래그하여 패널 순서 변경"
|
||||
>
|
||||
<h3 className="text-lg font-semibold">일정안내</h3>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{isLoading && (
|
||||
<div className="py-8 text-center">로딩 중...</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="py-8 text-center text-red-500">
|
||||
데이터를 불러올 수 없습니다.
|
||||
</div>
|
||||
)}
|
||||
{data && (
|
||||
<div className="space-y-4">
|
||||
{/* D-day 표시 */}
|
||||
<div className="rounded-lg bg-blue-50 p-4 text-center">
|
||||
<div className="mb-1 text-sm text-muted-foreground">
|
||||
{data.ddayLabel}
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-blue-600">
|
||||
{data.dday}
|
||||
</div>
|
||||
<div className="mt-1 text-sm text-muted-foreground">
|
||||
{data.currentSe}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TBD: 일정 목록 표시 형식 정의 필요 */}
|
||||
<div className="space-y-2">
|
||||
<div className="rounded border p-3">
|
||||
<div className="mb-1 text-sm font-medium">
|
||||
입력기간
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{data.inptBgngYmd} ~ {data.inptEndYmd}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded border p-3">
|
||||
<div className="mb-1 text-sm font-medium">
|
||||
검증기간
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{data.vrfcBgngYmd} ~ {data.vrfcEndYmd}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
"use client";
|
||||
|
||||
import { useDashboardStatsSubmissionRate } from "@/entities/statistics";
|
||||
import type { DashboardSearchParams } from "@/entities/statistics/model/dashboard/dashboard-interface";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
|
||||
import { useDragHandle } from "../sortable-panel";
|
||||
|
||||
interface StatsSubmissionPanelProps {
|
||||
params: DashboardSearchParams | null;
|
||||
}
|
||||
|
||||
export function StatsSubmissionPanel({ params }: StatsSubmissionPanelProps) {
|
||||
const { dragHandleProps, isDragging } = useDragHandle();
|
||||
const { data, isLoading, error } = useDashboardStatsSubmissionRate(params);
|
||||
|
||||
return (
|
||||
<div className="card card-border !m-0 h-full">
|
||||
<div
|
||||
className={`card-header cursor-grab active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
|
||||
{...dragHandleProps}
|
||||
title="헤더를 드래그하여 패널 순서 변경"
|
||||
>
|
||||
<h3 className="text-xl font-semibold">통계입력현황</h3>
|
||||
</div>
|
||||
<div className="card-body h-[calc(100%-3.25rem)] py-2.5">
|
||||
{isLoading && (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="flex h-full items-center justify-center py-8 text-center">
|
||||
데이터를 불러올 수 없습니다.
|
||||
</div>
|
||||
)}
|
||||
{data && (
|
||||
<div className="space-y-4">
|
||||
{/* TBD: statusLevel별 이모지 매핑 규칙 필요 */}
|
||||
<div className="text-center">
|
||||
<div className="mb-2 text-4xl">
|
||||
{/* TBD: statusLevel 값에 따른 이모지 표시 */}
|
||||
{data.statusLevel === "GOOD"
|
||||
? "😊"
|
||||
: data.statusLevel === "WARNING"
|
||||
? "⚠️"
|
||||
: "📊"}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{data.statusLevel}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 통계제출률 프로그레스바 */}
|
||||
<div>
|
||||
<div className="mb-2 flex justify-between text-sm">
|
||||
<span>통계제출률</span>
|
||||
<span className="font-semibold">
|
||||
{data.submissionRate?.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2.5 w-full rounded-full bg-gray-200">
|
||||
<div
|
||||
className="h-2.5 rounded-full bg-blue-600 transition-all"
|
||||
style={{ width: `${data.submissionRate}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-1 flex justify-between text-xs text-muted-foreground">
|
||||
<span>제출: {data.submittedCount}</span>
|
||||
<span>전체: {data.totalCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
|
||||
import { useDashboardUnSubmitCount } from "@/entities/statistics";
|
||||
import type { DashboardSearchParams } from "@/entities/statistics/model/dashboard/dashboard-interface";
|
||||
import { Badge, LoadingWithAnimation } from "@common/components";
|
||||
|
||||
import { useDragHandle } from "../sortable-panel";
|
||||
|
||||
interface UnSubmitPanelProps {
|
||||
params: DashboardSearchParams | null;
|
||||
}
|
||||
|
||||
export function UnSubmitPanel({ params }: UnSubmitPanelProps) {
|
||||
const { dragHandleProps, isDragging } = useDragHandle();
|
||||
const { data, isLoading, error } = useDashboardUnSubmitCount(params);
|
||||
|
||||
return (
|
||||
<div className="card card-border !m-0 h-full">
|
||||
<div
|
||||
className={`card-header cursor-grab active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
|
||||
{...dragHandleProps}
|
||||
title="헤더를 드래그하여 패널 순서 변경"
|
||||
>
|
||||
<h3 className="text-xl font-semibold">미제출 지표</h3>
|
||||
{data && (
|
||||
<Badge
|
||||
color="red"
|
||||
className="text-md rounded-full px-2 py-1 text-destructive-foreground"
|
||||
>
|
||||
{data.unSubmitCount}건
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="card-body h-[calc(100%-3.25rem)] py-2.5">
|
||||
{isLoading && (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
데이터를 불러올 수 없습니다.
|
||||
</div>
|
||||
)}
|
||||
{data && (
|
||||
<div className="h-full">
|
||||
{data.unSubmitList.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
미제출 지표가 없습니다.
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-h-[32rem] space-y-2 overflow-y-auto">
|
||||
{data.unSubmitList.map((item) => (
|
||||
<div
|
||||
key={item.statsCd}
|
||||
className="rounded-sm border p-3"
|
||||
>
|
||||
<p className="font-medium">
|
||||
{item.statsNm}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
"use client";
|
||||
|
||||
import { OctagonAlert, OctagonX } from "lucide-react";
|
||||
|
||||
import {
|
||||
useDashboardEcptReqCount,
|
||||
useDashboardVrfcErrCount,
|
||||
} from "@/entities/statistics";
|
||||
import type { DashboardSearchParams } from "@/entities/statistics/model/dashboard/dashboard-interface";
|
||||
|
||||
import { useDragHandle } from "../sortable-panel";
|
||||
|
||||
interface VrfcErrPanelProps {
|
||||
params: DashboardSearchParams | null;
|
||||
}
|
||||
|
||||
export function VrfcErrPanel({ params }: VrfcErrPanelProps) {
|
||||
const { dragHandleProps, isDragging } = useDragHandle();
|
||||
const vrfcErrQuery = useDashboardVrfcErrCount(params);
|
||||
const ecptReqQuery = useDashboardEcptReqCount(params);
|
||||
|
||||
return (
|
||||
<div className="card card-border !m-0 h-full">
|
||||
<div
|
||||
className={`card-header cursor-grab active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
|
||||
{...dragHandleProps}
|
||||
title="헤더를 드래그하여 패널 순서 변경"
|
||||
>
|
||||
<h3 className="text-xl font-semibold">검증오류 및 예외</h3>
|
||||
</div>
|
||||
<div className="card-body h-[calc(100%-3.25rem)] py-2.5">
|
||||
<div className="grid h-full grid-cols-2 gap-4">
|
||||
{/* 검증오류 수 카드 */}
|
||||
<div className="flex flex-col items-center justify-center gap-2 rounded-lg border border-red-200 bg-red-100 p-4 text-center">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<OctagonX className="text-red-600" size={20} />
|
||||
<span className="text-lg">검증오류 수</span>
|
||||
</div>
|
||||
{vrfcErrQuery.isLoading && (
|
||||
<div className="text-2xl font-bold">-</div>
|
||||
)}
|
||||
{vrfcErrQuery.error && (
|
||||
<div className="text-sm">
|
||||
데이터를 불러올 수 없습니다.
|
||||
</div>
|
||||
)}
|
||||
{vrfcErrQuery.data && (
|
||||
<div className="text-5xl font-bold text-red-600">
|
||||
{vrfcErrQuery.data.vrfcErrCount.toLocaleString()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 예외신청 수 카드 */}
|
||||
<div className="flex flex-col items-center justify-center gap-2 rounded-lg border border-orange-200 bg-orange-100 p-4 text-center">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<OctagonAlert
|
||||
className="text-orange-600"
|
||||
size={20}
|
||||
/>
|
||||
<span className="text-lg">예외신청 수</span>
|
||||
</div>
|
||||
{ecptReqQuery.isLoading && (
|
||||
<div className="text-2xl font-bold">-</div>
|
||||
)}
|
||||
{ecptReqQuery.error && (
|
||||
<div className="text-sm">
|
||||
데이터를 불러올 수 없습니다.
|
||||
</div>
|
||||
)}
|
||||
{ecptReqQuery.data && (
|
||||
<div className="text-5xl font-bold text-orange-600">
|
||||
{ecptReqQuery.data.ecptReqCount.toLocaleString()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
"use client";
|
||||
|
||||
import { useDashboardVrfcReport } from "@/entities/statistics";
|
||||
import type { DashboardSearchParams } from "@/entities/statistics/model/dashboard/dashboard-interface";
|
||||
|
||||
import { useDragHandle } from "../sortable-panel";
|
||||
|
||||
interface VrfcReportPanelProps {
|
||||
params: DashboardSearchParams | null;
|
||||
}
|
||||
|
||||
export function VrfcReportPanel({ params }: VrfcReportPanelProps) {
|
||||
const { dragHandleProps, isDragging } = useDragHandle();
|
||||
const { data, isLoading, error } = useDashboardVrfcReport(params);
|
||||
|
||||
return (
|
||||
<div className="card card-border !m-0 h-full">
|
||||
<div
|
||||
className={`card-header cursor-grab active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
|
||||
{...dragHandleProps}
|
||||
title="헤더를 드래그하여 패널 순서 변경"
|
||||
>
|
||||
<h3 className="text-lg font-semibold">이상치 선별보고서</h3>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{isLoading && (
|
||||
<div className="py-8 text-center">로딩 중...</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="py-8 text-center text-red-500">
|
||||
데이터를 불러올 수 없습니다.
|
||||
</div>
|
||||
)}
|
||||
{data && (
|
||||
<div className="space-y-4">
|
||||
{/* TBD: typeCounts 표시 방법 (탭? 필터?) */}
|
||||
{data.typeCounts &&
|
||||
Object.keys(data.typeCounts).length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{Object.entries(data.typeCounts).map(
|
||||
([type, count]) => (
|
||||
<span
|
||||
key={type}
|
||||
className="rounded-full bg-gray-100 px-3 py-1 text-sm"
|
||||
>
|
||||
{type}: {count}
|
||||
</span>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TBD: 테이블 컬럼 정의, 증감 표시 방법 */}
|
||||
<div className="max-h-96 overflow-y-auto">
|
||||
{data.list.length === 0 ? (
|
||||
<div className="py-8 text-center text-muted-foreground">
|
||||
이상치 항목이 없습니다.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{data.list.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="cursor-pointer rounded border p-3 transition-colors hover:bg-accent"
|
||||
/* TBD: 행 클릭 시 이동 라우트 */
|
||||
>
|
||||
<div className="mb-2 flex items-start justify-between">
|
||||
<div className="font-medium">
|
||||
{item.statsNm}
|
||||
</div>
|
||||
{/* TBD: vrfcCd 값별 표시 레이블 */}
|
||||
<span className="rounded bg-red-100 px-2 py-0.5 text-xs text-red-700">
|
||||
{item.vrfcCd}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mb-1 text-sm text-muted-foreground">
|
||||
{item.rsnExpln}
|
||||
</div>
|
||||
<div className="flex justify-between text-xs text-muted-foreground">
|
||||
<span>{item.admdstNm}</span>
|
||||
<span>
|
||||
{/* TBD: 증감 표시 방법 (nowVlCn - compVlCn 계산?) */}
|
||||
현재: {item.nowVlCn} / 비교:{" "}
|
||||
{item.compVlCn}
|
||||
</span>
|
||||
</div>
|
||||
{/* TBD: 등급/점수/단계/업데이트 시각 필드 미제공 */}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
|
||||
interface SortablePanelProps {
|
||||
id: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface DragHandleContextValue {
|
||||
dragHandleProps: React.HTMLAttributes<HTMLElement>;
|
||||
isDragging: boolean;
|
||||
}
|
||||
|
||||
const DragHandleContext = createContext<DragHandleContextValue | null>(null);
|
||||
|
||||
export function SortablePanel({ id, children, className }: SortablePanelProps) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id });
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
};
|
||||
|
||||
const contextValue: DragHandleContextValue = {
|
||||
dragHandleProps: { ...attributes, ...listeners },
|
||||
isDragging,
|
||||
};
|
||||
|
||||
return (
|
||||
<DragHandleContext.Provider value={contextValue}>
|
||||
<div ref={setNodeRef} style={style} className={className}>
|
||||
{children}
|
||||
</div>
|
||||
</DragHandleContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useDragHandle() {
|
||||
const context = useContext(DragHandleContext);
|
||||
|
||||
if (!context) {
|
||||
return {
|
||||
dragHandleProps: {},
|
||||
isDragging: false,
|
||||
};
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
@@ -74,6 +74,7 @@ export const ENDPOINT = {
|
||||
INPUTVERIFY: "/st-input-service/api/v1/verify",
|
||||
TOTAL: "/st-management-service/api/v1/stats",
|
||||
APPROVAL: "/st-management-service/api/stats",
|
||||
DASHBOARD: "/st-management-service/api/v1/dashboard",
|
||||
},
|
||||
STATVERIFY_SERVICE: {
|
||||
VRFCTGT_MNG: "/statverify-service/api/v1/vrfc-trgt-mng",
|
||||
|
||||
@@ -1,42 +1,46 @@
|
||||
import type { HTMLAttributes } from "react";
|
||||
|
||||
import type { UseQueryResult } from "@tanstack/react-query";
|
||||
import { useReactTable, type TableOptions } from "@tanstack/react-table";
|
||||
import { type TableOptions, useReactTable } from "@tanstack/react-table";
|
||||
|
||||
import { DataTable } from "@common/components";
|
||||
import { NoData } from "../ui/no-data";
|
||||
|
||||
import type { PageableList } from "../model";
|
||||
import { NoData } from "../ui/no-data";
|
||||
|
||||
import { matchQueryStatus } from "./match-query-status";
|
||||
|
||||
type UseDataTableOptions<T> = {
|
||||
query: UseQueryResult<PageableList<T>>;
|
||||
tableOptions: Omit<TableOptions<T>, "data" | "rowCount">;
|
||||
dataTableOptions?: DataTableOptions<T>;
|
||||
query: UseQueryResult<PageableList<T>>;
|
||||
tableOptions: Omit<TableOptions<T>, "data" | "rowCount">;
|
||||
dataTableOptions?: DataTableOptions<T>;
|
||||
};
|
||||
|
||||
type DataTableOptions<T> = {
|
||||
variant?: "default" | "secondary" | "line";
|
||||
className?: HTMLAttributes<HTMLTableElement>["className"];
|
||||
viewPagination?: boolean;
|
||||
onRowClick?: (row: T) => void;
|
||||
variant?: "default" | "secondary" | "line";
|
||||
className?: HTMLAttributes<HTMLTableElement>["className"];
|
||||
viewPagination?: boolean;
|
||||
onRowClick?: (row: T) => void;
|
||||
};
|
||||
|
||||
export const useDataTable = <T,>({
|
||||
query,
|
||||
tableOptions,
|
||||
dataTableOptions,
|
||||
query,
|
||||
tableOptions,
|
||||
dataTableOptions,
|
||||
}: UseDataTableOptions<T>) => {
|
||||
const table = useReactTable({
|
||||
...tableOptions,
|
||||
data: query.data?.content ?? [],
|
||||
rowCount: query.data?.totalElements ?? 0,
|
||||
});
|
||||
const table = useReactTable({
|
||||
...tableOptions,
|
||||
data: query.data?.content ?? [],
|
||||
rowCount: query.data?.totalElements ?? 0,
|
||||
});
|
||||
|
||||
return {
|
||||
tableComponent: matchQueryStatus(query, {
|
||||
Empty: <NoData />,
|
||||
Success: () => {
|
||||
return <DataTable table={table} {...dataTableOptions} />;
|
||||
},
|
||||
}),
|
||||
tableInstance: table,
|
||||
};
|
||||
return {
|
||||
tableComponent: matchQueryStatus(query, {
|
||||
Empty: <NoData />,
|
||||
Success: () => {
|
||||
return <DataTable table={table} {...dataTableOptions} />;
|
||||
},
|
||||
}),
|
||||
tableInstance: table,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { useStore } from "zustand";
|
||||
import { useContext } from "react";
|
||||
import type { UserStore } from "../store/user-store";
|
||||
|
||||
import { useStore } from "zustand";
|
||||
|
||||
import { StoreContext } from "../providers/store-provider";
|
||||
import type { UserStore } from "../store/user-store";
|
||||
|
||||
export const useUserStore = <T>(selector: (store: UserStore) => T): T => {
|
||||
const storeContext = useContext(StoreContext);
|
||||
const storeContext = useContext(StoreContext);
|
||||
|
||||
if (!storeContext) {
|
||||
throw new Error("useUserStore must be used within StoreProvider");
|
||||
}
|
||||
if (!storeContext) {
|
||||
throw new Error("useUserStore must be used within StoreProvider");
|
||||
}
|
||||
|
||||
return useStore(storeContext.userStore, selector);
|
||||
return useStore(storeContext.userStore, selector);
|
||||
};
|
||||
|
||||
@@ -1,59 +1,61 @@
|
||||
import { createStore } from "zustand/vanilla";
|
||||
|
||||
export interface IUserInfo {
|
||||
regDt: string;
|
||||
mdfcnDt: string;
|
||||
rgtr: string;
|
||||
mdfr: string;
|
||||
adminRoleYn: string;
|
||||
userUnqId: string;
|
||||
userId: string;
|
||||
userName: string;
|
||||
email: string;
|
||||
encryptedPassword: string;
|
||||
refreshToken: string;
|
||||
userStateCode: string;
|
||||
lastLoginDate: string;
|
||||
loginFailCount: number;
|
||||
pswdHnt: string;
|
||||
pswdAns: string;
|
||||
mphn: string;
|
||||
telno: string;
|
||||
zip: string;
|
||||
addr: string;
|
||||
daddr: string;
|
||||
laeSeCd: string;
|
||||
/** 기관 구분 코드 — 통계 공통 조회 등 query `instSeCd`로 전달 */
|
||||
instSeCd?: string;
|
||||
ogdpSeCd: string;
|
||||
aprvYn: string;
|
||||
autzrId: string;
|
||||
expryYmd: string;
|
||||
lastPswdChgDt: string;
|
||||
googleId: string;
|
||||
kakaoId: string;
|
||||
naverId: string;
|
||||
isSocialUser: boolean;
|
||||
hasPassword: boolean;
|
||||
regDt: string;
|
||||
mdfcnDt: string;
|
||||
rgtr: string;
|
||||
mdfr: string;
|
||||
adminRoleYn: string;
|
||||
userUnqId: string;
|
||||
userId: string;
|
||||
userName: string;
|
||||
email: string;
|
||||
encryptedPassword: string;
|
||||
enoCd: string;
|
||||
refreshToken: string;
|
||||
userStateCode: string;
|
||||
lastLoginDate: string;
|
||||
loginFailCount: number;
|
||||
pswdHnt: string;
|
||||
pswdAns: string;
|
||||
mphn: string;
|
||||
telno: string;
|
||||
zip: string;
|
||||
addr: string;
|
||||
daddr: string;
|
||||
dtlInstCd: string;
|
||||
laeSeCd: string;
|
||||
/** 기관 구분 코드 — 통계 공통 조회 등 query `instSeCd`로 전달 */
|
||||
instSeCd?: string;
|
||||
ogdpSeCd: string;
|
||||
aprvYn: string;
|
||||
autzrId: string;
|
||||
expryYmd: string;
|
||||
lastPswdChgDt: string;
|
||||
googleId: string;
|
||||
kakaoId: string;
|
||||
naverId: string;
|
||||
isSocialUser: boolean;
|
||||
hasPassword: boolean;
|
||||
}
|
||||
|
||||
export type UserState = {
|
||||
userInfo: IUserInfo | null;
|
||||
userInfo: IUserInfo | null;
|
||||
};
|
||||
|
||||
export type UserActions = {
|
||||
setUserInfo: (userInfo: IUserInfo | null) => void;
|
||||
setUserInfo: (userInfo: IUserInfo | null) => void;
|
||||
};
|
||||
|
||||
export type UserStore = UserState & UserActions;
|
||||
|
||||
export const defaultInitState: UserState = {
|
||||
userInfo: null,
|
||||
userInfo: null,
|
||||
};
|
||||
|
||||
export const createUserStore = (initState: UserState = defaultInitState) => {
|
||||
return createStore<UserStore>()((set) => ({
|
||||
...initState,
|
||||
setUserInfo: (userInfo: IUserInfo | null) => set(() => ({ userInfo })),
|
||||
}));
|
||||
return createStore<UserStore>()((set) => ({
|
||||
...initState,
|
||||
setUserInfo: (userInfo: IUserInfo | null) => set(() => ({ userInfo })),
|
||||
}));
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user