init studio admin frontend

This commit is contained in:
2026-07-01 17:34:15 +09:00
commit c50bd2b941
64 changed files with 22452 additions and 0 deletions
+77
View File
@@ -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));
}
+46
View File
@@ -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>;
}
+32
View File
@@ -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())}`;
}
+34
View File
@@ -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;
}
+21
View File
@@ -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);
}
+3
View File
@@ -0,0 +1,3 @@
export function cn(...classes: Array<string | false | null | undefined>) {
return classes.filter(Boolean).join(' ');
}