import { toApiUrl } from './runtime' function emitBackendStatus(detail) { if (typeof window === 'undefined') return window.dispatchEvent(new CustomEvent('tier-maker:backend-status', { detail })) } async function request(path, { method = 'GET', body, headers } = {}) { let res try { res = await fetch(toApiUrl(path), { method, credentials: 'include', headers: { ...(body ? { 'Content-Type': 'application/json' } : {}), ...(headers || {}), }, body: body ? JSON.stringify(body) : undefined, }) } catch (error) { emitBackendStatus({ state: 'offline', message: '서버 연결을 확인할 수 없어 잠시 후 다시 시도해주세요.', path, }) throw error } const contentType = res.headers.get('content-type') || '' const data = contentType.includes('application/json') ? await res.json() : await res.text() if (!res.ok) { if (res.status >= 500 && data?.error === 'db_init_failed') { emitBackendStatus({ state: 'maintenance', message: '서비스 점검 중이거나 데이터베이스 초기화 중입니다. 잠시 후 다시 이용해주세요.', path, }) } else if (res.status >= 500) { emitBackendStatus({ state: 'maintenance', message: '서비스 내부 점검이 필요합니다. 잠시 후 다시 이용해주세요.', path, }) } const err = new Error('request_failed') err.status = res.status err.data = data throw err } emitBackendStatus({ state: 'online', path }) return data } export const api = { me: () => request('/api/auth/me'), authMeta: () => request('/api/auth/meta'), signup: ({ email, nickname, password }) => request('/api/auth/signup', { method: 'POST', body: { email, nickname, password } }), login: ({ email, password }) => request('/api/auth/login', { method: 'POST', body: { email, password } }), verifyEmail: ({ token }) => request('/api/auth/email/verify', { method: 'POST', body: { token } }), resendVerificationEmail: ({ email }) => request('/api/auth/email/resend', { method: 'POST', body: { email } }), requestPasswordReset: ({ email }) => request('/api/auth/password-reset/request', { method: 'POST', body: { email } }), confirmPasswordReset: ({ token, password }) => request('/api/auth/password-reset/confirm', { method: 'POST', body: { token, password } }), logout: () => request('/api/auth/logout', { method: 'POST' }), listTopics: () => request('/api/topics'), getTopic: (topicId) => request(`/api/topics/${encodeURIComponent(topicId)}`), favoriteTopic: (topicId) => request(`/api/topics/${encodeURIComponent(topicId)}/favorite`, { method: 'POST' }), unfavoriteTopic: (topicId) => request(`/api/topics/${encodeURIComponent(topicId)}/favorite`, { method: 'DELETE' }), updateAdminTemplateDisplayOrder: (payload) => request('/api/admin/templates/display-order', { method: 'PATCH', body: payload }), updateAdminTemplateItemDisplayOrder: (templateId, payload) => request(`/api/admin/templates/${encodeURIComponent(templateId)}/items/display-order`, { method: 'PATCH', body: payload }), updateAdminTemplate: (templateId, payload) => request(`/api/admin/templates/${encodeURIComponent(templateId)}`, { method: 'PATCH', body: payload }), updateAdminTemplateItem: (templateId, itemId, payload) => request(`/api/admin/templates/${encodeURIComponent(templateId)}/items/${encodeURIComponent(itemId)}`, { method: 'PATCH', body: payload }), listAdminCustomItems: ({ q = '', page = 1, limit = 50, filter = 'all' } = {}) => request( `/api/admin/custom-items?q=${encodeURIComponent(q)}&page=${encodeURIComponent(page)}&limit=${encodeURIComponent(limit)}&filter=${encodeURIComponent(filter)}` ), listAdminTierLists: ({ q = '', topicId = '', page = 1, limit = 50 } = {}) => request(`/api/admin/tierlists?q=${encodeURIComponent(q)}&topicId=${encodeURIComponent(topicId)}&page=${encodeURIComponent(page)}&limit=${encodeURIComponent(limit)}`), getAdminTierListStats: ({ q = '', topicId = '' } = {}) => request(`/api/admin/tierlists/stats?q=${encodeURIComponent(q)}&topicId=${encodeURIComponent(topicId)}`), updateAdminTierList: (tierListId, payload) => request(`/api/admin/tierlists/${encodeURIComponent(tierListId)}`, { method: 'PATCH', body: payload }), deleteAdminTierList: (tierListId) => request(`/api/admin/tierlists/${encodeURIComponent(tierListId)}`, { method: 'DELETE' }), listAdminTemplateRequests: () => request('/api/admin/template-requests'), getAdminImageAssetStats: ({ month = '', limit = 12 } = {}) => { const query = new URLSearchParams() if (month) query.set('month', month) query.set('limit', String(limit)) return request(`/api/admin/image-assets/stats?${query.toString()}`) }, resetAdminImageAssetStats: (payload) => request('/api/admin/image-assets/stats/reset', { method: 'POST', body: payload || {} }), cleanupAdminMissingImageReferences: () => request('/api/admin/image-assets/missing/cleanup', { method: 'POST', body: {} }), listAdminUnusedImageAssets: ({ limit = 100, minAgeHours = 24 } = {}) => request(`/api/admin/image-assets/orphans?limit=${encodeURIComponent(limit)}&minAgeHours=${encodeURIComponent(minAgeHours)}`), cleanupAdminUnusedImageAssets: (payload) => request('/api/admin/image-assets/cleanup', { method: 'POST', body: payload || {} }), promoteAdminTemplateItem: (itemId, payload) => request(`/api/admin/custom-items/${encodeURIComponent(itemId)}/promote`, { method: 'POST', body: payload }), updateAdminCustomItemLabel: (itemId, payload) => request(`/api/admin/custom-items/${encodeURIComponent(itemId)}/label`, { method: 'PATCH', body: payload }), promoteAdminTierListItems: (tierListId, payload) => request(`/api/admin/tierlists/${encodeURIComponent(tierListId)}/promote-items`, { method: 'POST', body: payload }), createAdminTemplateFromTierList: (tierListId, payload) => request(`/api/admin/tierlists/${encodeURIComponent(tierListId)}/create-template`, { method: 'POST', body: payload }), startAdminTemplateRequestReview: (requestId) => request(`/api/admin/template-requests/${encodeURIComponent(requestId)}/review`, { method: 'POST', body: {} }), linkAdminTemplateRequestTemplate: (requestId, payload) => request(`/api/admin/template-requests/${encodeURIComponent(requestId)}/link-template`, { method: 'POST', body: payload }), promoteAdminTemplateRequestItems: (requestId, payload) => request(`/api/admin/template-requests/${encodeURIComponent(requestId)}/promote-items`, { method: 'POST', body: payload }), completeAdminTemplateRequest: (requestId) => request(`/api/admin/template-requests/${encodeURIComponent(requestId)}/complete`, { method: 'POST', body: {} }), approveAdminTemplateRequest: (requestId, payload) => request(`/api/admin/template-requests/${encodeURIComponent(requestId)}/approve`, { method: 'POST', body: payload || {} }), rejectAdminTemplateRequest: (requestId) => request(`/api/admin/template-requests/${encodeURIComponent(requestId)}/reject`, { method: 'POST', body: {} }), listAdminUsers: ({ q = '', sort = 'recent', direction = 'desc' } = {}) => request(`/api/admin/users?q=${encodeURIComponent(q)}&sort=${encodeURIComponent(sort)}&direction=${encodeURIComponent(direction)}`), updateAdminUser: (userId, payload) => request(`/api/admin/users/${encodeURIComponent(userId)}`, { method: 'PATCH', body: payload }), updateAdminUserPassword: (userId, payload) => request(`/api/admin/users/${encodeURIComponent(userId)}/password`, { method: 'PATCH', body: payload }), updateAdminUserAvatar: async (userId, { file, removeAvatar = false } = {}) => { const fd = new FormData() if (file) fd.append('avatar', file) if (removeAvatar) fd.append('removeAvatar', '1') const res = await fetch(toApiUrl(`/api/admin/users/${encodeURIComponent(userId)}/avatar`), { method: 'POST', credentials: 'include', body: fd, }) const data = await res.json() if (!res.ok) { const err = new Error('request_failed') err.status = res.status err.data = data throw err } return data }, deleteAdminUser: (userId) => request(`/api/admin/users/${encodeURIComponent(userId)}`, { method: 'DELETE' }), listPublicTierListsByTopic: (topicId) => request(`/api/tierlists/public?topicId=${encodeURIComponent(topicId || '')}`), searchPublicTierListsByTopic: (topicId, q = '') => request(`/api/tierlists/public?topicId=${encodeURIComponent(topicId || '')}&q=${encodeURIComponent(q || '')}`), searchAllPublicTierLists: (q = '') => request(`/api/tierlists/public?q=${encodeURIComponent(q || '')}`), listMyTierLists: () => request('/api/tierlists/me'), listMyFavoriteTierLists: ({ q = '', sort = 'favorited' } = {}) => request(`/api/tierlists/favorites/me?q=${encodeURIComponent(q)}&sort=${encodeURIComponent(sort)}`), getTierList: (id) => request(`/api/tierlists/${encodeURIComponent(id)}`), favoriteTierList: (id) => request(`/api/tierlists/${encodeURIComponent(id)}/favorite`, { method: 'POST' }), unfavoriteTierList: (id) => request(`/api/tierlists/${encodeURIComponent(id)}/favorite`, { method: 'DELETE' }), deleteTierList: (id) => request(`/api/tierlists/${encodeURIComponent(id)}`, { method: 'DELETE' }), duplicateTierList: (id) => request(`/api/tierlists/${encodeURIComponent(id)}/duplicate`, { method: 'POST' }), requestTierListTemplate: (payload) => request('/api/tierlists/template-request', { method: 'POST', body: payload }), saveTierList: (payload) => request('/api/tierlists', { method: 'POST', body: payload }), uploadTierListThumbnail: async (file) => { const fd = new FormData() fd.append('thumbnail', file) const res = await fetch(toApiUrl('/api/tierlists/thumbnail'), { method: 'POST', credentials: 'include', body: fd, }) const data = await res.json() if (!res.ok) { const err = new Error('request_failed') err.status = res.status err.data = data throw err } return data }, deleteAdminCustomItem: (itemId) => request(`/api/admin/custom-items/${encodeURIComponent(itemId)}`, { method: 'DELETE' }), deleteAdminUnusedCustomItems: ({ q = '' } = {}) => request(`/api/admin/custom-items?q=${encodeURIComponent(q)}`, { method: 'DELETE' }), }