메뉴 관리 개편, 추천 사이트·1뎁스 네비, 우측 Recommended 연동(v1.1.13)
상단 네비는 평면 테이블·드래그로 편집하고 한 단계 하위만 허용한다. 추천 사이트 탭·location recommended·공개 API와 우측 사이드 카드·파비콘 URL 유틸을 추가한다. 문서·배포 마이그레이션 안내·관리자 레이아웃·설정 화면 등 누적 변경을 반영한다.
This commit is contained in:
@@ -2,6 +2,6 @@ import { getPublicNavigation } from '../repositories/content-repository'
|
||||
|
||||
/**
|
||||
* 공개 네비게이션 API
|
||||
* @returns {Promise<{primary: Array<Object>, footer: Array<Object>}>} `primary`는 트리(`children` 선택), `footer`는 평면
|
||||
* @returns {Promise<{primary: Array<Object>, footer: Array<Object>, recommended: Array<Object>}>} `primary`는 트리(`children` 선택), `footer`·`recommended`는 평면
|
||||
*/
|
||||
export default defineEventHandler(() => getPublicNavigation())
|
||||
|
||||
@@ -894,7 +894,7 @@ export const listNavigationItems = async ({ visibleOnly = false } = {}) => {
|
||||
|
||||
/**
|
||||
* 공개 네비게이션 조회
|
||||
* @returns {Promise<{primary: Array<Object>, footer: Array<Object>}>} 위치별 공개 네비게이션
|
||||
* @returns {Promise<{primary: Array<Object>, footer: Array<Object>, recommended: Array<Object>}>} 위치별 공개 네비게이션
|
||||
*/
|
||||
export const getPublicNavigation = async () => {
|
||||
const flat = await listNavigationItems({ visibleOnly: true })
|
||||
@@ -902,6 +902,9 @@ export const getPublicNavigation = async () => {
|
||||
const footerFlat = flat
|
||||
.filter((item) => item.location === 'footer' && !item.parentId)
|
||||
.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0))
|
||||
const recommendedFlat = flat
|
||||
.filter((item) => item.location === 'recommended' && !item.parentId)
|
||||
.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0))
|
||||
|
||||
return {
|
||||
primary: buildPublicPrimaryTree(primaryFlat),
|
||||
@@ -910,6 +913,12 @@ export const getPublicNavigation = async () => {
|
||||
label: item.label,
|
||||
url: item.url,
|
||||
isVisible: item.isVisible
|
||||
})),
|
||||
recommended: recommendedFlat.map((item) => ({
|
||||
id: item.id,
|
||||
label: item.label,
|
||||
url: item.url,
|
||||
isVisible: item.isVisible
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
renumberSortOrderByTree(items, 'primary')
|
||||
renumberSortOrderByTree(items, 'footer')
|
||||
renumberSortOrderByTree(items, 'recommended')
|
||||
applyNavigationDerivedFlags(items)
|
||||
|
||||
try {
|
||||
|
||||
@@ -4,17 +4,17 @@ export const adminNavigationItemInputSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
label: z.string().trim().min(1),
|
||||
url: z.string().trim().min(1).regex(/^(\/|https?:\/\/|#)/),
|
||||
location: z.enum(['primary', 'footer']),
|
||||
location: z.enum(['primary', 'footer', 'recommended']),
|
||||
sortOrder: z.coerce.number().int().min(0).default(0),
|
||||
isVisible: z.boolean().default(true),
|
||||
parentId: z.union([z.string().uuid(), z.null()]).optional(),
|
||||
isFolder: z.boolean().default(false)
|
||||
}).superRefine((data, ctx) => {
|
||||
if (data.location === 'footer' && data.parentId) {
|
||||
if ((data.location === 'footer' || data.location === 'recommended') && data.parentId) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: ['parentId'],
|
||||
message: '하단 메뉴는 하위 항목을 가질 수 없습니다.'
|
||||
message: '해당 위치 메뉴는 하위 항목을 가질 수 없습니다.'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -35,6 +35,13 @@ export const validateNavigationItems = (items) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (loc === 'recommended') {
|
||||
const p = item.parentId
|
||||
if (p != null && String(p).trim() !== '') {
|
||||
return { ok: false, message: '추천 사이트에는 하위 항목을 둘 수 없습니다.' }
|
||||
}
|
||||
}
|
||||
|
||||
const p = item.parentId
|
||||
if (p != null && String(p).trim() !== '') {
|
||||
const pid = String(p).trim()
|
||||
@@ -43,7 +50,13 @@ export const validateNavigationItems = (items) => {
|
||||
}
|
||||
const parent = byId.get(pid)
|
||||
if (parent.location !== loc) {
|
||||
return { ok: false, message: '상위 메뉴는 같은 위치(상단/하단)에 있어야 합니다.' }
|
||||
return { ok: false, message: '상위 메뉴는 같은 위치(상단/하단/추천)에 있어야 합니다.' }
|
||||
}
|
||||
if (loc === 'primary') {
|
||||
const gp = parent.parentId
|
||||
if (gp != null && String(gp).trim() !== '') {
|
||||
return { ok: false, message: '상단 메뉴는 하위 한 단계까지만 허용됩니다.' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -136,7 +149,7 @@ export const orderNavigationItemsForInsert = (items) => {
|
||||
/**
|
||||
* location 기준 DFS로 sort_order를 10단위로 다시 부여한다.
|
||||
* @param {Array<Object>} items - 전체 항목(변경됨)
|
||||
* @param {'primary'|'footer'} location - 위치
|
||||
* @param {'primary'|'footer'|'recommended'} location - 위치
|
||||
* @returns {void}
|
||||
*/
|
||||
export const renumberSortOrderByTree = (items, location) => {
|
||||
@@ -212,8 +225,14 @@ export const buildPublicPrimaryTree = (flatPrimary) => {
|
||||
const p = row.parentId
|
||||
const pid = p != null && String(p).trim() !== '' ? String(p).trim() : ''
|
||||
if (pid && pid !== id && byId.has(pid)) {
|
||||
byId.get(pid).children.push(node)
|
||||
attachedAsChild.add(id)
|
||||
const parentRow = list.find((r) => String(r.id) === pid)
|
||||
const parentIsRoot =
|
||||
parentRow &&
|
||||
(parentRow.parentId == null || String(parentRow.parentId).trim() === '')
|
||||
if (parentIsRoot) {
|
||||
byId.get(pid).children.push(node)
|
||||
attachedAsChild.add(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user