init studio admin frontend
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
export const adminAppPermissionsStorageKey = 'studio-admin-app-permissions';
|
||||
|
||||
export type CurrentAdminLike = {
|
||||
id?: string;
|
||||
email?: string;
|
||||
name?: string;
|
||||
role?: string;
|
||||
};
|
||||
|
||||
type PermissionMap = Record<string, string[]>;
|
||||
|
||||
export function readCurrentAdmin(): CurrentAdminLike | null {
|
||||
try {
|
||||
const raw = window.localStorage.getItem('studio-admin');
|
||||
if (!raw) return null;
|
||||
const admin = JSON.parse(raw) as CurrentAdminLike;
|
||||
return admin?.id || admin?.email ? admin : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getAdminPermissionKey(admin: CurrentAdminLike) {
|
||||
return admin.id || admin.email || '';
|
||||
}
|
||||
|
||||
export function isOwnerAdmin(admin?: CurrentAdminLike | null) {
|
||||
return admin?.role === 'OWNER';
|
||||
}
|
||||
|
||||
export function getAdminAppPermissionMap(): PermissionMap {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(adminAppPermissionsStorageKey);
|
||||
if (!raw) return {};
|
||||
const parsed = JSON.parse(raw) as PermissionMap;
|
||||
return parsed && typeof parsed === 'object' ? parsed : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export function getAdminAppPermissions(admin: CurrentAdminLike) {
|
||||
const key = getAdminPermissionKey(admin);
|
||||
const map = getAdminAppPermissionMap();
|
||||
if (key && map[key]) return map[key];
|
||||
if (admin.id && map[admin.id]) return map[admin.id];
|
||||
if (admin.email && map[admin.email]) return map[admin.email];
|
||||
return [];
|
||||
}
|
||||
|
||||
export function setAdminAppPermissions(admin: CurrentAdminLike, appIds: string[]) {
|
||||
const key = getAdminPermissionKey(admin);
|
||||
if (!key) return;
|
||||
|
||||
const nextMap = getAdminAppPermissionMap();
|
||||
const nextAppIds = [...new Set(appIds.filter(Boolean))];
|
||||
nextMap[key] = nextAppIds;
|
||||
if (admin.id) nextMap[admin.id] = nextAppIds;
|
||||
if (admin.email) nextMap[admin.email] = nextAppIds;
|
||||
window.localStorage.setItem(adminAppPermissionsStorageKey, JSON.stringify(nextMap));
|
||||
window.dispatchEvent(new CustomEvent('studio-admin-app-permissions-change'));
|
||||
}
|
||||
|
||||
export function grantAppPermissionToCurrentAdmin(appId: string) {
|
||||
const admin = readCurrentAdmin();
|
||||
if (!admin || isOwnerAdmin(admin)) return;
|
||||
|
||||
setAdminAppPermissions(admin, [...getAdminAppPermissions(admin), appId]);
|
||||
}
|
||||
|
||||
export function filterAppsByCurrentAdmin<T extends { id: string }>(apps: T[]) {
|
||||
const admin = readCurrentAdmin();
|
||||
if (!admin || isOwnerAdmin(admin)) return apps;
|
||||
|
||||
const allowedAppIds = new Set(getAdminAppPermissions(admin));
|
||||
return apps.filter((app) => allowedAppIds.has(app.id));
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
export const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:4500/api';
|
||||
export const authExpiredEventName = 'studio-admin-auth-expired';
|
||||
|
||||
export function clearAdminSession() {
|
||||
localStorage.removeItem('studio-admin-token');
|
||||
localStorage.removeItem('studio-admin');
|
||||
}
|
||||
|
||||
function notifyAuthExpired() {
|
||||
clearAdminSession();
|
||||
window.dispatchEvent(new CustomEvent(authExpiredEventName));
|
||||
}
|
||||
|
||||
export function authHeaders(headers?: HeadersInit): HeadersInit {
|
||||
const token = localStorage.getItem('studio-admin-token');
|
||||
return {
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
...(headers ?? {})
|
||||
};
|
||||
}
|
||||
|
||||
export async function apiFetch(input: RequestInfo | URL, init?: RequestInit) {
|
||||
const response = await fetch(input, init);
|
||||
if (response.status === 401) {
|
||||
notifyAuthExpired();
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function apiRequest<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const response = await apiFetch(`${apiBaseUrl}${path}`, {
|
||||
...init,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...authHeaders(init?.headers)
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.json().catch(() => null);
|
||||
const message = Array.isArray(errorBody?.message) ? errorBody.message.join(', ') : errorBody?.message;
|
||||
throw new Error(message || `API request failed: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
function parseDate(value?: string | null) {
|
||||
if (!value) return null;
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed || trimmed === '-' || trimmed === '무기한') return null;
|
||||
|
||||
const koreanDate = trimmed.match(/^(\d{4})년\s*(\d{1,2})월\s*(\d{1,2})일(?:\s*(\d{1,2}):(\d{2}))?$/);
|
||||
if (koreanDate) {
|
||||
const [, year, month, day, hour = '0', minute = '0'] = koreanDate;
|
||||
return new Date(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute));
|
||||
}
|
||||
|
||||
const date = new Date(trimmed);
|
||||
return Number.isNaN(date.getTime()) ? null : date;
|
||||
}
|
||||
|
||||
function pad(value: number) {
|
||||
return String(value).padStart(2, '0');
|
||||
}
|
||||
|
||||
export function formatDateOnly(value?: string | null) {
|
||||
const date = parseDate(value);
|
||||
if (!date) return value || '-';
|
||||
return `${pad(date.getMonth() + 1)}.${pad(date.getDate())}.`;
|
||||
}
|
||||
|
||||
export function formatDateTime(value?: string | null) {
|
||||
const date = parseDate(value);
|
||||
if (!date) return value || '-';
|
||||
const period = date.getHours() < 12 ? '오전' : '오후';
|
||||
const hour = date.getHours() % 12 || 12;
|
||||
return `${pad(date.getMonth() + 1)}. ${pad(date.getDate())}. ${period} ${pad(hour)}:${pad(date.getMinutes())}`;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
export type RouteItem = {
|
||||
path: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
function normalizePath(path: string) {
|
||||
return path === '/' ? '/dashboard' : path;
|
||||
}
|
||||
|
||||
export function useRouter() {
|
||||
const [path, setPath] = useState(() => normalizePath(window.location.pathname));
|
||||
|
||||
useEffect(() => {
|
||||
const onPopState = () => setPath(normalizePath(window.location.pathname));
|
||||
window.addEventListener('popstate', onPopState);
|
||||
return () => window.removeEventListener('popstate', onPopState);
|
||||
}, []);
|
||||
|
||||
const router = useMemo(() => ({
|
||||
path,
|
||||
navigate(nextPath: string) {
|
||||
const normalizedPath = normalizePath(nextPath);
|
||||
if (normalizedPath !== window.location.pathname) {
|
||||
window.history.pushState(null, '', normalizedPath);
|
||||
}
|
||||
setPath(normalizedPath);
|
||||
}
|
||||
}), [path]);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
export const selectedAppStorageKey = 'studio-admin:selected-app-id';
|
||||
const legacySelectedAppStorageKey = 'studio-selected-app-id';
|
||||
|
||||
export function getStoredSelectedAppId() {
|
||||
const current = window.localStorage.getItem(selectedAppStorageKey);
|
||||
if (current) return current;
|
||||
|
||||
const legacy = window.localStorage.getItem(legacySelectedAppStorageKey);
|
||||
if (legacy) {
|
||||
setStoredSelectedAppId(legacy);
|
||||
window.localStorage.removeItem(legacySelectedAppStorageKey);
|
||||
return legacy;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
export function setStoredSelectedAppId(appId: string) {
|
||||
if (!appId) return;
|
||||
window.localStorage.setItem(selectedAppStorageKey, appId);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function cn(...classes: Array<string | false | null | undefined>) {
|
||||
return classes.filter(Boolean).join(' ');
|
||||
}
|
||||
Reference in New Issue
Block a user