# sori.studio 기술 명세 ## 프로젝트 개요 - **프로젝트명**: sori.studio - **유형**: 커스텀 블로그/CMS - **목표**: 개인 블로그 중심 운영, 기존 포털성 링크와 서비스 진입점은 블로그 내부 구조에 통합 - **참조**: Ghost(관리자 UX/글쓰기), Thred 테마(사용자 화면) - **현재 상태**: Nuxt 3.21(SSR)·PostgreSQL 저장소 계층 구성 완료. Node가 SSR 번들의 `#internal/nuxt/paths`를 해석하도록 루트 `package.json` `imports`와 `modules/nuxt-ssr-paths-write.mjs`(`.nuxt/paths.mjs` 디스크 기록)을 둔다. - **원격 저장소**: https://git.sori.studio/zenn/sori.studio.git - **스타일**: Tailwind 엔트리는 `assets/css/main.css` 한 곳(`nuxt.config`의 `tailwindcss.cssPath`)이며, `tailwind.config.js`의 `content`가 Vue·composables·modules·plugins를 스캔한다. --- ## 화면 구조 ### 메인 화면 (3단 레이아웃) | 요소 | 크기/속성 | |------|-----------| | Header | 높이 57px, `sticky top-0`, `shrink-0`. 내부는 `grid-cols-3`로 **좌(브랜드·메뉴) / 중앙(검색, `md+`에서만 표시) / 우(사용자 메뉴)** 배치해 검색 패널을 가운데 열에 정렬한다. 검색 버튼은 중앙 열 안에서 `max-w-[min(470px,100%)]`로 폭을 제한한다. | | Shell | `min-height: 100vh`, `flex` 세로 컬럼 | | 그리드(데스크톱 `lg+`) | `items-start`, 3열 그리드(`287px / minmax(0,1fr) / 287px`)를 사용하고 열 간 `column-gap`은 두지 않는다(`gap-x-0`). 경계선은 사이드바 보더로만 구분해 이중 패딩처럼 보이는 여백을 방지한다. 긴 본문은 **문서(`html`/`body`) 스크롤**로 처리한다. | | 그리드(모바일 `lg` 미만) | 단일 세로 흐름: **본문 → 오른쪽 사이드** 순. 왼쪽 사이드는 레이아웃 흐름에서 분리된 고정 슬라이드 패널로 표시 | | Left Aside | 너비 287px, `sticky top-[57px]`, `h-[calc(100vh-57px)]`와 `max-h` 동일(뷰포트 기준 고정 높이), 내부 상단은 `.site-sidebar-scroll`(스크롤바 숨김), 하단 푸터 `shrink-0`·상단 보더로 스크롤 영역과 구분, 푸터 좌우는 `px-4`~`sm:px-5`로 본문 블록과 유사한 여백. 푸터의 API `footer` 링크 영역은 `flex-wrap`·`min-w-0 flex-1`로 항목이 많을 때 줄바꿈되어 패널 밖으로 넘치지 않는다 | | Left Aside(모바일) | `fixed` 좌측 패널, 열림 시 `translate-x-0`, 닫힘 시 화면 밖으로 이동. 열린 동안 백드롭과 `html.site-mobile-nav-open`으로 문서 스크롤 잠금 | | Main | 중앙 열 안에서 `max-width: 720px`·`justify-self: start`, 별도 `overflow-y` 없음. 공개 페이지의 가로 패딩은 레이아웃 그리드(`public-layout__grid`)의 `px-*` 한 번만 사용하고, 본문 섹션의 `px-*`는 두지 않는다. | | Right Aside | Left와 동일 패턴(스티키·고정 높이·내부 무스크롤바 스크롤·하단 카피라이트) | | Right Aside(모바일) | 본문 아래에 전체 너비로 배치, 상단 보더로 본문과 구분, 좌우 안전 여백(`px-4`) 적용 | ### 메뉴 토글 - 헤더 좌측 아이콘은 브랜드 마크가 아니라 왼쪽 사이드바 열기/닫기 버튼 - 메뉴 상태는 Nuxt/Vue 상태로 관리 - 브라우저에서는 `localStorage.MENU_STATE`에 `open` 또는 `closed` 저장 - 닫힘 상태에서는 왼쪽 사이드바 폭을 0으로 줄이는 전환 애니메이션을 적용 - `lg` 미만에서는 왼쪽 사이드바를 화면 좌측 고정 슬라이드 패널로 표시하고, 열린 동안 백드롭을 탭하면 `closeMenu`로 닫는다. - `Escape` 키는 통합 검색 모달이 열려 있으면 최우선으로 닫고, 그다음 사용자 드롭다운, 이어서 모바일에서만 좌측 슬라이드 메뉴를 닫는다. - `/` 키는 `INPUT`·`TEXTAREA`·`SELECT`·`contenteditable`에 포커스가 없고 `Ctrl`/`Meta`/`Alt`와 함께 눌리지 않을 때 통합 검색 모달을 연다. 헤더 검색 영역(`md+`) 클릭으로도 동일하게 연다. - 헤더 우측 사용자 아이콘 버튼은 로그인 상태면 회원 아바타/닉네임과 설정·로그아웃 메뉴를, 비로그인 상태면 Anonymous·Sign up·Sign in 메뉴를 표시한다. ### 공개 화면 색상 - 라이트/다크 모드는 CSS 변수로 관리 - 기본 배경, 패널, 라인, 텍스트, 보조 텍스트, 입력, 강조 버튼 색상을 분리 - 라이트 모드 기본 배경은 `#fcfcfc`로 통일하고 패널 구분은 보더로 처리 - 시스템 다크 모드는 `prefers-color-scheme: dark` 기준으로 우선 대응 - 사용자 수동 테마 전환은 `html[data-theme]`와 `localStorage.SITE_THEME`로 관리한다. 첫 페인트 전 `lib/site-theme-init.js` 인라인 스크립트가 테마를 적용해 시스템 다크·저장 라이트 불일치 시 깜빡임을 줄인다. 공개 페이지 로딩 중에는 `#site-splash`에 캐시된 로고 이미지 URL(`SITE_BRAND_LOGO_URL`, localStorage) 또는 사이트 제목(`NUXT_PUBLIC_SITE_TITLE`)을 잠깐 표시하고, 앱 마운트 후 `site-app-ready`로 숨긴다. `site_settings.logo_text`(기본 `井`)는 **이미지 로고가 없을 때** 헤더·사이드바에 쓰는 짧은 기호이며 localStorage·스플래시와는 별개다. - Thred 참고 화면처럼 사이드바와 본문은 같은 화면 안에서 구분되는 패널과 라인으로 표현 ### 홈 Featured (인덱스) - 가로 카드 트랙은 `overflow-x-auto`와 `snap-x`/`snap-mandatory`로 슬라이드 느낌을 낸다. - Featured 영역은 추천 글(`isFeatured=true`)이 1개 이상 있을 때만 표시한다. - 모바일 터치: `touch-pan-x`, `-webkit-overflow-scrolling: touch`, `overscroll-x-contain`으로 가로 스크롤 우선·부모로의 스크롤 전파 완화. - 헤더의 이전·다음 화살표는 `scrollLeft`와 최대 스크롤 거리로 양 끝에서 `disabled` 처리하며, `scroll` 이벤트와 `ResizeObserver`로 동기화한다. ### Post 페이지 - 게시물 화면은 레이아웃 그리드의 좌우 패딩을 기본으로 사용하고, 섹션 단위 `px-*`는 내부 래퍼로만 두어 패딩이 2중 적용되지 않게 한다. - 댓글 시작 섹션의 구분선(`border-y`)은 패딩 없이 전체 폭으로 표시하고, 내용만 내부 래퍼에 패딩을 둔다. - 본문 끝과 댓글 섹션 시작 사이 간격은 `48px`(`mt-12`)로 유지한다. - 댓글은 로그인 회원만 작성 가능하며, 대댓글은 1단까지만 허용한다. - 댓글은 작성자 썸네일(없으면 이니셜)과 좋아요 수를 표시한다. - 댓글 시간은 24시간 이내일 때 상대 시간(분/시간 전), 이후에는 날짜로 표시한다. - 댓글 정렬은 `Best`(좋아요 우선), `Latest`, `Oldest`를 제공한다. - 댓글 아바타 이미지 로드 실패 시 이니셜 아바타로 즉시 대체한다. - 공개 게시물 본문은 콘텐츠 타입별 컴포넌트로 분리해 추후 스타일 변경이 쉽도록 구성 - 제목 우측 공유 버튼을 누르면 게시물 공유 모달을 연다. - 공유 모달은 게시물 썸네일/제목/요약 미리보기, X/Bluesky/Facebook/LinkedIn/Email 링크, 링크 복사 액션을 제공한다. ### 공개 목록·상세의 발행일 표시 - API의 ISO 8601 `publishedAt`를 공개 UI에서는 로컬 날짜 기준 `YYYY.MM.DD`로 표시한다. - 변환은 `composables/formatPostDate.js`의 `formatPostDate`를 사용한다. - 관리자 목록·수정일 보조 라벨은 `formatPostDateTime`(`YYYY.MM.DD 오전/오후 HH:MM`)을 사용한다. 발행 후 수정 여부는 `wasPostUpdatedAfterPublish`로 판별하며, `site_settings.show_post_updated_at`이 true일 때만 「수정: …」를 노출한다. - `