SNS 링크 설정 추가 v1.5.39
This commit is contained in:
@@ -12,6 +12,11 @@ import {
|
||||
normalizeSignupBlockedUsernames,
|
||||
parseSignupBlockedUsernamesFromText
|
||||
} from '~/lib/signup-blocked-usernames.js'
|
||||
import {
|
||||
SOCIAL_ICON_PRESETS,
|
||||
getSocialIconPreset,
|
||||
normalizeSocialLinks
|
||||
} from '~/lib/social-links.js'
|
||||
|
||||
definePageMeta({
|
||||
layout: 'admin'
|
||||
@@ -21,6 +26,7 @@ const router = useRouter()
|
||||
|
||||
const savingTitleDesc = ref(false)
|
||||
const savingMisc = ref(false)
|
||||
const savingSocial = ref(false)
|
||||
const savingPost = ref(false)
|
||||
const savingHomeCover = ref(false)
|
||||
const savingBrand = ref(false)
|
||||
@@ -61,6 +67,8 @@ const scrollSpySuspended = ref(false)
|
||||
const editTitleDesc = ref(false)
|
||||
/** 사이트 정보 카드 편집 모드 여부 */
|
||||
const editMisc = ref(false)
|
||||
/** SNS 정보 카드 편집 모드 여부 */
|
||||
const editSocial = ref(false)
|
||||
/** POST 설정 카드 편집 모드 여부 */
|
||||
const editPost = ref(false)
|
||||
/** 메인 화면 커버 카드 편집 모드 여부 */
|
||||
@@ -86,6 +94,10 @@ const miscSnapshot = reactive({
|
||||
faviconUrl: '',
|
||||
copyrightText: ''
|
||||
})
|
||||
/** 편집 시작 시점의 SNS 정보(취소 시 복원용) */
|
||||
const socialSnapshot = reactive({
|
||||
socialLinks: []
|
||||
})
|
||||
/** 편집 시작 시점의 POST 설정(취소 시 복원용) */
|
||||
const postSnapshot = reactive({
|
||||
showPostUpdatedAt: false
|
||||
@@ -139,6 +151,7 @@ const form = reactive({
|
||||
logoUrl: settings.value?.logoUrl || '',
|
||||
faviconUrl: settings.value?.faviconUrl || '',
|
||||
copyrightText: settings.value?.copyrightText || '©2026 sori.studio',
|
||||
socialLinks: normalizeSocialLinks(settings.value?.socialLinks || []),
|
||||
showPostUpdatedAt: Boolean(settings.value?.showPostUpdatedAt),
|
||||
homeCoverImageUrl: settings.value?.homeCoverImageUrl || '',
|
||||
homeCoverDarkImageUrl: settings.value?.homeCoverDarkImageUrl || '',
|
||||
@@ -177,6 +190,13 @@ const hasMiscChanges = computed(() => editMisc.value && (
|
||||
|| form.copyrightText !== miscSnapshot.copyrightText
|
||||
))
|
||||
|
||||
/**
|
||||
* SNS 정보 변경 여부
|
||||
* @returns {boolean} 변경 여부
|
||||
*/
|
||||
const hasSocialChanges = computed(() => editSocial.value
|
||||
&& JSON.stringify(normalizeSocialLinks(form.socialLinks)) !== JSON.stringify(normalizeSocialLinks(socialSnapshot.socialLinks)))
|
||||
|
||||
/**
|
||||
* POST 설정 변경 여부
|
||||
* @returns {boolean} 변경 여부
|
||||
@@ -484,7 +504,8 @@ const settingsNavGroups = [
|
||||
heading: '일반',
|
||||
items: [
|
||||
{ id: 'admin-settings-section-title', label: '블로그 제목·설명', keywords: 'title description site name', iconId: 'title-desc' },
|
||||
{ id: 'admin-settings-section-misc', label: '사이트 정보', keywords: 'logo url copyright favicon site info' }
|
||||
{ id: 'admin-settings-section-misc', label: '사이트 정보', keywords: 'logo url copyright favicon site info' },
|
||||
{ id: 'admin-settings-section-social', label: 'SNS 정보', keywords: 'social follow sns x github instagram youtube rss', iconId: 'site-code' }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1064,6 +1085,7 @@ const buildSiteSettingsPayload = () => ({
|
||||
logoUrl: form.logoUrl,
|
||||
faviconUrl: form.faviconUrl,
|
||||
copyrightText: form.copyrightText,
|
||||
socialLinks: normalizeSocialLinks(form.socialLinks),
|
||||
showPostUpdatedAt: Boolean(form.showPostUpdatedAt),
|
||||
homeCoverImageUrl: form.homeCoverImageUrl || '',
|
||||
homeCoverDarkImageUrl: form.homeCoverDarkImageUrl || '',
|
||||
@@ -1198,6 +1220,84 @@ const saveMiscSection = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SNS 정보 편집 모드 진입
|
||||
* @returns {void}
|
||||
*/
|
||||
const beginEditSocial = () => {
|
||||
socialSnapshot.socialLinks = normalizeSocialLinks(form.socialLinks)
|
||||
form.socialLinks = normalizeSocialLinks(form.socialLinks)
|
||||
editSocial.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* SNS 정보 편집 취소
|
||||
* @returns {void}
|
||||
*/
|
||||
const cancelEditSocial = () => {
|
||||
form.socialLinks = normalizeSocialLinks(socialSnapshot.socialLinks)
|
||||
editSocial.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* SNS 링크 항목을 추가한다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const addSocialLink = () => {
|
||||
form.socialLinks = [
|
||||
...normalizeSocialLinks(form.socialLinks),
|
||||
{
|
||||
id: `social-${Date.now()}`,
|
||||
icon: SOCIAL_ICON_PRESETS[0].icon,
|
||||
label: SOCIAL_ICON_PRESETS[0].label,
|
||||
url: ''
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* SNS 링크 항목을 제거한다.
|
||||
* @param {number} index - 제거할 항목 순서
|
||||
* @returns {void}
|
||||
*/
|
||||
const removeSocialLink = (index) => {
|
||||
form.socialLinks = form.socialLinks.filter((_, itemIndex) => itemIndex !== index)
|
||||
}
|
||||
|
||||
/**
|
||||
* SNS 링크 아이콘을 변경한다.
|
||||
* @param {number} index - 항목 순서
|
||||
* @param {string} icon - 아이콘 키
|
||||
* @returns {void}
|
||||
*/
|
||||
const updateSocialLinkIcon = (index, icon) => {
|
||||
const preset = getSocialIconPreset(icon)
|
||||
form.socialLinks[index].icon = preset.icon
|
||||
form.socialLinks[index].label = preset.label
|
||||
}
|
||||
|
||||
/**
|
||||
* SNS 정보를 저장한다.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const saveSocialSection = async () => {
|
||||
if (!hasSocialChanges.value) {
|
||||
return
|
||||
}
|
||||
|
||||
form.socialLinks = normalizeSocialLinks(form.socialLinks)
|
||||
|
||||
const ok = await persistSiteSettings({
|
||||
successToast: 'SNS 정보가 저장되었습니다.',
|
||||
savingFlag: savingSocial
|
||||
})
|
||||
|
||||
if (ok) {
|
||||
socialSnapshot.socialLinks = normalizeSocialLinks(form.socialLinks)
|
||||
editSocial.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST 설정 편집 모드 진입
|
||||
* @returns {void}
|
||||
@@ -2027,6 +2127,127 @@ onBeforeUnmount(() => {
|
||||
|
||||
</section>
|
||||
|
||||
<section
|
||||
id="admin-settings-section-social"
|
||||
class="admin-settings-screen__card rounded-xl border border-[#e6e8eb] bg-white p-6 shadow-[0_1px_2px_rgba(15,23,42,0.04)]"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="min-w-0 flex-1">
|
||||
<h2 class="text-base font-semibold text-[#15171a] md:text-lg">
|
||||
SNS 정보
|
||||
</h2>
|
||||
<p
|
||||
v-if="!editSocial"
|
||||
class="mr-5 mt-1 text-pretty text-sm leading-relaxed text-[#657080]"
|
||||
>
|
||||
오른쪽 사이드바 FOLLOW 영역에 표시할 아이콘과 주소를 관리합니다.
|
||||
</p>
|
||||
</div>
|
||||
<div class="-mr-1 mt-[-5px] flex shrink-0 items-center justify-start gap-2">
|
||||
<template v-if="!editSocial">
|
||||
<button
|
||||
class="inline-flex h-7 cursor-pointer items-center justify-center rounded px-3 text-sm font-semibold whitespace-nowrap text-[#15171a] transition hover:bg-[#eceff2] hover:text-black"
|
||||
type="button"
|
||||
@click="beginEditSocial"
|
||||
>
|
||||
편집
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button
|
||||
class="inline-flex h-7 cursor-pointer items-center justify-center rounded px-3 text-sm font-semibold whitespace-nowrap text-[#5d6673] transition hover:bg-[#eceff2] hover:text-[#15171a]"
|
||||
type="button"
|
||||
:disabled="savingSocial"
|
||||
@click="cancelEditSocial"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex h-7 cursor-pointer items-center justify-center rounded px-3 text-sm font-semibold whitespace-nowrap text-[#15171a] transition hover:bg-[#eceff2] hover:text-black disabled:opacity-50"
|
||||
type="button"
|
||||
:disabled="savingSocial || !hasSocialChanges"
|
||||
@click="saveSocialSection"
|
||||
>
|
||||
{{ savingSocial ? '저장 중' : '저장' }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!editSocial"
|
||||
class="admin-settings-screen__social-readonly border-t border-[#eceff2] pt-5"
|
||||
>
|
||||
<div
|
||||
v-if="normalizeSocialLinks(form.socialLinks).length"
|
||||
class="flex flex-wrap gap-2"
|
||||
>
|
||||
<span
|
||||
v-for="item in normalizeSocialLinks(form.socialLinks)"
|
||||
:key="item.id"
|
||||
class="inline-flex items-center gap-2 rounded-full border border-[#dce0e5] bg-[#f7f8fa] px-3 py-1.5 text-sm font-semibold text-[#15171a]"
|
||||
>
|
||||
<span>{{ item.label }}</span>
|
||||
<span class="max-w-[220px] truncate text-xs font-medium text-[#657080]">{{ item.url }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<p v-else class="text-sm text-[#657080]">
|
||||
등록된 SNS 링크가 없습니다. 공개 화면의 FOLLOW 아이콘도 표시되지 않습니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="admin-settings-screen__social-edit grid gap-4 border-t border-[#eceff2] pt-5">
|
||||
<div
|
||||
v-for="(item, index) in form.socialLinks"
|
||||
:key="item.id || index"
|
||||
class="grid gap-3 rounded-lg border border-[#edf0f2] bg-[#fafafa] p-3 md:grid-cols-[180px_minmax(0,1fr)_auto] md:items-center"
|
||||
>
|
||||
<label class="grid gap-1.5 text-sm">
|
||||
<span class="font-medium text-[#3f4650]">아이콘</span>
|
||||
<div class="relative">
|
||||
<select
|
||||
class="h-10 w-full appearance-none rounded-md border border-[#dce0e5] bg-white px-3 pr-9 text-[#15171a] outline-none focus:border-[#15171a] focus:ring-1 focus:ring-[#15171a]"
|
||||
:value="item.icon"
|
||||
@change="updateSocialLinkIcon(index, $event.target.value)"
|
||||
>
|
||||
<option
|
||||
v-for="preset in SOCIAL_ICON_PRESETS"
|
||||
:key="preset.icon"
|
||||
:value="preset.icon"
|
||||
>
|
||||
{{ preset.label }}
|
||||
</option>
|
||||
</select>
|
||||
<svg class="pointer-events-none absolute right-3 top-1/2 size-4 -translate-y-1/2 text-[#394047]" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
|
||||
</div>
|
||||
</label>
|
||||
<label class="grid gap-1.5 text-sm">
|
||||
<span class="font-medium text-[#3f4650]">주소</span>
|
||||
<input
|
||||
v-model="item.url"
|
||||
class="h-10 rounded-md border border-[#dce0e5] bg-white px-3 text-[#15171a] outline-none focus:border-[#15171a] focus:ring-1 focus:ring-[#15171a]"
|
||||
type="text"
|
||||
:placeholder="getSocialIconPreset(item.icon).placeholder"
|
||||
>
|
||||
</label>
|
||||
<button
|
||||
class="h-10 rounded-md border border-[#ffd1d1] bg-white px-4 text-sm font-semibold text-[#d73939] transition-colors hover:bg-[#fff5f5]"
|
||||
type="button"
|
||||
@click="removeSocialLink(index)"
|
||||
>
|
||||
삭제
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
class="inline-flex h-10 w-fit items-center justify-center rounded-md border border-[#dce0e5] bg-white px-4 text-sm font-semibold text-[#15171a] transition-colors hover:bg-[#f3f5f7]"
|
||||
type="button"
|
||||
@click="addSocialLink"
|
||||
>
|
||||
SNS 링크 추가
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<h2 class="admin-settings-screen__section-heading z-20 mb-px pt-10 text-2xl font-bold tracking-tight text-[#15171a]">
|
||||
게시물
|
||||
</h2>
|
||||
|
||||
Reference in New Issue
Block a user