From 6d74425415ae0231bb5dfe4f967016f03d661df8 Mon Sep 17 00:00:00 2001 From: zenn Date: Fri, 26 Jun 2026 17:42:16 +0900 Subject: [PATCH] =?UTF-8?q?=EC=9D=BC=EB=B0=98=20=ED=83=9C=EA=B7=B8=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EB=A9=94=ED=83=80=20=ED=91=9C=EC=8B=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/changelog.md | 5 +++++ docs/deploy.md | 8 ++++++- docs/history.md | 4 ++++ docs/map.md | 2 +- docs/spec.md | 4 ++-- docs/update.md | 6 ++++++ package-lock.json | 4 ++-- package.json | 2 +- pages/index.vue | 2 +- pages/post/[slug].vue | 2 +- pages/posts/index.vue | 19 ++++++++++++++++- pages/tag/[slug].vue | 46 ++++++++++++++++++++++++++--------------- server/api/tags.get.js | 13 +++++++++++- server/utils/sitemap.js | 2 +- 14 files changed, 90 insertions(+), 29 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 7ec1b7f..a60c210 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # 업데이트 요약 +## v1.8.6 + +- 일반 태그 페이지에서도 슬러그 대신 등록된 태그 이름과 설명이 보이도록 수정했다. +- 게시물 카드·상세의 태그 배지도 일반 태그의 이름과 색상을 사용할 수 있게 했다. + ## v1.8.5 - 요약글이 비어 있어 본문으로 카드 설명을 만들 때 `::red` 같은 색상 문법과 HTML 태그가 노출되지 않도록 정리했다. diff --git a/docs/deploy.md b/docs/deploy.md index b8ebd49..61904f7 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -1,6 +1,6 @@ # 배포 가이드 -> 로컬 기준 v1.8.5에서 `npm run verify` 검증을 통과했다. NAS 실제 컨테이너 기동과 도메인/프록시 접속 검증은 운영 배포 단계에서 진행한다. +> 로컬 기준 v1.8.6에서 `npm run verify` 검증을 통과했다. NAS 실제 컨테이너 기동과 도메인/프록시 접속 검증은 운영 배포 단계에서 진행한다. ## 빌드 유형 @@ -16,6 +16,12 @@ ## 로컬 개발 +### v1.8.6 참고 + +- 추가 DB 마이그레이션은 없다. +- `/tag/xenoblade3-hero`처럼 일반 태그 상세 URL에 접속했을 때 헤더에 등록된 태그명과 설명이 표시되는지 확인한다. +- 홈·게시물 목록·게시물 상세 태그 배지에서 일반 태그도 등록된 이름과 색상을 사용하는지 확인한다. + ### v1.8.5 참고 - 추가 DB 마이그레이션은 없다. diff --git a/docs/history.md b/docs/history.md index 69b3172..426dc56 100644 --- a/docs/history.md +++ b/docs/history.md @@ -1,5 +1,9 @@ # 의사결정 이력 +## 2026-06-26 v1.8.6 — 공개 태그 메타 조회 범위를 분리한다 + +왼쪽 사이드바와 `/tags` 목록은 기존처럼 메인 태그만 노출해야 하지만, `/tag/:slug` 상세와 게시물 배지는 일반 태그의 이름·설명·색상도 필요하다. 따라서 공개 태그 API의 기본값은 메인 태그로 유지하고, `tagType=all` 옵션을 추가해 필요한 화면에서만 전체 태그 메타를 조회하도록 정리했다. + ## 2026-06-26 v1.8.1 — 게시물 HTML은 범용 블록으로 분리한다 게시물 수가 아직 적어 기존 `:::embed` 값을 자동 변환·호환하는 코드보다 운영자가 직접 새 형식으로 고치는 편이 단순하다. 작성 기능은 `/html`, `/link`, `/iframe`으로 분리하고 저장 형식도 각각 `:::html`, `:::link`, `:::iframe`로 명확히 둔다. `/html`은 표처럼 마크다운으로 표현하기 어려운 일반 HTML을 표시하되, 게시물 본문에서 실행성 코드가 퍼지지 않도록 script, iframe, 이벤트 속성 등은 제거한다. iframe은 별도 `/iframe` 블록에서 안전 속성 기반 sandbox 렌더링을 사용한다. diff --git a/docs/map.md b/docs/map.md index 0e28f77..977bd2f 100644 --- a/docs/map.md +++ b/docs/map.md @@ -210,7 +210,7 @@ | server/api/posts/[slug].get.js | 공개 게시물 상세 API(`published`, VIP 이상 등급 시 `members`) | | server/api/pages.get.js | 공개 고정 페이지 목록 API(`published`만) | | server/api/pages/[slug].get.js | 공개 고정 페이지 상세 API(`published`만) | -| server/api/tags.get.js | 태그 목록 샘플 API | +| server/api/tags.get.js | 공개 태그 목록 API, 기본 메인 태그 반환·`tagType=all` 전체 태그 메타 반환 | | server/api/search.get.js | 통합 검색 API(`q` 쿼리) | | server/api/site-settings.get.js | 공개 사이트 설정 API | | server/api/homepage-widget.get.js | gethomepage customapi용 당일·7일·30일·180일·365일·누적·콘텐츠 통계 위젯 API | diff --git a/docs/spec.md b/docs/spec.md index 219064f..7f073b9 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -533,7 +533,7 @@ components/content/ - `POST /api/posts/:slug/comments/:commentId/like` - 댓글 좋아요 토글(회원 세션 필요) - `GET /api/pages` - 고정 페이지 목록 - `GET /api/pages/:slug` - 고정 페이지 상세 -- `GET /api/tags` - 태그 목록 +- `GET /api/tags` - 태그 목록. 기본값은 메인 태그(`managed`)만 반환하며, `?tagType=all`은 일반 태그를 포함한 전체 태그 메타를 반환한다. - `GET /api/search?q=` - 통합 검색(태그 `name`·`slug`, 게시물 `title`·`excerpt`·`content` 부분 일치, 각 최대 12건, 발행 게시물만) - `GET /api/site-settings` - 공개 사이트 설정(어나운스 바·홈 커버 등 포함) - `GET /api/homepage-widget` - gethomepage customapi용 공개 블로그 통계. 기본 필드 `title`, `updatedAt`, `todayVisitors`, `totalVisitors`, `todayPageViews`, `totalPageViews`, `visitorsLast7Days`, `visitorsLast30Days`, `visitorsLast180Days`, `visitorsLast365Days`, `pageViewsLast30Days`, `onlineNow`, `loggedInNow`, `avgEngagedSeconds`, `totalPosts`, `totalComments`, `totalPages`, `totalTags`, `totalMembers`, `items[]`를 반환한다. 기간별 방문자는 최근 7·30·180·365일의 일별 순방문자 합계다. `totalVisitors`는 날짜별 순방문자 수의 누적 합계라 동일 방문자가 다른 날짜에 다시 포함될 수 있다. 게시글·댓글·페이지 수는 현재 공개 가능한 항목만 집계한다. @@ -632,7 +632,7 @@ components/content/ - 관리자 글 목록 기본 정렬(최신순·오래된순)은 **발행일** 기준이며, `published_at`이 없는 초안 등은 **수정일**(`updated_at`)로 대체한다. API(`listAdminPosts`)와 화면 필터 정렬 모두 동일 규칙을 쓴다. - 태그 삭제 시 `post_tags` 연결도 데이터베이스 외래 키 규칙에 따라 함께 삭제된다. - 게시물 생성·수정 API는 사이트 설정의 `postTagLimit`보다 많은 태그가 전달되면 400 오류로 저장을 거부한다. -- 공개 `GET /api/tags`는 `managed`(메인 태그)만 반환한다. +- 공개 `GET /api/tags`는 기본적으로 `managed`(메인 태그)만 반환한다. 태그 상세·게시물 배지처럼 일반 태그 메타가 필요한 화면은 `?tagType=all`을 사용한다. - 관리자 태그 목록 응답은 각 태그의 `postCount`, `lastUsedAt`, `updatedAt`을 포함한다. - 관리자 태그 목록은 `managed` 우선, `sort_order ASC, 최근 사용/수정 DESC, name ASC` 기준으로 정렬한다. - 메인 태그 순서 저장은 드래그 드롭 직후 자동으로 실행되며, 드래그 순서를 받아 `sort_order`를 순차 값으로 다시 저장한다. diff --git a/docs/update.md b/docs/update.md index c0a32aa..e364b9a 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,11 @@ # 업데이트 이력 +## v1.8.6 + +- 공개 태그 API에 전체 태그 메타 조회 옵션을 추가. +- 태그 상세·게시물 목록·게시물 상세에서 일반 태그도 이름·설명·색상을 표시하도록 수정. +- sitemap 태그 URL 생성 기준을 게시물이 연결된 전체 태그로 정리. + ## v1.8.5 - 게시물 요약 자동 생성 시 인라인 색상 토큰과 HTML 태그가 목록 설명에 노출되지 않도록 수정. diff --git a/package-lock.json b/package-lock.json index cb26126..2d611c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sori.studio", - "version": "1.8.5", + "version": "1.8.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sori.studio", - "version": "1.8.5", + "version": "1.8.6", "hasInstallScript": true, "dependencies": { "@nuxtjs/tailwindcss": "^6.14.0", diff --git a/package.json b/package.json index 620da2d..c6fae9a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sori.studio", - "version": "1.8.5", + "version": "1.8.6", "private": true, "type": "module", "imports": { diff --git a/pages/index.vue b/pages/index.vue index e2cf6d6..45fc887 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -3,7 +3,7 @@ const { data: posts } = await useFetch('/api/posts', { default: () => [] }) -const { data: tags } = await useFetch('/api/tags', { +const { data: tags } = await useFetch('/api/tags?tagType=all', { default: () => [] }) diff --git a/pages/post/[slug].vue b/pages/post/[slug].vue index 36a0589..7ecc5e9 100644 --- a/pages/post/[slug].vue +++ b/pages/post/[slug].vue @@ -9,7 +9,7 @@ const route = useRoute() const slug = computed(() => String(route.params.slug || '')) const { data: post } = await useFetch(() => `/api/posts/${slug.value}`) -const { data: tags } = await useFetch('/api/tags', { +const { data: tags } = await useFetch('/api/tags?tagType=all', { default: () => [] }) const { data: posts } = await useFetch('/api/posts', { diff --git a/pages/posts/index.vue b/pages/posts/index.vue index fd7734b..e64d16b 100644 --- a/pages/posts/index.vue +++ b/pages/posts/index.vue @@ -3,6 +3,23 @@ const { data: posts } = await useFetch('/api/posts', { default: () => [] }) +const { data: tags } = await useFetch('/api/tags?tagType=all', { + default: () => [] +}) + +/** + * 태그 슬러그로 공개 태그 이름을 찾는다. + * @param {string | undefined} slug - 태그 슬러그 + * @returns {string} 태그 이름 + */ +const getTagName = (slug) => { + if (!slug) { + return '' + } + + return tags.value.find((tag) => tag.slug === slug)?.name || String(slug).toUpperCase() +} + const postCards = computed(() => posts.value.map((post) => ({ title: post.title, excerpt: createPostSummary(post.excerpt, post.content, { @@ -11,7 +28,7 @@ const postCards = computed(() => posts.value.map((post) => ({ }), featuredImage: post.featuredImage, featuredImageThumbnail: post.featuredImageThumbnail || '', - tag: post.tags?.[0] ? String(post.tags[0]).toUpperCase() : '', + tag: getTagName(post.tags?.[0]), publishedAt: formatPostDate(post.publishedAt), to: `/post/${post.slug}` }))) diff --git a/pages/tag/[slug].vue b/pages/tag/[slug].vue index d0ff511..b657d32 100644 --- a/pages/tag/[slug].vue +++ b/pages/tag/[slug].vue @@ -2,7 +2,7 @@ const route = useRoute() const slug = computed(() => String(route.params.slug || '')) -const { data: tags } = await useFetch('/api/tags', { +const { data: tags } = await useFetch('/api/tags?tagType=all', { default: () => [] }) @@ -12,24 +12,36 @@ const { data: posts } = await useFetch('/api/posts', { const tag = computed(() => tags.value.find((item) => item.slug === slug.value)) +/** + * 태그 슬러그로 공개 태그 메타를 찾는다. + * @param {string} tagSlug - 태그 슬러그 + * @returns {Object | null} 태그 메타 + */ +const findTagMeta = (tagSlug) => tags.value.find((item) => item.slug === tagSlug) || null + const tagPosts = computed(() => posts.value .filter((post) => post.tags.includes(slug.value)) - .map((post) => ({ - title: post.title, - excerpt: createPostSummary(post.excerpt, post.content, { - maxLength: 320, - appendEllipsis: false - }), - featuredImage: post.featuredImage, - featuredImageThumbnail: post.featuredImageThumbnail || '', - tag: tags.value.find((item) => item.slug === (post.tags?.[0] || slug.value))?.name || (post.tags?.[0] || slug.value).toUpperCase(), - tagColor: tags.value.find((item) => item.slug === (post.tags?.[0] || slug.value))?.color || '#4d4d4d', - isFeatured: Boolean(post.isFeatured), - commentCount: Number(post.commentCount || 0), - publishedAt: formatPostDate(post.publishedAt), - publishedAtIso: post.publishedAt || '', - to: `/post/${post.slug}` - }))) + .map((post) => { + const primaryTagSlug = post.tags?.[0] || slug.value + const primaryTag = findTagMeta(primaryTagSlug) + + return { + title: post.title, + excerpt: createPostSummary(post.excerpt, post.content, { + maxLength: 320, + appendEllipsis: false + }), + featuredImage: post.featuredImage, + featuredImageThumbnail: post.featuredImageThumbnail || '', + tag: primaryTag?.name || primaryTagSlug.toUpperCase(), + tagColor: primaryTag?.color || '#4d4d4d', + isFeatured: Boolean(post.isFeatured), + commentCount: Number(post.commentCount || 0), + publishedAt: formatPostDate(post.publishedAt), + publishedAtIso: post.publishedAt || '', + to: `/post/${post.slug}` + } + }))