SNS 링크 저장 복구 v1.5.41
This commit is contained in:
5
db/migrations/049_fix_social_links_jsonb_string.sql
Normal file
5
db/migrations/049_fix_social_links_jsonb_string.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
UPDATE site_settings
|
||||
SET social_links = (social_links #>> '{}')::jsonb
|
||||
WHERE jsonb_typeof(social_links) = 'string'
|
||||
AND (social_links #>> '{}') IS NOT NULL
|
||||
AND (social_links #>> '{}') ~ '^\s*\[';
|
||||
@@ -1,5 +1,10 @@
|
||||
# 업데이트 요약
|
||||
|
||||
## v1.5.41
|
||||
|
||||
- SNS 링크가 저장 후 사라져 보이던 문제를 수정했다.
|
||||
- SNS 링크 편집 화면을 아이콘과 주소 중심으로 단순화했다.
|
||||
|
||||
## v1.5.40
|
||||
|
||||
- SNS 링크 주소 입력 시 `https://`를 생략해도 자동으로 보정된다.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 배포 가이드
|
||||
|
||||
> 로컬 기준 v1.5.40에서 `npm run lint`, `npm run build` 검증을 통과했다. NAS 실제 컨테이너 기동과 도메인/프록시 접속 검증은 운영 배포 단계에서 진행한다.
|
||||
> 로컬 기준 v1.5.41에서 `npm run lint`, `npm run build` 검증을 통과했다. NAS 실제 컨테이너 기동과 도메인/프록시 접속 검증은 운영 배포 단계에서 진행한다.
|
||||
|
||||
## 빌드 유형
|
||||
|
||||
@@ -68,6 +68,11 @@ docker exec sori-studio-db pg_isready -U sori_studio -d sori_studio
|
||||
docker exec sori-studio-db psql -U sori_studio -d sori_studio -c 'SELECT count(*) AS posts_count FROM posts;'
|
||||
```
|
||||
|
||||
### v1.5.41 마이그레이션
|
||||
|
||||
- `049_fix_social_links_jsonb_string.sql`: 기존에 JSONB 문자열로 잘못 저장된 `site_settings.social_links` 값을 JSONB 배열로 복구한다.
|
||||
- 적용 후 관리자 사이트 설정의 SNS 정보 저장값이 읽기 모드에서 유지되는지 확인한다.
|
||||
|
||||
### v1.5.40 참고
|
||||
|
||||
- 추가 DB 마이그레이션은 없다.
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# 의사결정 이력
|
||||
|
||||
## 2026-06-02 v1.5.41 — SNS 편집은 아이콘과 주소만으로 충분하다
|
||||
|
||||
FOLLOW 영역은 아이콘 버튼 목록이므로 운영자가 직접 보는 편집 화면에서도 핵심 입력은 아이콘과 주소다. 이름은 접근성 라벨로 내부에서 프리셋명을 쓰면 충분하고, 별도 입력칸은 레이아웃을 복잡하게 만든다. 저장값은 JSONB 배열이어야 하므로 기존 문자열 저장값은 마이그레이션으로 복구하고, 저장 쿼리도 명시적으로 JSONB 배열로 캐스팅한다.
|
||||
|
||||
## 2026-06-02 v1.5.40 — SNS 아이콘은 프리셋과 직접 SVG를 함께 둔다
|
||||
|
||||
SNS 채널은 서비스 유행에 따라 계속 바뀌므로, Facebook·LinkedIn 같은 고정 필드만 두면 운영자가 쓰지 않는 항목과 새로 필요한 항목 사이에서 계속 코드 수정이 필요해진다. 기본 아이콘은 프리셋으로 빠르게 선택하게 두되, 프리셋에 없는 서비스는 관리자가 SVG 아이콘을 직접 붙여 넣어 같은 목록 구조로 저장하도록 했다. URL은 운영자가 `https://`를 생략해도 정상 링크로 보정해 저장 실패처럼 보이는 혼란을 줄인다.
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
| lib/analytics-shared.js | 통계 추적 경로 필터·체류/스크롤 상수(클라이언트·서버 공용) |
|
||||
| lib/analytics.js | 서버 전용 visitor/session hash(`node:crypto`) |
|
||||
| lib/analytics-traffic.js | referrer·User-Agent 기반 유입원·디바이스·검색 키워드 축약 분류 |
|
||||
| lib/social-links.js | 사이트 설정 SNS 링크 아이콘 프리셋·사용자 SVG 정리·URL 자동 보정·공개 노출 목록 생성 |
|
||||
| lib/social-links.js | 사이트 설정 SNS 링크 아이콘 프리셋·사용자 SVG 정리·URL 자동 보정·레거시 JSON 문자열 복구·공개 노출 목록 생성 |
|
||||
|
||||
## Nuxt 모듈
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
- `Escape` 키는 통합 검색 모달이 열려 있으면 최우선으로 닫고, 그다음 사용자 드롭다운, 이어서 모바일에서만 좌측 슬라이드 메뉴를 닫는다.
|
||||
- `/` 키는 `INPUT`·`TEXTAREA`·`SELECT`·`contenteditable`에 포커스가 없고 `Ctrl`/`Meta`/`Alt`와 함께 눌리지 않을 때 통합 검색 모달을 연다. 헤더 검색 영역(`md+`) 클릭으로도 동일하게 연다.
|
||||
- 헤더 우측 사용자 아이콘 버튼은 로그인 상태면 회원 아바타/닉네임과 설정·로그아웃 메뉴를, 비로그인 상태면 Guest·Sign up·Sign in 메뉴를 표시한다. 회원 아바타 이미지가 없거나 비로그인 상태인 경우 사용자 메뉴 버튼에는 사람 아이콘을 표시한다.
|
||||
- 오른쪽 사이드바의 FOLLOW 영역은 사이트 설정의 SNS 링크 목록을 기준으로 표시한다. 관리자가 아이콘 프리셋 또는 직접 SVG 아이콘과 주소를 등록한 항목만 노출하며, 등록된 항목이 없으면 FOLLOW 영역 자체를 숨긴다. SNS 주소는 `https://`를 생략해도 저장 시 자동 보정한다.
|
||||
- 오른쪽 사이드바의 FOLLOW 영역은 사이트 설정의 SNS 링크 목록을 기준으로 표시한다. 관리자가 아이콘 프리셋 또는 직접 SVG 아이콘과 주소를 등록한 항목만 노출하며, 등록된 항목이 없으면 FOLLOW 영역 자체를 숨긴다. SNS 주소는 `https://`를 생략해도 저장 시 자동 보정한다. 관리자 편집 화면은 아이콘과 주소를 기본 입력으로 사용하며, 직접 SVG 프리셋을 선택했을 때만 SVG 코드 입력을 추가로 표시한다.
|
||||
|
||||
### 공개 화면 색상
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# 업데이트 이력
|
||||
|
||||
## v1.5.41
|
||||
|
||||
- 관리자 사이트 설정: SNS 링크가 JSONB 배열이 아닌 JSON 문자열로 저장되어 저장 후 목록이 비어 보이던 문제 수정.
|
||||
- 관리자 사이트 설정: 기존에 잘못 저장된 SNS 링크 JSON 문자열을 배열로 복구하는 DB 마이그레이션 추가.
|
||||
- 관리자 사이트 설정: SNS 편집 UI에서 불필요한 이름 입력칸을 제거하고 아이콘·주소 중심 레이아웃으로 정리.
|
||||
|
||||
## v1.5.40
|
||||
|
||||
- 관리자 사이트 설정: SNS 링크 주소 입력 시 `https://`를 생략해도 자동 보정되도록 수정.
|
||||
|
||||
@@ -126,6 +126,14 @@ export const normalizeSocialLinkItem = (item, index = 0) => {
|
||||
* @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 }))
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sori.studio",
|
||||
"version": "1.5.40",
|
||||
"version": "1.5.41",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sori.studio",
|
||||
"version": "1.5.40",
|
||||
"version": "1.5.41",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sori.studio",
|
||||
"version": "1.5.40",
|
||||
"version": "1.5.41",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"imports": {
|
||||
|
||||
@@ -1273,12 +1273,8 @@ const removeSocialLink = (index) => {
|
||||
*/
|
||||
const updateSocialLinkIcon = (index, icon) => {
|
||||
const preset = getSocialIconPreset(icon)
|
||||
const currentPreset = getSocialIconPreset(form.socialLinks[index].icon)
|
||||
form.socialLinks[index].icon = preset.icon
|
||||
|
||||
if (!form.socialLinks[index].label || form.socialLinks[index].label === currentPreset.label) {
|
||||
form.socialLinks[index].label = preset.label
|
||||
}
|
||||
form.socialLinks[index].label = preset.label
|
||||
|
||||
if (preset.icon !== 'custom') {
|
||||
form.socialLinks[index].iconSvg = ''
|
||||
@@ -2185,7 +2181,7 @@ onBeforeUnmount(() => {
|
||||
|
||||
<div
|
||||
v-if="!editSocial"
|
||||
class="admin-settings-screen__social-readonly border-t border-[#eceff2] pt-5"
|
||||
class="admin-settings-screen__social-readonly pt-5"
|
||||
>
|
||||
<div
|
||||
v-if="normalizeSocialLinks(form.socialLinks).length"
|
||||
@@ -2209,7 +2205,7 @@ onBeforeUnmount(() => {
|
||||
<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-[170px_minmax(0,0.7fr)_minmax(0,1fr)_auto] md:items-start"
|
||||
class="grid gap-3 rounded-lg border border-[#edf0f2] bg-[#fafafa] p-3 md:grid-cols-[220px_minmax(0,1fr)_auto] md:items-start"
|
||||
>
|
||||
<label class="grid gap-1.5 text-sm">
|
||||
<span class="font-medium text-[#3f4650]">아이콘</span>
|
||||
@@ -2230,15 +2226,6 @@ onBeforeUnmount(() => {
|
||||
<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.label"
|
||||
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="표시 이름"
|
||||
>
|
||||
</label>
|
||||
<label class="grid gap-1.5 text-sm">
|
||||
<span class="font-medium text-[#3f4650]">주소</span>
|
||||
<input
|
||||
@@ -2257,7 +2244,7 @@ onBeforeUnmount(() => {
|
||||
</button>
|
||||
<label
|
||||
v-if="item.icon === 'custom'"
|
||||
class="grid gap-1.5 text-sm md:col-span-4"
|
||||
class="grid gap-1.5 text-sm md:col-span-3"
|
||||
>
|
||||
<span class="font-medium text-[#3f4650]">사용자 SVG 아이콘</span>
|
||||
<textarea
|
||||
|
||||
@@ -905,7 +905,7 @@ export const updateSiteSettings = async (input) => {
|
||||
${input.logoUrl || ''},
|
||||
${input.faviconUrl || ''},
|
||||
${input.copyrightText},
|
||||
${JSON.stringify(normalizeSocialLinks(input.socialLinks))},
|
||||
${JSON.stringify(normalizeSocialLinks(input.socialLinks))}::jsonb,
|
||||
${input.showPostUpdatedAt ? true : false},
|
||||
${input.homeCoverImageUrl || ''},
|
||||
${input.homeCoverDarkImageUrl || ''},
|
||||
|
||||
Reference in New Issue
Block a user