diff --git a/backend/src/db.js b/backend/src/db.js index 1cf0a58..3b942ef 100644 --- a/backend/src/db.js +++ b/backend/src/db.js @@ -37,6 +37,8 @@ function mapUserRow(row) { isAdmin: !!row.is_admin, avatarSrc: row.avatar_src || '', createdAt: Number(row.created_at), + tierListCount: Number(row.tierlist_count || 0), + recentActivityAt: Number(row.recent_activity_at || row.created_at || 0), } } @@ -326,9 +328,24 @@ async function updateUserProfile({ id, nickname, avatarSrc }) { } async function listUsers() { - const rows = await query( - 'SELECT id, email, nickname, is_admin, avatar_src, created_at FROM users ORDER BY created_at ASC, email ASC' - ) + const rows = await query(` + SELECT + u.id, + u.email, + u.nickname, + u.is_admin, + u.avatar_src, + u.created_at, + COUNT(t.id) AS tierlist_count, + GREATEST( + u.created_at, + COALESCE(MAX(t.updated_at), 0) + ) AS recent_activity_at + FROM users u + LEFT JOIN tierlists t ON t.author_id = u.id + GROUP BY u.id, u.email, u.nickname, u.is_admin, u.avatar_src, u.created_at + ORDER BY recent_activity_at DESC, u.created_at ASC, u.email ASC + `) return rows.map(mapUserRow) } diff --git a/docs/history.md b/docs/history.md index 9ff955a..0605760 100644 --- a/docs/history.md +++ b/docs/history.md @@ -1,5 +1,9 @@ # 의사결정 이력 +## 2026-03-27 v0.1.46 +- 티어표 편집 중 등급 행에 넣은 아이템도 다시 제외할 수 있어야 배치 실험이 쉬우므로, 별도 제거 버튼으로 아이템 풀로 되돌리는 흐름을 제공하기로 결정했다. +- 관리자 회원 관리는 수정 기능만으로는 부족하므로, 운영 판단에 바로 필요한 아바타, 작성 티어표 수, 최근 활동 시각을 함께 보여주기로 했다. + ## 2026-03-27 v0.1.45 - 카드형 목록의 별표는 개수 표시로 읽히기 쉬우므로, 목록에서는 상태/개수만 보여주고 실제 즐겨찾기 토글은 상세 화면에서 처리하는 편이 오해가 적다고 정리했다. - 토스트는 같은 메시지가 짧게 반복될 때 누적 표시가 더 낫고, 종료도 급격히 끊기지 않도록 페이드아웃을 넣는 편이 사용자 경험상 자연스럽다고 판단했다. diff --git a/docs/spec.md b/docs/spec.md index 1fbb59d..e3900eb 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -125,6 +125,7 @@ - `티어표 관리` 탭에서는 티어표 안의 커스텀 아이템을 개별 또는 일괄로 기존 게임 템플릿에 복제할 수 있다. - `freeform` 티어표는 관리자 화면에서 새 게임 ID/이름을 입력해 새로운 게임 템플릿으로 복제 생성할 수 있다. - 관리자 화면에서는 회원 이메일/닉네임/권한 수정, 비밀번호 초기화, 계정 삭제가 가능하다. +- 회원 관리 카드에는 아바타, 작성 티어표 수, 최근 활동 시각을 함께 표시한다. ## 티어표 접근 메모 - `new` 작성 경로는 로그인한 사용자만 진입할 수 있다. @@ -136,6 +137,7 @@ - `내 즐겨찾기` 화면에서는 즐겨찾기한 순, 최신 업데이트순, 인기순 정렬을 제공한다. - 커스텀 이미지 추가는 다중 파일 선택과 드래그 앤 드롭을 모두 지원한다. - 티어 행은 기본 5단으로 시작하지만, 사용자가 직접 추가하거나 제거할 수 있다. +- 티어 행에 넣은 아이템은 작은 제거 버튼으로 다시 우측 아이템 풀로 되돌릴 수 있다. - 신규 티어표의 공개 여부 기본값은 `ON`이며, 기존 티어표는 편집 화면과 `내 티어표` 목록에서 직접 삭제할 수 있다. - 제목이 비어 있는 상태로 저장하면 내부 제목은 현재 게임명을 기본값으로 사용한다. - 제목 입력이 비어 있는 동안에는 무분별한 도배 방지를 위한 관리자 임의 삭제 가능성 안내 문구를 표시한다. diff --git a/docs/todo.md b/docs/todo.md index b648eac..f33631e 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -1,7 +1,6 @@ # 할 일 및 이슈 ## 즉시 확인 필요 -- 회원 관리에서 아바타/작성 티어표 수/최근 활동 같은 보조 정보는 아직 표시하지 않는다. - 미사용 커스텀 이미지 일괄 삭제는 현재 "참조가 없는 항목" 기준이며, 보관 기간 정책 같은 운영 규칙은 아직 없다. - 업로드 이미지는 현재 원본 파일을 그대로 저장하므로, 운영 부담이 커지면 서버 저장 전 리사이즈/압축(예: 긴 변 제한, WebP 변환) 도입이 필요하다. - 관리자 기본 아이템 다중 업로드는 현재 파일명 기반 자동 라벨만 지원하므로, 필요하면 업로드 후 일괄 라벨 수정/정렬 UX를 추가 검토한다. @@ -10,7 +9,7 @@ - 관리자 티어표 관리의 템플릿 생성은 현재 `freeform`만 직접 지원하므로, 필요하면 일반 게임 티어표의 전체 아이템을 복제한 파생 템플릿 생성 UX도 검토한다. - 관리자 티어표 관리의 추가 아이템 승격은 현재 커스텀(origin=`custom`) 아이템 기준이므로, 필요하면 “기존 게임 아이템과 비교한 차집합” 기준으로 더 정교하게 확장할 수 있다. - 즐겨찾기는 현재 `내 즐겨찾기` 목록과 정렬까지 지원하므로, 필요하면 폴더 분류나 메모 같은 개인 정리 기능을 추가 검토한다. -- 전역 토스트는 기본 시간 기반 자동 종료만 지원하므로, 필요하면 중복 합치기나 액션 링크 포함 형태로 확장할 수 있다. +- 전역 토스트는 중복 합치기와 페이드아웃까지 지원하므로, 필요하면 액션 링크나 수동 고정(pin) 같은 상호작용 확장을 검토한다. - 공개 티어표 검색은 현재 게임별 허브 안에서만 제공하므로, 필요하면 홈 전역 통합 검색도 검토한다. - 즐겨찾기 토글은 현재 상세 화면 중심이므로, 필요하면 카드 목록에서도 안전한 보조 인터랙션(예: 길게 누르기, 별도 메뉴)을 검토한다. diff --git a/docs/update.md b/docs/update.md index c960baa..cce3cc9 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,9 @@ # 업데이트 로그 +## 2026-03-27 v0.1.46 +- **티어 행 아이템 제거 추가**: 티어표 편집 화면에서 이미 등급 행에 넣은 아이템도 작은 제거 버튼으로 다시 아이템 풀로 빼낼 수 있도록 보강 +- **회원 관리 보조 정보 확장**: 관리자 회원 관리 카드에 아바타, 작성 티어표 수, 최근 활동 시각을 함께 표시해 운영 판단에 필요한 정보를 바로 확인할 수 있도록 개선 + ## 2026-03-27 v0.1.45 - **즐겨찾기 카드 액션 보정**: 카드형 목록에서는 별표를 클릭 액션이 아닌 상태/개수 표시로만 보여주고, 실제 즐겨찾기 토글은 상세 화면에서 처리하도록 조정 - **토스트 중복/페이드아웃 개선**: 같은 메시지 토스트는 하나로 합치고 카운트를 올리도록 변경했으며, 사라질 때는 짧은 페이드아웃 애니메이션을 적용 diff --git a/frontend/src/views/AdminView.vue b/frontend/src/views/AdminView.vue index 0e98e28..65e4789 100644 --- a/frontend/src/views/AdminView.vue +++ b/frontend/src/views/AdminView.vue @@ -706,6 +706,18 @@ function fmt(ts) { }) } +function userAvatarUrl(user) { + return user?.avatarSrc ? toApiUrl(user.avatarSrc) : '' +} + +function userDisplayName(user) { + return user?.nickname || user?.email?.split('@')[0] || '알 수 없음' +} + +function userAvatarFallback(user) { + return (user?.email?.trim()?.[0] || '?').toUpperCase() +} + function addFeaturedGame(gameId) { resetMessages() if (!gameId || featuredGameIds.value.includes(gameId)) return @@ -1120,15 +1132,32 @@ async function saveFeaturedOrder() {