first commit
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
export class AppError extends Error {
|
||||
constructor(message, { statusCode = 500, code = 'INTERNAL_ERROR', cause } = {}) {
|
||||
super(message, { cause });
|
||||
this.name = 'AppError';
|
||||
this.statusCode = statusCode;
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
export class ValidationError extends AppError {
|
||||
constructor(message, options = {}) {
|
||||
super(message, { ...options, statusCode: 400, code: options.code ?? 'VALIDATION_ERROR' });
|
||||
}
|
||||
}
|
||||
|
||||
export class NotFoundError extends AppError {
|
||||
constructor(message = '요청한 항목을 찾을 수 없습니다.') {
|
||||
super(message, { statusCode: 404, code: 'NOT_FOUND' });
|
||||
}
|
||||
}
|
||||
|
||||
export class ConflictError extends AppError {
|
||||
constructor(message = '이미 등록된 레시피입니다.') {
|
||||
super(message, { statusCode: 409, code: 'DUPLICATE_RECIPE' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { ValidationError } from './errors.js';
|
||||
|
||||
const YOUTUBE_HOSTS = new Set([
|
||||
'youtube.com',
|
||||
'www.youtube.com',
|
||||
'm.youtube.com',
|
||||
'music.youtube.com',
|
||||
'youtu.be',
|
||||
'www.youtu.be',
|
||||
]);
|
||||
|
||||
const INSTAGRAM_HOSTS = new Set([
|
||||
'instagram.com',
|
||||
'www.instagram.com',
|
||||
'm.instagram.com',
|
||||
]);
|
||||
|
||||
const YOUTUBE_ID_PATTERN = /^[A-Za-z0-9_-]{11}$/;
|
||||
const INSTAGRAM_SHORTCODE_PATTERN = /^[A-Za-z0-9_-]+$/;
|
||||
|
||||
export function parseSourceUrl(value) {
|
||||
let url;
|
||||
try {
|
||||
url = new URL(value);
|
||||
} catch {
|
||||
throw new ValidationError('올바른 Instagram 또는 YouTube URL을 입력해 주세요.');
|
||||
}
|
||||
|
||||
if (url.protocol !== 'https:') {
|
||||
throw new ValidationError('가져오기 URL은 HTTPS여야 합니다.');
|
||||
}
|
||||
|
||||
url.hash = '';
|
||||
return url;
|
||||
}
|
||||
|
||||
export function detectPlatform(value) {
|
||||
const url = parseSourceUrl(value);
|
||||
if (YOUTUBE_HOSTS.has(url.hostname.toLowerCase())) return 'youtube';
|
||||
if (INSTAGRAM_HOSTS.has(url.hostname.toLowerCase())) return 'instagram';
|
||||
return 'unsupported';
|
||||
}
|
||||
|
||||
export function extractYouTubeId(value) {
|
||||
const url = parseSourceUrl(value);
|
||||
if (!YOUTUBE_HOSTS.has(url.hostname.toLowerCase())) {
|
||||
throw new ValidationError('지원하지 않는 YouTube URL입니다.');
|
||||
}
|
||||
|
||||
const pathParts = url.pathname.split('/').filter(Boolean);
|
||||
let videoId = null;
|
||||
|
||||
if (url.hostname.toLowerCase().endsWith('youtu.be')) {
|
||||
[videoId] = pathParts;
|
||||
} else if (url.pathname === '/watch') {
|
||||
videoId = url.searchParams.get('v');
|
||||
} else if (['shorts', 'embed', 'live'].includes(pathParts[0])) {
|
||||
videoId = pathParts[1];
|
||||
}
|
||||
|
||||
if (!videoId || !YOUTUBE_ID_PATTERN.test(videoId)) {
|
||||
throw new ValidationError('YouTube 영상 ID를 확인할 수 없습니다.');
|
||||
}
|
||||
|
||||
return videoId;
|
||||
}
|
||||
|
||||
export function extractInstagramShortcode(value) {
|
||||
const url = parseSourceUrl(value);
|
||||
if (!INSTAGRAM_HOSTS.has(url.hostname.toLowerCase())) {
|
||||
throw new ValidationError('지원하지 않는 Instagram URL입니다.');
|
||||
}
|
||||
|
||||
const [kind, shortcode] = url.pathname.split('/').filter(Boolean);
|
||||
if (!['p', 'reel', 'reels', 'tv'].includes(kind) || !INSTAGRAM_SHORTCODE_PATTERN.test(shortcode ?? '')) {
|
||||
throw new ValidationError('Instagram 게시물 shortcode를 확인할 수 없습니다.');
|
||||
}
|
||||
|
||||
return shortcode;
|
||||
}
|
||||
|
||||
export function canonicalSourceUrl(value) {
|
||||
const platform = detectPlatform(value);
|
||||
if (platform === 'youtube') {
|
||||
return `https://www.youtube.com/watch?v=${extractYouTubeId(value)}`;
|
||||
}
|
||||
if (platform === 'instagram') {
|
||||
const url = parseSourceUrl(value);
|
||||
const [kind] = url.pathname.split('/').filter(Boolean);
|
||||
const normalizedKind = kind === 'reels' ? 'reel' : kind;
|
||||
return `https://www.instagram.com/${normalizedKind}/${extractInstagramShortcode(value)}/`;
|
||||
}
|
||||
throw new ValidationError('지원하지 않는 URL입니다.');
|
||||
}
|
||||
Reference in New Issue
Block a user