42 KiB
42 KiB
기술 명세
현재 아키텍처
- 프런트엔드: Vue 3 + Vite + Pinia + Vue Router
- 백엔드: Express 5
- 데이터 저장소: MariaDB(MySQL 호환)
- 세션 저장소:
session-file-store기반 파일 세션 - 업로드 저장소: 로컬 파일 시스템(
backend/uploads/) - 운영 배포:
frontend(Nginx 정적 서빙 + /api,/uploads 프록시) + backend + mariadbDocker Compose 구조 - NAS HTTPS 리버스 프록시 운영 시 프런트 Nginx는 백엔드로
X-Forwarded-Proto: https를 전달하고, Express 세션은 프록시 환경에서secure쿠키를 허용하도록 설정한다. - 프런트 브라우저 탭 파비콘은 운영 정적 파일 차단 영향을 줄이기 위해
index.html의 인라인 SVG 데이터 URL로 제공하고, iOS 홈 화면용apple-touch-icon.png와 공유 미리보기용og-card.png만 정적 파일로 유지한다. - 프런트 앱 셸은
좌측 내비게이션 / 중앙 워크스페이스 / 우측 컨텍스트 패널3단 구조로 재정의되었고,preview=1모드에서도 같은 셸을 유지한 채 중앙 본문만 완성본 프리뷰로 렌더링한다. - 좌측 패널은
248px, 우측 패널은320px기준 폭을 사용하며, 우측 패널은 상단 토글 버튼으로 접고 펼칠 수 있다. - 좌측 패널은 필요 시 축소형 레일로 접을 수 있으며, 접힌 상태에서는 아이콘 중심 내비게이션과 축약된 바로가기만 유지한다.
- 모바일(
860px이하)에서는 좌측 패널도 고정 열을 차지하지 않고, 우측 패널과 같은 오버레이 방식으로 띄운다. - 앱 최초 진입 시에는 인증 상태 복원과 최소 140ms 대기 시간을 함께 묶은
bootGate초기 게이트를 먼저 보여주고, 그 뒤에만 실제 앱 셸을 렌더링한다. - 이 3단 셸 구조는 홈, 게임 허브, 에디터, 관리자 등 일반 페이지 전반의 공통 뼈대로 유지하고, 페이지별 차이는 중앙/우측에 어떤 콘텐츠를 넣는지만 달라지도록 관리한다.
- 비로그인 상태의 로그인 유도는 좌측 하단 버튼으로만 노출하고, 좌측 상단 사용자 카드 영역에는 별도 게스트 안내 카드를 렌더링하지 않는다.
- 공통 셸의 좌측 내비, 우측 패널, 빠른 점프 버튼은 간단한 선형 SVG 아이콘과 두꺼운 카드형 버튼 문법을 공유한다.
데이터 저장 구조
- 메인 데이터베이스: MariaDB
tier_cursor(기본값) - 세션 파일:
backend/.sessions/*.json - 업로드 파일:
- 게임 이미지:
backend/uploads/games/ - 아바타:
backend/uploads/avatars/ - 커스텀 아이템:
backend/uploads/custom/ - 시드 이미지:
backend/uploads/seeds/ - 최적화 이미지 자산: 신규 업로드는
backend/uploads/assets/<앞2글자>/<파일명>.webp형태로 1단계 샤딩 저장하고, 기존backend/uploads/assets/<파일명>.webp평면 경로도 계속 읽는다. - 기존 평면 자산을 샤딩 구조로 정리할 때는
npm --prefix backend run images:shard-assets를 실행하며, 스크립트가 파일 이동과 DB/JSON 참조 치환을 함께 처리한다.
- 게임 이미지:
화면 구조
- 좌측 패널
- 사용자 요약, 전체 공개 티어표 검색 입력, 주요 라우트 내비게이션, 최근 즐겨찾기 티어표 바로가기, 관리자 진입 버튼을 배치한다.
- 상단 토글 버튼은 항상 고정되어 있고, 패널을 축소하면 텍스트를 숨기고 아이콘 중심 레일로 전환한다.
Settings는 별도 메뉴 항목으로만 진입하며, 사용자 카드 자체는 정보 표시 용도로만 사용한다.- 사용자 아바타는 원형 보더 스타일을 유지하고,
Favorites영역은 최근 즐겨찾기 티어표 최대 10개를 메인 메뉴보다 작은 밀도의 바로가기 목록으로 보여준 뒤 하단즐겨찾기 더 보기링크로 전체 즐겨찾기 화면에 연결한다. - 사용자 아바타가 없을 때 표시하는 fallback 이니셜은 계정명보다 닉네임을 우선한다.
- 중앙 워크스페이스
- 현재 라우트의 핵심 콘텐츠를 렌더링하는 영역이며, 홈/목록 계열 화면은 카드형 대시보드 레이아웃을 우선 적용한다.
- 공통
workspaceBody는 별도 외곽 카드 테두리 없이 셸 여백만 제공하고, 실제 카드/패널 레이어는 각 화면 내부에서만 구성한다. - 홈, 게임 허브, 내 티어표, 즐겨찾기, 검색 결과 화면은 같은 카드 문법(상단 16:9 썸네일,
제목+좋아요1행,작성자+최종 수정일1행)을 공유하며, 데스크톱 기준 기본 4열 카드 그리드를 사용한다. /홈은 템플릿 선택 화면이 아니라공개 티어표 피드이며, 추천 티어표와 최신 공개 티어표를 같은 보드 카드 문법으로 보여준다./templates는 공개 템플릿 전용 화면이며, 템플릿 카드는 상단 메인 썸네일과주제명 + 작은 slug/id메타를 가진다.- 목록 계열 화면의 상단 도구 영역은 통계 카드와 액션 버튼을 공통 높이/반경으로 맞춰, 같은 라이브러리 대시보드로 읽히도록 정리한다.
주요 라우트/데이터 규칙
- 일반
GET /api/topics는 로그인한 관리자라도 공개 템플릿만 반환한다. - 관리자 전용 템플릿 목록은
GET /api/admin/templates를 사용하며, 비공개 템플릿까지 포함한다. - 홈 피드(
/)는GET /api/tierlists/public?q=...를 사용한다.featuredTierLists: 상단 추천 티어표tierLists: 추천 제외 최신 공개 티어표
- 홈, 템플릿, 나의 티어표, 즐겨찾기, 팔로우 피드 화면은 공통
viewToggle로그리드 / 리스트보기를 전환하며, 상태는 현재 라우트의?view=list쿼리로 반영한다. - 단, 모바일 브레이크포인트(
860px이하)에서는viewToggle을 노출하지 않는다. - 전역 단축키
S/ㄴ: 검색 포커스. 편집 화면에서는 아이템 검색창, 그 외 화면에서는 왼쪽 공통 검색창G/ㅎ: 그리드 보기L/ㅣ: 리스트 보기A/ㅁ: 관리자 계정일 때 관리자 화면으로 이동
- 설정의 가이드 모달은 좌우 화살표, 점 네비게이션, 좌측 단계 목록으로만 이동하고, 설명 영역은 최소 4줄 높이를 유지해 페이지별 높이 차이를 줄인다.
- 설정(
/profile) 화면은 상시 입력 폼보다현재 상태 요약 카드 + 필요 시 모달 편집흐름을 기본으로 한다. 닉네임과 비밀번호는 작은 액션 버튼으로만 모달을 열어 변경하고, 이메일은 현재 로그인 계정 정보로 읽기 전용 표시를 유지한다. 프로필 이미지는 아바타 원형 버튼 자체를 눌러 변경하며, 선택/삭제 시 즉시 자동 저장한다. - 이메일은 로그인 계정 식별자 역할을 하므로 현재 개인 설정 화면에서는 변경 기능을 제공하지 않는다.
- 닉네임 카드 본문에는 제한 설명을 상시 노출하지 않고, 변경 가능한 시점에만 아이콘 버튼을 보여준다. 제한 안내는 닉네임 변경 모달과 가이드 문구에서만 전달한다.
- 닉네임 변경 제한 기간은 기본 14일이지만, 서버 환경변수
NICKNAME_CHANGE_INTERVAL_MS또는NICKNAME_CHANGE_INTERVAL_DAYS로 조절할 수 있다.0이면 제한을 끈다. 인증 응답에는nicknameUpdatedAt,nicknameChangeAvailableAt,nicknameChangeIntervalMs,nicknameChangeIntervalLabel를 함께 포함하고, 프로필 저장 API는 제한 기간 안의 닉네임 변경 요청에nickname_change_locked오류를 반환한다. - 운영 환경 예시로는
.env.production에NICKNAME_CHANGE_INTERVAL_DAYS=20처럼 정수 일수를 넣어 주기를 바꾼다. - 왼쪽 공통 검색창은 현재 화면 범위만 검색한다.
- 홈: 전체 공개 티어표
- 템플릿: 공개 템플릿
- 나의 티어표: 내 저장 티어표
- 특정 주제 화면: 해당 주제의 공개 티어표
- 팔로우 피드: 팔로우한 작성자의 공개 티어표
- 즐겨찾기: 내가 즐겨찾기한 티어표
- 위 네 화면의 목록 데이터는 현재 페이지네이션이나 무한 스크롤 없이 조회 결과 전체를 한 번에 렌더링한다.
- 특정 주제 화면(
TopicHubView)의 헤더는 데이터 준비 전 문자열 fallback 대신 제목/설명 스켈레톤만 보여준다. - 저장된 티어표에는 댓글 스레드가 붙을 수 있다. 작성자 본인 편집 화면에서는
작업 팁아래, 작성자가 아닌 사용자의 보기 전용 화면에서는preview보드 아래에서 같은 댓글 카드를 사용한다. - 댓글 알림 메뉴는 좌측 사이드
댓글 관리로 노출하며, 읽지 않은 댓글이 하나라도 있으면 빨간 dot을 표시한다. - 댓글 관리(
/comments)는 기본적으로안 읽은 댓글만 보기를 켠 상태로 시작한다. - 댓글 정렬은 루트 댓글 최신순, 각 루트 내부의 답글은 오래된순을 기본 규칙으로 유지한다.
- 댓글 표시 밀도 제어를 위해 기본 노출 개수는 루트 댓글 10개, 각 루트의 답글 3개로 제한하고
더 보기버튼으로 추가 노출한다. - 댓글 관리 카드(
/comments)는 좌측16:9 썸네일 + 티어표 제목 + 템플릿 이름, 우측알림 제목 + 루트 댓글 정보 + 새 댓글/답글 정보의 2열 구조를 사용한다. - 댓글 관리 카드의 상단 우측 배지는 상태 라벨이 아니라 개별
읽음 처리액션으로 사용한다. - 티어표 즐겨찾기 API(
POST/DELETE /api/tierlists/:id/favorite)는 이미 존재하며, 보기 화면 우측 레일에는 이를 직접 호출하는 단독 CTA를 노출한다. - 티어표 즐겨찾기는 작성자 본인 저장 티어표에도 사용할 수 있다.
- 우측 패널
- 현재 화면 문맥에 맞는 설명, 빠른 액션, 계정 상태 같은 보조 정보를 배치한다.
- 에디터/관리자 세부 옵션은 후속 단계에서 이 패널로 점진 이관한다.
- 공통 토글 버튼은 패널이 닫혀 있을 때 중앙 헤더, 열려 있을 때 우측 헤더에 각각 아이콘만 표시하는 방식으로 동작한다.
- 오른쪽 패널 토글은 열기/닫기 모두
dock_to_left, 왼쪽 패널 토글은dock_to_right아이콘으로 통일한다. - 모바일에서는 중앙
workspaceHead오른쪽에 좌/우 패널 버튼을 함께 두고, 브랜드 타이틀을 터치하면 홈(/)으로 이동한다. - 좌우 레일의 주요 CTA는 스크롤되는 본문과 분리된 하단
56px액션 영역에 배치한다. - 하단 액션은 화면 바닥에 바로 붙지 않도록 푸터 내부에 추가 하단 여백을 둔다.
- 홈 화면 기준 우측 패널은 임시 정보 카드 여러 개보다 핵심 CTA 하나만 남겨, 시안처럼 단순한 보조 레일 역할을 우선 유지한다.
- 광고 영역은 상단 헤더와 시각적으로 너무 붙지 않도록
78px상단 여백을 두고, 하단 카피라이트는 중앙 정렬된 공통 footer로 표시한다. 카피라이트 링크는 다크/라이트 테마 모두에서 읽히도록 고정 민트색 대신 테마 텍스트 색과 굵기를 사용한다.
- 티어표 편집 화면
- 공통 우측 패널 대신 전용 로컬 편집 패널을 사용한다.
- 공통
workspaceBody카드 컨테이너를 벗기고, 중앙 보드 영역은 메인 컬럼에, 우측320px편집 패널은 공통 셸의 세 번째 컬럼 aside에 배치한다. - 공통 상단 토글 버튼은 Teleport로 이동한 로컬 편집 패널의 접힘/펼침 상태와도 연결되어, 우측 패널을 숨기면 중앙 보드 영역이 확장된다.
- 편집 모드와
preview=1뷰어 모드 모두 제목/보드/우측 패널 데이터가 준비되기 전까지는 실제 화면 대신 전용 스켈레톤 레이아웃을 먼저 보여준다. - 저장용 랜덤 제목은 내부 저장/도배 방지용으로만 쓰고, 화면 표시용 제목 fallback 에서는 내부
tierListId나 랜덤 문자열을 직접 노출하지 않는다. - 헤더 문법은 공통
pageHead흐름을 따르되, 편집 가능 상태에서는NEW/EDIT + 현재 템플릿 이름 + 작업 가이드, 보기 전용 상태에서는템플릿 이름 + 티어표 제목 + 티어표 설명규칙을 사용한다. - 제목, 설명, 대표 썸네일, 공개 여부, 저장/삭제/요청 액션을 우측 로컬 패널에 배치한다. 템플릿 등록/업데이트 요청 버튼은 저장된 티어표가 있을 때만 노출하며, 제목이 비어 있는 상태에서 저장하면 랜덤 고유 제목을 먼저 부여해 저장본을 만든다.
- 보기 전용 상태에서 상단 아이브로우(템플릿 이름)는 해당 주제 허브로 이동하는 액션으로 사용하며, 편집 중 미저장 변경이 있으면 이동 전에 확인 모달을 띄운다.
- 보드 바로 옆에는 드래그용 아이템 풀을 별도 패널로 두고, 커스텀 아이템 이름 정리 목록은 우측 편집 패널 내부에서 관리한다.
- 관리자 화면
- 공통 우측 패널 대신 전용 로컬 운영 패널을 사용한다.
- 공통
workspaceBody카드 컨테이너를 벗기고, 중앙 관리 목록은 메인 컬럼에, 우측 운영 패널은 공통 셸의 세 번째 컬럼 aside에 배치한다. - 우측 로컬 패널에는
게임/아이템/티어표/회원 관리탭, 검색, 필터, 새로고침, 빠른 작업 제어를 배치하고, 중앙 영역에는 실제 관리 대상 목록과 상세만 렌더링한다. 템플릿 요청 카드는 전체 티어표 카드와 같은 썸네일 좌측/정보 우측 구조를 따르며, 요청 미리보기와preview=1공유 프리뷰는 공통 앱 셸 안에서 일반 티어표 완성본과 같은 행·열 보드 문법으로 검수한다. - 상단 헤더에는 현재 탭 기준 요약 통계 카드를 배치해 운영 상태를 먼저 읽고, 각 관리 카드는 공통 대시보드 카드 문법(두꺼운 반경, 얕은 레이어 배경, 강조된 액션 버튼)을 공유한다.
DB 스키마
usersid: stringemail: stringnickname: stringnicknameUpdatedAt: numbernicknameChangeAvailableAt: numbernicknameChangeIntervalMs: numbernicknameChangeIntervalLabel: stringpasswordHash: stringemailVerified: booleanisAdmin: booleanavatarSrc: stringlastLoginAt: numbercreatedAt: number- 관리자 목록 집계 응답에서는
tierListCount,followerCount,receivedFavoriteCount,lastLoginAt,recentActivityAt도 함께 내려준다.
emailVerificationTokensid: stringuserId: stringtokenHash: stringexpiresAt: numberconsumedAt: numbercreatedAt: number
passwordResetTokensid: stringuserId: stringtokenHash: stringexpiresAt: numberconsumedAt: numbercreatedAt: number
topicsid: string- 서버가 자동 생성하는 내부 참조용 랜덤 ID이며, 공개 URL 노출값으로 직접 사용하지 않는다.
slug: string- 운영자가 지정/수정하는 공개 주소용 식별자이며, 영문 소문자/숫자/하이픈 조합만 허용한다.
name: stringthumbnailSrc: stringisPublic: booleandisplayRank: number | nullcreatedAt: number- 시스템 전용
freeform레코드는 홈 화면의직접 티어표 만들기저장 대상이며 일반 주제 목록에서는 숨긴다. 신규 빈 DB 초기화 시 자동 생성되는 템플릿은 이freeform한 건만 유지한다.
topicItemsid: stringtopicId: stringsrc: stringlabel: stringdisplayOrder: number | nullcreatedAt: number
customItemsid: stringownerId: stringsrc: stringlabel: stringcreatedAt: number
tierListsid: stringauthorId: stringtopicId: string- DB에는 내부
topics.id를 저장하고, API 응답에는 공개 경로용topicSlug도 함께 내려준다.
- DB에는 내부
title: stringthumbnailSrc: string- 사용자가 직접 지정하지 않으면 저장 시 티어표 대표 아이템 이미지로 자동 채운다.
description: stringisPublic: booleanisFeatured: booleanfeaturedAt: numberfeaturedBy: stringgroups:{ id, name, itemIds[] }[]pool:{ id, src, label, origin }[]createdAt: numberupdatedAt: number
favoriteTierListsuserId: stringtierListId: stringcreatedAt: number
userFollowsfollowerId: stringfollowingId: stringcreatedAt: number
favoriteTopicsuserId: stringtopicId: stringcreatedAt: number
tierListCommentsid: stringtierListId: stringauthorId: stringparentCommentId: stringcontent: stringcreatedAt: numberupdatedAt: number- 답글은 1단계까지만 허용한다.
commentNotificationsid: stringuserId: stringtierListId: stringcommentId: stringactorUserId: stringnotificationType:tierlist_comment | comment_replyisRead: booleanreadAt: numbercreatedAt: number- 기존 운영 DB에 예전 형태 테이블이 남아 있어도 서버 시작 시 스키마 보정으로 누락 컬럼을 자동 추가한다.
- 댓글 관리 카드 구성을 위해 조회 응답에는
parentCommentContent,parentCommentCreatedAt,parentAuthorName,parentAuthorAccountName,parentAuthorAvatarSrc를 함께 내려준다.
templateRequestsid: stringtype: stringrequesterId: stringsourceTierListId: string | nullsourceTopicId: stringtargetTopicId: stringstatus: stringsourceTierListTitle: stringsourceDescription: stringthumbnailSrc: stringitems:{ id, src, label, origin }[]snapshotGroups:{ id, name, itemIds[] }[]snapshotItems:{ id, src, label, origin }[]snapshotShowCharacterNames: booleancreatedAt: numberupdatedAt: number
주요 API
- 인증
POST /api/auth/signup- 첫 관리자 계정은 바로 로그인 세션을 만들고, 이후 일반 계정은 인증 메일 발송 후
verificationRequired상태를 반환한다.
- 첫 관리자 계정은 바로 로그인 세션을 만들고, 이후 일반 계정은 인증 메일 발송 후
POST /api/auth/login- 이메일 인증이 끝나지 않은 계정은
email_unverified로 차단한다.
- 이메일 인증이 끝나지 않은 계정은
POST /api/auth/logoutGET /api/auth/me- 로그인 세션이 살아 있는 사용자의
last_login_at을 주기적으로 갱신해, 회원 관리에서마지막 접속일을 따로 볼 수 있게 한다.
- 로그인 세션이 살아 있는 사용자의
GET /api/auth/metaPOST /api/auth/profilePOST /api/auth/password- 로그인한 사용자가 현재 비밀번호를 확인한 뒤 새 비밀번호로 직접 변경한다.
POST /api/auth/email/verifylogin?verifyToken=...링크에서 받은 토큰으로 이메일 인증을 완료하고 바로 로그인 세션을 만든다.- 인증 완료 직후 로그인 세션이 열리면서
last_login_at도 함께 갱신한다.
POST /api/auth/email/resend- 미인증 계정의 인증 메일을 다시 발송한다.
POST /api/auth/password-reset/request- 입력한 이메일로 비밀번호 재설정 링크를 발송한다.
POST /api/auth/password-reset/confirmlogin?resetToken=...링크의 토큰과 새 비밀번호로 비밀번호를 재설정하고 바로 로그인 세션을 만든다.- 재설정 완료 직후 로그인 세션이 열리면서
last_login_at도 함께 갱신한다.
- 주제
GET /api/topicsGET /api/topics/:topicId:topicId는 공개 URL에서는 보통slug를 받지만, 내부 ID를 넘겨도 같은 템플릿을 찾을 수 있게 서버가 레코드를 해석한다.
- 티어표
GET /api/tierlists/publicfeaturedTierLists와 일반 공개tierLists를 분리해서 반환한다.topicId에는 주제slug를 우선 전달하며,topicId없이q만 전달하면 전체 공개 티어표 검색에 사용한다.
GET /api/tierlists/meGET /api/tierlists/favorites/meGET /api/tierlists/:idGET /api/tierlists/:id/commentsPOST /api/tierlists/:id/commentsDELETE /api/tierlists/:id/comments/:commentIdPOST /api/tierlists/:id/template-requestPOST /api/tierlists/:id/favoriteDELETE /api/tierlists/:id/favoriteDELETE /api/tierlists/:idPOST /api/tierlists/thumbnailPOST /api/tierlists/custom-itemsPOST /api/tierlists
- 사용자/팔로우
GET /api/users/following-feed- 로그인한 사용자가 팔로우한 작성자의 공개 티어표를 최신 업데이트순으로 조회한다.
GET /api/users/:userId- 작성자 공개 프로필, 공개 티어표 수, 팔로워/팔로잉 수, 현재 로그인 사용자의 팔로우 여부를 반환한다.
GET /api/users/:userId/tierlists- 해당 작성자의 공개 티어표 목록을 반환한다.
POST /api/users/:userId/followDELETE /api/users/:userId/follow
- 댓글 알림
GET /api/comments/inbox- 알림 카드 렌더링을 위해 티어표 썸네일과 부모 댓글 내용도 함께 반환한다.
GET /api/comments/inbox/unread-countPOST /api/comments/inbox/read
- 관리자
POST /api/admin/templates- 요청 본문은
slug,name,isPublic,thumbnailSrc를 사용하고, 내부topics.id는 서버가 자동 생성한다.
- 요청 본문은
PATCH /api/admin/templates/:templateId- 내부 ID로 템플릿을 찾아
name,slug,isPublic을 수정한다.
- 내부 ID로 템플릿을 찾아
POST /api/admin/templates/:templateId/thumbnailPOST /api/admin/templates/:templateId/images- 여러 이미지를 한 번에 최대
100개까지 업로드할 수 있고, 별도 라벨이 없으면 파일명 기준으로 기본 아이템 이름을 만든다.
- 여러 이미지를 한 번에 최대
PATCH /api/admin/templates/:templateId/items/:itemIdGET /api/admin/tierlistssort=recent|created|favorites,minFavorites,topicId,q,page,limit으로 인기 티어표 후보를 정렬/필터링할 수 있다.
GET /api/admin/tierlists/stats- 현재 검색어/주제/최소 즐겨찾기 필터가 적용된 범위의 전체/공개/비공개/추천 수를 반환한다.
PATCH /api/admin/tierlists/:tierListId/featuredGET /api/admin/template-requestsPOST /api/admin/template-requests/:requestId/approvePOST /api/admin/template-requests/:requestId/rejectPOST /api/admin/template-requests/:requestId/link-templatePOST /api/admin/tierlists/:tierListId/promote-itemsPOST /api/admin/tierlists/:tierListId/create-templateGET /api/admin/custom-itemsfilter=library를 기본값으로 사용해 반복 사용 가능한템플릿 아이템 + 사용자 아이템만 먼저 보여주고,filter=thumbnail/filter=avatar로는 현재 참조 역할이 썸네일/프로필인 이미지를 따로 조회한다.filter=all|library|template|user|thumbnail|avatar|unused-user를 사용하며,filter=asset|unused-admin은 과거 UI 호환용으로만 유지한다.
POST /api/admin/custom-items/:itemId/promoteDELETE /api/admin/custom-items/:itemIdDELETE /api/admin/custom-itemsGET /api/admin/userssort=recent|lastLogin|created|tierlists|followers|favorites,direction=asc|desc로 회원을 콘텐츠 활동/마지막 접속/작성량/팔로워/받은 즐겨찾기 기준으로 정렬한다.
PATCH /api/admin/users/:userIdPATCH /api/admin/users/:userId/passwordDELETE /api/admin/users/:userIdDELETE /api/admin/templates/:templateId/items/:itemIdDELETE /api/admin/templates/:templateId
관리자 화면 메모
- 썸네일은 16:9 비율 미리보기 후
썸네일 적용버튼으로 실제 반영한다. - 게임 기본 아이템 추가는 드래그 앤 드롭 또는 다중 파일 선택으로 처리하고, 미리보기 카드에서 여러 장을 함께 확인할 수 있다.
- 현재 기본 아이템 목록에서는 등록된 아이템 이름을 직접 수정하고 저장할 수 있다.
- 기본 아이템 이름 저장 버튼은 값이 실제로 바뀐 경우에만 활성화된다.
- 아이템 미리보기는 반응형 환경에서도 최대
192px크기 안에서 표시한다. - 게임 전환 또는 업로드 성공 뒤에는 파일 입력값을 초기화해 같은 파일도 다시 선택할 수 있다.
- 게임 관리 탭에서는 홈 화면 상단에 먼저 노출할 게임을 최대 50개까지 지정하고, 드래그 또는 위/아래 버튼으로 순서를 저장할 수 있다.
- 사용자 아이템은 관리자 화면의 아이템 관리 탭에서 검색, 페이지네이션, 다운로드할 수 있다.
- 사용자 커스텀 아이템은 선택한 게임의 기본 템플릿으로 복제해 가져올 수 있다.
- 사용자 아이템은 사용 횟수(
usageCount)를 표시하며,미사용 아이템필터에서 저장 티어표/템플릿 참조가 더 이상 없는 항목만 개별/일괄 삭제할 수 있다. 사용자가 계정 탈퇴 등으로 삭제된 경우는custom_items.owner_id외래키로 레코드가 같이 사라지므로 보통미사용 아이템으로 남지 않는다. - 아이템 관리 기본 필터는
아이템(템플릿 + 사용자)이며, 우측 필터 순서는전체 이미지 → 아이템(템플릿 + 사용자) → 템플릿 아이템 → 사용자 아이템 → 썸네일 이미지 → 프로필 이미지 → 미사용 아이템을 사용한다. /uploads/assets/avatars/는프로필 아바타,/uploads/assets/tierlists/와/uploads/assets/topics/는썸네일 이미지, 그 외 보관 이미지는보관 자산배지로 표시한다. 최근처럼/uploads/assets/<파일명>.webp또는/uploads/assets/<앞2글자>/<파일명>.webp경로만 보고 종류를 알 수 없는 자산은 DB 참조(avatar_src,thumbnail_src,thumbnail_src_snapshot)를 역추적해 프로필/썸네일 여부를 분류한다. 실제 템플릿 기본 항목은템플릿 아이템, 사용자 커스텀 항목은사용자 아이템배지를 사용한다.- 같은 이미지
src가 해시 중복 재사용으로 템플릿 아이템/사용자 아이템과 프로필 아바타 또는 썸네일 자산에서 동시에 공유되더라도, 아바타/썸네일로 참조 중인src는 자산 카드도 함께 유지해프로필 이미지,썸네일 이미지,전체 이미지필터에서 누락되지 않게 한다. - 관리자 화면에는 별도
티어표 관리탭이 있으며, 내부에서템플릿 요청 관리 / 전체 티어표 관리를 분리해 볼 수 있고, 확인용 완성본은 탐색 UI 없는 preview 전용 모달로 연다. 전체 티어표 관리에서는 공개 티어표를추천 지정 / 추천 해제할 수 있고, 추천 지정된 티어표는 주제별 공개 목록 상단의추천 티어표섹션에 먼저 노출된다. 비공개 티어표는 추천 지정할 수 없고, 추천글을 비공개로 바꾸면 추천 상태도 함께 해제된다.전체 티어표 관리카드에는 받은 즐겨찾기 수를 함께 보여주며, 우측 운영 패널에서 즐겨찾기 많은 순 정렬과 최소 즐겨찾기 수 필터로 추천 후보를 좁힐 수 있다.티어표 관리탭의 추가 아이템은 작은 그리드 카드로 표시하고, 클릭 시기존 템플릿에 추가 / 새 템플릿 만들기모달을 통해 목적지를 선택한다.티어표 관리탭에서는 티어표 안의 커스텀 아이템을 개별 또는 일괄로 기존 게임 템플릿에 복제할 수 있다.freeform티어표는 관리자 화면에서 새 템플릿 slug/이름을 입력해 새로운 템플릿으로 복제 생성할 수 있다. 내부 ID는 서버가 자동 생성하므로 운영자가 직접 입력하지 않는다.- 관리자 티어표 관리 상단에는 사용자가 보낸 템플릿 등록/업데이트 요청 목록이 별도로 표시되며, 여기서 승인/반려를 바로 처리할 수 있다.
- 관리자 템플릿 요청 목록에서
반려 후 숨김을 누르면 해당 요청은 pending 목록에서 즉시 제외된다. - 관리자 화면에서는 회원 이메일/닉네임/권한 수정, 비밀번호 초기화, 계정 삭제가 가능하다.
- 단, 일반 운영자는 최고 관리자 계정의 프로필 이미지/회원 정보/비밀번호/삭제 버튼을 사용할 수 없고, 최고 관리자만 다른 관리자 권한을 변경할 수 있다.
- 관리자 회원 정보 수정은 운영상 필요한 경우 예약어 닉네임도 저장할 수 있지만, 일반 회원가입과 개인 프로필 수정에서는 운영자 사칭성 예약어 닉네임을 계속 차단한다.
- 회원 관리 카드에는 아바타, 작성 티어표 수, 팔로워 수, 받은 즐겨찾기 수, 최근 콘텐츠 활동, 마지막 접속일을 함께 표시한다.
- 운영자는 회원 목록을 작성 티어표 수뿐 아니라 팔로워 수와 받은 즐겨찾기 수 기준으로도 정렬할 수 있어, 핵심 작성자를 더 빠르게 찾을 수 있다.
- 마지막 접속일은 로그인/세션 확인 기준, 최근 콘텐츠 활동은 작성한 티어표의 마지막 수정일 기준으로 분리해서 보여준다. 따라서 장기 미접속 계정 정리 판단은 마지막 접속일을 우선 사용하고, 콘텐츠 기여가 최근인지 볼 때는 최근 콘텐츠 활동을 사용한다.
- 회원 카드의
프로필 보기버튼은 해당 회원의/users/:userId공개 프로필 화면으로 이동해, 팔로워/공개 티어표 현황을 관리자 화면 밖에서도 바로 확인할 수 있게 한다. - 회원 비밀번호를 운영자가 임의로 덮어쓰는 기능은 비상 상황용 API로만 유지하고, 일반 회원 관리 카드에서는 비밀번호 초기화 버튼과 모달을 숨긴다. 평소 사용자 비밀번호 변경은 이메일 재설정 메일과 설정 화면 직접 변경을 우선 사용한다.
티어표 접근 메모
new작성 경로는 로그인한 사용자만 진입할 수 있다.- 공유 링크로 여는
preview=1화면은뷰어 모드로 취급하며, 드래그/행열 편집/저장 같은 편집 UI 없이 완성본만 렌더링한다. - 비로그인 사용자나 작성자 본인이 아닌 로그인 사용자는 저장된 티어표를 기본적으로 뷰어 모드로 열람하며, 일반 편집 URL로 직접 진입해도 소유자가 아니면
preview=1주소로 자동 전환된다. - 비로그인 사용자도 뷰어 모드 우측 레일의
공유하기버튼으로 현재 공유 링크를 복사할 수 있다. - 로그인한 사용자는 뷰어 모드 우측 레일에서 저장된 티어표를 복사할 수 있고, 타인 티어표면
내 티어표로 복사, 본인 티어표면복사본 만들기문구를 사용한다. 작성자 본인은수정 모드로 전환도 사용할 수 있다. - 작성자 본인이 일반 편집 화면에서 저장된 본인 티어표를 보고 있을 때는 우측 패널의
뷰어 모드로 보기로 공유 화면 형태를 바로 확인할 수 있다. - 편집/뷰어 우측 패널의
작성자 프로필 보기로 해당 작성자의 공개 프로필과 공개 티어표 목록을 열 수 있고, 로그인 상태에서는 작성자 프로필에서 팔로우/언팔로우를 전환할 수 있다. /users/:userId공개 프로필 화면 상단 헤더는 고정 제목사용자 프로필과 안내 문구를 보여주고, 실제 닉네임/아바타는 본문 프로필 카드에서만 표시한다. 이메일 앞부분에서 파생된@accountName은 사용자가 직접 설정한 핸들이 아니므로 프로필 UI에 노출하지 않는다.- 같은
TierEditorView안에서topicId / tierListId / preview라우트 값만 바뀌어도 화면이 이전 티어표 데이터에 남지 않도록, 라우트 변경마다 에디터 상태를 다시 로드한다. - 복사한 티어표 상단의
원본링크를 누르면 원본 티어표로 이동하며, 편집 모드에서 저장하지 않은 변경이 있으면 이동 전에저장 없이 이동확인 모달을 먼저 띄운다. - 본인 티어표도 저장된 상태라면 편집/뷰어 우측 패널에서 복사본을 만들 수 있고, 편집 중 저장하지 않은 수정이 남아 있으면 복사 직전에 현재 수정본을 먼저 저장해 최신 상태 기준 복사본을 만든다.
- 비공개 티어표라도 관리자는 편집 화면에서 완성본을 열람할 수 있다.
- 공개 티어표는 목록과 상세 화면에서 즐겨찾기 토글 및 개수를 함께 표시한다.
- 카드형 목록에서는 즐겨찾기 수/상태만 표시하고, 실제 토글은 상세 화면에서 처리한다.
- 공개 티어표 목록은 현재 게임 기준으로 제목/작성자 검색을 지원한다.
- 주제별 공개 티어표 화면은 관리자 추천글을 상단
추천 티어표섹션으로 먼저 보여주고, 일반 공개 목록은 아래전체 공개 티어표섹션으로 분리해 중복 없이 렌더링한다. 추천 섹션은 최대 16개까지 표시한다. 내 즐겨찾기화면에서는 즐겨찾기한 순, 최신 업데이트순, 인기순 정렬을 제공한다.- 커스텀 이미지 추가는 다중 파일 선택과 드래그 앤 드롭을 모두 지원한다.
- 사용자가 직접 추가한 커스텀 아이템 이름은 편집 화면 우측 목록에서 정리할 수 있고, 저장 시 원본 커스텀 아이템 라벨과 함께 동기화된다.
- 티어 행은 기본 5단으로 시작하지만, 사용자가 직접 추가하거나 제거할 수 있다.
- 티어 행에 넣은 아이템은 작은 제거 버튼으로 다시 우측 아이템 풀로 되돌릴 수 있다.
- 편집 가능한 티어표에서는 아이템을 드래그로 옮길 수 있고, 아이템을 클릭해 선택한 뒤 원하는 셀이나 아이템 풀 빈 영역을 클릭하는 방식으로도 위치를 바꿀 수 있다.
- 클릭 배치 모드에서 선택된 아이템은 파란 포커스 테두리로 표시하며, 드래그를 시작하면 선택 상태를 해제하고 드래그 직후 짧은 클릭 입력은 무시해 드래그/클릭 조작이 섞이지 않게 한다.
- 보드 칸이나 미사용 아이템 풀의 아이템을 우클릭하면
아이템 복제메뉴가 열리고, 실행 시 같은 이미지/이름/출처를 가진 새 아이템 인스턴스를 미사용 풀 맨 앞에 추가한다. 복제본은dup-...형태의 새 ID를 쓰므로 원본과 복제본을 서로 다른 칸에 동시에 배치할 수 있다. - 기존 저장 티어표/복사본을 수정 가능한 상태로 다시 열 때는 저장본의 그룹/풀을 먼저 복원하고, 이후 현재 템플릿에 새로 추가된 기본 아이템만 미사용 풀 끝에 병합한다.
- 관리자 템플릿에서 삭제된 기본 아이템이라도 이미 저장된 티어표의 그룹/풀에 포함되어 있던 항목은 자동 제거하지 않고 그대로 보존한다.
- 신규 티어표의 공개 여부 기본값은
ON이며, 기존 티어표는 편집 화면과내 티어표목록에서 직접 삭제할 수 있다. - 제목이 비어 있는 상태로 저장하면 내부 제목은 현재 게임명을 기본값으로 사용한다.
- 제목 입력이 비어 있는 동안에는 무분별한 도배 방지를 위한 관리자 임의 삭제 가능성 안내 문구를 표시한다.
freeform티어표는 커스텀 아이템이 준비된 상태에서템플릿 등록 요청을 보낼 수 있다.템플릿 등록 요청전에는 체크리스트 모달로제목 직접 입력여부를 확인하고, 관리자가 식별하기 쉬운 게임 이름을 입력하도록 안내한다.- 신규 티어표를 막 저장한 직후에도, 템플릿 요청은 새로 발급된 실제 티어표 ID를 기준으로 이어서 처리한다.
- 기존 게임 티어표는 커스텀 아이템이 포함되어 있으면
템플릿 업데이트 요청을 보낼 수 있다. - 티어표는 편집 화면에서 16:9 썸네일 이미지를 별도로 선택해 저장할 수 있고, 목록 카드에서는 그 이미지를 상단 대표 썸네일로 사용한다.
- 티어표에 썸네일을 직접 지정하지 않으면 저장 시 대표 아이템 이미지 하나를 기본 썸네일로 자동 선택한다.
- 편집 화면 상단 헤더는 좌측 제목/설명, 우측 썸네일 카드 구조를 사용하며 모바일에서는 한 열로 접힌다.
- 티어표 편집 화면의 우측 패널은 공통
rightRail의localRightRailRoot에 직접 section들을 쌓는 구조이며, 별도 외곽 래퍼 카드 없이 공통 오른쪽 컬럼 문법을 그대로 따른다. - 뷰어 모드 우측 패널도 같은
localRightRailRoot를 사용하며, 상단에는 광고 블록을, 하단에는 공유/복사/수정 전환 액션 카드를 배치한다. - 공통 앱 셸은 좌측/중앙/우측 컬럼마다 높이
56px의 상단 헤더 블록을 유지하며, 중앙 헤더에는 사이트 타이틀Tier Maker와 현재 서비스 설명을 표시한다. - 티어표 편집기의 아이콘 기본 크기는
80px이며, 사용자가48 / 60 / 80 / 100 / 120단계로 즉시 조절할 수 있다. - 공개 티어표 목록과
내 티어표목록은 제목 옆에 작성자 아바타와 표시명을 함께 보여준다. - 작성자 아바타 이미지가 없을 경우 목록 썸네일 fallback은 닉네임이 아니라 계정명 기준 첫 글자를 사용한다.
- 티어표 목록 메타 정보는 최종 업데이트 시각만 간략하게 표시한다.
- 저장 성공 시에는 에디터 안에서 반투명 오버레이 기반 확인 모달을 띄우고, PNG export 이미지는 약
960px보드 폭과pixelRatio 1.5, 외곽 여백, 작성자/날짜 하단 메타를 포함해 생성한다. - 저장/삭제/가져오기 같은 사용자 행동 피드백은 전역 우측 상단 토스트로 표시한다.
- 전역 토스트는 동일 메시지/타입이 연속 발생하면 하나로 합쳐 카운트를 올리고, 종료 시 짧은 페이드아웃 애니메이션을 사용한다.
- 홈 게임 목록은 관리자 상단 고정 순서가 있으면 그 순서를 먼저 적용하고, 그 외 게임은 최근 생성순으로 뒤에 이어진다.
- 홈 주제 템플릿 목록의 실제 정렬 우선순위는
즐겨찾기 여부 → 관리자 수동 순서(displayRank) → 최신 생성순(createdAt DESC) → 이름순이다. 커스텀 티어표 만들기는 카드가 아니라 홈 우측 상단 버튼으로 진입한다.
업로드 제한 메모
- 프로필 아바타 업로드는 파일당 최대
3MB다. - 관리자 템플릿 썸네일/기본 아이템 업로드는 파일당 최대
20MB다. - 사용자 커스텀 이미지 업로드는 파일당 최대
6MB다. - 운영 프런트 Nginx는 다중 이미지 업로드 한 번의 요청 본문을 최대
1024MB까지 허용한다. - 현재는 업로드 전에 이미지 리사이즈/압축을 하지 않고 원본 파일을 그대로 저장한다.
운영 환경 변수
- 프런트엔드
VITE_API_ORIGIN: API 및 업로드 파일 절대 기준 주소
- 백엔드
DB_HOST: MariaDB 호스트DB_PORT: MariaDB 포트DB_USER: MariaDB 계정DB_PASSWORD: MariaDB 비밀번호DB_NAME: 데이터베이스명PORT: 서버 포트SESSION_SECRET: 세션 서명 키CORS_ORIGINS: 허용할 프런트 도메인 목록(쉼표 구분)TRUST_PROXY: 프록시 홉 수SESSION_COOKIE_SECURE:true면 HTTPS 전용 쿠키SESSION_COOKIE_SAME_SITE: 기본laxAPP_ORIGIN: 이메일 인증/비밀번호 재설정 링크를 만들 때 사용할 서비스 기준 주소SMTP_HOST: 메일 서버 호스트, Gmail SMTP 사용 시 보통smtp.gmail.comSMTP_PORT: 메일 서버 포트, Gmail SSL SMTP 기준 보통465SMTP_SECURE:true면 SMTP SSL/TLS 연결을 사용SMTP_USER: 발신용 Gmail 계정SMTP_PASS: Gmail 앱 비밀번호SMTP_FROM: 실제 메일 From 주소, 비워두면SMTP_USER를 기본값으로 사용한다
회원 인증 메모
- 첫 번째 가입 계정은 운영 초기 부트스트랩을 위해 이메일 인증 없이 바로 최고 관리자 계정으로 활성화한다.
- 두 번째 이후 일반 회원가입은 가입 직후 로그인 세션을 만들지 않고, 인증 메일 링크를 눌러
email_verified=1이 된 뒤에만 로그인할 수 있게 한다. - 인증 메일/비밀번호 재설정 메일 토큰은 원문을 DB에 저장하지 않고 SHA-256 해시만 저장하며, 새 토큰을 발급할 때는 같은 사용자의 이전 미사용 토큰을 먼저 만료 처리한다.
- 이메일 인증 토큰은 24시간, 비밀번호 재설정 토큰은 1시간 유효 기간을 사용한다.
- 비밀번호 재설정 링크로 새 비밀번호를 저장한 사용자는 같은 메일 주소를 확인한 것으로 보고, 기존에 미인증 상태였더라도 저장과 함께 이메일 인증을 완료 처리한다.
- 로그인한 상태로도
login?resetToken=...재설정 링크를 열 수 있으며, 이때는 기존 로그인 세션이 있어도 자동으로 내 티어표 화면으로 보내지 않고 새 비밀번호 입력 화면을 먼저 보여준다. - 설정 화면의 직접 비밀번호 변경은 현재 비밀번호가 맞는지 먼저 확인하고, 맞지 않으면
invalid_current_password로 차단한다.
운영 배포 메모
- 프로덕션 컴포즈 파일은 docker-compose.prod.yml이다.
- 외부 도메인
tmaker.sori.studio는frontend컨테이너의18080포트로 리버스 프록시하고,/api,/uploads,/health는 프런트 Nginx가 내부backend:5179로 전달한다. - 운영 볼륨은 MariaDB 데이터, 업로드 파일, 세션 파일을 각각 분리해 유지한다.
- MariaDB healthcheck는 NAS 첫 기동 지연을 고려해
root기준 ping과 긴start_period/retries를 사용한다.
NAS 배포 메모
- 현재 구조는 MariaDB/MySQL 계열이므로 NAS에 MariaDB를 올리면 phpMyAdmin 또는 Adminer로 직접 데이터 확인이 가능하다.
- 추천 구성:
- MariaDB 컨테이너 또는 NAS 패키지 설치
- phpMyAdmin 또는 Adminer 설치
- 앱은 환경변수로 해당 DB에 연결
로컬 개발 기준
- 기본 로컬 개발도
docker compose로 띄운 MariaDB를 사용한다. - 기본 백엔드 실행 스크립트
backend/package.json의dev,start는 로컬 MariaDB(127.0.0.1:3307) 기준으로 맞춰져 있다. backend/src/db.js는 MariaDB만 대상으로 동작하며, 파일 기반 fallback은 제거되었다.