공개 화면 카드 상호작용 수정

This commit is contained in:
2026-07-01 10:28:46 +09:00
parent e1455e9064
commit 7ec07bb7ea
12 changed files with 70 additions and 21 deletions

View File

@@ -8,7 +8,7 @@ defineProps({
</script>
<template>
<article class="post-card site-section site-panel-hover group">
<article class="post-card site-section site-panel-hover group relative">
<div class="post-card__body site-section-body flex gap-4">
<PostCardMedia
:to="post.to"
@@ -30,20 +30,45 @@ defineProps({
>
{{ post.excerpt }}
</p>
<p class="post-card__meta mt-2 text-xs site-muted">
<p class="post-card__meta mt-2 flex flex-wrap items-center gap-1.5 text-xs site-muted">
{{ post.publishedAt }}<template v-if="post.tag"> / </template>
<NuxtLink
v-if="post.tag && post.tagSlug"
class="post-card__tag-link site-interactive hover:text-[var(--site-accent)]"
class="post-card__tag-link site-interactive rounded-md px-1.5 py-px font-medium text-[var(--site-text)] hover:text-[var(--site-accent)]"
:style="post.tagColor ? { backgroundColor: `${post.tagColor}1a` } : undefined"
:to="`/tag/${post.tagSlug}`"
>
{{ post.tag }}
</NuxtLink>
<template v-else-if="post.tag">
<span
v-else-if="post.tag"
class="post-card__tag-label rounded-md px-1.5 py-px font-medium text-[var(--site-text)]"
:style="post.tagColor ? { backgroundColor: `${post.tagColor}1a` } : undefined"
>
{{ post.tag }}
</template>
</span>
</p>
</div>
<NuxtLink
class="post-card__arrow absolute right-6 bottom-4 flex cursor-pointer items-center gap-1 text-[var(--site-text)] opacity-100 transition-[opacity,visibility] duration-200 hover:opacity-75 md:invisible md:opacity-0 md:group-hover:visible md:group-hover:opacity-100"
:to="post.to"
aria-label="게시물 보기"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-4 w-4"
aria-hidden="true"
>
<path d="M17 7 7 17" />
<path d="M8 7h9v9" />
</svg>
</NuxtLink>
</div>
</article>
</template>

View File

@@ -197,7 +197,7 @@ watch([postTocItems, () => route.fullPath], async () => {
</script>
<template>
<aside class="right-sidebar site-sidebar flex w-full flex-col overflow-hidden border-[var(--site-line)] max-lg:border-l-0 max-lg:border-t max-lg:px-4 max-lg:pb-10 lg:sticky lg:top-[57px] lg:z-10 lg:h-[calc(100vh-57px)] lg:max-h-[calc(100vh-57px)] lg:w-[287px] lg:self-start lg:border-l lg:border-t-0 lg:px-0">
<aside class="right-sidebar site-sidebar flex w-full flex-col overflow-hidden border-[var(--site-line)] max-lg:border-l-0 max-lg:border-t max-lg:px-4 max-lg:pb-10 lg:sticky lg:top-[57px] lg:z-10 lg:h-[calc(100vh-57px)] lg:max-h-[calc(100vh-57px)] lg:w-[287px] lg:self-start lg:border-l-0 lg:border-t-0 lg:px-0">
<div class="right-sidebar__scroll site-sidebar-scroll flex min-h-0 flex-1 flex-col">
<div v-if="!isPostDetailRoute" class="right-sidebar__block site-sidebar-section py-5 pl-5 pr-5 sm:pr-0 max-lg:px-0">
<div class="right-sidebar__profile flex items-center gap-3">

View File

@@ -1,5 +1,11 @@
# 업데이트 요약
## v1.9.26
- 홈 Featured와 Pages 카드 클릭이 드래그 처리에 막히지 않도록 수정했다.
- 게시물 전체 목록 카드에 태그 색상 배지와 오른쪽 아래 화살표 링크를 표시했다.
- 오른쪽 사이드 구분선이 긴 게시물에서도 끝까지 이어지도록 보정했다.
## v1.9.25
- 공개 검색에서 한글 마지막 글자가 조합 중일 때 결과가 직전 검색어에 머무르던 문제를 수정했다.

View File

@@ -1,5 +1,9 @@
# 의사결정 이력
## 2026-07-01 v1.9.26 — 캐러셀 클릭과 드래그는 이동 거리로 구분한다
홈 Featured와 Pages 선반은 링크 카드 위에서 드래그 이동도 지원하므로, 포인터를 누른 즉시 기본 동작을 막으면 일반 클릭까지 링크 이동이 되지 않는다. 따라서 포인터 이동이 임계값을 넘은 경우에만 드래그로 전환하고, 단순 클릭은 브라우저 링크 동작을 그대로 허용한다. 오른쪽 사이드 구분선은 sticky 사이드바가 아니라 레이아웃 컬럼에서 그려 긴 게시물에서도 문서 높이만큼 이어지게 한다.
## 2026-07-01 v1.9.25 — 검색은 IME 조합 중 표시값도 따라간다
한글 IME에서 마지막 글자가 아직 밑줄 상태이면 사용자는 완성된 검색어를 보고 있어도 브라우저는 조합 중으로 유지할 수 있다. 조합 중 검색 요청을 완전히 멈추면 `소문`처럼 화면에는 두 글자가 보여도 결과는 직전 확정값인 `소`에 머문다. 따라서 통합 검색은 조합 중에도 입력창의 현재 표시값을 디바운스로 반영하고, 조합 종료 시 최종 값을 즉시 확정한다.

View File

@@ -6,8 +6,8 @@
| 파일 | 화면 |
|------|------|
| layouts/default.vue | 메인·목록·태그 — 3열 `gap`+중앙 `1fr`, `site-main` `max-w-[720px]`, 모바일 슬라이드 메뉴 |
| layouts/post.vue | 개별 게시물 — `default`와 동일 |
| layouts/default.vue | 메인·목록·태그 — 3열 `gap`+중앙 `1fr`, `site-main` `max-w-[720px]`, 오른쪽 컬럼 전체 높이 구분선, 모바일 슬라이드 메뉴 |
| layouts/post.vue | 개별 게시물 — `default`와 동일, 오른쪽 컬럼 전체 높이 구분선 |
| layouts/admin.vue | 관리자 전체, 320px 밝은 Ghost형 사이드바(대시보드 `/admin` 활성 링크·사이트 보기 새 창·콘텐츠 구간 여백), 우측 전체 높이 캔버스, 멤버 수 표시, 하단 사용자 드롭다운·설정, `내 프로필` 멤버 편집 이동, 게시글 `+` 새 글 진입, 글 작성/수정 화면의 전체 화면 편집 모드와 문서 스크롤 잠금, **`/admin/settings`에서는 사이드바 숨김·본문 전체 화면·설정용 문서 스크롤 잠금** |
| layouts/page.vue | 고정 페이지 전체 화면 |
| app.vue | 공개 사이트 설정 기반 파비콘 head 링크, 페이지 제목이 있으면 제목만 쓰고 없으면 사이트 이름을 쓰는 title template, 브랜드 컬러 `--site-accent` 적용 |
@@ -100,11 +100,11 @@
| components/site/SiteSearchModal.vue | `Teleport`·전체 화면 딤·Tags/Posts 결과·일치 구간 강조, 열림 시 `html.site-search-open` 스크롤 잠금, IME 조합 중 표시값 디바운스 반영·조합 종료 후 최종 검색어 즉시 반영과 공백·Unicode 정규화 |
| components/site/LeftSidebar.vue | 왼쪽 사이드바, `lg+`는 `sticky`+고정 높이+내부 무스크롤바 스크롤, `lg` 미만은 고정 슬라이드 패널, 상단 메뉴는 `SidebarPrimaryNavList`+`provide`로 트리·펼침 상태(`sori-primary-nav-expanded`), 태그 Categories 아래 공개 `GET /api/navigation`의 `recommended` 카드 목록(수동 썸네일 > 서버 캐시 파비콘 > 외부 프록시 fallback), Authors 영역은 비공개, 푸터 `footer` 링크는 `flex-wrap`·테마 버튼 `shrink-0`, 태그 카테고리·테마 점은 `site-sidebar-nav-row` 호버 |
| components/site/SidebarPrimaryNavList.vue | 상단 네비: 부모·리프 동일 `before` 막대/호버 원형, 내부 현재 경로 `--site-accent`, 행 `w-full`+`site-sidebar-nav-row` 호버(`#F7F4EF` 라이트), `inject`·`localStorage` 펼침 |
| components/site/RightSidebar.vue | 오른쪽 사이드바, 일반 화면은 설정 기반 사이트 로고 48px 정사각형 고정·SNS Follow(프리셋·사용자 SVG, 16px 아이콘 중앙 정렬)·오른쪽 사이드 광고를 표시, 게시글 상세 데스크톱은 블로그 소개·Follow 대신 최상단 H1~H3 TOC와 TOC 아래 게시물 사이드 광고(모바일 숨김, 스크롤 위치 기반 활성 표시·내부 자동 스크롤, 세로 기준선), About 영역은 비공개, `lg+` 스티키 |
| components/site/RightSidebar.vue | 오른쪽 사이드바, 일반 화면은 설정 기반 사이트 로고 48px 정사각형 고정·SNS Follow(프리셋·사용자 SVG, 16px 아이콘 중앙 정렬)·오른쪽 사이드 광고를 표시, 게시글 상세 데스크톱은 블로그 소개·Follow 대신 최상단 H1~H3 TOC와 TOC 아래 게시물 사이드 광고(모바일 숨김, 스크롤 위치 기반 활성 표시·내부 자동 스크롤, 세로 기준선), About 영역은 비공개, `lg+` 스티키이며 데스크톱 외곽 구분선은 레이아웃 오른쪽 컬럼이 담당 |
| components/site/SiteAdSlot.vue | 사이트 설정 Ads HTML 코드 렌더링, 메인 피드·메인 인피드·오른쪽 사이드·게시물 오른쪽 사이드·게시물 본문 상단·인아티클·하단 광고 슬롯 |
| components/site/MainColumn.vue | 메인 화면 중앙, `lg:max-w-[720px]`로 본문 상한 |
| components/site/HomeHero.vue | 홈 상단 720px 커버 배너, 라이트·다크 테마별 이미지 교체, 왼쪽 하단 오버레이 제목·본문 |
| components/site/PostCard.vue | 목록의 게시물 카드, 카드 hover 인터랙션, 태그는 있을 때만 메타에 표시하고 태그 페이지 링크 제공 |
| components/site/PostCard.vue | 목록의 게시물 카드, 카드 hover 인터랙션, 태그는 있을 때만 색상 배지 메타에 표시하고 태그 페이지 링크 제공, 오른쪽 아래 게시물 링크 화살표 |
| components/site/PostCardMedia.vue | 게시물 카드 썸네일(`featuredImageThumbnail` 우선, 없으면 대표 이미지 원본 사용. 대표 이미지 없으면 모바일 3줄·`sm` 이상 4줄 제목 placeholder 표시, 이미지 유무와 관계없이 썸네일 영역 크기 고정) |
| components/site/TagHeader.vue | 태그 페이지 헤더 |
| components/comments/PostComments.vue | 게시물 상세 `#comments` 영역, 회원 댓글/답글(1단) 작성 및 목록 표시, 입력값 기반 등록 버튼 활성화, 작성자 썸네일/좋아요/상대시간 표시 |
@@ -197,7 +197,7 @@
| 파일 | 화면 |
|------|------|
| pages/index.vue | 홈, `site_settings` 커버가 있을 때만 라이트·다크 이미지 지원 `HomeHero`, Featured/Latest, 사이트 설정 표시개수 기준 Latest·Featured·Pages 데이터 제한, 홈 표시 페이지를 섹션별 도서 진열장형 Pages 선반으로 노출, Featured와 Pages 선반의 독립 이전·다음 버튼 및 문서 레벨 포인터 추적 기반 드래그 슬라이드·스냅, Pages 선반 ResizeObserver 기반 버튼 상태 갱신, Pages 카드 hover 상단 여백 보정, Latest 피드 Compact 기본값·List·Cards 보기, Compact 모바일 썸네일 80px 정사각형·Compact/List 설명 2줄 제한, Latest 메타는 태그가 있을 때만 링크 배지 표시, 카드 오른쪽 아래 화살표는 게시물 상세 링크, Featured는 추천 글이 있을 때만 표시하며 이미지 없는 추천 글은 제목 placeholder 썸네일과 모바일 터치 가로 스크롤·스냅, 끝에서 화살표 비활성 |
| pages/index.vue | 홈, `site_settings` 커버가 있을 때만 라이트·다크 이미지 지원 `HomeHero`, Featured/Latest, 사이트 설정 표시개수 기준 Latest·Featured·Pages 데이터 제한, 홈 표시 페이지를 섹션별 도서 진열장형 Pages 선반으로 노출, Featured와 Pages 선반의 독립 이전·다음 버튼 및 문서 레벨 포인터 추적 기반 드래그 슬라이드·스냅(이동 임계값 미만 클릭은 링크 이동), Pages 선반 ResizeObserver 기반 버튼 상태 갱신, Pages 카드 hover 상단 여백 보정, Latest 피드 Compact 기본값·List·Cards 보기, Compact 모바일 썸네일 80px 정사각형·Compact/List 설명 2줄 제한, Latest 메타는 태그가 있을 때만 링크 배지 표시, 카드 오른쪽 아래 화살표는 게시물 상세 링크, Featured는 추천 글이 있을 때만 표시하며 이미지 없는 추천 글은 제목 placeholder 썸네일과 모바일 터치 가로 스크롤·스냅, 끝에서 화살표 비활성 |
| pages/posts/index.vue | 게시물 전체 목록, 사이트 설정 Posts 표시개수 기준 서버 페이지네이션, 태그는 있을 때만 카드 메타에 링크로 표시 |
| pages/posts/[slug].vue | `/post/:slug` 리다이렉트 |
| pages/post/[slug].vue | 블로그 글 상세, 첫 태그가 있을 때만 메타 행에 태그 링크, H1~H3 본문 제목을 오른쪽 TOC 상태로 제공, 공개 회원 로그인 세션의 회원이 글쓴이(`author_id`)이면 공유 버튼 옆 새 탭 편집 링크 표시, 게시물 SEO/OG 메타 출력(요약 없으면 본문 fallback), 공유 모달(X/Bluesky/Facebook/LinkedIn/Email/링크복사), 회원 댓글 섹션 |

View File

@@ -61,7 +61,7 @@
- Featured 대표 이미지가 게시물 업로드 이미지이고 카드 썸네일이 생성되어 있으면 원본 대신 `featuredImageThumbnail`을 우선 표시한다.
- 모바일 터치: `touch-pan-x`, `-webkit-overflow-scrolling: touch`, `overscroll-x-contain`으로 가로 스크롤 우선·부모로의 스크롤 전파 완화.
- 헤더의 이전·다음 화살표는 `scrollLeft`와 최대 스크롤 거리로 양 끝에서 `disabled` 처리하며, `scroll` 이벤트와 `ResizeObserver`로 동기화한다. Pages 선반은 각 선반의 스크롤 컨테이너와 카드 크기 변화를 감시해 이미지 로딩·반응형 폭 변화 후에도 버튼 상태를 다시 계산한다.
- 마우스/트랙패드 환경에서는 카드 트랙을 직접 드래그해 넘길 수 있고, 드래그 종료 시 가까운 카드 위치로 스냅한다. 트랙 안의 이미지 기본 드래그와 텍스트 선택은 막아 캐러셀 조작을 우선하며, 작은 드래그도 일정 거리 이상이면 다음·이전 카드 위치로 붙는다. 드래그 중 포인터가 카드 밖으로 이동해도 문서 레벨 포인터 추적으로 조작을 유지한다. 드래그 직후에는 카드 링크 클릭을 억제해 의도치 않은 이동을 막는다.
- 마우스/트랙패드 환경에서는 카드 트랙을 직접 드래그해 넘길 수 있고, 드래그 종료 시 가까운 카드 위치로 스냅한다. 트랙 안의 이미지 기본 드래그와 텍스트 선택은 막아 캐러셀 조작을 우선하며, 클릭과 구분되는 이동 임계값을 넘었을 때만 드래그로 처리한다. 드래그 중 포인터가 카드 밖으로 이동해도 문서 레벨 포인터 추적으로 조작을 유지한다. 드래그 직후에는 카드 링크 클릭을 억제해 의도치 않은 이동을 막는다.
### 홈 Latest 피드
@@ -97,7 +97,7 @@
- 공유 모달은 게시물 썸네일/제목/요약 미리보기, X/Bluesky/Facebook/LinkedIn/Email 링크, 링크 복사 액션을 제공한다.
- 공유·SEO 설명은 SEO 설명이 있으면 우선 사용하고, 없으면 게시물 요약, 요약도 없으면 본문에서 마크다운 기호를 제거한 짧은 텍스트를 사용한다.
- 공개 페이지 `<title>`은 개별 페이지 제목이 있으면 제목만 사용하고, 제목이 없는 기본 화면에서만 사이트 이름을 사용한다. 게시물 상세 제목 뒤에 사이트 이름을 자동으로 붙이지 않는다.
- 홈 Latest·게시물 목록·태그 목록의 카드 설명도 동일하게 요약이 비어 있으면 본문에서 `createPostSummary`로 짧은 텍스트를 만든다. 이때 코드·HTML/iframe 블록, HTML 태그, 인라인 색상 토큰 같은 표시용 문법은 목록 설명에 노출하지 않는다. 목록용 설명은 문자열에 수동 말줄임을 붙이지 않고 `post-summary-clamp` 전용 클래스가 실제 표시 줄 끝에서 말줄임을 처리한다. 목록 카드의 제목·썸네일·오른쪽 아래 화살표는 게시물 상세로 이동하고, 태그 배지는 `/tag/:slug`로 이동한다.
- 홈 Latest·게시물 목록·태그 목록의 카드 설명도 동일하게 요약이 비어 있으면 본문에서 `createPostSummary`로 짧은 텍스트를 만든다. 이때 코드·HTML/iframe 블록, HTML 태그, 인라인 색상 토큰 같은 표시용 문법은 목록 설명에 노출하지 않는다. 목록용 설명은 문자열에 수동 말줄임을 붙이지 않고 `post-summary-clamp` 전용 클래스가 실제 표시 줄 끝에서 말줄임을 처리한다. 목록 카드의 제목·썸네일·오른쪽 아래 화살표는 게시물 상세로 이동하고, 태그 배지는 태그 고유 색상 배경으로 표시하며 `/tag/:slug`로 이동한다.
- 게시물 상세 제목 아래 대표 이미지는 관리자 글쓰기 오른쪽 사이드의 `대표 이미지 표시` 옵션이 켜진 게시물에서만 표시한다. 해당 토글은 대표 이미지 유무와 상관없이 항상 활성 상태이며, 대표 이미지를 나중에 추가할 경우에도 미리 저장한 표시 설정을 유지한다. 대표 이미지는 목록·공유용으로는 계속 사용할 수 있으며, 본문 상단 표시 기본값은 꺼짐이다.
### 게시물 업로드 이미지 썸네일

View File

@@ -1,5 +1,11 @@
# 업데이트 이력
## v1.9.26
- 홈 Featured·Pages 캐러셀 클릭이 드래그 시작 처리에 막혀 링크가 열리지 않던 문제 수정.
- 공개 게시물 전체 목록 카드에 태그 색상 배지와 hover 화살표 링크 표시 추가.
- 오른쪽 사이드바 구분선을 레이아웃 컬럼에서 그려 긴 게시물에서도 끝까지 이어지도록 수정.
## v1.9.25
- 공개 통합 검색이 한글 IME 조합 중에도 입력창의 현재 표시값을 디바운스 검색어로 반영하도록 수정.

View File

@@ -57,7 +57,9 @@ onBeforeUnmount(() => {
>
<slot />
</main>
<RightSidebar class="lg:col-start-3 lg:row-start-1" />
<div class="public-layout__right-column lg:col-start-3 lg:row-start-1 lg:self-stretch lg:border-l lg:border-[var(--site-line)]">
<RightSidebar />
</div>
<LeftSidebar :menu-open="menuOpen" class="lg:col-start-1 lg:row-start-1" />
</div>
</div>

View File

@@ -57,7 +57,9 @@ onBeforeUnmount(() => {
>
<slot />
</main>
<RightSidebar class="lg:col-start-3 lg:row-start-1" />
<div class="post-layout__right-column lg:col-start-3 lg:row-start-1 lg:self-stretch lg:border-l lg:border-[var(--site-line)]">
<RightSidebar />
</div>
<LeftSidebar :menu-open="menuOpen" class="lg:col-start-1 lg:row-start-1" />
</div>
</div>

View File

@@ -1,6 +1,6 @@
{
"name": "sori.studio",
"version": "1.9.25",
"version": "1.9.26",
"private": true,
"type": "module",
"imports": {

View File

@@ -236,6 +236,7 @@ const carouselDragState = reactive({
startX: 0,
startScrollLeft: 0,
lastDeltaX: 0,
dragThreshold: 4,
moved: false,
suppressClick: false
})
@@ -605,13 +606,13 @@ const startCarouselDrag = (event, key) => {
carouselDragState.startX = event.clientX
carouselDragState.startScrollLeft = track.scrollLeft
carouselDragState.lastDeltaX = 0
carouselDragState.dragThreshold = event.pointerType === 'mouse' ? 4 : 8
carouselDragState.moved = false
carouselDragTrack = track
bindCarouselDragDocumentEvents()
if (track.setPointerCapture) {
track.setPointerCapture(event.pointerId)
}
event.preventDefault()
}
/**
@@ -631,7 +632,7 @@ const moveCarouselDrag = (event) => {
const deltaX = event.clientX - carouselDragState.startX
carouselDragState.lastDeltaX = deltaX
if (Math.abs(deltaX) > 1) {
if (Math.abs(deltaX) >= carouselDragState.dragThreshold) {
carouselDragState.moved = true
}

View File

@@ -49,13 +49,14 @@ const { data: postsResponse } = await useFetch('/api/posts', {
/**
* 태그 슬러그로 공개 태그 정보를 찾는다.
* @param {string | undefined} slug - 태그 슬러그
* @returns {{ name: string, slug: string }} 태그 정보
* @returns {{ name: string, slug: string, color: string }} 태그 정보
*/
const getTagMeta = (slug) => {
if (!slug) {
return {
name: '',
slug: ''
slug: '',
color: '#4d4d4d'
}
}
@@ -63,7 +64,8 @@ const getTagMeta = (slug) => {
return {
name: matchedTag?.name || String(slug).toUpperCase(),
slug
slug,
color: matchedTag?.color || '#4d4d4d'
}
}
@@ -87,6 +89,7 @@ const postCards = computed(() => (postsResponse.value?.items || []).map((post) =
featuredImageThumbnail: post.featuredImageThumbnail || '',
tag: primaryTag.name,
tagSlug: primaryTag.slug,
tagColor: primaryTag.color,
publishedAt: formatPostDate(post.publishedAt),
to: `/post/${post.slug}`
}