From b2a838ff34310e56e80739c0899f59d23fdd39b6 Mon Sep 17 00:00:00 2001 From: zenn Date: Wed, 1 Apr 2026 11:15:49 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A6=B4=EB=A6=AC=EC=8A=A4:=20v1.3.12=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=20=EC=A0=95=EB=A0=AC=20=EB=B0=A9=ED=96=A5?= =?UTF-8?q?=EA=B3=BC=20=EC=9E=85=EB=A0=A5=20=EA=B8=B8=EC=9D=B4=20=ED=94=BC?= =?UTF-8?q?=EB=93=9C=EB=B0=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/db.js | 15 +++++--- backend/src/routes/admin.js | 3 +- docs/update.md | 5 +++ frontend/src/lib/api.js | 4 +-- frontend/src/views/AdminView.vue | 49 ++++++++++++++++++++++----- frontend/src/views/LoginView.vue | 10 +++--- frontend/src/views/ProfileView.vue | 4 +-- frontend/src/views/TierEditorView.vue | 15 ++++++-- 8 files changed, 81 insertions(+), 24 deletions(-) diff --git a/backend/src/db.js b/backend/src/db.js index 366e70a..f84abba 100644 --- a/backend/src/db.js +++ b/backend/src/db.js @@ -513,7 +513,7 @@ async function findPrimaryAdminUser() { return mapUserRow(rows[0]) } -async function listUsers({ queryText = '', sort = 'recent' } = {}) { +async function listUsers({ queryText = '', sort = 'recent', direction = 'desc' } = {}) { const where = [] const params = [] const trimmedQuery = typeof queryText === 'string' ? queryText.trim() : '' @@ -523,12 +523,19 @@ async function listUsers({ queryText = '', sort = 'recent' } = {}) { params.push(`%${trimmedQuery}%`, `%${trimmedQuery}%`) } + const isAsc = direction === 'asc' const orderBy = sort === 'created' - ? 'u.created_at DESC, recent_activity_at DESC, u.email ASC' + ? isAsc + ? 'u.created_at ASC, recent_activity_at ASC, u.email ASC' + : 'u.created_at DESC, recent_activity_at DESC, u.email ASC' : sort === 'tierlists' - ? 'tierlist_count DESC, recent_activity_at DESC, u.email ASC' - : 'recent_activity_at DESC, u.created_at ASC, u.email ASC' + ? isAsc + ? 'tierlist_count ASC, recent_activity_at ASC, u.email ASC' + : 'tierlist_count DESC, recent_activity_at DESC, u.email ASC' + : isAsc + ? 'recent_activity_at ASC, u.created_at ASC, u.email ASC' + : 'recent_activity_at DESC, u.created_at ASC, u.email ASC' const rows = await query( ` diff --git a/backend/src/routes/admin.js b/backend/src/routes/admin.js index 1306669..88e2065 100644 --- a/backend/src/routes/admin.js +++ b/backend/src/routes/admin.js @@ -565,12 +565,13 @@ router.get('/users', requireAdmin, async (req, res) => { const schema = z.object({ q: z.string().trim().max(120).optional().default(''), sort: z.enum(['recent', 'created', 'tierlists']).optional().default('recent'), + direction: z.enum(['asc', 'desc']).optional().default('desc'), }) const parsed = schema.safeParse(req.query) if (!parsed.success) return res.status(400).json({ error: 'bad_request' }) const [users, primaryAdmin] = await Promise.all([ - listUsers({ queryText: parsed.data.q, sort: parsed.data.sort }), + listUsers({ queryText: parsed.data.q, sort: parsed.data.sort, direction: parsed.data.direction }), findPrimaryAdminUser(), ]) res.json({ users: users.map((user) => decorateAdminUser(user, primaryAdmin)) }) diff --git a/docs/update.md b/docs/update.md index 997d786..986f99a 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,10 @@ # 업데이트 로그 +## 2026-04-01 v1.3.12 +- 관리자 회원 관리 상단에 정렬 방향 선택을 추가해, 최근 활동순·가입순·작성 티어표순을 각각 오름차순/내림차순으로 다시 볼 수 있게 확장함. +- 회원 정보 수정, 새 게임 생성, 비밀번호 초기화 모달은 Settings 톤 입력 스타일을 유지하면서 각 입력칸에 글자 수 피드백을 함께 보여주도록 정리함. +- 로그인, 설정, 티어 에디터 제목·설명·요청 제목·요청 설명·티어 행 이름에도 최대 길이와 현재 입력 길이 안내를 붙여, 제출 전에 제한을 바로 인지할 수 있게 개선함. + ## 2026-04-01 v1.3.11 - **회원 관리 편집 모달 전환**: 관리자 회원 카드를 읽기 전용 정보 카드로 바꾸고, `회원 정보 수정` 버튼으로 Settings 톤의 편집 모달에서 이메일/닉네임/운영자 권한을 저장하도록 재구성 - **회원 검색/정렬 추가**: 회원 관리 상단에 이메일/닉네임 검색과 `최근 활동순`, `가입순`, `작성 티어표 많은 순` 정렬을 추가해 운영자가 원하는 기준으로 목록을 다시 볼 수 있도록 확장 diff --git a/frontend/src/lib/api.js b/frontend/src/lib/api.js index 3c9b4ba..e65be02 100644 --- a/frontend/src/lib/api.js +++ b/frontend/src/lib/api.js @@ -62,8 +62,8 @@ export const api = { approveAdminTemplateRequest: (requestId, payload) => request(`/api/admin/template-requests/${encodeURIComponent(requestId)}/approve`, { method: 'POST', body: payload || {} }), rejectAdminTemplateRequest: (requestId) => request(`/api/admin/template-requests/${encodeURIComponent(requestId)}/reject`, { method: 'POST', body: {} }), - listAdminUsers: ({ q = '', sort = 'recent' } = {}) => - request(`/api/admin/users?q=${encodeURIComponent(q)}&sort=${encodeURIComponent(sort)}`), + listAdminUsers: ({ q = '', sort = 'recent', direction = 'desc' } = {}) => + request(`/api/admin/users?q=${encodeURIComponent(q)}&sort=${encodeURIComponent(sort)}&direction=${encodeURIComponent(direction)}`), updateAdminUser: (userId, payload) => request(`/api/admin/users/${encodeURIComponent(userId)}`, { method: 'PATCH', body: payload }), updateAdminUserPassword: (userId, payload) => diff --git a/frontend/src/views/AdminView.vue b/frontend/src/views/AdminView.vue index eeb6076..cbae610 100644 --- a/frontend/src/views/AdminView.vue +++ b/frontend/src/views/AdminView.vue @@ -64,6 +64,7 @@ const modalTargetCustomItem = ref(null) const users = ref([]) const userQuery = ref('') const userSort = ref('recent') +const userSortDirection = ref('desc') const imageStats = ref(null) const imageQueue = ref({ concurrency: 1, activeCount: 0, pendingCount: 0 }) const imageRecentJobs = ref([]) @@ -515,7 +516,7 @@ async function refreshTemplateRequests() { async function refreshUsers() { if (!auth.user?.isAdmin) return try { - const data = await api.listAdminUsers({ q: userQuery.value, sort: userSort.value }) + const data = await api.listAdminUsers({ q: userQuery.value, sort: userSort.value, direction: userSortDirection.value }) users.value = (data.users || []).map((user) => ({ ...user, isAvatarBusy: false, @@ -1589,6 +1590,10 @@ async function saveFeaturedOrder() { + @@ -1658,8 +1663,22 @@ async function saveFeaturedOrder() {
새 게임 만들기
게임 이름과 고유 ID를 입력한 뒤 생성하면 바로 아래 상세 관리 화면으로 이어집니다.
- - + +
@@ -1675,14 +1694,14 @@ async function saveFeaturedOrder() {
{{ modalTargetUser ? `${userDisplayName(modalTargetUser)} 계정에 설정할 새 비밀번호를 입력해주세요.` : '' }}
- +
@@ -2528,11 +2558,11 @@ async function saveFeaturedOrder() { border: 0; } .btn { + height: 100%; font-size: 12px; line-height: 1.2; white-space: nowrap; word-break: keep-all; - margin-top: 12px; padding: 11px 13px; border-radius: 14px; border: 1px solid rgba(255, 255, 255, 0.14); @@ -3110,6 +3140,7 @@ async function saveFeaturedOrder() { .userCard__actions--compact { grid-template-columns: auto auto minmax(0, 1fr); align-items: center; + margin-top: 12px; } .roleBadge { width: fit-content; diff --git a/frontend/src/views/LoginView.vue b/frontend/src/views/LoginView.vue index 1798eb7..991da1a 100644 --- a/frontend/src/views/LoginView.vue +++ b/frontend/src/views/LoginView.vue @@ -79,8 +79,8 @@ async function submit() {
첫 회원가입 계정은 자동으로 관리자 권한이 부여됩니다.
diff --git a/frontend/src/views/ProfileView.vue b/frontend/src/views/ProfileView.vue index 583a92f..db0b43f 100644 --- a/frontend/src/views/ProfileView.vue +++ b/frontend/src/views/ProfileView.vue @@ -156,8 +156,8 @@ async function logout() {
@@ -1438,6 +1445,10 @@ onUnmounted(() => { font-size: 12px; color: rgba(255, 255, 255, 0.64); } +.templateRequestDraft__hint { + font-size: 12px; + color: rgba(255, 255, 255, 0.46); +} .templateRequestDraft__input { width: 100%; padding: 14px 0;