diff --git a/.gitignore b/.gitignore index c5082bba3..e4fab5cdd 100644 --- a/.gitignore +++ b/.gitignore @@ -26,5 +26,9 @@ pnpm-lock.yaml settings.json .npmrc .cursor +.agent apps/**/tsconfig.tsbuildinfo + +# next-agents-md +.next-docs/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..1da991844 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1 @@ +[Next.js Docs Index]|root: ./.next-docs|STOP. What you remember about Next.js is WRONG for this project. Always search docs and read before any task.|01-app/01-getting-started:{01-installation.mdx,02-project-structure.mdx,03-layouts-and-pages.mdx,04-images-and-fonts.mdx,05-css.mdx,06-fetching-data.mdx,07-updating-data.mdx,08-error-handling.mdx,10-metadata-and-og-images.mdx,12-upgrading.mdx}|01-app/03-building-your-application/01-routing:{03-layouts-and-templates.mdx,04-linking-and-navigating.mdx,05-error-handling.mdx,06-loading-ui-and-streaming.mdx,07-redirecting.mdx,08-route-groups.mdx,10-dynamic-routes.mdx,11-parallel-routes.mdx,12-intercepting-routes.mdx,13-route-handlers.mdx,14-middleware.mdx,15-internationalization.mdx}|01-app/03-building-your-application/02-data-fetching:{01-fetching.mdx,03-server-actions-and-mutations.mdx,04-incremental-static-regeneration.mdx}|01-app/03-building-your-application/03-rendering:{01-server-components.mdx,02-client-components.mdx,03-composition-patterns.mdx,04-partial-prerendering.mdx,05-edge-and-nodejs-runtimes.mdx}|01-app/03-building-your-application/05-styling:{01-css.mdx,02-tailwind-css.mdx,03-sass.mdx,04-css-in-js.mdx}|01-app/03-building-your-application/06-optimizing:{01-images.mdx,02-videos.mdx,03-fonts.mdx,04-metadata.mdx,05-scripts.mdx,06-package-bundling.mdx,07-lazy-loading.mdx,08-analytics.mdx,09-instrumentation.mdx,10-open-telemetry.mdx,11-static-assets.mdx,12-third-party-libraries.mdx,13-memory-usage.mdx,14-local-development.mdx}|01-app/03-building-your-application/07-configuring:{03-environment-variables.mdx,05-mdx.mdx,06-src-directory.mdx,10-custom-server.mdx,11-draft-mode.mdx,15-content-security-policy.mdx,16-debugging.mdx,16-progressive-web-apps.mdx}|01-app/03-building-your-application/08-testing:{01-vitest.mdx,02-jest.mdx,03-playwright.mdx,04-cypress.mdx}|01-app/03-building-your-application/10-deploying:{01-production-checklist.mdx,02-static-exports.mdx,03-multi-zones.mdx}|01-app/03-building-your-application/11-upgrading:{01-codemods.mdx,02-canary.mdx,03-version-15.mdx,04-version-14.mdx,05-app-router-migration.mdx,06-from-create-react-app.mdx,07-from-vite.mdx,08-single-page-applications.mdx}|01-app/05-api-reference:{07-edge.mdx,08-turbopack.mdx}|01-app/05-api-reference/01-directives:{use-cache.mdx,use-client.mdx,use-server.mdx}|01-app/05-api-reference/02-components:{font.mdx,form.mdx,image.mdx,link.mdx,script.mdx}|01-app/05-api-reference/03-file-conventions/01-metadata:{app-icons.mdx,manifest.mdx,opengraph-image.mdx,robots.mdx,sitemap.mdx}|01-app/05-api-reference/03-file-conventions:{default.mdx,error.mdx,forbidden.mdx,instrumentation-client.mdx,instrumentation.mdx,layout.mdx,loading.mdx,mdx-components.mdx,middleware.mdx,not-found.mdx,page.mdx,route-segment-config.mdx,route.mdx,template.mdx,unauthorized.mdx}|01-app/05-api-reference/04-functions:{after.mdx,cacheLife.mdx,cacheTag.mdx,connection.mdx,cookies.mdx,draft-mode.mdx,fetch.mdx,forbidden.mdx,generate-image-metadata.mdx,generate-metadata.mdx,generate-sitemaps.mdx,generate-static-params.mdx,generate-viewport.mdx,headers.mdx,image-response.mdx,next-request.mdx,next-response.mdx,not-found.mdx,permanentRedirect.mdx,redirect.mdx,revalidatePath.mdx,revalidateTag.mdx,unauthorized.mdx,unstable_cache.mdx,unstable_noStore.mdx,unstable_rethrow.mdx,use-link-status.mdx,use-params.mdx,use-pathname.mdx,use-report-web-vitals.mdx,use-router.mdx,use-search-params.mdx,use-selected-layout-segment.mdx,use-selected-layout-segments.mdx,userAgent.mdx}|01-app/05-api-reference/05-config/01-next-config-js:{allowedDevOrigins.mdx,appDir.mdx,assetPrefix.mdx,authInterrupts.mdx,basePath.mdx,cacheLife.mdx,compress.mdx,crossOrigin.mdx,cssChunking.mdx,devIndicators.mdx,distDir.mdx,dynamicIO.mdx,env.mdx,eslint.mdx,expireTime.mdx,exportPathMap.mdx,generateBuildId.mdx,generateEtags.mdx,headers.mdx,htmlLimitedBots.mdx,httpAgentOptions.mdx,images.mdx,incrementalCacheHandlerPath.mdx,inlineCss.mdx,logging.mdx,mdxRs.mdx,onDemandEntries.mdx,optimizePackageImports.mdx,output.mdx,pageExtensions.mdx,poweredByHeader.mdx,ppr.mdx,productionBrowserSourceMaps.mdx,reactCompiler.mdx,reactMaxHeadersLength.mdx,reactStrictMode.mdx,redirects.mdx,rewrites.mdx,sassOptions.mdx,serverActions.mdx,serverComponentsHmrCache.mdx,serverExternalPackages.mdx,staleTimes.mdx,staticGeneration.mdx,trailingSlash.mdx,transpilePackages.mdx,turbopack.mdx,typedRoutes.mdx,typescript.mdx,urlImports.mdx,useCache.mdx,useLightningcss.mdx,viewTransition.mdx,webVitalsAttribution.mdx,webpack.mdx}|01-app/05-api-reference/05-config:{02-typescript.mdx,03-eslint.mdx}|01-app/05-api-reference/06-cli:{create-next-app.mdx,next.mdx}|02-pages/01-getting-started:{01-installation.mdx,02-project-structure.mdx}|02-pages/02-building-your-application/01-routing:{01-pages-and-layouts.mdx,02-dynamic-routes.mdx,03-linking-and-navigating.mdx,04-redirecting.mdx,05-custom-app.mdx,06-custom-document.mdx,07-api-routes.mdx,08-custom-error.mdx,10-internationalization.mdx,11-middleware.mdx}|02-pages/02-building-your-application/02-rendering:{01-server-side-rendering.mdx,02-static-site-generation.mdx,04-automatic-static-optimization.mdx,05-client-side-rendering.mdx,06-edge-and-nodejs-runtimes.mdx}|02-pages/02-building-your-application/03-data-fetching:{01-get-static-props.mdx,02-get-static-paths.mdx,03-forms-and-mutations.mdx,03-get-server-side-props.mdx,04-incremental-static-regeneration.mdx,05-client-side.mdx}|02-pages/02-building-your-application/04-styling:{01-css.mdx,02-tailwind-css.mdx,03-css-in-js.mdx,04-sass.mdx}|02-pages/02-building-your-application/05-optimizing:{01-images.mdx,02-fonts.mdx,03-scripts.mdx,05-static-assets.mdx,06-package-bundling.mdx,07-analytics.mdx,08-lazy-loading.mdx,09-instrumentation.mdx,10-open-telemetry.mdx,11-third-party-libraries.mdx}|02-pages/02-building-your-application/06-configuring:{03-environment-variables.mdx,05-src-directory.mdx,06-mdx.mdx,07-amp.mdx,08-babel.mdx,09-post-css.mdx,10-custom-server.mdx,11-draft-mode.mdx,12-error-handling.mdx,13-preview-mode.mdx,14-content-security-policy.mdx,16-debugging.mdx}|02-pages/02-building-your-application/07-testing:{01-vitest.mdx,02-jest.mdx,03-playwright.mdx,04-cypress.mdx}|02-pages/02-building-your-application/09-deploying:{01-production-checklist.mdx,02-static-exports.mdx,03-multi-zones.mdx,04-ci-build-caching.mdx}|02-pages/02-building-your-application/10-upgrading:{01-codemods.mdx,02-app-router-migration.mdx,03-from-vite.mdx,04-from-create-react-app.mdx,05-version-14.mdx,06-version-13.mdx,07-version-12.mdx,08-version-11.mdx,09-version-10.mdx,10-version-9.mdx}|02-pages/03-api-reference:{06-edge.mdx,08-turbopack.mdx}|02-pages/03-api-reference/01-components:{font.mdx,form.mdx,head.mdx,image-legacy.mdx,image.mdx,link.mdx,script.mdx}|02-pages/03-api-reference/02-file-conventions:{instrumentation.mdx}|02-pages/03-api-reference/03-functions:{get-initial-props.mdx,get-server-side-props.mdx,get-static-paths.mdx,get-static-props.mdx,next-request.mdx,next-response.mdx,use-amp.mdx,use-report-web-vitals.mdx,use-router.mdx,userAgent.mdx}|02-pages/03-api-reference/04-config/01-next-config-js:{allowedDevOrigins.mdx,assetPrefix.mdx,basePath.mdx,bundlePagesRouterDependencies.mdx,compress.mdx,crossOrigin.mdx,devIndicators.mdx,distDir.mdx,env.mdx,eslint.mdx,exportPathMap.mdx,generateBuildId.mdx,generateEtags.mdx,headers.mdx,httpAgentOptions.mdx,images.mdx,onDemandEntries.mdx,optimizePackageImports.mdx,output.mdx,pageExtensions.mdx,poweredByHeader.mdx,productionBrowserSourceMaps.mdx,reactStrictMode.mdx,redirects.mdx,rewrites.mdx,runtime-configuration.mdx,serverExternalPackages.mdx,trailingSlash.mdx,transpilePackages.mdx,turbo.mdx,typescript.mdx,urlImports.mdx,useLightningcss.mdx,webVitalsAttribution.mdx,webpack.mdx}|02-pages/03-api-reference/04-config:{01-typescript.mdx,02-eslint.mdx}|02-pages/03-api-reference/05-cli:{create-next-app.mdx,next.mdx}|03-architecture:{accessibility.mdx,fast-refresh.mdx,nextjs-compiler.mdx,supported-browsers.mdx}|04-community:{01-contribution-guide.mdx} diff --git a/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx b/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx index 0f44b8e4a..4da13d0e2 100644 --- a/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx +++ b/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx @@ -180,6 +180,7 @@ const HbStatisticsVerification = () => { const params: StAnlsMstSearchParams = { page: historyPagination.pageIndex, size: historyPagination.pageSize, + sort: "anlsSn,desc", }; if (appliedHistoryDateRange?.from) { @@ -455,6 +456,7 @@ const HbStatisticsVerification = () => { diff --git a/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx b/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx index 11eb6c8fd..90ffd5350 100644 --- a/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx +++ b/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx @@ -2,10 +2,10 @@ import React, { useState } from "react"; import { Button } from "@common/components"; -import AreaDIndicators from "./Indicators"; -import AreaBRegionalGrid from "./RegionalGrid"; -import AreaCScatterChart from "./ScatterChart"; -import AreaAVerificationList from "./VerificationList"; +import Indicators from "./Indicators"; +import RegionalGrid from "./RegionalGrid"; +import ScatterChart from "./ScatterChart"; +import VerificationList from "./VerificationList"; interface HbVerificationDetailProps { anlsSn: number; @@ -41,7 +41,7 @@ const HbVerificationDetail: React.FC = ({ - = ({ - = ({ - - void; } +interface AppliedFilter { + sido: string | null; + gugun: string; + status: string; +} + const RegionalGrid: React.FC = ({ anlsSn, vrfcTrgtSn, selectedSido, onSidoSelect, }) => { - const [selectedGugun, setSelectedGugun] = useState("ALL"); - const [selectedStatus, setSelectedStatus] = useState("ALL"); + const [selectedGugun, setSelectedGugun] = useState(""); + const [selectedStatus, setSelectedStatus] = useState(""); + const [showAllOnEmptyFilter, setShowAllOnEmptyFilter] = + useState(true); + const [appliedFilter, setAppliedFilter] = useState({ + sido: null, + gugun: "", + status: "", + }); const { data: regionCodes } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.CODE, "LAESECD"], @@ -51,7 +65,7 @@ const RegionalGrid: React.FC = ({ }, [regionCodes]); const gugunOptions = useMemo(() => { - if (!regionCodes || !selectedSido || selectedSido === "ALL") return []; + if (!regionCodes || !selectedSido) return []; const sidoPrefix = selectedSido; return regionCodes.filter( @@ -60,11 +74,7 @@ const RegionalGrid: React.FC = ({ ); }, [regionCodes, selectedSido]); - const { - data: resultData, - isLoading, - refetch, - } = useQuery({ + const { data: resultData, isLoading } = useQuery({ queryKey: [ ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT, "list", @@ -78,43 +88,49 @@ const RegionalGrid: React.FC = ({ page: 0, size: 1000, }), - enabled: false, + enabled: !!vrfcTrgtSn, }); const rowData = useMemo(() => { let data = (resultData?.content ?? []) as StAnlsRsltResponse[]; - if (selectedSido && selectedSido !== "ALL") { + if ( + !appliedFilter.sido && + !appliedFilter.gugun && + !appliedFilter.status + ) { + return showAllOnEmptyFilter ? data : []; + } + + if (appliedFilter.sido) { const sidoName = sidoOptions.find( - (opt) => opt.lwpoCd === selectedSido, + (opt) => opt.lwpoCd === appliedFilter.sido, )?.lwpoCdNm; if (sidoName) { data = data.filter((item) => item.sido === sidoName); } } - if (selectedGugun && selectedGugun !== "ALL") { + if (appliedFilter.gugun) { const gugunName = gugunOptions.find( - (opt) => opt.lwpoCd === selectedGugun, + (opt) => opt.lwpoCd === appliedFilter.gugun, )?.lwpoCdNm; if (gugunName) { - data = data.filter((item) => item.gugun === gugunName); + data = data.filter((item) => gugunName.includes(item.gugun)); } } - if (selectedStatus && selectedStatus !== "ALL") { - const statusTarget = selectedStatus === "NORMAL" ? "y" : "n"; + if (appliedFilter.status) { data = data.filter( - (item) => item.otlrYn.toLowerCase() === statusTarget, + (item) => item.otlrYn.toLowerCase() === appliedFilter.status, ); } return data; }, [ resultData, - selectedSido, - selectedGugun, - selectedStatus, + appliedFilter, + showAllOnEmptyFilter, sidoOptions, gugunOptions, ]); @@ -150,19 +166,13 @@ const RegionalGrid: React.FC = ({ size: 60, cell: (info) => { const val = info.getValue(); - if (val === "y") - return ( - - 정상 - - ); - if (val === "n") - return ( - - 이상치 - - ); - return "-"; + return ( + + {val === "0" ? "정상" : "이상치"} + + ); }, }, ], @@ -180,8 +190,29 @@ const RegionalGrid: React.FC = ({ }); const handleSidoChange = (val: string) => { - onSidoSelect(val === "ALL" ? null : val); - setSelectedGugun("ALL"); + onSidoSelect(!val ? null : val); + setSelectedGugun(""); + }; + + const handleSearch = () => { + setAppliedFilter({ + sido: selectedSido, + gugun: selectedGugun, + status: selectedStatus, + }); + setShowAllOnEmptyFilter(false); + }; + + const handleReset = () => { + onSidoSelect(null); + setSelectedGugun(""); + setSelectedStatus(""); + setAppliedFilter({ + sido: null, + gugun: "", + status: "", + }); + setShowAllOnEmptyFilter(true); }; return ( @@ -189,14 +220,13 @@ const RegionalGrid: React.FC = ({ - 전체 (시도) {sidoOptions.map((opt) => ( {opt.lwpoCdNm} @@ -207,13 +237,12 @@ const RegionalGrid: React.FC = ({ - 전체 (시군구) {gugunOptions.map((opt) => ( {opt.lwpoCdNm} @@ -226,41 +255,47 @@ const RegionalGrid: React.FC = ({ onValueChange={setSelectedStatus} > - + - 전체 상태 - 정상 - 이상치 + 정상 + 이상치 - refetch()}>조회 - 다운로드 + + 초기화 + + 조회 + 다운로드 - {isLoading && ( - - 데이터를 불러오는 중... + + + + )} {!isLoading && (!vrfcTrgtSn || rowData.length === 0) && ( {vrfcTrgtSn ? "표시할 항목이 없습니다." - : "항목을 선택해주세요"} + : "항목을 선택해주세요."} )} + {!isLoading && rowData.length > 0 && ( + + )} ); diff --git a/apps/st/src/components/statistics/verification/detail/VerificationList.tsx b/apps/st/src/components/statistics/verification/detail/VerificationList.tsx index 62463b036..ddb6f88d3 100644 --- a/apps/st/src/components/statistics/verification/detail/VerificationList.tsx +++ b/apps/st/src/components/statistics/verification/detail/VerificationList.tsx @@ -1,7 +1,11 @@ import React, { useEffect, useMemo } from "react"; import { useQuery } from "@tanstack/react-query"; -import { getCoreRowModel, useReactTable } from "@tanstack/react-table"; +import { + type ColumnDef, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table"; import { ENDPOINT } from "@/shared/config"; import { stAnlsDtlApi } from "@/entities/statistics/api/statistics-analysis-detail-api"; @@ -47,50 +51,47 @@ const VerificationList: React.FC = ({ return (detailData?.content ?? []) as StAnlsDtlResponse[]; }, [detailData]); - const columns = useMemo( + const columns: ColumnDef[] = useMemo( () => [ + { + header: "통계 년도", + accessorKey: "statsYr", + }, + { + header: "통계 항목", + accessorKey: "statsNm", + }, { header: "검증 항목", - accessorFn: (row: StAnlsDtlResponse) => - targetCodeMap.get(String(row.vrfcTrgtSn)) || - String(row.vrfcTrgtSn), - id: "vrfcTrgtNm", - size: 150, + accessorKey: "vrfcTrgtNm", }, { header: "μ", accessorKey: "ipcrCntVl", - size: 60, }, { header: "Percentile", accessorKey: "pctlCrtrVl", - size: 60, }, { header: "A", accessorKey: "scopAjmtCntVl", - size: 60, }, { header: "C", accessorKey: "thrsCntVl", - size: 60, }, { - header: "정상", + header: "정상건수", accessorKey: "nmlDataNocs", - size: 70, }, { - header: "제외", + header: "제외건수", accessorKey: "exclDataNocs", - size: 70, }, { - header: "이상", + header: "이상건수", accessorKey: "otlrDataNocs", - size: 70, }, ], [targetCodeMap], diff --git a/apps/st/src/entities/statistics/api/analysis-history-api.ts b/apps/st/src/entities/statistics/api/analysis-history-api.ts index 56375bf5f..5d6be6a4d 100644 --- a/apps/st/src/entities/statistics/api/analysis-history-api.ts +++ b/apps/st/src/entities/statistics/api/analysis-history-api.ts @@ -2,35 +2,34 @@ import { client } from "@/shared/api"; import { ENDPOINT } from "@/shared/config"; import type { - StAnlsMstListApiResponse, - StAnlsMstSearchParams, - StAnlsOutlierListResponse, + StAnlsMstListApiResponse, + StAnlsMstSearchParams, + StAnlsOutlierListResponse, } from "../model/analysis-history-interface"; export class AnalysisHistoryApiService { - public async getAnalysisHistoryList( - params: StAnlsMstSearchParams, - ): Promise { - const response = await client.get( - `${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST}`, - { - params, - }, - ); + public async getAnalysisHistoryList( + params: StAnlsMstSearchParams, + ): Promise { + const response = await client.get( + `${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST}`, + { + params, + }, + ); - return response.data; - } + return response.data; + } - public async getOutlierList( - anlsSn: number, - ): Promise { - const response = await client.get( - `${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST}/outlierList/${anlsSn}`, - ); + public async getOutlierList( + anlsSn: number, + ): Promise { + const response = await client.get( + `${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST}/outlierList/${anlsSn}`, + ); - return response.data; - } + return response.data; + } } export const analysisHistoryApi = new AnalysisHistoryApiService(); - diff --git a/apps/st/src/entities/statistics/model/analysis-history-interface.ts b/apps/st/src/entities/statistics/model/analysis-history-interface.ts index 7e5b95ba8..3ba448b16 100644 --- a/apps/st/src/entities/statistics/model/analysis-history-interface.ts +++ b/apps/st/src/entities/statistics/model/analysis-history-interface.ts @@ -1,4 +1,4 @@ -import type { PageableList } from "@/shared/model"; +import type { PageableList, PageableParams } from "@/shared/model"; export interface StAnlsMstResponse { anlsSn: number; @@ -16,12 +16,9 @@ export interface StAnlsMstResponse { rgtrNm: string; } -export interface StAnlsMstSearchParams { +export interface StAnlsMstSearchParams extends PageableParams { anlsSttsCd?: string; jobUuid?: string; - page?: number; - size?: number; - sort?: string; verifyDtStart?: string; verifyDtEnd?: string; } diff --git a/apps/st/src/entities/statistics/model/statistics-analysis-result-interface.ts b/apps/st/src/entities/statistics/model/statistics-analysis-result-interface.ts index 38c44b7a9..399be99e8 100644 --- a/apps/st/src/entities/statistics/model/statistics-analysis-result-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-analysis-result-interface.ts @@ -20,7 +20,7 @@ export interface StAnlsRsltResponse { chgRt: number | null; // 변경율 (r_i) trsfRt: number | null; // 변환 비율 (s_i) otlrTrsfRt: number | null; // 이상치 변환 비율 (E_i) - otlrYn: string; // 이상치 여부 (y/n) + otlrYn: string; // 이상치 여부 (0: 정상, 1: 이상치) } export interface StAnlsRsltSearchParams extends PageableParams {