플로우 위젯 검색 리스트
This commit is contained in:
parent
d13422f7ac
commit
2a968ab3cf
|
|
@ -59,12 +59,56 @@ export class AuthController {
|
||||||
logger.info(`- userName: ${userInfo.userName}`);
|
logger.info(`- userName: ${userInfo.userName}`);
|
||||||
logger.info(`- companyCode: ${userInfo.companyCode}`);
|
logger.info(`- companyCode: ${userInfo.companyCode}`);
|
||||||
|
|
||||||
|
// 사용자의 첫 번째 접근 가능한 메뉴 조회
|
||||||
|
let firstMenuPath: string | null = null;
|
||||||
|
try {
|
||||||
|
const { AdminService } = await import("../services/adminService");
|
||||||
|
const paramMap = {
|
||||||
|
userId: loginResult.userInfo.userId,
|
||||||
|
userCompanyCode: loginResult.userInfo.companyCode || "ILSHIN",
|
||||||
|
userType: loginResult.userInfo.userType,
|
||||||
|
userLang: "ko",
|
||||||
|
};
|
||||||
|
|
||||||
|
const menuList = await AdminService.getUserMenuList(paramMap);
|
||||||
|
logger.info(`로그인 후 메뉴 조회: 총 ${menuList.length}개 메뉴`);
|
||||||
|
|
||||||
|
// 접근 가능한 첫 번째 메뉴 찾기
|
||||||
|
// 조건:
|
||||||
|
// 1. LEV (레벨)이 2 이상 (최상위 폴더 제외)
|
||||||
|
// 2. MENU_URL이 있고 비어있지 않음
|
||||||
|
// 3. 이미 PATH, SEQ로 정렬되어 있으므로 첫 번째로 찾은 것이 첫 번째 메뉴
|
||||||
|
const firstMenu = menuList.find((menu: any) => {
|
||||||
|
const level = menu.lev || menu.level;
|
||||||
|
const url = menu.menu_url || menu.url;
|
||||||
|
|
||||||
|
return level >= 2 && url && url.trim() !== "" && url !== "#";
|
||||||
|
});
|
||||||
|
|
||||||
|
if (firstMenu) {
|
||||||
|
firstMenuPath = firstMenu.menu_url || firstMenu.url;
|
||||||
|
logger.info(`✅ 첫 번째 접근 가능한 메뉴 발견:`, {
|
||||||
|
name: firstMenu.menu_name_kor || firstMenu.translated_name,
|
||||||
|
url: firstMenuPath,
|
||||||
|
level: firstMenu.lev || firstMenu.level,
|
||||||
|
seq: firstMenu.seq,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.info(
|
||||||
|
"⚠️ 접근 가능한 메뉴가 없습니다. 메인 페이지로 이동합니다."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (menuError) {
|
||||||
|
logger.warn("메뉴 조회 중 오류 발생 (무시하고 계속):", menuError);
|
||||||
|
}
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
message: "로그인 성공",
|
message: "로그인 성공",
|
||||||
data: {
|
data: {
|
||||||
userInfo,
|
userInfo,
|
||||||
token: loginResult.token,
|
token: loginResult.token,
|
||||||
|
firstMenuPath, // 첫 번째 접근 가능한 메뉴 경로 추가
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { FlowComponent } from "@/types/screen-management";
|
import { FlowComponent } from "@/types/screen-management";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { AlertCircle, Loader2, ChevronUp } from "lucide-react";
|
import { AlertCircle, Loader2, ChevronUp, Filter, X } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
getFlowById,
|
getFlowById,
|
||||||
getAllStepCounts,
|
getAllStepCounts,
|
||||||
|
|
@ -27,6 +27,16 @@ import {
|
||||||
PaginationPrevious,
|
PaginationPrevious,
|
||||||
} from "@/components/ui/pagination";
|
} from "@/components/ui/pagination";
|
||||||
import { useFlowStepStore } from "@/stores/flowStepStore";
|
import { useFlowStepStore } from "@/stores/flowStepStore";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
|
||||||
interface FlowWidgetProps {
|
interface FlowWidgetProps {
|
||||||
component: FlowComponent;
|
component: FlowComponent;
|
||||||
|
|
@ -62,6 +72,13 @@ export function FlowWidget({
|
||||||
const [selectedRows, setSelectedRows] = useState<Set<number>>(new Set());
|
const [selectedRows, setSelectedRows] = useState<Set<number>>(new Set());
|
||||||
const [columnLabels, setColumnLabels] = useState<Record<string, string>>({}); // 컬럼명 -> 라벨 매핑
|
const [columnLabels, setColumnLabels] = useState<Record<string, string>>({}); // 컬럼명 -> 라벨 매핑
|
||||||
|
|
||||||
|
// 🆕 검색 필터 관련 상태
|
||||||
|
const [searchFilterColumns, setSearchFilterColumns] = useState<Set<string>>(new Set()); // 검색 필터로 사용할 컬럼
|
||||||
|
const [isFilterSettingOpen, setIsFilterSettingOpen] = useState(false); // 필터 설정 다이얼로그
|
||||||
|
const [searchValues, setSearchValues] = useState<Record<string, string>>({}); // 검색 값
|
||||||
|
const [allAvailableColumns, setAllAvailableColumns] = useState<string[]>([]); // 전체 컬럼 목록
|
||||||
|
const [filteredData, setFilteredData] = useState<any[]>([]); // 필터링된 데이터
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 🆕 컬럼 표시 결정 함수
|
* 🆕 컬럼 표시 결정 함수
|
||||||
* 1순위: 플로우 스텝 기본 설정 (displayConfig)
|
* 1순위: 플로우 스텝 기본 설정 (displayConfig)
|
||||||
|
|
@ -97,6 +114,113 @@ export function FlowWidget({
|
||||||
// 🆕 플로우 컴포넌트 ID (버튼이 이 플로우를 참조할 때 사용)
|
// 🆕 플로우 컴포넌트 ID (버튼이 이 플로우를 참조할 때 사용)
|
||||||
const flowComponentId = component.id;
|
const flowComponentId = component.id;
|
||||||
|
|
||||||
|
// 🆕 localStorage 키 생성
|
||||||
|
const filterSettingKey = useMemo(() => {
|
||||||
|
if (!flowId || selectedStepId === null) return null;
|
||||||
|
return `flowWidget_searchFilters_${flowId}_${selectedStepId}`;
|
||||||
|
}, [flowId, selectedStepId]);
|
||||||
|
|
||||||
|
// 🆕 저장된 필터 설정 불러오기
|
||||||
|
useEffect(() => {
|
||||||
|
if (!filterSettingKey || allAvailableColumns.length === 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem(filterSettingKey);
|
||||||
|
if (saved) {
|
||||||
|
const savedFilters = JSON.parse(saved);
|
||||||
|
setSearchFilterColumns(new Set(savedFilters));
|
||||||
|
} else {
|
||||||
|
// 초기값: 빈 필터 (사용자가 선택해야 함)
|
||||||
|
setSearchFilterColumns(new Set());
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("필터 설정 불러오기 실패:", error);
|
||||||
|
setSearchFilterColumns(new Set());
|
||||||
|
}
|
||||||
|
}, [filterSettingKey, allAvailableColumns]);
|
||||||
|
|
||||||
|
// 🆕 필터 설정 저장
|
||||||
|
const saveFilterSettings = useCallback(() => {
|
||||||
|
if (!filterSettingKey) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem(filterSettingKey, JSON.stringify(Array.from(searchFilterColumns)));
|
||||||
|
setIsFilterSettingOpen(false);
|
||||||
|
toast.success("검색 필터 설정이 저장되었습니다");
|
||||||
|
|
||||||
|
// 검색 값 초기화
|
||||||
|
setSearchValues({});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("필터 설정 저장 실패:", error);
|
||||||
|
toast.error("설정 저장에 실패했습니다");
|
||||||
|
}
|
||||||
|
}, [filterSettingKey, searchFilterColumns]);
|
||||||
|
|
||||||
|
// 🆕 필터 컬럼 토글
|
||||||
|
const toggleFilterColumn = useCallback((columnName: string) => {
|
||||||
|
setSearchFilterColumns((prev) => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
if (newSet.has(columnName)) {
|
||||||
|
newSet.delete(columnName);
|
||||||
|
} else {
|
||||||
|
newSet.add(columnName);
|
||||||
|
}
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 🆕 전체 선택/해제
|
||||||
|
const toggleAllFilters = useCallback(() => {
|
||||||
|
if (searchFilterColumns.size === allAvailableColumns.length) {
|
||||||
|
// 전체 해제
|
||||||
|
setSearchFilterColumns(new Set());
|
||||||
|
} else {
|
||||||
|
// 전체 선택
|
||||||
|
setSearchFilterColumns(new Set(allAvailableColumns));
|
||||||
|
}
|
||||||
|
}, [searchFilterColumns, allAvailableColumns]);
|
||||||
|
|
||||||
|
// 🆕 검색 실행
|
||||||
|
const handleSearch = useCallback(() => {
|
||||||
|
if (!stepData || stepData.length === 0) return;
|
||||||
|
|
||||||
|
const filtered = stepData.filter((row) => {
|
||||||
|
// 모든 검색 조건을 만족하는지 확인
|
||||||
|
return Array.from(searchFilterColumns).every((col) => {
|
||||||
|
const searchValue = searchValues[col];
|
||||||
|
if (!searchValue || searchValue.trim() === "") return true; // 빈 값은 필터링하지 않음
|
||||||
|
|
||||||
|
const cellValue = row[col];
|
||||||
|
if (cellValue === null || cellValue === undefined) return false;
|
||||||
|
|
||||||
|
// 문자열로 변환하여 대소문자 무시 검색
|
||||||
|
return String(cellValue).toLowerCase().includes(searchValue.toLowerCase());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
setFilteredData(filtered);
|
||||||
|
console.log("🔍 검색 실행:", {
|
||||||
|
totalRows: stepData.length,
|
||||||
|
filteredRows: filtered.length,
|
||||||
|
searchValues,
|
||||||
|
});
|
||||||
|
}, [stepData, searchFilterColumns, searchValues]);
|
||||||
|
|
||||||
|
// 🆕 검색 초기화
|
||||||
|
const handleClearSearch = useCallback(() => {
|
||||||
|
setSearchValues({});
|
||||||
|
setFilteredData([]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 검색 값이 변경될 때마다 자동 검색
|
||||||
|
useEffect(() => {
|
||||||
|
if (Object.keys(searchValues).length > 0) {
|
||||||
|
handleSearch();
|
||||||
|
} else {
|
||||||
|
setFilteredData([]);
|
||||||
|
}
|
||||||
|
}, [searchValues, handleSearch]);
|
||||||
|
|
||||||
// 선택된 스텝의 데이터를 다시 로드하는 함수
|
// 선택된 스텝의 데이터를 다시 로드하는 함수
|
||||||
const refreshStepData = async () => {
|
const refreshStepData = async () => {
|
||||||
if (!flowId) return;
|
if (!flowId) return;
|
||||||
|
|
@ -149,14 +273,18 @@ export function FlowWidget({
|
||||||
// 🆕 컬럼 추출 및 우선순위 적용
|
// 🆕 컬럼 추출 및 우선순위 적용
|
||||||
if (rows.length > 0) {
|
if (rows.length > 0) {
|
||||||
const allColumns = Object.keys(rows[0]);
|
const allColumns = Object.keys(rows[0]);
|
||||||
|
setAllAvailableColumns(allColumns); // 전체 컬럼 목록 저장
|
||||||
const visibleColumns = getVisibleColumns(selectedStepId, allColumns);
|
const visibleColumns = getVisibleColumns(selectedStepId, allColumns);
|
||||||
setStepDataColumns(visibleColumns);
|
setStepDataColumns(visibleColumns);
|
||||||
} else {
|
} else {
|
||||||
|
setAllAvailableColumns([]);
|
||||||
setStepDataColumns([]);
|
setStepDataColumns([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 선택 초기화
|
// 선택 초기화
|
||||||
setSelectedRows(new Set());
|
setSelectedRows(new Set());
|
||||||
|
setSearchValues({}); // 검색 값도 초기화
|
||||||
|
setFilteredData([]); // 필터링된 데이터 초기화
|
||||||
onSelectedDataChange?.([], selectedStepId);
|
onSelectedDataChange?.([], selectedStepId);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|
@ -242,6 +370,7 @@ export function FlowWidget({
|
||||||
setStepData(rows);
|
setStepData(rows);
|
||||||
if (rows.length > 0) {
|
if (rows.length > 0) {
|
||||||
const allColumns = Object.keys(rows[0]);
|
const allColumns = Object.keys(rows[0]);
|
||||||
|
setAllAvailableColumns(allColumns); // 전체 컬럼 목록 저장
|
||||||
// sortedSteps를 직접 전달하여 타이밍 이슈 해결
|
// sortedSteps를 직접 전달하여 타이밍 이슈 해결
|
||||||
const visibleColumns = getVisibleColumns(firstStep.id, allColumns, sortedSteps);
|
const visibleColumns = getVisibleColumns(firstStep.id, allColumns, sortedSteps);
|
||||||
setStepDataColumns(visibleColumns);
|
setStepDataColumns(visibleColumns);
|
||||||
|
|
@ -335,9 +464,11 @@ export function FlowWidget({
|
||||||
// 🆕 컬럼 추출 및 우선순위 적용
|
// 🆕 컬럼 추출 및 우선순위 적용
|
||||||
if (rows.length > 0) {
|
if (rows.length > 0) {
|
||||||
const allColumns = Object.keys(rows[0]);
|
const allColumns = Object.keys(rows[0]);
|
||||||
|
setAllAvailableColumns(allColumns); // 전체 컬럼 목록 저장
|
||||||
const visibleColumns = getVisibleColumns(stepId, allColumns);
|
const visibleColumns = getVisibleColumns(stepId, allColumns);
|
||||||
setStepDataColumns(visibleColumns);
|
setStepDataColumns(visibleColumns);
|
||||||
} else {
|
} else {
|
||||||
|
setAllAvailableColumns([]);
|
||||||
setStepDataColumns([]);
|
setStepDataColumns([]);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|
@ -385,9 +516,12 @@ export function FlowWidget({
|
||||||
onSelectedDataChange?.(selectedData, selectedStepId);
|
onSelectedDataChange?.(selectedData, selectedStepId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 🆕 표시할 데이터 결정 (필터링된 데이터 또는 전체 데이터)
|
||||||
|
const displayData = filteredData.length > 0 ? filteredData : stepData;
|
||||||
|
|
||||||
// 🆕 페이지네이션된 스텝 데이터
|
// 🆕 페이지네이션된 스텝 데이터
|
||||||
const paginatedStepData = stepData.slice((stepDataPage - 1) * stepDataPageSize, stepDataPage * stepDataPageSize);
|
const paginatedStepData = displayData.slice((stepDataPage - 1) * stepDataPageSize, stepDataPage * stepDataPageSize);
|
||||||
const totalStepDataPages = Math.ceil(stepData.length / stepDataPageSize);
|
const totalStepDataPages = Math.ceil(displayData.length / stepDataPageSize);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -513,15 +647,77 @@ export function FlowWidget({
|
||||||
<div className="bg-muted/30 mt-4 flex w-full flex-col rounded-lg border sm:mt-6 lg:mt-8">
|
<div className="bg-muted/30 mt-4 flex w-full flex-col rounded-lg border sm:mt-6 lg:mt-8">
|
||||||
{/* 헤더 - 자동 높이 */}
|
{/* 헤더 - 자동 높이 */}
|
||||||
<div className="bg-background flex-shrink-0 border-b px-4 py-3 sm:px-6 sm:py-4">
|
<div className="bg-background flex-shrink-0 border-b px-4 py-3 sm:px-6 sm:py-4">
|
||||||
<h4 className="text-foreground text-base font-semibold sm:text-lg">
|
<div className="flex items-start justify-between gap-3">
|
||||||
{steps.find((s) => s.id === selectedStepId)?.stepName}
|
<div className="flex-1">
|
||||||
</h4>
|
<h4 className="text-foreground text-base font-semibold sm:text-lg">
|
||||||
<p className="text-muted-foreground mt-1 text-xs sm:text-sm">
|
{steps.find((s) => s.id === selectedStepId)?.stepName}
|
||||||
총 {stepData.length}건의 데이터
|
</h4>
|
||||||
{selectedRows.size > 0 && (
|
<p className="text-muted-foreground mt-1 text-xs sm:text-sm">
|
||||||
<span className="text-primary ml-2 font-medium">({selectedRows.size}건 선택됨)</span>
|
총 {stepData.length}건의 데이터
|
||||||
|
{filteredData.length > 0 && (
|
||||||
|
<span className="text-primary ml-2 font-medium">(필터링: {filteredData.length}건)</span>
|
||||||
|
)}
|
||||||
|
{selectedRows.size > 0 && (
|
||||||
|
<span className="text-primary ml-2 font-medium">({selectedRows.size}건 선택됨)</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 🆕 필터 설정 버튼 */}
|
||||||
|
{allAvailableColumns.length > 0 && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setIsFilterSettingOpen(true)}
|
||||||
|
className="h-8 shrink-0 text-xs sm:text-sm"
|
||||||
|
>
|
||||||
|
<Filter className="mr-2 h-3 w-3 sm:h-4 sm:w-4" />
|
||||||
|
검색 필터 설정
|
||||||
|
{searchFilterColumns.size > 0 && (
|
||||||
|
<Badge variant="secondary" className="ml-2 h-5 px-1.5 text-[10px]">
|
||||||
|
{searchFilterColumns.size}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
</p>
|
</div>
|
||||||
|
|
||||||
|
{/* 🆕 검색 필터 입력 영역 */}
|
||||||
|
{searchFilterColumns.size > 0 && (
|
||||||
|
<div className="bg-muted/30 mt-4 space-y-3 rounded border p-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h5 className="text-sm font-medium">검색 필터</h5>
|
||||||
|
{Object.keys(searchValues).length > 0 && (
|
||||||
|
<Button variant="ghost" size="sm" onClick={handleClearSearch} className="h-7 text-xs">
|
||||||
|
<X className="mr-1 h-3 w-3" />
|
||||||
|
초기화
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{Array.from(searchFilterColumns).map((col) => (
|
||||||
|
<div key={col} className="space-y-1.5">
|
||||||
|
<Label htmlFor={`search-${col}`} className="text-xs">
|
||||||
|
{columnLabels[col] || col}
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id={`search-${col}`}
|
||||||
|
value={searchValues[col] || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSearchValues((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[col]: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
placeholder={`${columnLabels[col] || col} 검색...`}
|
||||||
|
className="h-8 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 데이터 영역 - 고정 높이 + 스크롤 */}
|
{/* 데이터 영역 - 고정 높이 + 스크롤 */}
|
||||||
|
|
@ -746,6 +942,76 @@ export function FlowWidget({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 🆕 검색 필터 설정 다이얼로그 */}
|
||||||
|
<Dialog open={isFilterSettingOpen} onOpenChange={setIsFilterSettingOpen}>
|
||||||
|
<DialogContent className="max-w-[95vw] sm:max-w-[500px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-base sm:text-lg">검색 필터 설정</DialogTitle>
|
||||||
|
<DialogDescription className="text-xs sm:text-sm">
|
||||||
|
검색 필터로 사용할 컬럼을 선택하세요. 선택한 컬럼의 검색 입력 필드가 표시됩니다.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-3 sm:space-y-4">
|
||||||
|
{/* 전체 선택/해제 */}
|
||||||
|
<div className="bg-muted/50 flex items-center gap-3 rounded border p-3">
|
||||||
|
<Checkbox
|
||||||
|
id="select-all-filters"
|
||||||
|
checked={searchFilterColumns.size === allAvailableColumns.length && allAvailableColumns.length > 0}
|
||||||
|
onCheckedChange={toggleAllFilters}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="select-all-filters" className="flex-1 cursor-pointer text-xs font-semibold sm:text-sm">
|
||||||
|
전체 선택/해제
|
||||||
|
</Label>
|
||||||
|
<span className="text-muted-foreground text-xs">
|
||||||
|
{searchFilterColumns.size} / {allAvailableColumns.length}개
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 컬럼 목록 */}
|
||||||
|
<div className="max-h-[50vh] space-y-2 overflow-y-auto rounded border p-2">
|
||||||
|
{allAvailableColumns.map((col) => (
|
||||||
|
<div key={col} className="hover:bg-muted/50 flex items-center gap-3 rounded p-2">
|
||||||
|
<Checkbox
|
||||||
|
id={`filter-${col}`}
|
||||||
|
checked={searchFilterColumns.has(col)}
|
||||||
|
onCheckedChange={() => toggleFilterColumn(col)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor={`filter-${col}`} className="flex-1 cursor-pointer text-xs font-normal sm:text-sm">
|
||||||
|
{columnLabels[col] || col}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 선택된 컬럼 개수 안내 */}
|
||||||
|
<div className="text-muted-foreground bg-muted/30 rounded p-3 text-center text-xs">
|
||||||
|
{searchFilterColumns.size === 0 ? (
|
||||||
|
<span>검색 필터를 사용하려면 최소 1개 이상의 컬럼을 선택하세요</span>
|
||||||
|
) : (
|
||||||
|
<span>
|
||||||
|
총 <span className="text-primary font-semibold">{searchFilterColumns.size}개</span>의 검색 필터가
|
||||||
|
표시됩니다
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="gap-2 sm:gap-0">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setIsFilterSettingOpen(false)}
|
||||||
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
||||||
|
>
|
||||||
|
취소
|
||||||
|
</Button>
|
||||||
|
<Button onClick={saveFilterSettings} className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
|
||||||
|
저장
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -141,8 +141,18 @@ export const useLogin = () => {
|
||||||
// 쿠키에도 저장 (미들웨어에서 사용)
|
// 쿠키에도 저장 (미들웨어에서 사용)
|
||||||
document.cookie = `authToken=${result.data.token}; path=/; max-age=86400; SameSite=Lax`;
|
document.cookie = `authToken=${result.data.token}; path=/; max-age=86400; SameSite=Lax`;
|
||||||
|
|
||||||
// 로그인 성공
|
// 로그인 성공 - 첫 번째 접근 가능한 메뉴로 리다이렉트
|
||||||
router.push(AUTH_CONFIG.ROUTES.MAIN);
|
const firstMenuPath = result.data?.firstMenuPath;
|
||||||
|
|
||||||
|
if (firstMenuPath) {
|
||||||
|
// 접근 가능한 메뉴가 있으면 해당 메뉴로 이동
|
||||||
|
console.log("첫 번째 접근 가능한 메뉴로 이동:", firstMenuPath);
|
||||||
|
router.push(firstMenuPath);
|
||||||
|
} else {
|
||||||
|
// 접근 가능한 메뉴가 없으면 메인 페이지로 이동
|
||||||
|
console.log("접근 가능한 메뉴가 없어 메인 페이지로 이동");
|
||||||
|
router.push(AUTH_CONFIG.ROUTES.MAIN);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 로그인 실패
|
// 로그인 실패
|
||||||
setError(result.message || FORM_VALIDATION.MESSAGES.LOGIN_FAILED);
|
setError(result.message || FORM_VALIDATION.MESSAGES.LOGIN_FAILED);
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,11 @@ export interface LoginFormData {
|
||||||
export interface LoginResponse {
|
export interface LoginResponse {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
message?: string;
|
message?: string;
|
||||||
data?: any;
|
data?: {
|
||||||
|
token?: string;
|
||||||
|
userInfo?: any;
|
||||||
|
firstMenuPath?: string | null;
|
||||||
|
};
|
||||||
errorCode?: string;
|
errorCode?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue