288 lines
9.5 KiB
TypeScript
288 lines
9.5 KiB
TypeScript
"use client";
|
|
import { useEffect, useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import Image from "next/image";
|
|
import { tokenManager } from "@/entities/auth/lib/token-manager";
|
|
import { client } from "@/shared/api";
|
|
import { ENDPOINT } from "@/shared/config";
|
|
import { KoreaBasinMap } from "../lib/korea-basin-map";
|
|
import { useDndSensors } from "../lib/hooks/use-dnd-sensors";
|
|
import { usePanelOrder } from "../lib/hooks/use-panel-order";
|
|
import {
|
|
Chart as ChartJS,
|
|
CategoryScale,
|
|
LinearScale,
|
|
PointElement,
|
|
LineElement,
|
|
BarElement,
|
|
Filler,
|
|
Legend,
|
|
Tooltip,
|
|
ArcElement,
|
|
} from "chart.js";
|
|
import { DndContext, closestCenter } from "@dnd-kit/core";
|
|
import { SortableContext, rectSortingStrategy } from "@dnd-kit/sortable";
|
|
import {
|
|
getInitialTopPanels,
|
|
getInitialBottomPanels,
|
|
} from "../model/panel-config";
|
|
import MyPostList from "./mypost-list";
|
|
import ScheduleList from "./schedule-list";
|
|
import { SortablePanel } from "./sortable-panel";
|
|
|
|
// Chart.js 등록
|
|
ChartJS.register(
|
|
CategoryScale,
|
|
LinearScale,
|
|
PointElement,
|
|
LineElement,
|
|
BarElement,
|
|
Filler,
|
|
Legend,
|
|
Tooltip,
|
|
ArcElement,
|
|
);
|
|
|
|
interface UserInfo {
|
|
email?: string;
|
|
userId?: string;
|
|
userName?: string;
|
|
twoFactorEnabled?: boolean;
|
|
}
|
|
|
|
function DashboardPage() {
|
|
const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
|
|
const [error, setError] = useState("");
|
|
const [isMounted, setIsMounted] = useState(false);
|
|
const router = useRouter();
|
|
|
|
// dnd-kit 센서 설정
|
|
const sensors = useDndSensors();
|
|
|
|
// 패널 순서 관리
|
|
const { panels: topPanels, handleDragEnd: handleTopDragEnd } = usePanelOrder({
|
|
initialPanels: getInitialTopPanels(),
|
|
storageKey: "dashboard-top-panels-order",
|
|
});
|
|
|
|
const { panels: bottomPanels, handleDragEnd: handleBottomDragEnd } =
|
|
usePanelOrder({
|
|
initialPanels: getInitialBottomPanels(),
|
|
storageKey: "dashboard-bottom-panels-order",
|
|
});
|
|
|
|
// 클라이언트에서만 드래그 기능 활성화 (hydration 문제 방지)
|
|
useEffect(() => {
|
|
setIsMounted(true);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const fetchUserInfo = async () => {
|
|
console.log("fetchUserInfo called");
|
|
|
|
try {
|
|
// tokenManager를 통해 토큰 확인
|
|
const accessToken = tokenManager.getToken("Authorization");
|
|
const userId = tokenManager.getToken("token-id");
|
|
|
|
if (!accessToken || !userId) {
|
|
console.log("No tokens found, redirecting to login");
|
|
setError("로그인이 필요합니다.");
|
|
router.push("/auth/login");
|
|
return;
|
|
}
|
|
|
|
console.log("Fetching user info with userId:", userId);
|
|
|
|
// client를 사용하여 API 호출 (자동 토큰 처리)
|
|
const response = await client.get(
|
|
`${ENDPOINT.USER_SERVICE.USERS}/${userId}`,
|
|
);
|
|
|
|
if (response.data) {
|
|
console.log("User info received:", response.data);
|
|
setUserInfo(response.data);
|
|
} else {
|
|
setError("사용자 정보를 불러올 수 없습니다.");
|
|
}
|
|
} catch (error) {
|
|
console.error("Error fetching user info:", error);
|
|
if (error instanceof Error && error.message.includes("401")) {
|
|
setError("인증이 만료되었습니다. 다시 로그인해주세요.");
|
|
tokenManager.clearAuthToken();
|
|
router.push("/auth/login");
|
|
} else {
|
|
setError("네트워크 오류가 발생했습니다.");
|
|
}
|
|
}
|
|
};
|
|
|
|
fetchUserInfo();
|
|
}, [router]);
|
|
|
|
const handleLogout = () => {
|
|
console.log("Logging out...");
|
|
// localStorage 정리
|
|
localStorage.removeItem("tempToken");
|
|
// tokenManager를 통해 쿠키 정리
|
|
tokenManager.clearAuthToken();
|
|
router.push("/auth/logout");
|
|
};
|
|
|
|
const handleChangePassword = () => {
|
|
router.push("/otp?next=change-password");
|
|
};
|
|
|
|
return (
|
|
<div className="p-2 sm:p-4 md:p-6">
|
|
<div className="flex flex-col gap-1">
|
|
{/* 첫번째 라인 - 좌측 6개 패널(2x3) + 우측 지도/사용자 정보 */}
|
|
<div className="flex flex-col gap-1 lg:flex-row">
|
|
{/* 좌측: 드래그 가능한 6개 패널 (2x3 그리드, 화면의 50%) */}
|
|
<div className="w-full lg:w-1/2">
|
|
{isMounted ? (
|
|
<DndContext
|
|
sensors={sensors}
|
|
collisionDetection={closestCenter}
|
|
onDragEnd={handleTopDragEnd}
|
|
>
|
|
<SortableContext
|
|
items={topPanels.map((p) => p.id)}
|
|
strategy={rectSortingStrategy}
|
|
>
|
|
<div className="grid grid-cols-2 gap-1">
|
|
{topPanels.map((panel) => (
|
|
<SortablePanel
|
|
key={panel.id}
|
|
id={panel.id}
|
|
className={panel.className}
|
|
>
|
|
{panel.component}
|
|
</SortablePanel>
|
|
))}
|
|
</div>
|
|
</SortableContext>
|
|
</DndContext>
|
|
) : (
|
|
<div className="grid grid-cols-2 gap-1">
|
|
{topPanels.map((panel) => (
|
|
<div key={panel.id} className={panel.className}>
|
|
{panel.component}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 우측: 고정된 지도와 사용자 정보 (화면의 50%) */}
|
|
<div className="flex w-full gap-1 lg:w-1/2">
|
|
{/* 지도영역 - 고정 (드래그 불가) */}
|
|
<div className="flex-1">
|
|
<KoreaBasinMap />
|
|
</div>
|
|
|
|
{/* 사용자 정보 카드 - 고정 (드래그 불가) */}
|
|
<div className="flex w-full flex-col gap-1 !p-0 lg:w-[300px]">
|
|
{error && <div className="mb-4 text-red-500">{error}</div>}
|
|
{userInfo ? (
|
|
<div
|
|
className="card card_border login_info_box"
|
|
style={{ minHeight: "150px", height: "auto" }}
|
|
>
|
|
<div
|
|
className="login_header"
|
|
style={{ paddingLeft: "10px", paddingRight: "10px" }}
|
|
>
|
|
<div className="login_avatar">
|
|
<Image
|
|
src="/images/icon_loginID.png"
|
|
alt="프로필"
|
|
width={40}
|
|
height={40}
|
|
onError={(e) => {
|
|
const target = e.target as HTMLImageElement;
|
|
target.src =
|
|
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' viewBox='0 0 40 40'%3E%3Ccircle cx='20' cy='20' r='18' fill='%23e1e5e9'/%3E%3Ctext x='20' y='26' font-size='20' text-anchor='middle' fill='%23999'%3E%3F%3C/text%3E%3C/svg%3E";
|
|
}}
|
|
/>
|
|
</div>
|
|
<div className="login_user_info">
|
|
<div className="user_name">
|
|
{userInfo.userName || "-"}
|
|
</div>
|
|
<div className="user_status">온라인</div>
|
|
<div className="user_status text-xs sm:text-sm">
|
|
<b>2FA 활성화:</b>{" "}
|
|
{userInfo.twoFactorEnabled ? "예" : "아니오"}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div
|
|
className="login_actions"
|
|
style={{ paddingLeft: "10px", paddingRight: "10px" }}
|
|
>
|
|
<button
|
|
className="login_btn text-xs sm:text-sm"
|
|
onClick={handleLogout}
|
|
>
|
|
로그아웃
|
|
</button>
|
|
<button
|
|
className="signup_btn text-xs sm:text-sm"
|
|
onClick={handleChangePassword}
|
|
>
|
|
마이페이지
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
!error && <div>사용자 정보를 불러오는 중...</div>
|
|
)}
|
|
|
|
<ScheduleList />
|
|
|
|
<MyPostList />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 두번째 라인 - 드래그 가능한 4개 패널 */}
|
|
{isMounted ? (
|
|
<DndContext
|
|
sensors={sensors}
|
|
collisionDetection={closestCenter}
|
|
onDragEnd={handleBottomDragEnd}
|
|
>
|
|
<SortableContext
|
|
items={bottomPanels.map((p) => p.id)}
|
|
strategy={rectSortingStrategy}
|
|
>
|
|
<div className="grid grid-cols-1 gap-1 sm:grid-cols-2 lg:grid-cols-4">
|
|
{bottomPanels.map((panel) => (
|
|
<SortablePanel
|
|
key={panel.id}
|
|
id={panel.id}
|
|
className="card card-border"
|
|
>
|
|
<div style={{ height: "250px" }}>{panel.component}</div>
|
|
</SortablePanel>
|
|
))}
|
|
</div>
|
|
</SortableContext>
|
|
</DndContext>
|
|
) : (
|
|
<div className="grid grid-cols-1 gap-1 sm:grid-cols-2 lg:grid-cols-4">
|
|
{bottomPanels.map((panel) => (
|
|
<div key={panel.id} className="card card-border">
|
|
<div style={{ height: "250px" }}>{panel.component}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export { DashboardPage };
|