platform/lms-adm-fe release promotion snapshot
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
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'));
|
||||
}
|
||||
Reference in New Issue
Block a user