v1.7.1: 멤버 권한·추천 사이트·네비게이션 UI 개선
관리자는 VIP·멤버만 수정 가능하도록 서버·UI 권한을 정리하고 소유자 권한 이전 API와 확인 모달을 추가했다. 추천 사이트 파비콘 캐시·URL 접두사·카드 편집 UI를 보강하고 확인 얼럿을 AdminConfirmModal로 통일했다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
98
lib/navigation-url.js
Normal file
98
lib/navigation-url.js
Normal file
@@ -0,0 +1,98 @@
|
||||
/** @typedef {'https://' | 'http://' | ''} NavigationUrlPrefix */
|
||||
|
||||
/**
|
||||
* 네비게이션 URL 접두사 옵션
|
||||
* @type {Array<{ value: NavigationUrlPrefix, label: string }>}
|
||||
*/
|
||||
export const navigationUrlPrefixOptions = [
|
||||
{ value: 'https://', label: 'https://' },
|
||||
{ value: 'http://', label: 'http://' },
|
||||
{ value: '', label: '내부 경로 (/)' }
|
||||
]
|
||||
|
||||
/**
|
||||
* 저장된 URL을 접두사와 본문으로 분리한다.
|
||||
* @param {string} url - 저장된 URL
|
||||
* @returns {{ prefix: NavigationUrlPrefix, body: string }} 접두사와 본문
|
||||
*/
|
||||
export const splitNavigationUrl = (url) => {
|
||||
const trimmed = String(url || '').trim()
|
||||
if (!trimmed) {
|
||||
return { prefix: 'https://', body: '' }
|
||||
}
|
||||
if (trimmed === '#') {
|
||||
return { prefix: '', body: '#' }
|
||||
}
|
||||
if (trimmed.startsWith('/') && !trimmed.startsWith('//')) {
|
||||
return { prefix: '', body: trimmed }
|
||||
}
|
||||
if (trimmed.startsWith('http://')) {
|
||||
return { prefix: 'http://', body: trimmed.slice('http://'.length) }
|
||||
}
|
||||
if (trimmed.startsWith('https://')) {
|
||||
return { prefix: 'https://', body: trimmed.slice('https://'.length) }
|
||||
}
|
||||
if (trimmed.startsWith('//')) {
|
||||
return { prefix: 'https://', body: trimmed.slice('//'.length) }
|
||||
}
|
||||
return { prefix: 'https://', body: trimmed }
|
||||
}
|
||||
|
||||
/**
|
||||
* 접두사와 본문을 저장용 URL로 합친다.
|
||||
* @param {NavigationUrlPrefix|string} prefix - URL 접두사
|
||||
* @param {string} body - 호스트·경로 또는 내부 경로
|
||||
* @returns {string} 합쳐진 URL
|
||||
*/
|
||||
export const composeNavigationUrl = (prefix, body) => {
|
||||
const input = String(body || '').trim()
|
||||
if (!input) {
|
||||
return ''
|
||||
}
|
||||
if (input === '#') {
|
||||
return '#'
|
||||
}
|
||||
|
||||
const normalizedPrefix = String(prefix || '')
|
||||
if (!normalizedPrefix) {
|
||||
if (input.startsWith('/')) {
|
||||
return input
|
||||
}
|
||||
return `/${input.replace(/^\/+/, '')}`
|
||||
}
|
||||
|
||||
const withoutProto = input.replace(/^https?:\/\//i, '').replace(/^\/+/, '')
|
||||
if (!withoutProto) {
|
||||
return ''
|
||||
}
|
||||
return `${normalizedPrefix}${withoutProto}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 네비게이션 URL을 저장용 형식으로 정규화한다.
|
||||
* @param {string} rawUrl - 입력 URL
|
||||
* @returns {string} 정규화된 URL
|
||||
*/
|
||||
export const normalizeNavigationUrl = (rawUrl) => {
|
||||
const trimmed = String(rawUrl || '').trim()
|
||||
if (!trimmed || trimmed === '#') {
|
||||
return trimmed
|
||||
}
|
||||
if (trimmed.startsWith('/') && !trimmed.startsWith('//')) {
|
||||
return trimmed
|
||||
}
|
||||
if (trimmed.startsWith('//')) {
|
||||
return `https:${trimmed}`
|
||||
}
|
||||
if (/^https?:\/\//i.test(trimmed)) {
|
||||
return trimmed
|
||||
}
|
||||
return `https://${trimmed.replace(/^\/+/, '')}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 저장 가능한 네비게이션 URL인지 확인한다.
|
||||
* @param {string} url - URL
|
||||
* @returns {boolean} 유효 여부
|
||||
*/
|
||||
export const isValidNavigationUrl = (url) => /^(\/|https?:\/\/|#)/.test(String(url || '').trim())
|
||||
32
lib/recommended-site-icon-url.js
Normal file
32
lib/recommended-site-icon-url.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { getExternalFaviconUrl } from './external-favicon-url.js'
|
||||
|
||||
/**
|
||||
* 추천 사이트 외부 URL인지 확인한다.
|
||||
* @param {string} url - 링크 URL
|
||||
* @returns {boolean} 외부 URL 여부
|
||||
*/
|
||||
export const isExternalRecommendedSiteUrl = (url) => /^https?:\/\//i.test(String(url || '').trim())
|
||||
|
||||
/**
|
||||
* 추천 사이트에 표시할 아이콘 URL을 반환한다.
|
||||
* 수동 썸네일 > 서버 캐시 파비콘 > 외부 파비콘 프록시 순이다.
|
||||
* @param {Object} item - 추천 사이트 항목
|
||||
* @param {string} [item.thumbnailUrl] - 수동 썸네일 URL
|
||||
* @param {string} [item.cachedFaviconUrl] - 서버에 저장된 파비콘 URL
|
||||
* @param {string} [item.url] - 사이트 URL
|
||||
* @param {number} [sizePx] - 외부 프록시 요청 크기(px)
|
||||
* @returns {string} 아이콘 URL
|
||||
*/
|
||||
export const getRecommendedSiteIconUrl = (item, sizePx = 64) => {
|
||||
const thumbnailUrl = String(item?.thumbnailUrl || '').trim()
|
||||
if (thumbnailUrl) {
|
||||
return thumbnailUrl
|
||||
}
|
||||
|
||||
const cachedFaviconUrl = String(item?.cachedFaviconUrl || '').trim()
|
||||
if (cachedFaviconUrl) {
|
||||
return cachedFaviconUrl
|
||||
}
|
||||
|
||||
return getExternalFaviconUrl(item?.url, sizePx)
|
||||
}
|
||||
Reference in New Issue
Block a user