87 lines
3.1 KiB
TypeScript
87 lines
3.1 KiB
TypeScript
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<Blob | null> {
|
|
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<void> {
|
|
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<void> {
|
|
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<Blob> {
|
|
if (!file.type.startsWith('image/')) throw new Error('이미지 파일만 등록할 수 있습니다.');
|
|
|
|
const dataUrl = await new Promise<string>((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<HTMLImageElement>((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<Blob | null>((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'));
|
|
}
|