Initial commit - EENE Dashboard

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
EENE Dashboard
2026-05-29 18:07:10 +09:00
commit 22366dde72
64 changed files with 10483 additions and 0 deletions

View File

@@ -0,0 +1,80 @@
import { createContext, useContext, useState, useEffect, type ReactNode } from 'react';
import { apiClient } from '../lib/apiClient';
interface User {
id: string;
email: string;
name: string;
role: 'ADMIN' | 'MANAGER' | 'MEMBER';
department: string | null;
}
interface AuthContextValue {
user: User | null;
token: string | null;
isAuthenticated: boolean;
isLoading: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthContextValue | null>(null);
const TOKEN_KEY = 'eee_token';
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [token, setToken] = useState<string | null>(() => localStorage.getItem(TOKEN_KEY));
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
if (!token) {
setIsLoading(false);
return;
}
apiClient.defaults.headers.common['Authorization'] = `Bearer ${token}`;
apiClient
.get<User>('/auth/me')
.then(({ data }) => setUser(data))
.catch(() => {
localStorage.removeItem(TOKEN_KEY);
setToken(null);
})
.finally(() => setIsLoading(false));
}, [token]);
const login = async (email: string, password: string) => {
const { data } = await apiClient.post<{ token: string; user: User }>('/auth/login', {
email,
password,
});
localStorage.setItem(TOKEN_KEY, data.token);
apiClient.defaults.headers.common['Authorization'] = `Bearer ${data.token}`;
setToken(data.token);
setUser(data.user);
};
const logout = () => {
localStorage.removeItem(TOKEN_KEY);
delete apiClient.defaults.headers.common['Authorization'];
setToken(null);
setUser(null);
};
return (
<AuthContext.Provider
value={{ user, token, isAuthenticated: !!user, isLoading, login, logout }}
>
{children}
</AuthContext.Provider>
);
}
export function useAuth(): AuthContextValue {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error('useAuth must be used inside AuthProvider');
return ctx;
}

View File

@@ -0,0 +1,36 @@
import { createContext, useContext, useEffect, useRef, type ReactNode } from 'react';
import { io, type Socket } from 'socket.io-client';
const SocketContext = createContext<Socket | null>(null);
// 같은 네트워크 팀원도 접속 가능: 백엔드 주소를 현재 페이지 호스트에서 자동 감지
const SOCKET_URL =
import.meta.env.VITE_SOCKET_URL ||
`${window.location.protocol}//${window.location.hostname}:4000`;
export function SocketProvider({ children }: { children: ReactNode }) {
const socketRef = useRef<Socket | null>(null);
useEffect(() => {
const socket = io(SOCKET_URL, { transports: ['websocket'] });
socketRef.current = socket;
socket.on('connect', () => console.log('[Socket] Connected'));
socket.on('disconnect', () => console.log('[Socket] Disconnected'));
return () => {
socket.disconnect();
socketRef.current = null;
};
}, []);
return (
<SocketContext.Provider value={socketRef.current}>
{children}
</SocketContext.Provider>
);
}
export function useSocket(): Socket | null {
return useContext(SocketContext);
}