sori.studio 기술 명세
프로젝트 개요
- 프로젝트명: sori.studio
- 유형: 커스텀 블로그/CMS
- 목표: 개인 블로그 중심 운영, 기존 포털성 링크와 서비스 진입점은 블로그 내부 구조에 통합
- 참조: Ghost(관리자 UX/글쓰기), Thred 테마(사용자 화면)
- 현재 상태: Nuxt 3 초기 스캐폴딩과 PostgreSQL 저장소 계층 구성 완료
- 원격 저장소: https://git.sori.studio/zenn/sori.studio.git
화면 구조
메인 화면 (3단 레이아웃)
| 요소 |
크기/속성 |
| Header |
높이 57px, sticky top-0, shrink-0. lgxl 구간은 내부 px-5px-6로 좌우 여백을 두고, 검색창은 뷰포트에 맞춰 max-w로 단계 축소한다(2xl에서 고정 470px). |
| Shell |
min-height: 100vh, flex 세로 컬럼 |
그리드(데스크톱 lg+) |
items-start, 본문(중앙) 높이에 맞춰 행이 늘어남 — 문서(html/body) 스크롤로 긴 본문 처리(스크롤바는 브라우저 오른쪽) |
그리드(모바일 lg 미만) |
단일 세로 흐름: 본문 → 오른쪽 사이드 순. 왼쪽 사이드는 레이아웃 흐름에서 분리된 고정 슬라이드 패널로 표시 |
| Left Aside |
너비 287px, sticky top-[57px], h-[calc(100vh-57px)]와 max-h 동일(뷰포트 기준 고정 높이), 내부 상단은 .site-sidebar-scroll(스크롤바 숨김), 하단 푸터 shrink-0·상단 보더로 스크롤 영역과 구분 |
| Left Aside(모바일) |
fixed 좌측 패널, 열림 시 translate-x-0, 닫힘 시 화면 밖으로 이동. 열린 동안 백드롭과 html.site-mobile-nav-open으로 문서 스크롤 잠금 |
| Main |
너비 720px, 별도 overflow-y 없음 — 뷰포트와 동일한 문서 스크롤에 포함 |
| Right Aside |
Left와 동일 패턴(스티키·고정 높이·내부 무스크롤바 스크롤·하단 카피라이트) |
| Right Aside(모바일) |
본문 아래에 전체 너비로 배치, 상단 보더로 본문과 구분, 좌우 안전 여백(px-4) 적용 |
메뉴 토글
- 헤더 좌측 아이콘은 브랜드 마크가 아니라 왼쪽 사이드바 열기/닫기 버튼
- 메뉴 상태는 Nuxt/Vue 상태로 관리
- 브라우저에서는
localStorage.MENU_STATE에 open 또는 closed 저장
- 닫힘 상태에서는 왼쪽 사이드바 폭을 0으로 줄이는 전환 애니메이션을 적용
lg 미만에서는 왼쪽 사이드바를 화면 좌측 고정 슬라이드 패널로 표시하고, 열린 동안 백드롭을 탭하면 closeMenu로 닫는다.
Escape 키는 사용자 드롭다운이 열려 있으면 우선 닫고, 그렇지 않으면 모바일에서만 좌측 슬라이드 메뉴를 닫는다.
- 헤더 우측 사용자 아이콘 버튼은 비로그인 기준 사용자 메뉴(Anonymous, Sign up, Sign in)만 표시한다.
공개 화면 색상
- 라이트/다크 모드는 CSS 변수로 관리
- 기본 배경, 패널, 라인, 텍스트, 보조 텍스트, 입력, 강조 버튼 색상을 분리
- 라이트 모드 기본 배경은
#fcfcfc로 통일하고 패널 구분은 보더로 처리
- 시스템 다크 모드는
prefers-color-scheme: dark 기준으로 우선 대응
- 사용자 수동 테마 전환은
html[data-theme]와 localStorage.SITE_THEME로 관리
- Thred 참고 화면처럼 사이드바와 본문은 같은 화면 안에서 구분되는 패널과 라인으로 표현
Post 페이지
- Main 좌우 패딩: 24px → 20px
- 공개 게시물 본문은 콘텐츠 타입별 컴포넌트로 분리해 추후 스타일 변경이 쉽도록 구성
- 제목 우측 공유 버튼을 누르면 게시물 공유 모달을 연다.
- 공유 모달은 게시물 썸네일/제목/요약 미리보기, X/Bluesky/Facebook/LinkedIn/Email 링크, 링크 복사 액션을 제공한다.
공개 목록·상세의 발행일 표시
- API의 ISO 8601
publishedAt를 공개 UI에서는 로컬 날짜 기준 YYYY.MM.DD로 표시한다.
- 변환은
composables/formatPostDate.js의 formatPostDate를 사용한다.
<time>에는 표시용 문자열과 함께 가능한 경우 원본 시각을 datetime 속성으로 둔다.
Page 페이지
- About, Projects, Links, Contact, 서비스 소개 페이지 등 고정 콘텐츠에 사용
- 기본 게시물 목록에는 노출하지 않음
- 헤더와 사이드바를 사용하지 않고 본문 중심 전체 화면으로 표시
- 진입 경로는 추후 메뉴/링크 설정을 통해 연결
공개 URL 구조
/posts - 게시물 전체 목록
/post/:slug - 개별 게시물 상세
/tags - 태그 전체 목록
/tag/:slug - 태그별 게시물 목록
/signup - 회원가입(3단계: 환영/입력/이메일 확인)
/signin - 로그인
- 기존
/posts/:slug, /tags/:slug 상세 경로는 새 단수형 상세 경로로 리다이렉트한다.
공개 인증 화면(초기)
- 회원가입 화면은 AFFiNE 참고 다크 테마 3단계 플로우를 제공한다.
- 3단계는 인증 메일 발송 안내와 재전송 버튼(쿨다운)을 제공한다.
- 이메일 링크 확인 전에는 회원가입이 완료되지 않으며, 인증 완료 액션 이후 로그인 화면으로 이동한다.
- 로그인 화면은 동일한 다크 톤 폼 레이아웃을 사용한다.
- 로그인 비밀번호 입력은 보기/숨기기 토글을 제공한다.
- 인증 화면 상태 메시지는 오류/안내를 분리해
aria-live로 노출한다.
- 회원가입 1단계의 타이틀/설명은
GET /api/site-settings의 title, description 값을 우선 사용한다.
레이아웃 파일
컴포넌트 구조
사이트 컴포넌트
콘텐츠 렌더러
공개 본문 스타일 가이드(Thred 기준)
- 리스트
- Unordered:
- 항목
- Ordered:
1. 항목
- 렌더링:
ProseList.vue (마커 컬러, 간격, 줄높이 통일)
- 인용구
- 기본:
> 한 줄 또는 > 연속 여러 줄(멀티라인)
- 대체 스타일(Alternative):
>>>로 시작해 <<<로 끝나는 블록
- 렌더링:
ProseBlockquote.vue (variant=default|alt)
- 이미지
- 기본:

- 와이드/풀:
{width=wide|full}
- 렌더링:
ProseImage.vue (라운드/보더/패널 배경)
- 이미지 갤러리
:::gallery ~ ::: fenced block 내부에 이미지 마크다운 행을 여러 개 작성
- 렌더링:
ContentMarkdownRenderer.vue (그리드 + 라이트박스)
- 카드류
- Callout:
:::callout ~ ::: (왼쪽 강조선은 var(--site-accent))
- Toggle:
:::toggle 제목 ~ :::
- Bookmark:
:::bookmark ~ ::: (본문은 url=, title=, description=, thumbnail= 키값 또는 첫 줄 URL·이어지는 제목/설명 줄)
- Signup:
:::signup ~ ::: (선택: title=, description=, button=, placeholder=)
- Embed:
:::embed ~ ::: (YouTube·YouTube Shorts URL은 iframe, twitter.com·x.com 게시물 URL은 X 공식 embed iframe, 그 외는 외부 링크 카드)
- 렌더링:
ProseCallout.vue, ProseToggle.vue, ProseBookmark.vue, ProseSignup.vue, ProseEmbed.vue
데이터베이스 구조
환경 분리 원칙
- 데이터베이스는 PostgreSQL을 기준으로 한다.
- 로컬 개발 환경과 NAS 운영 환경은 서로 다른 DB를 사용
- 로컬 개발 서버는 개발 DB만 연결
- NAS 배포 환경은 운영 DB만 연결
- 운영 DB 접속 정보는 로컬 기본
.env에 기록하지 않음
- DB 관리 도구는 CloudBeaver 등을 사용할 수 있도록 접속 정보를 환경별로 분리
Posts (블로그 글)
| 필드 |
타입 |
설명 |
| id |
UUID |
Primary Key |
| title |
String |
제목 |
| slug |
String |
URL 슬러그 |
| content |
Text |
마크다운 콘텐츠 |
| excerpt |
String |
요약 |
| featured_image |
String nullable |
대표 이미지 |
| seo_title |
String |
SEO 제목 |
| seo_description |
String |
SEO 설명 |
| canonical_url |
String |
canonical URL |
| noindex |
Boolean |
검색엔진 노출 제외 여부 |
| og_image |
String nullable |
OG 이미지 |
| status |
Enum |
published/draft/private |
| published_at |
DateTime |
발행일 |
| created_at |
DateTime |
생성일 |
| updated_at |
DateTime |
수정일 |
Pages (고정 페이지)
| 필드 |
타입 |
설명 |
| id |
UUID |
Primary Key |
| title |
String |
제목 |
| slug |
String |
URL 슬러그 |
| content |
Text |
마크다운 콘텐츠 |
| featured_image |
String nullable |
대표 이미지 |
| created_at |
DateTime |
생성일 |
| updated_at |
DateTime |
수정일 |
Tags
| 필드 |
타입 |
설명 |
| id |
UUID |
Primary Key |
| name |
String |
태그명 |
| slug |
String |
URL 슬러그 |
| description |
String |
설명 |
| sort_order |
Integer |
사용자 화면 표시 순서 |
| color |
String |
태그 색상 코드 |
| created_at |
DateTime |
생성일 |
| updated_at |
DateTime |
수정일 |
SiteSettings
| 필드 |
타입 |
설명 |
| id |
Integer |
단일 설정 레코드 ID, 항상 1 |
| title |
String |
사이트 이름 |
| description |
String |
사이트 설명 |
| site_url |
String |
사이트 기본 URL |
| logo_text |
String |
텍스트 로고 |
| copyright_text |
String |
저작권 문구 |
| updated_at |
DateTime |
수정일 |
NavigationItems
| 필드 |
타입 |
설명 |
| id |
UUID |
Primary Key |
| label |
String |
메뉴 표시 이름 |
| url |
String |
내부 경로 또는 외부 URL |
| location |
Enum |
primary/footer |
| sort_order |
Integer |
표시 순서 |
| is_visible |
Boolean |
공개 화면 표시 여부 |
| created_at |
DateTime |
생성일 |
| updated_at |
DateTime |
수정일 |
MediaMetadata
| 필드 |
타입 |
설명 |
| url |
String |
업로드 미디어 URL |
| category |
String |
관리자 미디어 폴더 경로 |
| created_at |
DateTime |
생성일 |
| updated_at |
DateTime |
수정일 |
MediaFolders
| 필드 |
타입 |
설명 |
| path |
String |
미디어 폴더 경로 |
| created_at |
DateTime |
생성일 |
| updated_at |
DateTime |
수정일 |
PostTags (다대다)
| 필드 |
타입 |
설명 |
| post_id |
UUID |
FK → Posts |
| tag_id |
UUID |
FK → Tags |
| created_at |
DateTime |
생성일 |
API 구조
현재 API는 Nuxt server/api 내부에 샘플 데이터 기반으로 구현되어 있다. DB 연결 후 같은 응답 구조를 유지하되 저장소만 교체한다.
백엔드 구성
- 별도
backend/ 앱을 두지 않고 Nuxt/Nitro 서버 기능을 사용
- 공개 API는
server/api에 작성
- 서버 공통 스키마와 샘플 데이터는
server/utils에 작성
- PostgreSQL 연결과 조회 로직은
server/repositories에 작성
DATABASE_URL이 없으면 샘플 데이터 저장소를 사용
- 초기 단계에서는 같은 앱 배포로 관리 비용을 낮춤
- 독립 API 서버가 필요해지는 시점에만 백엔드 분리를 재검토
공개 API (/api/)
GET /api/posts - 게시물 목록
GET /api/posts/:slug - 게시물 상세
GET /api/pages - 고정 페이지 목록
GET /api/pages/:slug - 고정 페이지 상세
GET /api/tags - 태그 목록
GET /api/site-settings - 공개 사이트 설정
GET /api/navigation - 공개 네비게이션
관리자 API (/admin/api/)
POST /admin/api/auth/login - 로그인
POST /admin/api/auth/logout - 로그아웃
GET /admin/api/auth/me - 현재 관리자 세션 조회
GET /admin/api/posts - 글 목록
POST /admin/api/posts - 글 작성
GET /admin/api/posts/:id - 글 상세
PUT /admin/api/posts/:id - 글 수정
DELETE /admin/api/posts/:id - 글 삭제
GET /admin/api/pages - 고정 페이지 목록
POST /admin/api/pages - 고정 페이지 작성
GET /admin/api/pages/:id - 고정 페이지 상세
PUT /admin/api/pages/:id - 고정 페이지 수정
DELETE /admin/api/pages/:id - 고정 페이지 삭제
GET /admin/api/media - 업로드 미디어 목록
PUT /admin/api/media - 업로드 미디어 파일명 변경 또는 단일/복수 미디어 폴더 변경
DELETE /admin/api/media - 업로드 미디어 삭제
GET /admin/api/media-folders - 미디어 폴더 목록
POST /admin/api/media-folders - 미디어 폴더 생성
POST /admin/api/uploads - 관리자 이미지 업로드
GET /admin/api/tags - 태그 목록
POST /admin/api/tags - 태그 생성
GET /admin/api/tags/:id - 태그 상세
PUT /admin/api/tags/:id - 태그 수정
DELETE /admin/api/tags/:id - 태그 삭제
GET /admin/api/settings - 사이트 설정 조회
PUT /admin/api/settings - 사이트 설정 수정
GET /admin/api/navigation - 네비게이션 항목 목록
PUT /admin/api/navigation - 네비게이션 항목 일괄 저장
글 발행/초안/비공개 전환은 현재 PUT /admin/api/posts/:id의 status 값으로 처리한다.
태그 삭제 시 post_tags 연결도 데이터베이스 외래 키 규칙에 따라 함께 삭제된다.
태그 목록은 sort_order ASC, name ASC 기준으로 정렬한다.
태그 color는 #RRGGBB 형식이며 사용자 화면 태그 색상 표시와 배지 배경색에 사용한다.
관리자 글 편집
- 글 작성/수정 화면은 Ghost 스타일을 참고한 블록형 에디터를 사용한다.
- 저장 데이터는 기존
content 필드의 마크다운 문자열을 유지한다.
/ 입력 시 블록 선택 메뉴를 표시한다.
/ 명령 메뉴는 화면 하단 공간이 부족하면 현재 블록 위쪽으로 표시한다.
/ 명령 메뉴가 열린 블록 행은 아래 블록보다 위 stacking 순서로 표시해 메뉴와 본문 텍스트가 겹쳐 보이지 않게 한다.
/ 명령 메뉴가 열린 상태에서 Enter를 누르면 현재 강조된 메뉴 항목을 적용한다.
/ 명령 메뉴가 열린 상태에서 위/아래 방향키로 강조 항목을 이동한다.
/ 명령 메뉴 필터는 한글 조합 입력 완료와 방향키/Enter 입력 직전에 현재 DOM 텍스트를 기준으로 동기화한다.
/갤처럼 필터 결과가 하나로 좁혀진 상태에서 Enter를 누르면 해당 블록 명령을 적용한다.
- 블록 메뉴는 문단, 제목 2, 제목 3, 이미지, 갤러리, 콜아웃, 토글, 임베드, 인용, 목록, 코드, 구분선을 제공한다.
#, ##, ###, >, - 입력 후 공백을 누르거나 ```을 입력하면 현재 블록 타입을 즉시 변환한다.
- 한글 등 조합형 입력 중에는 단축 문법과 슬래시 메뉴 상태를 확정하지 않고 조합 종료 뒤 반영한다.
- 한글 등 조합형 입력 직후 확정된 텍스트로 슬래시 메뉴 필터와 Enter 블록 이동을 반영한다.
- 한글 등 조합형 입력 중 Shift+Enter가 들어오면 조합 완료 직후 줄바꿈을 예약 적용한다.
- Shift+Enter는 같은 텍스트 블록 안에 줄바꿈 문자를 직접 삽입하고 커서를 줄바꿈 뒤로 유지하며, Enter는 다음 문단 블록을 생성한다.
- 빈 문단에서 Enter를 누르면 다음 빈 문단 블록을 생성한다.
- 문단 간 기본 간격은 다음 블록의
margin-top: 32px 기준으로 조정한다.
- 블록 왼쪽 핸들은 hover/focus 상태에서 AFFiNE 참고 스타일의 세로 막대로 표시되며, hover 시 해당 블록 높이만큼 확장해 선택 범위를 드러낸다.
- 블록 왼쪽 핸들을 클릭하면 블록을 선택하고 Delete 또는 Backspace로 해당 블록을 삭제할 수 있다.
- 블록 왼쪽 핸들을 드래그하면 블록 순서를 이동할 수 있다.
- 블록 드래그 중에는 현재 포인터 위치 기준으로 대상 블록 위 또는 아래에 삽입선을 표시하고, 드롭 또는 드래그 종료 시 표시 위치와 같은 곳으로 이동한다.
- 빈 블록 placeholder는 현재 활성 블록 또는 첫 빈 블록에만 표시한다.
- 에디터 마지막에는 클릭 가능한 빈 문단 블록을 항상 유지하며, 해당 블록이 비어 있으면 저장 콘텐츠에는 포함하지 않는다.
- 제목은 별도 라벨 영역이 아니라 에디터 상단의 큰 제목 입력으로 표시한다.
- 글 작성/수정 화면은 페이지 제목 헤더를 별도로 두지 않고, 폼 상단의 Ghost 스타일 도구막대에서 목록 이동, 상태, 미리보기, 저장, 설정 패널 토글을 제공한다.
- 글 작성/수정 화면은 관리자 사이드바 네비게이션을 숨기고, 관리자 공통 내부 패딩 없이 전체 화면 편집 모드로 표시한다.
- 글 작성/수정 화면은 브라우저 문서가 아니라 에디터 작업 영역 내부에서만 세로 스크롤한다.
- 글 작성/수정 폼은 에디터 작업 영역과 우측 설정 패널을 먼저 좌우로 나누고, 에디터 작업 영역 안에 상단 도구막대와 본문 스크롤 영역을 배치한다.
- 본문 에디터는 데스크톱에서 좌우 32px 패딩을 포함한 804px 중앙 컬럼으로 배치해 실제 입력 영역 최대 폭을 740px로 유지한다.
- 게시물 설정은 데스크톱에서 420px 우측 패널로 표시하며, 도구막대 버튼으로 열고 닫을 수 있고 너비 전환 애니메이션을 적용한다.
- 공개 가능한 글의 보기 액션은 게시물 설정 패널의 Post URL 라벨 오른쪽에 표시한다.
- 글 삭제 액션은 게시물 설정 패널 하단의 빨간 outline 버튼으로 제공한다.
- 대표 이미지는 본문 제목 위의 에디터 흐름 안에서 추가, 변경, 삭제한다.
- 대표 이미지가 설정된 상태의 변경/삭제 액션은 실제 이미지 레이아웃을 바꾸지 않도록 이미지 hover 또는 focus 오버레이로 표시한다.
- 대표 이미지 선택 모달은 이미지 업로드 탭과 미디어 라이브러리 탭을 제공한다.
- 제목 입력에서 Enter를 누르면 본문 첫 블록으로 포커스를 이동한다.
- 새 글 작성처럼 본문이 비어 있는 경우에도 빈 문단 블록을 먼저 생성한다.
- 글 작성/수정 중인 입력값은 브라우저
localStorage에 자동 저장한다.
- 자동 저장 키는
SORI_ADMIN_POST_AUTOSAVE:new 또는 SORI_ADMIN_POST_AUTOSAVE:{postId} 형식을 사용한다.
- 자동 저장본이 있으면 작성 화면에서 복원 또는 삭제를 선택할 수 있다.
- 글 저장 성공 시 해당 자동 저장본은 삭제한다.
- 미리보기 버튼은 현재 작성 폼 값을
SORI_ADMIN_POST_PREVIEW 브라우저 저장소에 기록한 뒤 /admin/posts/preview 새 탭에서 렌더링한다.
- 미리보기 화면은 공개 게시물 상세와 같은 본문 렌더링 컴포넌트를 사용하되, 데이터베이스에는 임시 글을 저장하지 않는다.
- 글 저장/수정/삭제 진행과 성공/실패 상태는 화면 우측 상단 토스트로 표시한다.
- 발행 상태에서 발행 시각을 미래로 지정하면 예약 발행으로 저장한다.
- 예약 발행 글은 관리자 목록에서 예약 상태로 표시하되 공개 게시물 목록과 상세 API에는 발행 시각 이후부터 노출한다.
- 관리자 글 에디터의 실제 입력 텍스트 색상은 placeholder보다 진하게 표시한다.
- 관리자 작성 화면과 공개 본문은 같은 마크다운 렌더링 기준을 사용한다.
- 태그 입력은 배지형 입력으로 제공하며 Enter 또는 쉼표 입력 시 태그를 추가하고, 배지의 x 버튼으로 삭제한다.
- 대표 이미지는 URL 직접 입력이 아니라 미디어 선택 또는 이미지 업로드 탭으로 설정한다.
- 대표 이미지 선택 모달에서 미디어를 클릭하면 선택 상태만 표시하고, 하단 대표 이미지로 적용 버튼을 눌렀을 때 글 폼에 반영한다.
- 대표 이미지가 설정되면 관리자 글 폼에 썸네일과 hover 오버레이 삭제/변경 액션을 표시한다.
- 글 SEO 설정은 SEO 제목, SEO 설명, 검색엔진 노출 제외 여부를 저장한다.
- Canonical URL은 별도 입력을 받지 않고 기본 글 주소를 사용한다.
- 공개 게시물 상세 화면은 SEO 제목이 없으면 글 제목, SEO 설명이 없으면 요약을 메타 태그 기본값으로 사용한다.
- 검색엔진 노출 제외가 켜진 글은 robots 메타를
noindex, nofollow로 출력한다.
- 공개 상세 화면의
og:image와 Twitter large image 카드는 대표 이미지를 기본값으로 사용한다.
- 이미지 블록은 관리자 업로드 API로 이미지를 업로드하고
{width=wide} 형식으로 저장한다.
- 이미지/갤러리 삽입 시 파일명은 alt 값으로 자동 입력하지 않는다.
- 이미지 블록 alt 입력은 이미지 hover 또는 focus 상태에서만 표시한다.
- 이미지 블록 표시 옵션은
regular, wide, full 값을 사용하며 regular는 width 옵션을 생략한다.
- 갤러리 블록은
:::gallery fenced block 안에 이미지 마크다운 행을 여러 개 저장한다.
- 공개 갤러리는 한 줄에 2~3개 이미지 그리드로 표시하고 클릭 시 라이트박스로 크게 확인한다.
- 이미지와 갤러리 블록은 기존 업로드 미디어 선택 또는 새 이미지 업로드를 제공한다.
- 콜아웃 블록은
:::callout fenced block 안에 본문을 저장한다.
- 토글 블록은
:::toggle 제목 fenced block 안에 펼침 본문을 저장한다.
- 임베드 블록은
:::embed fenced block 안에 URL을 저장한다.
- YouTube 임베드 URL은 공개 화면에서 iframe으로 렌더링한다.
- Twitter/X 게시물 URL(
twitter.com·x.com·mobile.twitter.com, 경로에 status 포함)은 platform.twitter.com/embed/Tweet.html iframe으로 렌더링하며, 테마는 useThemeMode()와 동기화한다.
- 그 외 URL은 외부 링크 텍스트 카드로 표시한다.
- 북마크 블록은
:::bookmark fenced block으로 저장할 수 있으며 공개 화면에서 Thred형 가로 카드로 렌더링한다.
- 회원가입(뉴스레터) CTA는
:::signup fenced block으로 저장할 수 있으며 실제 폼 연동은 후속 작업으로 분리한다.
관리자 페이지 편집
- 고정 페이지 작성/수정 화면은 게시물과 같은 블록형 에디터를 사용한다.
- 고정 페이지는 제목, 슬러그, 본문, 대표 이미지만 저장한다.
- 고정 페이지는 게시물 목록과 태그 목록에 노출하지 않는다.
- 고정 페이지 공개 보기 경로는
/pages/:slug를 사용한다.
사이트 설정
- 사이트 설정은
site_settings 테이블의 단일 레코드로 관리한다.
- 관리자는 사이트 이름, 설명, 사이트 URL, 텍스트 로고, 저작권 문구를 수정할 수 있다.
- 공개 헤더와 오른쪽 사이드바는 공개 사이트 설정 API 값을 사용한다.
- DB 연결이 없는 환경에서는 환경 변수와 기본값 기반 설정을 사용한다.
메뉴/네비게이션
- 네비게이션은
navigation_items 테이블로 관리한다.
- 관리자는 메뉴 라벨, URL, 위치, 순서, 표시 여부를 수정할 수 있다.
- 공개 왼쪽 사이드바의 상단 메뉴는
primary 위치 항목을 사용한다.
- 공개 왼쪽 사이드바 하단 메뉴는
footer 위치 항목을 사용한다.
- URL은
/로 시작하는 내부 경로 또는 http://, https:// 외부 URL을 허용한다.
관리자 인증
- 초기 관리자 인증은
ADMIN_EMAIL, ADMIN_PASSWORD 환경 변수를 사용
- 로그인 성공 시 httpOnly 세션 쿠키를
/admin 경로에 설정
- 관리자 페이지 접근은
/admin/api/auth/me 확인 후 허용
- 세션 토큰은
ADMIN_PASSWORD 기반 HMAC 서명으로 검증
미디어 관리
업로드 경로 규칙
- 관리자 이미지 업로드 API는
image/jpeg, image/png, image/webp, image/gif만 허용한다.
- 업로드 파일 크기 제한은
MAX_FILE_SIZE 환경 변수를 따른다.
- 로컬 개발 업로드 파일은
public/uploads/posts/YYYY/MM/ 아래 저장하고 /uploads/posts/YYYY/MM/filename URL로 제공한다.
- 관리자 미디어 화면은 왼쪽 폴더 트리와 오른쪽 고밀도 썸네일 갤러리, 검색, 파일명 변경, 개별 삭제를 제공한다.
- 관리자는 폴더 트리에서 새 폴더와 하위 폴더를 만들 수 있다.
- 미디어는 Ctrl/Command 클릭으로 복수 선택하고 Shift 클릭으로 범위 선택할 수 있다.
- 선택한 미디어를 폴더로 드래그하면 해당 미디어들의 폴더 경로가 일괄 변경된다.
- 미디어 파일 경로, 사용 현황, 용량 등 세부 정보는 썸네일 더블 클릭 시 열리는 상세 모달에서 표시한다.
- 미디어 폴더는 실제 파일 경로를 옮기지 않고
media_metadata 테이블에 URL별 경로 메타데이터로 저장한다.
- 글쓰기 미디어 선택 창은 업로드 미디어 목록에서 이미지를 선택해 단일 이미지 또는 갤러리에 삽입한다.
- 미디어 사용 현황은 게시물/페이지의 대표 이미지와 본문 내 URL을 기준으로 표시한다.
- 사용 중인 미디어는 저장된 콘텐츠 URL이 깨지지 않도록 파일명 변경과 삭제를 차단한다.
- 향후 미디어 라이브러리는 프로필/사이트 설정 이미지 사용처 추적을 제공한다.
환경 변수 (.env)
공통 키
환경 파일 기준
| 파일 |
용도 |
DB |
.env.development |
로컬 개발, Git 제외 |
개발 DB |
.env.production |
NAS 운영, Git 제외 |
운영 DB |
.env.example |
공유 예시, Git 포함 |
실제 접속 정보 없음 |
.env.example에는 실제 이메일, 비밀번호, 토큰, 운영 서버 주소를 기록하지 않음
.env.development와 .env.production의 DB 비밀번호와 관리자 비밀번호는 서로 다른 랜덤 값을 사용
- 로컬 개발
DATABASE_URL은 호스트 기준 127.0.0.1:43119를 사용
- NAS Docker 내부
DATABASE_URL은 서비스명 기준 sori-studio-db:5432를 사용
- 로컬 DB 확인은 PostgreSQL 클라이언트에서
127.0.0.1:43119로 접속하거나 docker exec sori-studio-db psql 명령으로 확인한다.
포트 기준
| 용도 |
포트 |
| 로컬 개발 서버 |
43117 |
| NAS Docker 외부 포트 |
43118 |
| 컨테이너 내부 포트 |
3000 |
| PostgreSQL 외부 포트 |
43119 |
버전 관리
- 현재 버전: v0.0.56
- 첫 커밋 이후 변경사항을 커밋할 때마다 패치 버전 증가
- 메이저/마이너 버전은 구조 변경 또는 기능 묶음 단위로 결정