193 lines
11 KiB
JavaScript
193 lines
11 KiB
JavaScript
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 } }),
|
|
changePassword: ({ currentPassword, nextPassword }) =>
|
|
request('/api/auth/password', { method: 'POST', body: { currentPassword, nextPassword } }),
|
|
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 = '', sort = 'recent', minFavorites = 0, page = 1, limit = 50 } = {}) =>
|
|
request(`/api/admin/tierlists?q=${encodeURIComponent(q)}&topicId=${encodeURIComponent(topicId)}&sort=${encodeURIComponent(sort)}&minFavorites=${encodeURIComponent(minFavorites)}&page=${encodeURIComponent(page)}&limit=${encodeURIComponent(limit)}`),
|
|
getAdminTierListStats: ({ q = '', topicId = '', minFavorites = 0 } = {}) =>
|
|
request(`/api/admin/tierlists/stats?q=${encodeURIComponent(q)}&topicId=${encodeURIComponent(topicId)}&minFavorites=${encodeURIComponent(minFavorites)}`),
|
|
updateAdminTierList: (tierListId, payload) =>
|
|
request(`/api/admin/tierlists/${encodeURIComponent(tierListId)}`, { method: 'PATCH', body: payload }),
|
|
updateAdminTierListFeatured: (tierListId, payload) =>
|
|
request(`/api/admin/tierlists/${encodeURIComponent(tierListId)}/featured`, { 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)}`),
|
|
getUserProfile: (userId) => request(`/api/users/${encodeURIComponent(userId)}`),
|
|
listUserPublicTierLists: (userId, { q = '' } = {}) =>
|
|
request(`/api/users/${encodeURIComponent(userId)}/tierlists?q=${encodeURIComponent(q || '')}`),
|
|
listFollowingFeed: ({ q = '' } = {}) =>
|
|
request(`/api/users/following-feed?q=${encodeURIComponent(q || '')}`),
|
|
followUser: (userId) => request(`/api/users/${encodeURIComponent(userId)}/follow`, { method: 'POST', body: {} }),
|
|
unfollowUser: (userId) => request(`/api/users/${encodeURIComponent(userId)}/follow`, { method: 'DELETE' }),
|
|
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' }),
|
|
}
|