summaryrefslogtreecommitdiff
path: root/lib/evaluation/table
diff options
context:
space:
mode:
Diffstat (limited to 'lib/evaluation/table')
-rw-r--r--lib/evaluation/table/evaluation-filter-sheet.tsx234
-rw-r--r--lib/evaluation/table/evaluation-table.tsx223
2 files changed, 339 insertions, 118 deletions
diff --git a/lib/evaluation/table/evaluation-filter-sheet.tsx b/lib/evaluation/table/evaluation-filter-sheet.tsx
index b0bf9139..c2dd9734 100644
--- a/lib/evaluation/table/evaluation-filter-sheet.tsx
+++ b/lib/evaluation/table/evaluation-filter-sheet.tsx
@@ -7,7 +7,6 @@ import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Search, X } from "lucide-react";
import { customAlphabet } from "nanoid";
-import { parseAsStringEnum, useQueryState } from "nuqs";
import { Button } from "@/components/ui/button";
import {
@@ -28,7 +27,6 @@ import {
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
-import { getFiltersStateParser } from "@/lib/parsers";
import { EVALUATION_TARGET_FILTER_OPTIONS } from "@/lib/evaluation-target-list/validation";
/*****************************************************************************************
@@ -49,7 +47,6 @@ const statusOptions = [
{ value: "FINALIZED", label: "결과확정" },
];
-
const documentsSubmittedOptions = [
{ value: "true", label: "제출완료" },
{ value: "false", label: "미제출" },
@@ -104,14 +101,13 @@ export function PeriodicEvaluationFilterSheet({
isLoading = false,
onFiltersApply,
}: PeriodicEvaluationFilterSheetProps) {
- /** Router (needed only for pathname) */
+ /** Router (needed for URL updates) */
const router = useRouter();
/** Track pending state while we update URL */
const [isPending, startTransition] = useTransition();
const [joinOperator, setJoinOperator] = useState<"and" | "or">("and")
-
/** React‑Hook‑Form */
const form = useForm<PeriodicEvaluationFilterFormValues>({
resolver: zodResolver(periodicEvaluationFilterSchema),
@@ -131,82 +127,156 @@ export function PeriodicEvaluationFilterSheet({
},
});
-
/*****************************************************************************************
- * 3️⃣ Submit → build filter array → push to URL (and reset page=1)
+ * 3️⃣ Submit → build filter array → callback + URL (동기적 처리)
*****************************************************************************************/
async function onSubmit(data: PeriodicEvaluationFilterFormValues) {
startTransition(() => {
try {
- const newFilters: any[] = [];
-
- const pushFilter = (
- id: string,
- value: any,
- type: "text" | "select" | "number" | "boolean",
- operator: "eq" | "iLike" | "gte" | "lte"
- ) => {
- newFilters.push({ id, value, type, operator, rowId: generateId() });
- };
-
- if (data.evaluationYear?.trim())
- pushFilter("evaluationYear", Number(data.evaluationYear), "number", "eq");
-
- if (data.division?.trim())
- pushFilter("division", data.division.trim(), "select", "eq");
-
- if (data.status?.trim())
- pushFilter("status", data.status.trim(), "select", "eq");
-
- if (data.domesticForeign?.trim())
- pushFilter("domesticForeign", data.domesticForeign.trim(), "select", "eq");
-
- if (data.materialType?.trim())
- pushFilter("materialType", data.materialType.trim(), "select", "eq");
-
- if (data.vendorCode?.trim())
- pushFilter("vendorCode", data.vendorCode.trim(), "text", "iLike");
-
- if (data.vendorName?.trim())
- pushFilter("vendorName", data.vendorName.trim(), "text", "iLike");
-
- if (data.documentsSubmitted?.trim())
- pushFilter(
- "documentsSubmitted",
- data.documentsSubmitted.trim() === "true",
- "boolean",
- "eq"
- );
-
- if (data.evaluationGrade?.trim())
- pushFilter("evaluationGrade", data.evaluationGrade.trim(), "select", "eq");
-
- if (data.finalGrade?.trim())
- pushFilter("finalGrade", data.finalGrade.trim(), "select", "eq");
-
- if (data.minTotalScore?.trim())
- pushFilter("totalScore", Number(data.minTotalScore), "number", "gte");
-
- if (data.maxTotalScore?.trim())
- pushFilter("totalScore", Number(data.maxTotalScore), "number", "lte");
-
- setJoinOperator(joinOperator);
-
-
+ const newFilters = []
+
+ // 필터 생성 로직
+ if (data.evaluationYear?.trim()) {
+ newFilters.push({
+ id: "evaluationYear",
+ value: parseInt(data.evaluationYear.trim()),
+ type: "number",
+ operator: "eq",
+ rowId: generateId()
+ })
+ }
+
+ if (data.division?.trim()) {
+ newFilters.push({
+ id: "division",
+ value: data.division.trim(),
+ type: "select",
+ operator: "eq",
+ rowId: generateId()
+ })
+ }
+
+ if (data.status?.trim()) {
+ newFilters.push({
+ id: "status",
+ value: data.status.trim(),
+ type: "select",
+ operator: "eq",
+ rowId: generateId()
+ })
+ }
+
+ if (data.domesticForeign?.trim()) {
+ newFilters.push({
+ id: "domesticForeign",
+ value: data.domesticForeign.trim(),
+ type: "select",
+ operator: "eq",
+ rowId: generateId()
+ })
+ }
+
+ if (data.materialType?.trim()) {
+ newFilters.push({
+ id: "materialType",
+ value: data.materialType.trim(),
+ type: "select",
+ operator: "eq",
+ rowId: generateId()
+ })
+ }
+
+ if (data.vendorCode?.trim()) {
+ newFilters.push({
+ id: "vendorCode",
+ value: data.vendorCode.trim(),
+ type: "text",
+ operator: "iLike",
+ rowId: generateId()
+ })
+ }
+
+ if (data.vendorName?.trim()) {
+ newFilters.push({
+ id: "vendorName",
+ value: data.vendorName.trim(),
+ type: "text",
+ operator: "iLike",
+ rowId: generateId()
+ })
+ }
+
+ if (data.documentsSubmitted?.trim()) {
+ newFilters.push({
+ id: "documentsSubmitted",
+ value: data.documentsSubmitted.trim() === "true",
+ type: "boolean",
+ operator: "eq",
+ rowId: generateId()
+ })
+ }
+
+ if (data.evaluationGrade?.trim()) {
+ newFilters.push({
+ id: "evaluationGrade",
+ value: data.evaluationGrade.trim(),
+ type: "select",
+ operator: "eq",
+ rowId: generateId()
+ })
+ }
+
+ if (data.finalGrade?.trim()) {
+ newFilters.push({
+ id: "finalGrade",
+ value: data.finalGrade.trim(),
+ type: "select",
+ operator: "eq",
+ rowId: generateId()
+ })
+ }
+
+ if (data.minTotalScore?.trim()) {
+ newFilters.push({
+ id: "totalScore",
+ value: parseFloat(data.minTotalScore.trim()),
+ type: "number",
+ operator: "gte",
+ rowId: generateId()
+ })
+ }
+
+ if (data.maxTotalScore?.trim()) {
+ newFilters.push({
+ id: "totalScore",
+ value: parseFloat(data.maxTotalScore.trim()),
+ type: "number",
+ operator: "lte",
+ rowId: generateId()
+ })
+ }
+
+ console.log("=== 생성된 필터들 ===", newFilters);
+ console.log("=== 조인 연산자 ===", joinOperator);
+
+
+ // ✅ 부모 컴포넌트에 필터 전달 (동기적으로 즉시 호출)
onFiltersApply(newFilters, joinOperator);
- } catch (err) {
- // eslint-disable-next-line no-console
- console.error("정기평가 필터 적용 오류:", err);
+
+ console.log("=== 필터 적용 완료 ===");
+ } catch (error) {
+ console.error("정기평가 필터 적용 오류:", error);
}
});
}
/*****************************************************************************************
- * 4️⃣ Reset → clear form & URL
+ * 4️⃣ Reset → clear form & URL (동기적 처리)
*****************************************************************************************/
- async function handleReset() {
+ function handleReset() {
+ // 1. 폼 초기화
form.reset({
- evaluationYear: new Date().getFullYear().toString(),
+ evaluationYear: "",
division: "",
status: "",
domesticForeign: "",
@@ -220,9 +290,26 @@ export function PeriodicEvaluationFilterSheet({
maxTotalScore: "",
});
- onFiltersApply([], "and");
+ // 2. 조인 연산자 초기화
setJoinOperator("and");
+ // 3. URL 파라미터 초기화 (필터를 빈 배열로 설정)
+ const currentUrl = new URL(window.location.href);
+ const newSearchParams = new URLSearchParams(currentUrl.search);
+
+ // 필터 관련 파라미터 초기화
+ newSearchParams.set("filters", JSON.stringify([]));
+ newSearchParams.set("joinOperator", "and");
+ newSearchParams.set("page", "1");
+ newSearchParams.delete("search"); // 검색어 제거
+
+ // URL 업데이트
+ router.replace(`${currentUrl.pathname}?${newSearchParams.toString()}`);
+
+ // 4. 빈 필터 배열 전달 (즉시 UI 업데이트를 위해)
+ onFiltersApply([], "and");
+
+ console.log("=== 필터 완전 초기화 완료 ===");
}
/*****************************************************************************************
@@ -306,7 +393,6 @@ export function PeriodicEvaluationFilterSheet({
)}
/>
-
{/* 구분 */}
<FormField
control={form.control}
@@ -472,7 +558,7 @@ export function PeriodicEvaluationFilterSheet({
className="-mr-2 h-4 w-4"
onClick={(e) => {
e.stopPropagation();
- form.setValue("materialType", "");
+ form.setValue("materialType", "");
}}
disabled={isPending}
>
@@ -798,7 +884,7 @@ export function PeriodicEvaluationFilterSheet({
type="button"
variant="outline"
onClick={handleReset}
- disabled={isPending }
+ disabled={isPending}
className="px-4"
>
초기화
@@ -806,7 +892,7 @@ export function PeriodicEvaluationFilterSheet({
<Button
type="submit"
variant="samsung"
- disabled={isPending || isLoading }
+ disabled={isPending || isLoading}
className="px-4"
>
<Search className="mr-2 size-4" />
@@ -818,4 +904,4 @@ export function PeriodicEvaluationFilterSheet({
</Form>
</div>
);
-}
+} \ No newline at end of file
diff --git a/lib/evaluation/table/evaluation-table.tsx b/lib/evaluation/table/evaluation-table.tsx
index 257225c8..4404967a 100644
--- a/lib/evaluation/table/evaluation-table.tsx
+++ b/lib/evaluation/table/evaluation-table.tsx
@@ -28,7 +28,7 @@ import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-adv
import { cn } from "@/lib/utils"
import { useTablePresets } from "@/components/data-table/use-table-presets"
import { TablePresetManager } from "@/components/data-table/data-table-preset"
-import { PeriodicEvaluationFilterSheet } from "./evaluation-filter-sheet"
+import { PeriodicEvaluationFilterSheet } from "./evaluation-filter-sheet" // ✅ 올바른 컴포넌트 이름
import { getPeriodicEvaluationsColumns } from "./evaluation-columns"
import { PeriodicEvaluationView, PeriodicEvaluationAggregatedView } from "@/db/schema"
import {
@@ -300,9 +300,20 @@ export function PeriodicEvaluationsTable({
const [detailedCount, setDetailedCount] = React.useState<number | undefined>(undefined)
const [aggregatedCount, setAggregatedCount] = React.useState<number | undefined>(undefined)
+ // ✅ 외부 필터 상태 (폼에서 전달받은 필터) - EvaluationTargetsTable 패턴과 동일
const [externalFilters, setExternalFilters] = React.useState<any[]>([]);
const [externalJoinOperator, setExternalJoinOperator] = React.useState<"and" | "or">("and");
+
+ // ✅ 폼에서 전달받은 필터를 처리하는 핸들러 - EvaluationTargetsTable 패턴과 동일
+ const handleFiltersApply = React.useCallback((filters: any[], joinOperator: "and" | "or") => {
+ console.log("=== 폼에서 필터 전달받음 ===", filters, joinOperator);
+ setExternalFilters(filters);
+ setExternalJoinOperator(joinOperator);
+ // 필터 적용 후 패널 닫기
+ setIsFilterPanelOpen(false);
+ }, []);
+
// ✅ 뷰 모드 변경 시 URL 업데이트
const handleViewModeChange = React.useCallback((newMode: "detailed" | "aggregated") => {
setViewMode(newMode);
@@ -321,14 +332,116 @@ export function PeriodicEvaluationsTable({
router.push(`?${newSearchParams.toString()}`, { scroll: false })
}, [router, searchParams])
- const handleFiltersApply = React.useCallback((filters: any[], joinOperator: "and" | "or") => {
- console.log("=== 폼에서 필터 전달받음 ===", filters, joinOperator);
- setExternalFilters(filters);
- setExternalJoinOperator(joinOperator);
- setIsFilterPanelOpen(false);
- }, []);
+ const searchString = React.useMemo(
+ () => searchParams.toString(),
+ [searchParams]
+ )
+
+ const getSearchParam = React.useCallback(
+ (key: string, def = "") =>
+ new URLSearchParams(searchString).get(key) ?? def,
+ [searchString]
+ )
+
+ // ✅ 초기 데이터 설정 - EvaluationTargetsTable 패턴과 동일
+ const [initialPromiseData] = React.use(promises)
+ const [tableData, setTableData] = React.useState(initialPromiseData)
+ const [isDataLoading, setIsDataLoading] = React.useState(false)
+
+ // ✅ URL 필터 변경 감지 및 데이터 새로고침 - EvaluationTargetsTable 패턴과 동일
+ React.useEffect(() => {
+ const refetchData = async () => {
+ try {
+ setIsDataLoading(true)
+
+ // 현재 URL 파라미터 기반으로 새 검색 파라미터 생성
+ const currentFilters = getSearchParam("filters")
+ const currentJoinOperator = getSearchParam("joinOperator", "and")
+ const currentPage = parseInt(getSearchParam("page", "1"))
+ const currentPerPage = parseInt(getSearchParam("perPage", "10"))
+ const currentSort = getSearchParam('sort') ? JSON.parse(getSearchParam('sort')!) : [{ id: "createdAt", desc: true }]
+ const currentSearch = getSearchParam("search", "")
+ const currentAggregated = getSearchParam("aggregated") === "true"
+
+ const searchParams = {
+ filters: currentFilters ? JSON.parse(currentFilters) : [],
+ joinOperator: currentJoinOperator as "and" | "or",
+ page: currentPage,
+ perPage: currentPerPage,
+ sort: currentSort,
+ search: currentSearch,
+ evaluationYear: evaluationYear,
+ aggregated: currentAggregated
+ }
+
+ console.log("=== 새 데이터 요청 ===", searchParams)
+
+ // 서버 액션 직접 호출
+ const newData = await getPeriodicEvaluationsWithAggregation(searchParams)
+ setTableData(newData)
+
+ console.log("=== 데이터 업데이트 완료 ===", newData.data.length, "건")
+ } catch (error) {
+ console.error("데이터 새로고침 오류:", error)
+ } finally {
+ setIsDataLoading(false)
+ }
+ }
+
+ // 필터나 검색 파라미터가 변경되면 데이터 새로고침 (디바운스 적용)
+ const timeoutId = setTimeout(() => {
+ // 필터, 검색, 페이지네이션, 정렬 중 하나라도 변경되면 새로고침
+ const hasChanges = getSearchParam("filters") ||
+ getSearchParam("search") ||
+ getSearchParam("page") !== "1" ||
+ getSearchParam("perPage") !== "10" ||
+ getSearchParam("sort") ||
+ getSearchParam("aggregated")
+
+ if (hasChanges) {
+ refetchData()
+ }
+ }, 300) // 디바운스 시간 단축
+
+ return () => clearTimeout(timeoutId)
+ }, [searchString, evaluationYear, getSearchParam])
- // 컨테이너 위치 추적
+ const refreshData = React.useCallback(async () => {
+ try {
+ setIsDataLoading(true)
+
+ // 현재 URL 파라미터로 데이터 새로고침
+ const currentFilters = getSearchParam("filters")
+ const currentJoinOperator = getSearchParam("joinOperator", "and")
+ const currentPage = parseInt(getSearchParam("page", "1"))
+ const currentPerPage = parseInt(getSearchParam("perPage", "10"))
+ const currentSort = getSearchParam('sort') ? JSON.parse(getSearchParam('sort')!) : [{ id: "createdAt", desc: true }]
+ const currentSearch = getSearchParam("search", "")
+ const currentAggregated = getSearchParam("aggregated") === "true"
+
+ const searchParams = {
+ filters: currentFilters ? JSON.parse(currentFilters) : [],
+ joinOperator: currentJoinOperator as "and" | "or",
+ page: currentPage,
+ perPage: currentPerPage,
+ sort: currentSort,
+ search: currentSearch,
+ evaluationYear: evaluationYear,
+ aggregated: currentAggregated
+ }
+
+ const newData = await getPeriodicEvaluationsWithAggregation(searchParams)
+ setTableData(newData)
+
+ console.log("=== 데이터 새로고침 완료 ===", newData.data.length, "건")
+ } catch (error) {
+ console.error("데이터 새로고침 오류:", error)
+ } finally {
+ setIsDataLoading(false)
+ }
+ }, [evaluationYear, getSearchParam])
+
+ // 컨테이너 위치 추적 - EvaluationTargetsTable 패턴과 동일
const containerRef = React.useRef<HTMLDivElement>(null)
const [containerTop, setContainerTop] = React.useState(0)
@@ -347,42 +460,47 @@ export function PeriodicEvaluationsTable({
React.useEffect(() => {
updateContainerBounds()
- const throttledHandler = () => {
- let timeoutId: NodeJS.Timeout
- return () => {
- clearTimeout(timeoutId)
- timeoutId = setTimeout(updateContainerBounds, 16)
- }
+
+ const handleResize = () => {
+ updateContainerBounds()
}
-
- const handler = throttledHandler()
- window.addEventListener('resize', updateContainerBounds)
- window.addEventListener('scroll', handler)
-
+
+ window.addEventListener('resize', handleResize)
+ window.addEventListener('scroll', updateContainerBounds)
+
return () => {
- window.removeEventListener('resize', updateContainerBounds)
- window.removeEventListener('scroll', handler)
+ window.removeEventListener('resize', handleResize)
+ window.removeEventListener('scroll', updateContainerBounds)
}
}, [updateContainerBounds])
- // 데이터 로드
- const [promiseData] = React.use(promises)
- const tableData = promiseData
+ const parseSearchParamHelper = React.useCallback((key: string, defaultValue: any): any => {
+ try {
+ const value = getSearchParam(key)
+ return value ? JSON.parse(value) : defaultValue
+ } catch {
+ return defaultValue
+ }
+ }, [getSearchParam])
+
+ const parseSearchParam = <T,>(key: string, defaultValue: T): T => {
+ return parseSearchParamHelper(key, defaultValue)
+ }
// 테이블 설정
const initialSettings = React.useMemo(() => ({
- page: currentParams.page || 1,
- perPage: currentParams.perPage || 10,
- sort: currentParams.sort || [{ id: "createdAt", desc: true }],
- filters: currentParams.filters || [],
- joinOperator: currentParams.joinOperator || "and",
- search: "",
+ page: parseInt(getSearchParam("page", "1")),
+ perPage: parseInt(getSearchParam("perPage", "10")),
+ sort: getSearchParam('sort') ? JSON.parse(getSearchParam('sort')!) : [{ id: "createdAt", desc: true }],
+ filters: parseSearchParam("filters", []),
+ joinOperator: (getSearchParam("joinOperator") as "and" | "or") || "and",
+ search: getSearchParam("search", ""),
columnVisibility: {},
columnOrder: [],
pinnedColumns: { left: [], right: ["actions"] },
groupBy: [],
expandedRows: []
- }), [currentParams])
+ }), [getSearchParam, parseSearchParam])
const {
presets,
@@ -469,11 +587,17 @@ export function PeriodicEvaluationsTable({
const getActiveFilterCount = React.useCallback(() => {
try {
- return currentParams.filters?.length || 0;
+ // URL에서 현재 필터 수 확인
+ const filtersParam = getSearchParam("filters")
+ if (filtersParam) {
+ const filters = JSON.parse(filtersParam)
+ return Array.isArray(filters) ? filters.length : 0
+ }
+ return 0
} catch {
- return 0;
+ return 0
}
- }, [currentParams.filters]);
+ }, [getSearchParam])
const FILTER_PANEL_WIDTH = 400;
@@ -491,14 +615,13 @@ export function PeriodicEvaluationsTable({
height: `calc(100vh - ${containerTop}px)`
}}
>
- <div className="h-full">
- <PeriodicEvaluationFilterSheet
- isOpen={isFilterPanelOpen}
- onClose={() => setIsFilterPanelOpen(false)}
- onFiltersApply={handleFiltersApply}
- isLoading={false}
- />
- </div>
+ {/* ✅ 올바른 컴포넌트 사용 */}
+ <PeriodicEvaluationFilterSheet
+ isOpen={isFilterPanelOpen}
+ onClose={() => setIsFilterPanelOpen(false)}
+ onFiltersApply={handleFiltersApply} // ✅ 필터 적용 콜백 전달
+ isLoading={false}
+ />
</div>
{/* Main Content Container */}
@@ -567,9 +690,18 @@ export function PeriodicEvaluationsTable({
</div>
{/* Table Content Area */}
- <div className="flex-1 overflow-hidden" style={{ height: 'calc(100vh - 500px)' }}>
+ <div className="flex-1 overflow-hidden relative" style={{ height: 'calc(100vh - 500px)' }}>
+ {isDataLoading && (
+ <div className="absolute inset-0 bg-background/50 backdrop-blur-sm z-10 flex items-center justify-center">
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
+ <div className="w-4 h-4 border-2 border-primary border-t-transparent rounded-full animate-spin" />
+ 필터링 중...
+ </div>
+ </div>
+ )}
<div className="h-full w-full">
<DataTable table={table} className="h-full">
+ {/* ✅ EvaluationTargetsTable 패턴과 동일하게 수정 */}
<DataTableAdvancedToolbar
table={table}
filterFields={advancedFilterFields}
@@ -596,7 +728,10 @@ export function PeriodicEvaluationsTable({
onRenamePreset={renamePreset}
/>
- <PeriodicEvaluationsTableToolbarActions table={table} />
+ <PeriodicEvaluationsTableToolbarActions
+ table={table}
+ onRefresh={refreshData}
+ />
</div>
</DataTableAdvancedToolbar>
</DataTable>