diff --git a/docs/map.md b/docs/map.md index 19b2383..d34058e 100644 --- a/docs/map.md +++ b/docs/map.md @@ -258,7 +258,7 @@ | pages/admin/tags/index.vue | 태그 관리(메인 태그 드래그 정렬 자동 저장·more vert 메뉴, 일반 태그 배지 more vert 메뉴·검색/정렬, 태그 추가 버튼), 액션 피드백 토스트 | | pages/admin/tags/new.vue | 태그 생성 | | pages/admin/tags/[id].vue | 태그 수정 | -| pages/admin/settings/index.vue | 사이트 설정 Ghost형 전체 화면, 카드별 필드만 PATCH하고 성공 필드만 폼에 반영하는 독립 저장, 설정 검색 상태·활성 구역 계산·스크롤 이동 처리, **블로그 제목·설명** 편집 상태·저장 요청·스냅샷 복원 연결, **사이트 정보** 편집 상태·저장 요청·로고 업로드 처리 연결, **SNS 정보** 편집 상태·저장 요청 연결, **POST 설정** 편집 상태·저장 요청·스냅샷 복원 연결, **메인 화면** 편집 상태·저장 요청·커버 업로드 처리 연결, **브랜드** 컬러 정규화·편집 상태·저장 요청 연결, **어나운스 바** 편집 상태·저장 요청 연결, **스팸 필터** 편집 상태·저장 요청·값 정규화 연결, **사이트 용량** 데이터 조회·새로고침 이벤트 연결, **게시물 내보내기** 요청 body 생성·작업 등록·목록 갱신·작업 액션 연결, **게시물 가져오기** 파일 검증·작업 생성 요청·작업 센터 모달 연결, 진행 중 요청 버튼 잠금 | +| pages/admin/settings/index.vue | 사이트 설정 Ghost형 전체 화면, 카드별 필드만 PATCH하고 성공 필드만 폼에 반영하는 독립 저장, 단순 설정 섹션 공통 저장 헬퍼, 설정 검색 상태·활성 구역 계산·스크롤 이동 처리, **블로그 제목·설명** 편집 상태·저장 요청·스냅샷 복원 연결, **사이트 정보** 편집 상태·저장 요청·로고 업로드 처리 연결, **SNS 정보** 편집 상태·저장 요청 연결, **POST 설정** 편집 상태·저장 요청·스냅샷 복원 연결, **메인 화면** 편집 상태·저장 요청·커버 업로드 처리 연결, **브랜드** 컬러 정규화·편집 상태·저장 요청 연결, **어나운스 바** 편집 상태·저장 요청 연결, **스팸 필터** 편집 상태·저장 요청·값 정규화 연결, **사이트 용량** 데이터 조회·새로고침 이벤트 연결, **게시물 내보내기** 요청 body 생성·작업 등록·목록 갱신·작업 액션 연결, **게시물 가져오기** 파일 검증·작업 생성 요청·작업 센터 모달 연결, 진행 중 요청 버튼 잠금 | | components/admin/AdminSettingsNavigation.vue | 관리자 설정 좌측 내비게이션(설정 검색 입력·검색 결과 목록·모바일 구역 선택·구역 이동 이벤트) | | components/admin/AdminHomeCoverSettingsCard.vue | 관리자 설정 메인 화면 카드(라이트·다크 커버 미리보기·드롭존·파일 선택·오버레이 문구·저장 이벤트) | | components/admin/AdminBrandSettingsCard.vue | 관리자 설정 브랜드 컬러 카드(읽기 미리보기·색상 입력·텍스트 입력·예시 배지·저장 이벤트) | diff --git a/docs/todo.md b/docs/todo.md index 245c002..8198c23 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -4,7 +4,7 @@ - 거대 컴포넌트 책임 분리 진행. - `components/admin/AdminMarkdownEditor.vue`: 미디어 모달, 슬래시 명령, 히스토리·선택 복원, 라이브/소스 전환 로직을 단계적으로 composable 또는 하위 컴포넌트로 분리. - - `pages/admin/settings/index.vue`: 설정 카드별 편집 상태·스냅샷·저장 요청 로직을 composable 단위로 추가 분리. + - `pages/admin/settings/index.vue`: 정규화가 필요한 설정 섹션(SNS·POST 설정·브랜드·어나운스 바·스팸 필터)의 편집 상태·스냅샷·저장 요청 로직을 composable 단위로 추가 분리. - `pages/admin/settings/index.vue`: 업로드 제한 셀렉트가 블로그 제목·설명 읽기 카드 안에 있으나 저장 필드는 POST 설정에 묶여 있어, 별도 업로드 제한 카드 또는 POST 설정 편집 영역으로 위치와 저장 흐름을 정리. - `components/content/ContentMarkdownRenderer.vue`: 파싱·블록 렌더링·라이브 편집 중개·라이트박스·광고 삽입 책임을 분리. - `components/admin/AdminPostForm.vue`: 태그 입력, 대표 이미지 선택, 발행/초안 저장 상태, 우측 설정 패널 책임을 분리. diff --git a/docs/update.md b/docs/update.md index 5ce2ae7..c8a08f9 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,11 @@ # 업데이트 이력 +## v1.11.32 + +- 관리자 설정의 단순 섹션 저장 흐름을 `saveSimpleSettingsSection` 헬퍼로 정리. +- 제목·사이트 정보·메인 화면·사이트 코드·광고 슬롯의 스냅샷 복사·복원·저장 후 편집 종료 반복 로직을 필드 목록 기반으로 통합. +- 설정 화면의 카드 상태 로직을 이후 composable로 분리하기 쉽도록 공통 저장 경로를 정리. + ## v1.11.31 - 관리자 설정 좌측 내비게이션을 `AdminSettingsNavigation` 컴포넌트로 분리. diff --git a/package-lock.json b/package-lock.json index 635d688..4b8e265 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sori.studio", - "version": "1.11.31", + "version": "1.11.32", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sori.studio", - "version": "1.11.31", + "version": "1.11.32", "hasInstallScript": true, "dependencies": { "@nuxtjs/tailwindcss": "^6.14.0", diff --git a/package.json b/package.json index f34f9cc..8f03e69 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sori.studio", - "version": "1.11.31", + "version": "1.11.32", "private": true, "type": "module", "imports": { diff --git a/pages/admin/settings/index.vue b/pages/admin/settings/index.vue index e2abdf4..de69af7 100644 --- a/pages/admin/settings/index.vue +++ b/pages/admin/settings/index.vue @@ -111,6 +111,19 @@ const postExportDateRangeModeOptions = [ { value: 'month', label: '특정월' }, { value: 'custom', label: '직접 지정' } ] +const titleDescFields = ['title', 'description'] +const miscFields = ['siteUrl', 'logoText', 'logoUrl', 'faviconUrl', 'copyrightText'] +const homeCoverFields = ['homeCoverImageUrl', 'homeCoverDarkImageUrl', 'homeCoverTitle', 'homeCoverText'] +const siteCodeFields = ['adsTxt', 'customHeadCode', 'customFooterCode'] +const adsFields = [ + 'adHomeFeedCode', + 'adHomeInfeedCode', + 'adSidebarCode', + 'adPostSidebarCode', + 'adPostTopCode', + 'adPostInArticleCode', + 'adPostBottomCode' +] /** 블로그 제목·설명 카드 편집 모드 여부 */ const editTitleDesc = ref(false) @@ -1279,13 +1292,56 @@ const persistSiteSettings = async (options) => { } } +/** + * 지정한 필드를 원본 객체에서 대상 객체로 복사한다. + * @param {Object} target - 값을 받을 객체 + * @param {Object} source - 값을 제공할 객체 + * @param {Array} fields - 복사할 필드 목록 + * @returns {void} + */ +const copySettingsFields = (target, source, fields) => { + fields.forEach((field) => { + target[field] = source[field] + }) +} + +/** + * 단순 설정 섹션 저장 후 스냅샷 갱신과 편집 종료를 처리한다. + * @param {{ hasChanges: import('vue').ComputedRef, savingFlag: import('vue').Ref, editingFlag: import('vue').Ref, snapshot: Object, fields: Array, successToast: string, beforeSave?: Function, afterSnapshot?: Function }} options - 섹션 저장 옵션 + * @returns {Promise} + */ +const saveSimpleSettingsSection = async (options) => { + if (typeof options.beforeSave === 'function') { + options.beforeSave() + } + + if (!options.hasChanges.value) { + return + } + + const ok = await persistSiteSettings({ + successToast: options.successToast, + savingFlag: options.savingFlag, + fields: options.fields + }) + + if (!ok) { + return + } + + copySettingsFields(options.snapshot, form, options.fields) + if (typeof options.afterSnapshot === 'function') { + options.afterSnapshot() + } + options.editingFlag.value = false +} + /** * 블로그 제목·설명 편집 모드 진입 * @returns {void} */ const beginEditTitleDesc = () => { - titleDescSnapshot.title = form.title - titleDescSnapshot.description = form.description + copySettingsFields(titleDescSnapshot, form, titleDescFields) editTitleDesc.value = true } @@ -1294,8 +1350,7 @@ const beginEditTitleDesc = () => { * @returns {void} */ const cancelEditTitleDesc = () => { - form.title = titleDescSnapshot.title - form.description = titleDescSnapshot.description + copySettingsFields(form, titleDescSnapshot, titleDescFields) editTitleDesc.value = false } @@ -1304,19 +1359,14 @@ const cancelEditTitleDesc = () => { * @returns {Promise} */ const saveTitleDescSection = async () => { - if (!hasTitleDescChanges.value) { - return - } - - const ok = await persistSiteSettings({ + await saveSimpleSettingsSection({ + hasChanges: hasTitleDescChanges, successToast: '블로그 제목·설명이 저장되었습니다.', savingFlag: savingTitleDesc, - fields: ['title', 'description'] + editingFlag: editTitleDesc, + snapshot: titleDescSnapshot, + fields: titleDescFields }) - - if (ok) { - editTitleDesc.value = false - } } /** @@ -1324,11 +1374,7 @@ const saveTitleDescSection = async () => { * @returns {void} */ const beginEditMisc = () => { - miscSnapshot.siteUrl = form.siteUrl - miscSnapshot.logoText = form.logoText - miscSnapshot.logoUrl = form.logoUrl - miscSnapshot.faviconUrl = form.faviconUrl - miscSnapshot.copyrightText = form.copyrightText + copySettingsFields(miscSnapshot, form, miscFields) editMisc.value = true } @@ -1337,11 +1383,7 @@ const beginEditMisc = () => { * @returns {void} */ const cancelEditMisc = () => { - form.siteUrl = miscSnapshot.siteUrl - form.logoText = miscSnapshot.logoText - form.logoUrl = miscSnapshot.logoUrl - form.faviconUrl = miscSnapshot.faviconUrl - form.copyrightText = miscSnapshot.copyrightText + copySettingsFields(form, miscSnapshot, miscFields) editMisc.value = false } @@ -1350,24 +1392,14 @@ const cancelEditMisc = () => { * @returns {Promise} */ const saveMiscSection = async () => { - if (!hasMiscChanges.value) { - return - } - - const ok = await persistSiteSettings({ + await saveSimpleSettingsSection({ + hasChanges: hasMiscChanges, successToast: '사이트 정보가 저장되었습니다.', savingFlag: savingMisc, - fields: ['siteUrl', 'logoText', 'logoUrl', 'faviconUrl', 'copyrightText'] + editingFlag: editMisc, + snapshot: miscSnapshot, + fields: miscFields }) - - if (ok) { - miscSnapshot.siteUrl = form.siteUrl - miscSnapshot.logoText = form.logoText - miscSnapshot.logoUrl = form.logoUrl - miscSnapshot.faviconUrl = form.faviconUrl - miscSnapshot.copyrightText = form.copyrightText - editMisc.value = false - } } /** @@ -1597,10 +1629,7 @@ const clearHomeCoverImage = (variant = 'light') => { * @returns {void} */ const beginEditHomeCover = () => { - homeCoverSnapshot.homeCoverImageUrl = form.homeCoverImageUrl - homeCoverSnapshot.homeCoverDarkImageUrl = form.homeCoverDarkImageUrl - homeCoverSnapshot.homeCoverTitle = form.homeCoverTitle - homeCoverSnapshot.homeCoverText = form.homeCoverText + copySettingsFields(homeCoverSnapshot, form, homeCoverFields) editHomeCover.value = true } @@ -1609,10 +1638,7 @@ const beginEditHomeCover = () => { * @returns {void} */ const cancelEditHomeCover = () => { - form.homeCoverImageUrl = homeCoverSnapshot.homeCoverImageUrl - form.homeCoverDarkImageUrl = homeCoverSnapshot.homeCoverDarkImageUrl - form.homeCoverTitle = homeCoverSnapshot.homeCoverTitle - form.homeCoverText = homeCoverSnapshot.homeCoverText + copySettingsFields(form, homeCoverSnapshot, homeCoverFields) editHomeCover.value = false } @@ -1621,28 +1647,14 @@ const cancelEditHomeCover = () => { * @returns {Promise} */ const saveHomeCoverSection = async () => { - if (!hasHomeCoverChanges.value) { - return - } - - const ok = await persistSiteSettings({ + await saveSimpleSettingsSection({ + hasChanges: hasHomeCoverChanges, successToast: '메인 화면 설정이 저장되었습니다.', savingFlag: savingHomeCover, - fields: [ - 'homeCoverImageUrl', - 'homeCoverDarkImageUrl', - 'homeCoverTitle', - 'homeCoverText' - ] + editingFlag: editHomeCover, + snapshot: homeCoverSnapshot, + fields: homeCoverFields }) - - if (ok) { - homeCoverSnapshot.homeCoverImageUrl = form.homeCoverImageUrl - homeCoverSnapshot.homeCoverDarkImageUrl = form.homeCoverDarkImageUrl - homeCoverSnapshot.homeCoverTitle = form.homeCoverTitle - homeCoverSnapshot.homeCoverText = form.homeCoverText - editHomeCover.value = false - } } /** @@ -1796,9 +1808,7 @@ const saveSpamSection = async () => { * @returns {void} */ const beginEditSiteCode = () => { - siteCodeSnapshot.adsTxt = form.adsTxt - siteCodeSnapshot.customHeadCode = form.customHeadCode - siteCodeSnapshot.customFooterCode = form.customFooterCode + copySettingsFields(siteCodeSnapshot, form, siteCodeFields) editSiteCode.value = true } @@ -1807,9 +1817,7 @@ const beginEditSiteCode = () => { * @returns {void} */ const cancelEditSiteCode = () => { - form.adsTxt = siteCodeSnapshot.adsTxt - form.customHeadCode = siteCodeSnapshot.customHeadCode - form.customFooterCode = siteCodeSnapshot.customFooterCode + copySettingsFields(form, siteCodeSnapshot, siteCodeFields) editSiteCode.value = false } @@ -1818,22 +1826,14 @@ const cancelEditSiteCode = () => { * @returns {Promise} */ const saveSiteCodeSection = async () => { - if (!hasSiteCodeChanges.value) { - return - } - - const ok = await persistSiteSettings({ + await saveSimpleSettingsSection({ + hasChanges: hasSiteCodeChanges, successToast: '사이트 코드 설정이 저장되었습니다.', savingFlag: savingSiteCode, - fields: ['adsTxt', 'customHeadCode', 'customFooterCode'] + editingFlag: editSiteCode, + snapshot: siteCodeSnapshot, + fields: siteCodeFields }) - - if (ok) { - siteCodeSnapshot.adsTxt = form.adsTxt - siteCodeSnapshot.customHeadCode = form.customHeadCode - siteCodeSnapshot.customFooterCode = form.customFooterCode - editSiteCode.value = false - } } /** @@ -1841,13 +1841,7 @@ const saveSiteCodeSection = async () => { * @returns {void} */ const beginEditAds = () => { - adsSnapshot.adHomeFeedCode = form.adHomeFeedCode - adsSnapshot.adHomeInfeedCode = form.adHomeInfeedCode - adsSnapshot.adSidebarCode = form.adSidebarCode - adsSnapshot.adPostSidebarCode = form.adPostSidebarCode - adsSnapshot.adPostTopCode = form.adPostTopCode - adsSnapshot.adPostInArticleCode = form.adPostInArticleCode - adsSnapshot.adPostBottomCode = form.adPostBottomCode + copySettingsFields(adsSnapshot, form, adsFields) editAds.value = true } @@ -1856,13 +1850,7 @@ const beginEditAds = () => { * @returns {void} */ const cancelEditAds = () => { - form.adHomeFeedCode = adsSnapshot.adHomeFeedCode - form.adHomeInfeedCode = adsSnapshot.adHomeInfeedCode - form.adSidebarCode = adsSnapshot.adSidebarCode - form.adPostSidebarCode = adsSnapshot.adPostSidebarCode - form.adPostTopCode = adsSnapshot.adPostTopCode - form.adPostInArticleCode = adsSnapshot.adPostInArticleCode - form.adPostBottomCode = adsSnapshot.adPostBottomCode + copySettingsFields(form, adsSnapshot, adsFields) editAds.value = false } @@ -1871,34 +1859,14 @@ const cancelEditAds = () => { * @returns {Promise} */ const saveAdsSection = async () => { - if (!hasAdsChanges.value) { - return - } - - const ok = await persistSiteSettings({ + await saveSimpleSettingsSection({ + hasChanges: hasAdsChanges, successToast: '광고 슬롯 설정이 저장되었습니다.', savingFlag: savingAds, - fields: [ - 'adHomeFeedCode', - 'adHomeInfeedCode', - 'adSidebarCode', - 'adPostSidebarCode', - 'adPostTopCode', - 'adPostInArticleCode', - 'adPostBottomCode' - ] + editingFlag: editAds, + snapshot: adsSnapshot, + fields: adsFields }) - - if (ok) { - adsSnapshot.adHomeFeedCode = form.adHomeFeedCode - adsSnapshot.adHomeInfeedCode = form.adHomeInfeedCode - adsSnapshot.adSidebarCode = form.adSidebarCode - adsSnapshot.adPostSidebarCode = form.adPostSidebarCode - adsSnapshot.adPostTopCode = form.adPostTopCode - adsSnapshot.adPostInArticleCode = form.adPostInArticleCode - adsSnapshot.adPostBottomCode = form.adPostBottomCode - editAds.value = false - } } /**