From bcff96aa4c7151d6ff0ec1b71465278cef059a91 Mon Sep 17 00:00:00 2001 From: zenn Date: Tue, 12 May 2026 11:16:43 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A9=94=EB=89=B4=20=EA=B4=80=EB=A6=AC:=20?= =?UTF-8?q?=ED=83=AD=20=EB=B6=84=EB=A6=AC=C2=B7=EB=93=9C=EB=9E=98=EA=B7=B8?= =?UTF-8?q?=20=EC=A0=95=EB=A0=AC=C2=B7=EC=83=81=EB=8B=A8=20=ED=8A=B8?= =?UTF-8?q?=EB=A6=AC=C2=B7=EA=B3=B5=EA=B0=9C=20=EC=A0=91=EA=B8=B0=20(v0.0.?= =?UTF-8?q?94)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- components/admin/AdminNavPrimaryBranch.vue | 160 +++++++ components/site/LeftSidebar.vue | 129 +++++- components/site/SidebarPrimaryNavList.vue | 91 ++++ db/migrations/017_navigation_hierarchy.sql | 9 + docs/convention.md | 1 + docs/deploy.md | 1 + docs/history.md | 6 + docs/map.md | 18 +- docs/spec.md | 14 +- docs/update.md | 6 + lib/navigation-editor-tree.js | 44 ++ package.json | 2 +- pages/admin/navigation/index.vue | 504 ++++++++++++++++----- server/api/navigation.get.js | 2 +- server/repositories/content-repository.js | 36 +- server/routes/admin/api/navigation.put.js | 25 +- server/utils/admin-navigation-input.js | 16 +- server/utils/navigation-items.js | 33 +- server/utils/navigation-tree.js | 230 ++++++++++ 19 files changed, 1146 insertions(+), 181 deletions(-) create mode 100644 components/admin/AdminNavPrimaryBranch.vue create mode 100644 components/site/SidebarPrimaryNavList.vue create mode 100644 db/migrations/017_navigation_hierarchy.sql create mode 100644 lib/navigation-editor-tree.js create mode 100644 server/utils/navigation-tree.js diff --git a/components/admin/AdminNavPrimaryBranch.vue b/components/admin/AdminNavPrimaryBranch.vue new file mode 100644 index 0000000..5ccb101 --- /dev/null +++ b/components/admin/AdminNavPrimaryBranch.vue @@ -0,0 +1,160 @@ + + + diff --git a/components/site/LeftSidebar.vue b/components/site/LeftSidebar.vue index 52f8a4d..e8430da 100644 --- a/components/site/LeftSidebar.vue +++ b/components/site/LeftSidebar.vue @@ -18,6 +18,120 @@ const { data: navigation } = await useFetch('/api/navigation', { footer: [] }) }) + +const STORAGE_KEY = 'sori-primary-nav-expanded' + +/** + * 트리에서 하위가 있는 노드 id를 모은다. + * @param {Array} list - 노드 목록 + * @returns {string[]} id 목록 + */ +const collectBranchIds = (list) => { + const out = [] + for (const node of list || []) { + if (node.children?.length) { + out.push(String(node.id)) + out.push(...collectBranchIds(node.children)) + } + } + return out +} + +/** + * localStorage에서 펼침 상태를 읽는다. + * @returns {string[]|null} id 배열 + */ +const readStoredExpanded = () => { + if (typeof window === 'undefined') { + return null + } + try { + const raw = window.localStorage.getItem(STORAGE_KEY) + if (!raw) { + return null + } + const parsed = JSON.parse(raw) + return Array.isArray(parsed) ? parsed.map(String) : null + } catch { + return null + } +} + +/** + * 펼침 상태를 localStorage에 저장한다. + * @param {Set} set - id 집합 + * @returns {void} + */ +const persistExpanded = (set) => { + if (typeof window === 'undefined') { + return + } + window.localStorage.setItem(STORAGE_KEY, JSON.stringify([...set])) +} + +const primaryNavExpandedSet = ref(new Set()) + +/** + * 트리 구조에 맞게 펼침 집합을 맞춘다. + * @param {Array} nodes - primary 트리 + * @param {boolean} useStorage - 최초·복원 시 저장값 반영 + * @returns {void} + */ +const syncPrimaryNavExpanded = (nodes, useStorage = false) => { + const allBranch = new Set(collectBranchIds(nodes)) + if (useStorage) { + const stored = readStoredExpanded() + if (stored && stored.length) { + const next = new Set() + for (const id of stored) { + if (allBranch.has(id)) { + next.add(id) + } + } + primaryNavExpandedSet.value = next.size ? next : allBranch + return + } + } + const next = new Set() + for (const id of primaryNavExpandedSet.value) { + if (allBranch.has(id)) { + next.add(id) + } + } + primaryNavExpandedSet.value = next.size ? next : allBranch +} + +/** + * 상단 네비 폴더 펼침 토글 + * @param {string} id - 노드 id + * @returns {void} + */ +const togglePrimaryNavBranch = (id) => { + const key = String(id) + const next = new Set(primaryNavExpandedSet.value) + if (next.has(key)) { + next.delete(key) + } else { + next.add(key) + } + primaryNavExpandedSet.value = next + persistExpanded(next) +} + +provide('sidebarPrimaryNavExpandedSet', primaryNavExpandedSet) +provide('sidebarPrimaryNavToggle', togglePrimaryNavBranch) + +watch( + () => navigation.value?.primary, + (nodes) => { + syncPrimaryNavExpanded(nodes || [], false) + }, + { deep: true } +) + +onMounted(() => { + syncPrimaryNavExpanded(navigation.value?.primary || [], true) +})