import { appendClientIdHeader } from './clientIdentity'; import { getRegisteredAccessToken, isAllowedRegistrationToken } from './tokenAccess'; export type ResourceManagerEntryType = 'file' | 'directory'; export type ResourceManagerTreeNode = { name: string; path: string; type: ResourceManagerEntryType; extension: string | null; size: number | null; modifiedAt: string; previewUrl: string | null; children?: ResourceManagerTreeNode[]; }; export type ResourceManagerTreeRoot = { label: string; rootPath: string; tree: ResourceManagerTreeNode; }; export type ResourceManagerDirectoryEntry = { name: string; path: string; type: ResourceManagerEntryType; extension: string | null; size: number | null; modifiedAt: string; previewUrl: string | null; }; export type ResourceManagerDirectoryDetail = { path: string; items: ResourceManagerDirectoryEntry[]; }; export type ResourceManagerFileDetail = { name: string; path: string; extension: string | null; size: number; modifiedAt: string; mimeType: string; previewUrl: string; isTextEditable: boolean; content: string | null; }; class ResourceManagerApiError extends Error { status: number; constructor(message: string, status: number) { super(message); this.name = 'ResourceManagerApiError'; this.status = status; } } function resolveApiBaseUrl() { if (import.meta.env.VITE_WORK_SERVER_URL) { return import.meta.env.VITE_WORK_SERVER_URL; } return '/api'; } function resolveFallbackBaseUrl() { if (typeof window === 'undefined') { return null; } const hostname = window.location.hostname; const isLocalHost = hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '0.0.0.0'; if (!isLocalHost) { return null; } const fallbackUrl = new URL(window.location.origin); fallbackUrl.port = '3100'; fallbackUrl.pathname = '/api'; fallbackUrl.search = ''; fallbackUrl.hash = ''; return fallbackUrl.toString().replace(/\/+$/, ''); } const RESOURCE_MANAGER_API_BASE_URL = resolveApiBaseUrl(); const RESOURCE_MANAGER_API_FALLBACK_BASE_URL = !import.meta.env.VITE_WORK_SERVER_URL && RESOURCE_MANAGER_API_BASE_URL === '/api' ? resolveFallbackBaseUrl() : null; function appendPreviewAccessToken(previewUrl: string | null) { if (!previewUrl) { return null; } const token = getRegisteredAccessToken(); if (!token) { return previewUrl; } const separator = previewUrl.includes('?') ? '&' : '?'; return `${previewUrl}${separator}token=${encodeURIComponent(token)}`; } function normalizeTreeNode(node: ResourceManagerTreeNode): ResourceManagerTreeNode { return { ...node, previewUrl: appendPreviewAccessToken(node.previewUrl), children: node.children?.map((child) => normalizeTreeNode(child)), }; } async function requestOnce(baseUrl: string, path: string, init?: RequestInit): Promise { const headers = appendClientIdHeader(init?.headers); const hasBody = init?.body !== undefined && init.body !== null; const method = init?.method?.toUpperCase() ?? 'GET'; const token = getRegisteredAccessToken(); if (!isAllowedRegistrationToken(token)) { throw new ResourceManagerApiError('권한 토큰 등록 후에만 리소스 관리를 사용할 수 있습니다.', 403); } if (hasBody && !headers.has('Content-Type')) { headers.set('Content-Type', 'application/json'); } if (token && !headers.has('X-Access-Token')) { headers.set('X-Access-Token', token); } const response = await fetch(`${baseUrl}${path}`, { ...init, headers, cache: init?.cache ?? (method === 'GET' ? 'no-store' : undefined), }); if (!response.ok) { const text = await response.text(); try { const payload = JSON.parse(text) as { message?: string }; throw new ResourceManagerApiError(payload.message || '리소스 관리 요청에 실패했습니다.', response.status); } catch { throw new ResourceManagerApiError(text || '리소스 관리 요청에 실패했습니다.', response.status); } } return response.json() as Promise; } async function request(path: string, init?: RequestInit): Promise { try { return await requestOnce(RESOURCE_MANAGER_API_BASE_URL, path, init); } catch (error) { const shouldRetryWithFallback = RESOURCE_MANAGER_API_FALLBACK_BASE_URL && RESOURCE_MANAGER_API_FALLBACK_BASE_URL !== RESOURCE_MANAGER_API_BASE_URL && (error instanceof ResourceManagerApiError ? error.status === 404 || error.status === 408 || error.status === 502 : error instanceof Error && /404|Failed to fetch|NetworkError/i.test(error.message)); if (!shouldRetryWithFallback) { throw error; } return requestOnce(RESOURCE_MANAGER_API_FALLBACK_BASE_URL, path, init); } } export async function fetchResourceManagerTree() { const response = await request<{ ok: boolean; item: ResourceManagerTreeRoot }>('/resource-manager/tree'); return { ...response.item, tree: normalizeTreeNode(response.item.tree), }; } export async function fetchResourceManagerDirectory(targetPath = '') { const query = new URLSearchParams({ path: targetPath }); const response = await request<{ ok: boolean; item: ResourceManagerDirectoryDetail }>( `/resource-manager/directory?${query.toString()}`, ); return { ...response.item, items: response.item.items.map((item) => ({ ...item, previewUrl: appendPreviewAccessToken(item.previewUrl), })), }; } export async function fetchResourceManagerFile(targetPath: string) { const query = new URLSearchParams({ path: targetPath }); const response = await request<{ ok: boolean; item: ResourceManagerFileDetail }>(`/resource-manager/file?${query.toString()}`); return { ...response.item, previewUrl: appendPreviewAccessToken(response.item.previewUrl) ?? response.item.previewUrl, }; } export async function createResourceManagerDirectory(parentPath: string, name: string) { await request('/resource-manager/directories', { method: 'POST', body: JSON.stringify({ parentPath, name }), }); } export async function createResourceManagerFile(parentPath: string, name: string, content = '') { await request('/resource-manager/files', { method: 'POST', body: JSON.stringify({ parentPath, name, content }), }); } export async function saveResourceManagerFile(targetPath: string, content: string) { await request('/resource-manager/files/content', { method: 'PUT', body: JSON.stringify({ path: targetPath, content }), }); } async function readFileAsBase64(file: File) { const buffer = await file.arrayBuffer(); let binary = ''; const bytes = new Uint8Array(buffer); bytes.forEach((byte) => { binary += String.fromCharCode(byte); }); return btoa(binary); } export async function uploadResourceManagerFile(parentPath: string, file: File) { const contentBase64 = await readFileAsBase64(file); await request('/resource-manager/files/upload', { method: 'POST', body: JSON.stringify({ parentPath, fileName: file.name, contentBase64, }), }); } export async function copyResourceManagerItem(targetPath: string, targetDirectoryPath: string, nextName?: string) { await request('/resource-manager/items/copy', { method: 'POST', body: JSON.stringify({ path: targetPath, targetDirectoryPath, nextName: nextName?.trim() || null, }), }); } export async function moveResourceManagerItem(targetPath: string, targetDirectoryPath: string, nextName?: string) { await request('/resource-manager/items/move', { method: 'POST', body: JSON.stringify({ path: targetPath, targetDirectoryPath, nextName: nextName?.trim() || null, }), }); } export async function deleteResourceManagerItem(targetPath: string) { const query = new URLSearchParams({ path: targetPath }); await request(`/resource-manager/items?${query.toString()}`, { method: 'DELETE', }); }