/** * SNS 아이콘 프리셋 * @type {ReadonlyArray<{ icon: string, label: string, placeholder: string }>} */ export const SOCIAL_ICON_PRESETS = [ { icon: 'x', label: 'X', placeholder: 'https://x.com/...' }, { icon: 'github', label: 'GitHub', placeholder: 'https://github.com/...' }, { icon: 'instagram', label: 'Instagram', placeholder: 'https://instagram.com/...' }, { icon: 'youtube', label: 'YouTube', placeholder: 'https://youtube.com/...' }, { icon: 'facebook', label: 'Facebook', placeholder: 'https://facebook.com/...' }, { icon: 'linkedin', label: 'LinkedIn', placeholder: 'https://linkedin.com/in/...' }, { icon: 'rss', label: 'RSS', placeholder: '/rss.xml' }, { icon: 'custom', label: '직접 SVG', placeholder: 'https://...' }, { icon: 'link', label: 'Link', placeholder: 'https://...' } ] /** @type {number} 최대 SNS 링크 개수 */ export const MAX_SOCIAL_LINK_COUNT = 12 /** * SNS 아이콘 프리셋을 찾는다. * @param {unknown} icon - 아이콘 키 * @returns {{ icon: string, label: string, placeholder: string }} 아이콘 프리셋 */ export const getSocialIconPreset = (icon) => { const key = String(icon || '').trim().toLowerCase() return SOCIAL_ICON_PRESETS.find((preset) => preset.icon === key) || SOCIAL_ICON_PRESETS[SOCIAL_ICON_PRESETS.length - 1] } /** * SNS 링크 URL을 정리한다. * @param {unknown} value - 입력 URL * @returns {string} 정리된 URL */ export const normalizeSocialLinkUrl = (value) => { const trimmed = String(value || '').trim() if (!trimmed) { return '' } if (trimmed.startsWith('/')) { return trimmed } if (/^mailto:[^\s@]+@[^\s@]+\.[^\s@]+$/i.test(trimmed)) { return trimmed } const normalized = trimmed.startsWith('//') ? `https:${trimmed}` : (/^[a-z][a-z0-9+.-]*:/i.test(trimmed) ? trimmed : `https://${trimmed}`) try { const parsed = new URL(normalized) if (parsed.protocol === 'http:' || parsed.protocol === 'https:') { return parsed.toString() } } catch { return '' } return '' } /** * 사용자 지정 SNS SVG 아이콘을 정리한다. * @param {unknown} value - SVG 입력값 * @returns {string} 정리된 SVG */ export const normalizeSocialIconSvg = (value) => { const svg = String(value || '').trim().slice(0, 4000) const lower = svg.toLowerCase() if (!svg || !lower.startsWith('')) { return '' } if ( lower.includes(' { if (!item || typeof item !== 'object' || Array.isArray(item)) { return null } const preset = getSocialIconPreset(item.icon) const url = normalizeSocialLinkUrl(item.url) const iconSvg = preset.icon === 'custom' ? normalizeSocialIconSvg(item.iconSvg || item.customIconSvg) : '' if (!url) { return null } return { id: String(item.id || `${preset.icon}-${index + 1}`).trim().slice(0, 80), icon: preset.icon, label: String(item.label || preset.label).trim().slice(0, 80), url, iconSvg } } /** * SNS 링크 목록을 저장 가능한 형태로 정리한다. * @param {unknown} value - 입력 목록 * @returns {Array<{ id: string, icon: string, label: string, url: string, iconSvg: string }>} 정리된 SNS 링크 목록 */ export const normalizeSocialLinks = (value) => { if (typeof value === 'string') { try { return normalizeSocialLinks(JSON.parse(value)) } catch { return [] } } const source = Array.isArray(value) ? value : Object.entries(value && typeof value === 'object' ? value : {}).map(([icon, url]) => ({ icon, url })) return source .slice(0, MAX_SOCIAL_LINK_COUNT) .map(normalizeSocialLinkItem) .filter(Boolean) } /** * 저장된 SNS 링크 중 실제 노출할 항목만 반환한다. * @param {unknown} value - SNS 링크 목록 * @returns {Array<{ id: string, label: string, href: string, icon: string, iconSvg: string, external: boolean }>} 노출 링크 */ export const getVisibleSocialLinks = (value) => normalizeSocialLinks(value).map((item) => ({ id: item.id, label: item.label, href: item.url, icon: item.icon, iconSvg: item.iconSvg, external: !item.url.startsWith('/') && !item.url.startsWith('mailto:') }))