Add category view, pagination, and persist login across deployments
- Add 2-panel category view: sidebar tree + filtered item list - Category counts use DISTINCT with descendant inclusion - Hide empty categories, show category badges on item cards - Add client-side pagination (10 items/page) for both views - Persist access token in localStorage to survive page refresh - Fix token refresh retry on backend restart Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,11 +13,26 @@ interface KnowledgeItem {
|
||||
SOURCE_URL: string;
|
||||
STATUS: string;
|
||||
CREATED_AT: string;
|
||||
CATEGORIES?: string;
|
||||
}
|
||||
|
||||
interface Category {
|
||||
ID: string;
|
||||
NAME: string;
|
||||
PARENT_ID: string | null;
|
||||
DEPTH: number;
|
||||
FULL_PATH: string;
|
||||
ITEM_COUNT: number;
|
||||
}
|
||||
|
||||
interface CategoryTree extends Category {
|
||||
children: CategoryTree[];
|
||||
}
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
PENDING: "bg-yellow-500/20 text-yellow-400",
|
||||
EXTRACTING: "bg-blue-500/20 text-blue-400",
|
||||
STRUCTURING: "bg-orange-500/20 text-orange-400",
|
||||
CHUNKING: "bg-purple-500/20 text-purple-400",
|
||||
CATEGORIZING: "bg-indigo-500/20 text-indigo-400",
|
||||
EMBEDDING: "bg-cyan-500/20 text-cyan-400",
|
||||
@@ -31,10 +46,111 @@ const typeLabels: Record<string, string> = {
|
||||
TEXT: "Text",
|
||||
};
|
||||
|
||||
function buildTree(categories: Category[]): CategoryTree[] {
|
||||
const map = new Map<string, CategoryTree>();
|
||||
const roots: CategoryTree[] = [];
|
||||
|
||||
for (const cat of categories) {
|
||||
map.set(cat.ID, { ...cat, children: [] });
|
||||
}
|
||||
|
||||
for (const cat of categories) {
|
||||
const node = map.get(cat.ID)!;
|
||||
if (cat.PARENT_ID && map.has(cat.PARENT_ID)) {
|
||||
map.get(cat.PARENT_ID)!.children.push(node);
|
||||
} else {
|
||||
roots.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
// 항목이 0개인 카테고리 제거
|
||||
function prune(nodes: CategoryTree[]): CategoryTree[] {
|
||||
return nodes
|
||||
.map((n) => ({ ...n, children: prune(n.children) }))
|
||||
.filter((n) => n.ITEM_COUNT > 0 || n.children.length > 0);
|
||||
}
|
||||
|
||||
return prune(roots);
|
||||
}
|
||||
|
||||
// ITEM_COUNT는 백엔드에서 하위 카테고리 포함 고유 항목 수를 이미 계산함
|
||||
|
||||
function CategoryNode({
|
||||
node,
|
||||
selectedId,
|
||||
onSelect,
|
||||
expanded,
|
||||
onToggle,
|
||||
}: {
|
||||
node: CategoryTree;
|
||||
selectedId: string | null;
|
||||
onSelect: (id: string, path: string) => void;
|
||||
expanded: Set<string>;
|
||||
onToggle: (id: string) => void;
|
||||
}) {
|
||||
const isExpanded = expanded.has(node.ID);
|
||||
const isSelected = selectedId === node.ID;
|
||||
const hasChildren = node.children.length > 0;
|
||||
const totalCount = node.ITEM_COUNT;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className={`flex items-center gap-1 px-2 py-1.5 rounded-lg cursor-pointer text-sm transition-colors ${
|
||||
isSelected
|
||||
? "bg-[var(--color-primary)]/20 text-[var(--color-primary)]"
|
||||
: "hover:bg-[var(--color-bg-hover)] text-[var(--color-text-muted)]"
|
||||
}`}
|
||||
style={{ paddingLeft: `${(node.DEPTH - 1) * 16 + 8}px` }}
|
||||
onClick={() => {
|
||||
onSelect(node.ID, node.FULL_PATH);
|
||||
if (hasChildren) onToggle(node.ID);
|
||||
}}
|
||||
>
|
||||
<span className="w-4 text-center text-xs">
|
||||
{hasChildren ? (isExpanded ? "▼" : "▶") : "·"}
|
||||
</span>
|
||||
<span className="flex-1 truncate">{node.NAME}</span>
|
||||
{totalCount > 0 && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded-full bg-[var(--color-bg-hover)]">
|
||||
{totalCount}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{isExpanded && hasChildren && (
|
||||
<div>
|
||||
{node.children.map((child) => (
|
||||
<CategoryNode
|
||||
key={child.ID}
|
||||
node={child}
|
||||
selectedId={selectedId}
|
||||
onSelect={onSelect}
|
||||
expanded={expanded}
|
||||
onToggle={onToggle}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function KnowledgePage() {
|
||||
const { request } = useApi();
|
||||
const [items, setItems] = useState<KnowledgeItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [viewMode, setViewMode] = useState<"list" | "category">("category");
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
// 카테고리 뷰 상태
|
||||
const [categoryTree, setCategoryTree] = useState<CategoryTree[]>([]);
|
||||
const [uncategorized, setUncategorized] = useState<KnowledgeItem[]>([]);
|
||||
const [selectedCategoryId, setSelectedCategoryId] = useState<string | null>("__all__");
|
||||
const [selectedCategoryPath, setSelectedCategoryPath] = useState<string>("전체");
|
||||
const [categoryItems, setCategoryItems] = useState<KnowledgeItem[]>([]);
|
||||
const [expanded, setExpanded] = useState<Set<string>>(new Set());
|
||||
const [catLoading, setCatLoading] = useState(false);
|
||||
|
||||
const fetchItems = async () => {
|
||||
try {
|
||||
@@ -47,19 +163,117 @@ export default function KnowledgePage() {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
const data = await request<{ categories: Category[]; uncategorized: KnowledgeItem[] }>({
|
||||
method: "GET",
|
||||
url: "/api/knowledge/categories",
|
||||
});
|
||||
const tree = buildTree(data.categories);
|
||||
setCategoryTree(tree);
|
||||
setUncategorized(data.uncategorized);
|
||||
// 최상위 카테고리를 펼침
|
||||
setExpanded(new Set(tree.map((n) => n.ID)));
|
||||
} catch (err) {
|
||||
console.error("Failed to load categories:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchCategoryItems = async (categoryId: string) => {
|
||||
setCatLoading(true);
|
||||
try {
|
||||
const data = await request<KnowledgeItem[]>({
|
||||
method: "GET",
|
||||
url: `/api/knowledge/categories/${categoryId}/items`,
|
||||
});
|
||||
setCategoryItems(data);
|
||||
} catch (err) {
|
||||
console.error("Failed to load category items:", err);
|
||||
} finally {
|
||||
setCatLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectCategory = (id: string, path: string) => {
|
||||
setSelectedCategoryId(id);
|
||||
setSelectedCategoryPath(path);
|
||||
setCurrentPage(1);
|
||||
fetchCategoryItems(id);
|
||||
};
|
||||
|
||||
const handleSelectUncategorized = () => {
|
||||
setSelectedCategoryId("__uncategorized__");
|
||||
setSelectedCategoryPath("미분류");
|
||||
setCategoryItems(uncategorized as KnowledgeItem[]);
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
const handleSelectAll = () => {
|
||||
setSelectedCategoryId("__all__");
|
||||
setSelectedCategoryPath("전체");
|
||||
setCategoryItems(items);
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
const handleToggle = (id: string) => {
|
||||
setExpanded((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(id)) next.delete(id);
|
||||
else next.add(id);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchItems();
|
||||
// Poll for status updates every 5 seconds
|
||||
fetchCategories();
|
||||
const interval = setInterval(fetchItems, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// 카테고리 뷰에서 표시할 항목 결정
|
||||
const allDisplayItems =
|
||||
viewMode === "list"
|
||||
? items
|
||||
: selectedCategoryId === "__all__"
|
||||
? items
|
||||
: selectedCategoryId === "__uncategorized__"
|
||||
? uncategorized
|
||||
: categoryItems;
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(allDisplayItems.length / PAGE_SIZE));
|
||||
const displayItems = allDisplayItems.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE);
|
||||
|
||||
return (
|
||||
<AuthGuard>
|
||||
<NavBar />
|
||||
<main className="max-w-7xl mx-auto px-4 py-8">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-2xl font-bold">Knowledge</h1>
|
||||
<div className="flex items-center gap-4">
|
||||
<h1 className="text-2xl font-bold">Knowledge</h1>
|
||||
<div className="flex gap-1 bg-[var(--color-bg-card)] rounded-lg p-0.5 border border-[var(--color-border)]">
|
||||
<button
|
||||
onClick={() => setViewMode("list")}
|
||||
className={`px-3 py-1 text-xs rounded-md transition-colors ${
|
||||
viewMode === "list"
|
||||
? "bg-[var(--color-primary)] text-white"
|
||||
: "text-[var(--color-text-muted)] hover:text-white"
|
||||
}`}
|
||||
>
|
||||
목록
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setViewMode("category"); fetchCategories(); }}
|
||||
className={`px-3 py-1 text-xs rounded-md transition-colors ${
|
||||
viewMode === "category"
|
||||
? "bg-[var(--color-primary)] text-white"
|
||||
: "text-[var(--color-text-muted)] hover:text-white"
|
||||
}`}
|
||||
>
|
||||
카테고리
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
href="/knowledge/add"
|
||||
className="px-4 py-2 bg-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] rounded-lg transition-colors"
|
||||
@@ -70,44 +284,192 @@ export default function KnowledgePage() {
|
||||
|
||||
{loading ? (
|
||||
<p className="text-[var(--color-text-muted)]">Loading...</p>
|
||||
) : items.length === 0 ? (
|
||||
<div className="bg-[var(--color-bg-card)] rounded-xl p-6 border border-[var(--color-border)]">
|
||||
<p className="text-[var(--color-text-muted)]">No knowledge items yet. Add your first item to get started.</p>
|
||||
</div>
|
||||
) : viewMode === "list" ? (
|
||||
/* 목록 뷰 */
|
||||
items.length === 0 ? (
|
||||
<div className="bg-[var(--color-bg-card)] rounded-xl p-6 border border-[var(--color-border)]">
|
||||
<p className="text-[var(--color-text-muted)]">No knowledge items yet. Add your first item to get started.</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="space-y-3">
|
||||
{displayItems.map((item) => (
|
||||
<KnowledgeItemCard key={item.ID} item={item} />
|
||||
))}
|
||||
</div>
|
||||
{totalPages > 1 && <Pagination currentPage={currentPage} totalPages={totalPages} onPageChange={setCurrentPage} />}
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{items.map((item) => (
|
||||
<Link
|
||||
key={item.ID}
|
||||
href={`/knowledge/${item.ID}`}
|
||||
className="block bg-[var(--color-bg-card)] rounded-xl p-4 border border-[var(--color-border)] hover:border-[var(--color-primary)] transition-colors"
|
||||
/* 카테고리 뷰 — 2단 레이아웃 */
|
||||
<div className="flex gap-4" style={{ minHeight: "60vh" }}>
|
||||
{/* 왼쪽: 카테고리 트리 */}
|
||||
<div className="w-64 shrink-0 bg-[var(--color-bg-card)] rounded-xl border border-[var(--color-border)] p-3 overflow-y-auto" style={{ maxHeight: "75vh" }}>
|
||||
{/* 전체 보기 */}
|
||||
<div
|
||||
className={`flex items-center gap-1 px-2 py-1.5 rounded-lg cursor-pointer text-sm transition-colors mb-1 ${
|
||||
selectedCategoryId === "__all__"
|
||||
? "bg-[var(--color-primary)]/20 text-[var(--color-primary)]"
|
||||
: "hover:bg-[var(--color-bg-hover)] text-[var(--color-text-muted)]"
|
||||
}`}
|
||||
onClick={handleSelectAll}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<span className="text-xs px-2 py-0.5 rounded bg-[var(--color-bg-hover)] text-[var(--color-text-muted)]">
|
||||
{typeLabels[item.TYPE] || item.TYPE}
|
||||
</span>
|
||||
<span className={`text-xs px-2 py-0.5 rounded ${statusColors[item.STATUS] || ""}`}>
|
||||
{item.STATUS}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="font-medium truncate">
|
||||
{item.TITLE || item.SOURCE_URL || "Untitled"}
|
||||
</h3>
|
||||
{item.SOURCE_URL && (
|
||||
<p className="text-sm text-[var(--color-text-muted)] truncate mt-1">{item.SOURCE_URL}</p>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-sm text-[var(--color-text-muted)] ml-4 whitespace-nowrap">
|
||||
{new Date(item.CREATED_AT).toLocaleDateString()}
|
||||
</span>
|
||||
<span className="w-4 text-center text-xs">*</span>
|
||||
<span className="flex-1">전체</span>
|
||||
<span className="text-xs px-1.5 py-0.5 rounded-full bg-[var(--color-bg-hover)]">{items.length}</span>
|
||||
</div>
|
||||
|
||||
{/* 카테고리 트리 */}
|
||||
{categoryTree.map((node) => (
|
||||
<CategoryNode
|
||||
key={node.ID}
|
||||
node={node}
|
||||
selectedId={selectedCategoryId}
|
||||
onSelect={handleSelectCategory}
|
||||
expanded={expanded}
|
||||
onToggle={handleToggle}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* 미분류 */}
|
||||
{uncategorized.length > 0 && (
|
||||
<div
|
||||
className={`flex items-center gap-1 px-2 py-1.5 rounded-lg cursor-pointer text-sm transition-colors mt-2 ${
|
||||
selectedCategoryId === "__uncategorized__"
|
||||
? "bg-[var(--color-primary)]/20 text-[var(--color-primary)]"
|
||||
: "hover:bg-[var(--color-bg-hover)] text-[var(--color-text-muted)]"
|
||||
}`}
|
||||
onClick={handleSelectUncategorized}
|
||||
>
|
||||
<span className="w-4 text-center text-xs">?</span>
|
||||
<span className="flex-1">미분류</span>
|
||||
<span className="text-xs px-1.5 py-0.5 rounded-full bg-[var(--color-bg-hover)]">{uncategorized.length}</span>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 오른쪽: 선택된 카테고리의 항목 목록 */}
|
||||
<div className="flex-1 min-w-0">
|
||||
{/* 브레드크럼 */}
|
||||
<div className="text-sm text-[var(--color-text-muted)] mb-3">
|
||||
{selectedCategoryPath.split("/").map((part, i, arr) => (
|
||||
<span key={i}>
|
||||
{i > 0 && <span className="mx-1">{">"}</span>}
|
||||
<span className={i === arr.length - 1 ? "text-white" : ""}>{part}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{catLoading ? (
|
||||
<p className="text-[var(--color-text-muted)]">Loading...</p>
|
||||
) : (displayItems as KnowledgeItem[]).length === 0 ? (
|
||||
<div className="bg-[var(--color-bg-card)] rounded-xl p-6 border border-[var(--color-border)]">
|
||||
<p className="text-[var(--color-text-muted)]">
|
||||
{selectedCategoryId === "__all__"
|
||||
? "등록된 지식이 없습니다."
|
||||
: "이 카테고리에 항목이 없습니다."}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="space-y-3">
|
||||
{(displayItems as KnowledgeItem[]).map((item) => (
|
||||
<KnowledgeItemCard key={item.ID} item={item} />
|
||||
))}
|
||||
</div>
|
||||
{totalPages > 1 && <Pagination currentPage={currentPage} totalPages={totalPages} onPageChange={setCurrentPage} />}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</AuthGuard>
|
||||
);
|
||||
}
|
||||
|
||||
function KnowledgeItemCard({ item }: { item: KnowledgeItem }) {
|
||||
return (
|
||||
<Link
|
||||
href={`/knowledge/${item.ID}`}
|
||||
className="block bg-[var(--color-bg-card)] rounded-xl p-4 border border-[var(--color-border)] hover:border-[var(--color-primary)] transition-colors"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<span className="text-xs px-2 py-0.5 rounded bg-[var(--color-bg-hover)] text-[var(--color-text-muted)]">
|
||||
{typeLabels[item.TYPE] || item.TYPE}
|
||||
</span>
|
||||
<span className={`text-xs px-2 py-0.5 rounded ${statusColors[item.STATUS] || ""}`}>
|
||||
{item.STATUS}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="font-medium truncate">
|
||||
{item.TITLE || item.SOURCE_URL || "Untitled"}
|
||||
</h3>
|
||||
{item.CATEGORIES && (
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{item.CATEGORIES.split(", ").map((cat, i) => (
|
||||
<span key={i} className="text-xs px-1.5 py-0.5 rounded bg-[var(--color-primary)]/10 text-[var(--color-primary)]">
|
||||
{cat}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-sm text-[var(--color-text-muted)] ml-4 whitespace-nowrap">
|
||||
{new Date(item.CREATED_AT).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function Pagination({ currentPage, totalPages, onPageChange }: { currentPage: number; totalPages: number; onPageChange: (page: number) => void }) {
|
||||
const pages: number[] = [];
|
||||
const start = Math.max(1, currentPage - 2);
|
||||
const end = Math.min(totalPages, currentPage + 2);
|
||||
for (let i = start; i <= end; i++) pages.push(i);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-1 mt-6">
|
||||
<button
|
||||
onClick={() => onPageChange(currentPage - 1)}
|
||||
disabled={currentPage <= 1}
|
||||
className="px-3 py-1.5 text-sm rounded-lg bg-[var(--color-bg-card)] border border-[var(--color-border)] disabled:opacity-30 hover:bg-[var(--color-bg-hover)]"
|
||||
>
|
||||
이전
|
||||
</button>
|
||||
{start > 1 && (
|
||||
<>
|
||||
<button onClick={() => onPageChange(1)} className="px-3 py-1.5 text-sm rounded-lg hover:bg-[var(--color-bg-hover)]">1</button>
|
||||
{start > 2 && <span className="px-1 text-[var(--color-text-muted)]">...</span>}
|
||||
</>
|
||||
)}
|
||||
{pages.map((p) => (
|
||||
<button
|
||||
key={p}
|
||||
onClick={() => onPageChange(p)}
|
||||
className={`px-3 py-1.5 text-sm rounded-lg ${
|
||||
p === currentPage ? "bg-[var(--color-primary)] text-white" : "hover:bg-[var(--color-bg-hover)]"
|
||||
}`}
|
||||
>
|
||||
{p}
|
||||
</button>
|
||||
))}
|
||||
{end < totalPages && (
|
||||
<>
|
||||
{end < totalPages - 1 && <span className="px-1 text-[var(--color-text-muted)]">...</span>}
|
||||
<button onClick={() => onPageChange(totalPages)} className="px-3 py-1.5 text-sm rounded-lg hover:bg-[var(--color-bg-hover)]">{totalPages}</button>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
onClick={() => onPageChange(currentPage + 1)}
|
||||
disabled={currentPage >= totalPages}
|
||||
className="px-3 py-1.5 text-sm rounded-lg bg-[var(--color-bg-card)] border border-[var(--color-border)] disabled:opacity-30 hover:bg-[var(--color-bg-hover)]"
|
||||
>
|
||||
다음
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,21 +22,40 @@ const AuthContext = createContext<AuthContextType>({
|
||||
});
|
||||
|
||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||
const [accessToken, setAccessTokenState] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const logoutRef = useRef<() => void>(() => {});
|
||||
|
||||
// localStorage와 동기화하는 setter
|
||||
const setAccessToken = useCallback((token: string | null) => {
|
||||
setAccessTokenState(token);
|
||||
if (token) {
|
||||
localStorage.setItem("accessToken", token);
|
||||
} else {
|
||||
localStorage.removeItem("accessToken");
|
||||
}
|
||||
}, []);
|
||||
|
||||
// interceptor 콜백 등록
|
||||
useEffect(() => {
|
||||
setAuthCallbacks(
|
||||
(token: string) => setAccessToken(token),
|
||||
() => logoutRef.current()
|
||||
);
|
||||
}, []);
|
||||
}, [setAccessToken]);
|
||||
|
||||
useEffect(() => {
|
||||
// Try to restore session from refresh token cookie
|
||||
const tryRefresh = async () => {
|
||||
const restoreSession = async () => {
|
||||
// 1차: localStorage에서 복원
|
||||
const stored = localStorage.getItem("accessToken");
|
||||
if (stored) {
|
||||
setAccessTokenState(stored);
|
||||
api.defaults.headers.common["Authorization"] = `Bearer ${stored}`;
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2차: refresh token cookie로 복원
|
||||
try {
|
||||
const res = await api.post<LoginResponse>("/api/auth/refresh");
|
||||
const token = res.data.accessToken;
|
||||
@@ -48,8 +67,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
tryRefresh();
|
||||
}, []);
|
||||
restoreSession();
|
||||
}, [setAccessToken]);
|
||||
|
||||
useEffect(() => {
|
||||
if (accessToken) {
|
||||
@@ -61,7 +80,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
|
||||
const login = useCallback((response: LoginResponse) => {
|
||||
setAccessToken(response.accessToken);
|
||||
}, []);
|
||||
}, [setAccessToken]);
|
||||
|
||||
const logout = useCallback(async () => {
|
||||
try {
|
||||
@@ -71,7 +90,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
setAccessToken(null);
|
||||
window.location.href = "/login";
|
||||
}, []);
|
||||
}, [setAccessToken]);
|
||||
|
||||
// ref로 최신 logout 유지 (interceptor에서 사용)
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user