태그를 관리용/일반용으로 분리하고 관리자 드래그 정렬을 추가.

댓글/회원/관리자 인증·프로필 흐름 보완과 관련 마이그레이션 및 문서를 함께 반영해 운영 동선을 안정화.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-11 18:34:23 +09:00
parent b18aca4dcc
commit cdc16c72b2
35 changed files with 1721 additions and 138 deletions

View File

@@ -1,5 +1,26 @@
# 의사결정 이력
## 2026-05-11 v0.0.80
### 태그를 관리용/일반용으로 분리하고 관리용만 정렬
태그 수가 많아질수록 모든 태그에 순번을 강제하면 운영 비용이 커지므로, 홈페이지 카테고리로 쓰는 태그만 `managed`로 분리해 순서를 관리하고 나머지는 `general`로 분리했다. 관리용 태그는 드래그 앤 드롭으로 순서를 바꾸고 일괄 저장 API로 반영해 중복 숫자 입력 문제를 제거했다.
공개 `GET /api/tags`는 관리용 태그만 반환하도록 바꿔, 카테고리 노출 목적과 일반 배지 태그 목적이 섞이지 않게 했다.
## 2026-05-11 v0.0.79
### 최초 사용자 관리자 부트스트랩 전환
관리자 계정을 환경변수로 고정하면 실제 운영에서 초기 세팅 흐름이 불명확하고, 관리자 프로필/권한 관리가 회원 데이터와 분리되어 확장성이 떨어진다. 따라서 최초 사용자가 회원가입을 시도하는 시점에 `관리자 등록` 모드로 안내하고, 첫 계정에 `is_admin=true`를 부여하는 부트스트랩 방식으로 전환했다. 관리자 로그인도 동일한 `users` 인증 체계를 사용하도록 맞춰, 관리자/회원 계정 모델을 일원화했다.
관리자 화면에서 썸네일/이름을 바로 수정할 수 있도록, 관리자 로그인 시 회원 세션도 함께 발급해 기존 회원 프로필 API를 재사용하는 방향을 선택했다.
권한은 향후 기능 확장을 위해 `owner`/`admin`/`member` 3단계로 먼저 분리하고, 현재 단계에서는 관리자 멤버 화면에서 권한 값을 변경할 수 있게 준비했다.
## 2026-05-11 v0.0.79
### 댓글 아바타/좋아요/상대시간 표시 정렬
댓글 영역은 텍스트 중심 구조만으로는 SNS형 피드백 흐름이 약해 참여 지표를 확인하기 어려웠다. 작성자 썸네일과 좋아요 토글을 기본 액션으로 배치하고, 시간 표기는 최근 24시간 동안 상대 시간으로 보여 즉시성을 높였다. 24시간 이후에는 날짜로 전환해 장기 글에서도 타임라인 문맥을 유지한다.
## 2026-05-11 v0.0.78
### 관리자 미디어에서 회원 썸네일 가시성 복구

View File

@@ -35,7 +35,7 @@
| components/site/MainColumn.vue | 메인 화면 중앙, `lg:max-w-[720px]`로 본문 상한 |
| components/site/PostCard.vue | 목록의 게시물 카드, 대표 이미지 썸네일, 카드 hover 인터랙션 |
| components/site/TagHeader.vue | 태그 페이지 헤더 |
| components/comments/PostComments.vue | 게시물 상세 `#comments` 영역, 회원 댓글/답글(1단) 작성 및 목록 표시 |
| components/comments/PostComments.vue | 게시물 상세 `#comments` 영역, 회원 댓글/답글(1단) 작성 및 목록 표시, 작성자 썸네일/좋아요/상대시간 표시 |
## 관리자 컴포넌트
@@ -83,10 +83,10 @@
| pages/admin/pages/[id].vue | 페이지 수정, 저장/삭제 토스트 |
| pages/admin/media/index.vue | 미디어 관리, 폴더 트리, 복수 선택, 드래그 이동 |
| pages/admin/navigation/index.vue | 메뉴/네비게이션 관리 |
| pages/admin/tags/index.vue | 태그 관리 |
| pages/admin/tags/index.vue | 태그 관리(관리용/일반용 분리, 관리용 드래그 정렬 저장) |
| pages/admin/tags/new.vue | 태그 생성 |
| pages/admin/tags/[id].vue | 태그 수정 |
| pages/admin/settings/index.vue | 사이트 설정 |
| pages/admin/settings/index.vue | 사이트 설정 + 관리자 프로필(썸네일/이름) + 관리자 비밀번호 변경 |
| pages/admin/members/index.vue | 관리자 멤버 목록(닉네임, 이메일, 최근 접속, IP, 댓글 수, 활동 상태) |
## 공개 페이지
@@ -118,6 +118,7 @@
| server/api/site-settings.get.js | 공개 사이트 설정 API |
| server/api/navigation.get.js | 공개 네비게이션 API |
| server/api/auth/signup.post.js | 회원 가입 API |
| server/api/auth/bootstrap-status.get.js | 최초 관리자 등록 필요 여부 조회 API |
| server/api/auth/login.post.js | 회원 로그인 API |
| server/api/auth/me.get.js | 회원 세션 조회 API |
| server/api/auth/logout.post.js | 회원 로그아웃 API |
@@ -130,6 +131,7 @@
| server/api/auth/account.delete.js | 회원 탈퇴 API |
| server/api/posts/[slug]/comments.get.js | 게시물 댓글 목록 조회 API |
| server/api/posts/[slug]/comments.post.js | 게시물 댓글 작성 API |
| server/api/posts/[slug]/comments/[commentId]/like.post.js | 댓글 좋아요 토글 API |
| server/routes/admin/api/auth/login.post.js | 관리자 로그인 API |
| server/routes/admin/api/auth/logout.post.js | 관리자 로그아웃 API |
| server/routes/admin/api/auth/me.get.js | 관리자 세션 조회 API |
@@ -154,11 +156,13 @@
| server/routes/admin/api/tags/[id].get.js | 관리자 태그 상세 API |
| server/routes/admin/api/tags/[id].put.js | 관리자 태그 수정 API |
| server/routes/admin/api/tags/[id].delete.js | 관리자 태그 삭제 API |
| server/routes/admin/api/tags/reorder.put.js | 관리자 관리용 태그 순서 일괄 저장 API |
| server/routes/admin/api/settings.get.js | 관리자 사이트 설정 조회 API |
| server/routes/admin/api/settings.put.js | 관리자 사이트 설정 수정 API |
| server/routes/admin/api/navigation.get.js | 관리자 네비게이션 목록 API |
| server/routes/admin/api/navigation.put.js | 관리자 네비게이션 일괄 저장 API |
| server/routes/admin/api/members.get.js | 관리자 멤버 목록 API |
| server/routes/admin/api/members/[id]/role.put.js | 관리자 멤버 권한 변경 API |
| server/utils/content-schema.js | Zod 콘텐츠 스키마 |
| server/utils/sample-content.js | 샘플 콘텐츠 저장소 |
| server/utils/admin-auth.js | 관리자 세션 쿠키 인증 유틸리티 |
@@ -192,6 +196,10 @@
| db/migrations/009_add_post_og_image.sql | 게시물 OG 이미지 필드 추가 |
| db/migrations/010_add_members_and_comments.sql | 회원/댓글 테이블 추가 |
| db/migrations/011_add_member_profile_and_activity.sql | 회원 아바타/최근 활동 컬럼 추가 및 닉네임 유니크 인덱스 추가 |
| db/migrations/012_add_comment_likes.sql | 댓글 좋아요 테이블 추가 |
| db/migrations/014_add_user_role_levels.sql | 회원 권한 3단계(owner/admin/member) 컬럼 추가 |
| db/migrations/013_add_user_admin_role.sql | 회원 관리자 권한 컬럼 추가 및 첫 사용자 관리자 승격 |
| db/migrations/015_add_tag_type_and_reorder_support.sql | 태그 유형(`managed`/`general`) 컬럼 추가 |
## 설정/배포

View File

@@ -20,7 +20,7 @@
|------|-----------|
| Header | 높이 57px, `sticky top-0`, `shrink-0`. `lg`~`xl` 구간은 내부 `px-5`~`px-6`로 좌우 여백을 두고, 검색창은 뷰포트에 맞춰 `max-w`로 단계 축소한다(`2xl`에서 고정 470px). |
| Shell | `min-height: 100vh`, `flex` 세로 컬럼 |
| 그리드(데스크톱 `lg+`) | `items-start`, `column-gap`(`lg:gap-x-4`, `xl:gap-x-5`)으로 열 사이 간격. 중앙 열은 `minmax(0,1fr)`로 패딩이 있어도 가로 합이 넘치지 않게 함 — **문서(`html`/`body`) 스크롤**로 긴 본문 처리(스크롤바는 브라우저 오른쪽) |
| 그리드(데스크톱 `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`로 본문 블록과 유사한 여백 |
| Left Aside(모바일) | `fixed` 좌측 패널, 열림 시 `translate-x-0`, 닫힘 시 화면 밖으로 이동. 열린 동안 백드롭과 `html.site-mobile-nav-open`으로 문서 스크롤 잠금 |
@@ -59,6 +59,10 @@
- 게시물 화면은 레이아웃 그리드의 좌우 패딩을 기본으로 사용하고, 섹션 단위 `px-*`는 내부 래퍼로만 두어 패딩이 2중 적용되지 않게 한다.
- 댓글 시작 섹션의 구분선(`border-y`)은 패딩 없이 전체 폭으로 표시하고, 내용만 내부 래퍼에 패딩을 둔다.
- 댓글은 로그인 회원만 작성 가능하며, 대댓글은 1단까지만 허용한다.
- 댓글은 작성자 썸네일(없으면 이니셜)과 좋아요 수를 표시한다.
- 댓글 시간은 24시간 이내일 때 상대 시간(분/시간 전), 이후에는 날짜로 표시한다.
- 댓글 정렬은 `Best`(좋아요 우선), `Latest`, `Oldest`를 제공한다.
- 댓글 아바타 이미지 로드 실패 시 이니셜 아바타로 즉시 대체한다.
- 공개 게시물 본문은 콘텐츠 타입별 컴포넌트로 분리해 추후 스타일 변경이 쉽도록 구성
- 제목 우측 공유 버튼을 누르면 게시물 공유 모달을 연다.
- 공유 모달은 게시물 썸네일/제목/요약 미리보기, X/Bluesky/Facebook/LinkedIn/Email 링크, 링크 복사 액션을 제공한다.
@@ -212,6 +216,8 @@ components/content/
| email | String | 로그인 이메일(유니크) |
| password_hash | String | bcrypt 해시 비밀번호 |
| avatar_url | String | 프로필 썸네일 URL |
| is_admin | Boolean | 관리자 권한 여부 |
| user_role | Enum | 권한 단계(`owner`/`admin`/`member`) |
| last_seen_at | DateTime nullable | 마지막 접속 시각 |
| last_seen_ip | String | 마지막 접속 IP |
| created_at | DateTime | 생성일 |
@@ -230,6 +236,14 @@ components/content/
| created_at | DateTime | 생성일 |
| updated_at | DateTime | 수정일 |
### CommentLikes
| 필드 | 타입 | 설명 |
|------|------|------|
| comment_id | UUID | FK → Comments |
| user_id | UUID | FK → Users |
| created_at | DateTime | 생성일 |
### Pages (고정 페이지)
| 필드 | 타입 | 설명 |
@@ -250,8 +264,9 @@ components/content/
| name | String | 태그명 |
| slug | String | URL 슬러그 |
| description | String | 설명 |
| sort_order | Integer | 사용자 화면 표시 순서 |
| sort_order | Integer | 관리용 태그 정렬 순서 |
| color | String | 태그 색상 코드 |
| tag_type | Enum | 태그 유형(`managed`/`general`) |
| created_at | DateTime | 생성일 |
| updated_at | DateTime | 수정일 |
@@ -327,6 +342,7 @@ components/content/
- `GET /api/posts/:slug` - 게시물 상세
- `GET /api/posts/:slug/comments` - 게시물 댓글 목록
- `POST /api/posts/:slug/comments` - 게시물 댓글 작성(회원 세션 필요, 대댓글 1단)
- `POST /api/posts/:slug/comments/:commentId/like` - 댓글 좋아요 토글(회원 세션 필요)
- `GET /api/pages` - 고정 페이지 목록
- `GET /api/pages/:slug` - 고정 페이지 상세
- `GET /api/tags` - 태그 목록
@@ -334,6 +350,7 @@ components/content/
- `GET /api/site-settings` - 공개 사이트 설정
- `GET /api/navigation` - 공개 네비게이션
- `POST /api/auth/signup` - 회원 가입
- `GET /api/auth/bootstrap-status` - 최초 관리자 등록 필요 여부 조회
- `POST /api/auth/login` - 회원 로그인
- `GET /api/auth/me` - 현재 회원 세션 조회
- `POST /api/auth/logout` - 회원 로그아웃
@@ -377,15 +394,19 @@ components/content/
- `GET /admin/api/tags/:id` - 태그 상세
- `PUT /admin/api/tags/:id` - 태그 수정
- `DELETE /admin/api/tags/:id` - 태그 삭제
- `PUT /admin/api/tags/reorder` - 관리용 태그 순서 일괄 저장
- `GET /admin/api/settings` - 사이트 설정 조회
- `PUT /admin/api/settings` - 사이트 설정 수정
- `GET /admin/api/navigation` - 네비게이션 항목 목록
- `PUT /admin/api/navigation` - 네비게이션 항목 일괄 저장
- `GET /admin/api/members` - 회원 목록(최근 접속, 접속 IP, 댓글 수 포함)
- `GET /admin/api/members` - 회원 목록(권한, 최근 접속, 접속 IP, 댓글 수 포함)
- `PUT /admin/api/members/:id/role` - 회원 권한 변경(`owner`/`admin`/`member`)
> 글 발행/초안/비공개 전환은 현재 `PUT /admin/api/posts/:id`의 `status` 값으로 처리한다.
> 태그 삭제 시 `post_tags` 연결도 데이터베이스 외래 키 규칙에 따라 함께 삭제된다.
> 태그 목록은 `sort_order ASC, name ASC` 기준으로 정렬한다.
> 공개 `GET /api/tags`는 `managed` 태그만 반환한다.
> 관리자 태그 목록은 `managed` 우선, `sort_order ASC, name ASC` 기준으로 정렬한다.
> 관리용 태그 순서 저장은 드래그 순서를 받아 `sort_order`를 순차 값으로 다시 저장한다.
> 태그 `color`는 `#RRGGBB` 형식이며 사용자 화면 태그 색상 표시와 배지 배경색에 사용한다.
### 관리자 글 편집
@@ -487,16 +508,22 @@ components/content/
### 관리자 인증
- 초기 관리자 인증은 `ADMIN_EMAIL`, `ADMIN_PASSWORD` 환경 변수를 사용
- 로그인 성공 시 httpOnly 세션 쿠키를 `/admin` 경로에 설정
- 관리자 페이지 접근은 `/admin/api/auth/me` 확인 후 허용
- 세션 토큰은 `ADMIN_PASSWORD` 기반 HMAC 서명으로 검증
- 관리자 인증은 `users.is_admin=true` 회원의 이메일/비밀번호(bcrypt 해시) 기반으로 처리한다.
- DB에 사용자가 없으면 `/signup`에서 관리자 등록 모드(관리자 이름/이메일/비밀번호/비밀번호 확인)로 첫 계정을 생성한다.
- 첫 계정 생성 시 `is_admin=true`, `user_role=owner`를 자동 부여한다.
- 관리자 로그인 성공 시 httpOnly 세션 쿠키를 `/admin` 경로에 설정한다.
- 관리자 로그인 성공 시 관리자/회원 세션 쿠키를 함께 설정해 관리자 화면에서도 프로필(썸네일, 이름) 수정 API를 공통으로 사용한다.
- 관리자 설정 화면에서 관리자 프로필(이름, 썸네일)과 관리자 비밀번호를 수정할 수 있다.
- 관리자 멤버 화면에서 권한은 `소유자(owner)`, `관리자(admin)`, `멤버(member)` 3단계로 표시하고 변경할 수 있다.
- 관리자 페이지 접근은 `/admin/api/auth/me` 확인 후 허용한다.
- 관리자 세션 토큰은 `ADMIN_PASSWORD` 기반 HMAC 서명으로 검증한다.
### 회원 인증
- 회원 인증은 `users` 테이블의 이메일/비밀번호(bcrypt 해시) 기반으로 처리한다.
- 로그인 성공 시 httpOnly 세션 쿠키(`sori_member_session`)를 `/` 경로에 설정한다.
- 회원 API(`POST /api/auth/signup`, `POST /api/auth/login`, `GET /api/auth/me`, `POST /api/auth/logout`)로 세션을 관리한다.
- 첫 회원가입 시 관리자 세션도 함께 설정되어 관리자 화면(`/admin`)으로 바로 진입할 수 있다.
- 회원 세션 서명은 `MEMBER_SESSION_SECRET`을 우선 사용하고, 값이 없으면 개발 편의를 위해 `ADMIN_PASSWORD`를 fallback으로 사용한다.
---
@@ -590,6 +617,6 @@ APP_PORT=43118
## 버전 관리
- 현재 버전: v0.0.78
- 현재 버전: v0.0.80
- 첫 커밋 이후 변경사항을 커밋할 때마다 패치 버전 증가
- 메이저/마이너 버전은 구조 변경 또는 기능 묶음 단위로 결정

View File

@@ -1,5 +1,37 @@
# 업데이트 이력
## v0.0.80
- 태그에 유형(`managed`/`general`) 컬럼을 추가하는 마이그레이션(`015_add_tag_type_and_reorder_support.sql`)을 추가.
- 관리자 태그 입력 폼에 태그 유형 선택을 추가하고, 일반 태그는 정렬 순서를 사용하지 않도록 정리.
- 관리자 태그 목록을 `관리용 태그`/`일반 태그`로 분리.
- 관리용 태그 목록에 드래그 앤 드롭 정렬 UI를 추가하고, 저장 버튼으로 순서를 일괄 반영하도록 개선.
- `PUT /admin/api/tags/reorder` API를 추가해 관리용 태그의 `sort_order`를 순차 재정렬하도록 확장.
- 공개 태그 API(`GET /api/tags`)는 관리용 태그만 반환하도록 변경해 좌측 카테고리 영역과 태그 관리 목적을 분리.
## v0.0.79
- 댓글 목록에 작성자 썸네일(`avatar_url`)을 노출하고, 썸네일이 없으면 이니셜 아바타를 표시하도록 UI를 개선.
- 댓글/대댓글에 좋아요 토글을 추가하고, 좋아요 수/내 좋아요 상태를 함께 반환하도록 API와 저장소를 확장.
- 댓글 시간 표기를 `n분 전`, `n시간 전`, 24시간 이후 날짜 형식으로 변경.
- `comment_likes` 테이블 마이그레이션(`012_add_comment_likes.sql`)을 추가.
- 댓글 정렬 옵션(`Best`, `Latest`, `Oldest`)을 실제 동작하도록 연결.
- 댓글/대댓글 카드 보더를 최소화하고 간격·아이콘 스타일을 정리해 더 단정한 레이아웃으로 조정.
- 대댓글 아바타가 깨질 때 이니셜로 안전하게 대체되도록 이미지 로드 실패 처리 보강.
- 공개 레이아웃(`default`, `post`)의 데스크톱 3열 `gap-x`를 제거해 본문-사이드 사이가 이중 패딩처럼 보이던 간격을 정리.
- 좌측 사이드바 네비게이션/카테고리 hover 배경을 `site-panel-hover` 기반으로 통일해 다크 모드에서 텍스트 가독성 저하를 수정.
- 관리자 멤버 목록에 권한 컬럼을 추가하고, 환경변수 관리자 계정을 `관리자` 권한으로 함께 표시하도록 보강.
- 사용자 테이블에 관리자 권한 컬럼(`is_admin`)을 추가하는 마이그레이션(`013_add_user_admin_role.sql`)을 추가.
- 최초 사용자 생성 시 관리자 권한을 자동 부여하고, 가입 응답에 관리자 여부를 함께 반환하도록 수정.
- 관리자 로그인은 환경변수 고정 계정 대신 `is_admin` 회원 계정 인증으로 전환.
- `GET /api/auth/bootstrap-status`를 추가해 최초 관리자 등록 필요 여부를 조회하도록 확장.
- 회원가입 화면에서 최초 사용자일 때 `관리자 등록` 타이틀/설명/필드 라벨을 노출하도록 수정.
- 관리자 로그인 시 회원 세션도 함께 설정하고, 관리자 로그아웃 시 회원 세션도 함께 정리하도록 수정.
- 관리자 설정 화면에 관리자 프로필(썸네일 업로드/제거, 이름 저장) 섹션을 추가.
- 관리자 설정 화면에서 현재 비밀번호 확인 기반 관리자 비밀번호 변경을 지원하도록 추가.
- 회원 권한 3단계(`owner`/`admin`/`member`)를 위한 마이그레이션(`014_add_user_role_levels.sql`)을 추가.
- 관리자 멤버 화면에서 권한 선택/저장 UI와 `PUT /admin/api/members/:id/role` 권한 변경 API를 추가.
## v0.0.78
- 관리자 미디어 목록에서 회원 썸네일 경로(`/uploads/members/avatars/`)를 다시 포함해 `회원/썸네일` 폴더에서 확인 가능하도록 수정.