221 lines
7.1 KiB
JavaScript
221 lines
7.1 KiB
JavaScript
/**
|
|
* UNIARE API 클라이언트
|
|
*/
|
|
const UaDeckApi = (() => {
|
|
const TOKEN_KEY = 'uadeck_token';
|
|
const USER_KEY = 'uadeck_user';
|
|
|
|
/**
|
|
* API 베이스 URL
|
|
* @returns {string}
|
|
*/
|
|
function getBaseUrl() {
|
|
if (window.UNIARE_API_BASE) return window.UNIARE_API_BASE;
|
|
if (window.UADECK_API_BASE) return window.UADECK_API_BASE;
|
|
|
|
const { hostname, port, protocol } = window.location;
|
|
const isLocal = hostname === 'localhost' || hostname === '127.0.0.1';
|
|
// Live Server 등 정적 전용 포트 → API 컨테이너/프로세스(3000)로 직접 연결
|
|
const proxiedWebPorts = new Set(['44082', '8080', '80', '443', '']);
|
|
if (isLocal && port && !proxiedWebPorts.has(port)) {
|
|
return `${protocol}//${hostname}:3000/api/v1`;
|
|
}
|
|
|
|
return '/api/v1';
|
|
}
|
|
|
|
/**
|
|
* API 미디어 경로를 브라우저에서 로드 가능한 URL로 변환
|
|
* (Live Server 등 API 호스트가 다른 경우 대비)
|
|
* @param {string|null} apiPath
|
|
* @returns {string|null}
|
|
*/
|
|
function resolveMediaUrl(apiPath) {
|
|
if (!apiPath) return null;
|
|
if (apiPath.startsWith('http://') || apiPath.startsWith('https://')) return apiPath;
|
|
|
|
const base = getBaseUrl();
|
|
if (base.startsWith('http')) {
|
|
const origin = base.replace(/\/api\/v1\/?$/, '');
|
|
return `${origin}${apiPath}`;
|
|
}
|
|
return apiPath;
|
|
}
|
|
|
|
/**
|
|
* HTTP 오류 메시지
|
|
* @param {Response} res
|
|
* @param {object} data
|
|
* @returns {string}
|
|
*/
|
|
function formatError(res, data) {
|
|
if (res.status === 405 || res.status === 404) {
|
|
return 'API 서버에 연결되지 않았습니다. Docker 또는 npm run api:dev 로 API를 실행하세요.';
|
|
}
|
|
if (res.status === 413) {
|
|
return data.error || '업로드 크기가 제한을 초과했습니다. 고화질 옵션을 끄고 다시 시도해 주세요.';
|
|
}
|
|
if (res.status === 0 || res.type === 'opaque') {
|
|
return '네트워크 오류입니다. API 서버가 실행 중인지 확인하세요.';
|
|
}
|
|
return data.error || `요청 실패 (${res.status})`;
|
|
}
|
|
|
|
/**
|
|
* 저장된 토큰
|
|
* @returns {string|null}
|
|
*/
|
|
function getToken() {
|
|
return localStorage.getItem(TOKEN_KEY);
|
|
}
|
|
|
|
/**
|
|
* 캐시된 사용자
|
|
* @returns {object|null}
|
|
*/
|
|
function getCachedUser() {
|
|
try {
|
|
const raw = localStorage.getItem(USER_KEY);
|
|
return raw ? JSON.parse(raw) : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 인증 정보 저장
|
|
* @param {string} token
|
|
* @param {object} user
|
|
*/
|
|
function saveAuth(token, user) {
|
|
localStorage.setItem(TOKEN_KEY, token);
|
|
localStorage.setItem(USER_KEY, JSON.stringify(user));
|
|
}
|
|
|
|
/**
|
|
* 로그아웃
|
|
*/
|
|
function logout() {
|
|
localStorage.removeItem(TOKEN_KEY);
|
|
localStorage.removeItem(USER_KEY);
|
|
}
|
|
|
|
/**
|
|
* API 요청
|
|
* @param {string} path
|
|
* @param {object} [options]
|
|
* @returns {Promise<object>}
|
|
*/
|
|
async function request(path, options = {}) {
|
|
const headers = { 'Content-Type': 'application/json', ...(options.headers || {}) };
|
|
const token = getToken();
|
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
|
|
const res = await fetch(`${getBaseUrl()}${path}`, { ...options, headers });
|
|
const data = await res.json().catch(() => ({}));
|
|
|
|
if (!res.ok) {
|
|
throw new Error(formatError(res, data));
|
|
}
|
|
return data;
|
|
}
|
|
|
|
return {
|
|
getToken,
|
|
getCachedUser,
|
|
saveAuth,
|
|
logout,
|
|
getBaseUrl,
|
|
resolveMediaUrl,
|
|
|
|
/** @param {string} email @param {string} password */
|
|
login: (email, password) =>
|
|
request('/auth/login', { method: 'POST', body: JSON.stringify({ email, password }) }),
|
|
|
|
/** @param {object} body */
|
|
register: (body) =>
|
|
request('/auth/register', { method: 'POST', body: JSON.stringify(body) }),
|
|
|
|
getMe: () => request('/auth/me'),
|
|
|
|
/** @param {object} body */
|
|
updateProfile: (body) => request('/auth/me', { method: 'PUT', body: JSON.stringify(body) }),
|
|
|
|
/** @param {string} currentPassword @param {string} newPassword */
|
|
changePassword: (currentPassword, newPassword) =>
|
|
request('/auth/me/password', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ currentPassword, newPassword }),
|
|
}),
|
|
|
|
/** @param {string} shareCode */
|
|
getDeck: (shareCode) => request(`/decks/${shareCode}`),
|
|
|
|
getMyDecks: () => request('/decks'),
|
|
|
|
/** @param {object} body */
|
|
saveDeck: (body) => request('/decks', { method: 'POST', body: JSON.stringify(body) }),
|
|
|
|
/** @param {string} shareCode @param {object} body */
|
|
updateDeck: (shareCode, body) =>
|
|
request(`/decks/${shareCode}`, { method: 'PUT', body: JSON.stringify(body) }),
|
|
|
|
/** @param {string} shareCode */
|
|
deleteDeck: (shareCode) => request(`/decks/${shareCode}`, { method: 'DELETE' }),
|
|
|
|
getPresets: () => request('/decks/presets'),
|
|
|
|
getFavorites: () => request('/decks/favorites'),
|
|
|
|
/** @param {string[]} shareCodes */
|
|
syncFavorites: (shareCodes) =>
|
|
request('/decks/favorites', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ shareCodes }),
|
|
}),
|
|
|
|
/** @param {string} shareCode */
|
|
addFavorite: (shareCode) =>
|
|
request(`/decks/favorites/${shareCode}`, { method: 'PUT' }),
|
|
|
|
/** @param {string} shareCode */
|
|
removeFavorite: (shareCode) =>
|
|
request(`/decks/favorites/${shareCode}`, { method: 'DELETE' }),
|
|
|
|
/** 관리자: 회원 목록 */
|
|
adminGetUsers: (q = '', page = 1) =>
|
|
request(`/admin/users?q=${encodeURIComponent(q)}&page=${page}`),
|
|
|
|
/** @param {number} id */
|
|
adminGetUser: (id) => request(`/admin/users/${id}`),
|
|
|
|
/** @param {number} id @param {object} body */
|
|
adminUpdateUser: (id, body) =>
|
|
request(`/admin/users/${id}`, { method: 'PUT', body: JSON.stringify(body) }),
|
|
|
|
/** @param {number} id @param {string} newPassword */
|
|
adminResetPassword: (id, newPassword) =>
|
|
request(`/admin/users/${id}/reset-password`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ newPassword }),
|
|
}),
|
|
|
|
adminGetDecks: () => request('/admin/decks'),
|
|
|
|
adminGetOrphanPreviews: () => request('/admin/previews/orphans'),
|
|
|
|
adminDeleteOrphanPreviews: () =>
|
|
request('/admin/previews/orphans', { method: 'DELETE' }),
|
|
|
|
/** @param {object} body */
|
|
adminCreatePreset: (body) =>
|
|
request('/admin/decks/preset', { method: 'POST', body: JSON.stringify(body) }),
|
|
|
|
/** @param {string} shareCode */
|
|
adminDeleteDeck: (shareCode) =>
|
|
request(`/admin/decks/${shareCode}`, { method: 'DELETE' }),
|
|
};
|
|
})();
|
|
|
|
window.UaDeckApi = UaDeckApi;
|