const PROFILE_API_BASE_URL = (import.meta.env.VITE_LAUNCHER_API_BASE_URL ?? 'http://localhost:4200/api').replace(/\/+$/, ''); const AVATAR_MAX_BYTES = 80 * 1024; export async function fetchProfileAvatar(accessToken: string, version: number): Promise { const response = await fetch(`${PROFILE_API_BASE_URL}/auth/profile/avatar?v=${version}`, { headers: { Authorization: `Bearer ${accessToken}`, }, credentials: 'include', }); if (response.status === 404) return null; if (!response.ok) throw new Error('avatar_load_failed'); return response.blob(); } export async function uploadProfileAvatar(accessToken: string, imageBlob: Blob): Promise { const response = await fetch(`${PROFILE_API_BASE_URL}/auth/profile/avatar`, { method: 'PUT', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': imageBlob.type, }, credentials: 'include', body: imageBlob, }); if (!response.ok) throw new Error('avatar_upload_failed'); } export async function deleteProfileAvatar(accessToken: string): Promise { const response = await fetch(`${PROFILE_API_BASE_URL}/auth/profile/avatar`, { method: 'DELETE', headers: { Authorization: `Bearer ${accessToken}`, }, credentials: 'include', }); if (!response.ok) throw new Error('avatar_delete_failed'); } export async function resizeImageToWebp(file: File, size = 128): Promise { if (!file.type.startsWith('image/')) throw new Error('이미지 파일만 등록할 수 있습니다.'); const dataUrl = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(String(reader.result)); reader.onerror = () => reject(new Error('이미지를 읽을 수 없습니다.')); reader.readAsDataURL(file); }); const image = await new Promise((resolve, reject) => { const img = new Image(); img.onload = () => resolve(img); img.onerror = () => reject(new Error('이미지를 불러올 수 없습니다.')); img.src = dataUrl; }); const sourceSize = Math.min(image.naturalWidth, image.naturalHeight); const sourceX = Math.max(0, (image.naturalWidth - sourceSize) / 2); const sourceY = Math.max(0, (image.naturalHeight - sourceSize) / 2); const canvas = document.createElement('canvas'); canvas.width = size; canvas.height = size; const context = canvas.getContext('2d'); if (!context) throw new Error('이미지를 처리할 수 없습니다.'); context.drawImage(image, sourceX, sourceY, sourceSize, sourceSize, 0, 0, size, size); const blob = await new Promise((resolve) => { canvas.toBlob(resolve, 'image/webp', 0.82); }); if (!blob) throw new Error('이미지를 압축할 수 없습니다.'); if (blob.size > AVATAR_MAX_BYTES) throw new Error('이미지 용량이 너무 큽니다.'); return blob; } export function notifyProfileAvatarUpdated(): void { window.dispatchEvent(new Event('profile-avatar-updated')); } export function notifyProfileAvatarDeleted(): void { window.dispatchEvent(new Event('profile-avatar-deleted')); }