릴리스: v1.3.12 회원 정렬 방향과 입력 길이 피드백
This commit is contained in:
@@ -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) =>
|
||||
|
||||
@@ -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() {
|
||||
<option value="created">가입순</option>
|
||||
<option value="tierlists">작성 티어표 많은 순</option>
|
||||
</select>
|
||||
<select v-model="userSortDirection" class="select toolbar__select" @change="submitUserFilters">
|
||||
<option value="desc">내림차순</option>
|
||||
<option value="asc">오름차순</option>
|
||||
</select>
|
||||
<button class="btn btn--ghost toolbar__button" type="button" @click="submitUserFilters">조회</button>
|
||||
</div>
|
||||
|
||||
@@ -1658,8 +1663,22 @@ async function saveFeaturedOrder() {
|
||||
<div class="modalCard__title">새 게임 만들기</div>
|
||||
<div class="modalCard__desc">게임 이름과 고유 ID를 입력한 뒤 생성하면 바로 아래 상세 관리 화면으로 이어집니다.</div>
|
||||
<div class="modalCard__form">
|
||||
<input v-model="newGameName" class="input" placeholder="게임 이름" />
|
||||
<input v-model="newGameId" class="input" placeholder="game id (영문/숫자)" @keydown.enter.prevent="createGame" />
|
||||
<label class="field">
|
||||
<span class="field__label">게임 이름</span>
|
||||
<input v-model="newGameName" class="field__input" maxlength="60" placeholder="게임 이름" />
|
||||
<span class="field__hint">{{ newGameName.length }}/60자</span>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="field__label">게임 ID</span>
|
||||
<input
|
||||
v-model="newGameId"
|
||||
class="field__input"
|
||||
maxlength="120"
|
||||
placeholder="game id (영문/숫자)"
|
||||
@keydown.enter.prevent="createGame"
|
||||
/>
|
||||
<span class="field__hint">영문, 숫자, 하이픈 조합 권장 · {{ newGameId.length }}/120자</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="modalCard__actions">
|
||||
<button class="btn btn--ghost" @click="closeGameCreateModal">취소</button>
|
||||
@@ -1675,14 +1694,14 @@ async function saveFeaturedOrder() {
|
||||
<div class="userEditForm">
|
||||
<label class="field">
|
||||
<span class="field__label">이메일</span>
|
||||
<input v-model="modalUserDraftEmail" class="field__input" placeholder="계정 이메일" />
|
||||
<span class="field__hint">로그인 계정으로 사용하는 이메일입니다.</span>
|
||||
<input v-model="modalUserDraftEmail" class="field__input" maxlength="255" placeholder="계정 이메일" />
|
||||
<span class="field__hint">로그인 계정으로 사용하는 이메일입니다. {{ modalUserDraftEmail.length }}/255자</span>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span class="field__label">닉네임</span>
|
||||
<input v-model="modalUserDraftNickname" class="field__input" placeholder="표시용 닉네임" />
|
||||
<span class="field__hint">티어표 작성자명과 프로필에 표시됩니다.</span>
|
||||
<input v-model="modalUserDraftNickname" class="field__input" maxlength="40" placeholder="표시용 닉네임" />
|
||||
<span class="field__hint">티어표 작성자명과 프로필에 표시됩니다. {{ modalUserDraftNickname.length }}/40자</span>
|
||||
</label>
|
||||
|
||||
<button
|
||||
@@ -1706,7 +1725,18 @@ async function saveFeaturedOrder() {
|
||||
<div class="modalCard__title">비밀번호 초기화</div>
|
||||
<div class="modalCard__desc">{{ modalTargetUser ? `${userDisplayName(modalTargetUser)} 계정에 설정할 새 비밀번호를 입력해주세요.` : '' }}</div>
|
||||
<div class="modalCard__form">
|
||||
<input v-model="modalPasswordDraft" class="input" type="password" placeholder="초기화할 비밀번호 입력" @keydown.enter.prevent="confirmUserPasswordReset" />
|
||||
<label class="field">
|
||||
<span class="field__label">새 비밀번호</span>
|
||||
<input
|
||||
v-model="modalPasswordDraft"
|
||||
class="field__input"
|
||||
type="password"
|
||||
maxlength="120"
|
||||
placeholder="초기화할 비밀번호 입력"
|
||||
@keydown.enter.prevent="confirmUserPasswordReset"
|
||||
/>
|
||||
<span class="field__hint">6~120자 권장 · {{ modalPasswordDraft.length }}/120자</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="modalCard__actions">
|
||||
<button class="btn btn--ghost" @click="closeUserPasswordModal">취소</button>
|
||||
@@ -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;
|
||||
|
||||
@@ -79,8 +79,8 @@ async function submit() {
|
||||
<form class="authFields" @submit.prevent="submit">
|
||||
<label class="field">
|
||||
<span class="field__label">이메일</span>
|
||||
<input v-model="email" class="field__input" placeholder="you@example.com" autocomplete="email" />
|
||||
<span class="field__hint">로그인과 알림에 사용되는 계정 이메일입니다.</span>
|
||||
<input v-model="email" class="field__input" placeholder="you@example.com" autocomplete="email" maxlength="255" />
|
||||
<span class="field__hint">로그인과 알림에 사용되는 계정 이메일입니다. {{ email.length }}/255자</span>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
@@ -91,8 +91,9 @@ async function submit() {
|
||||
type="password"
|
||||
placeholder="********"
|
||||
autocomplete="current-password"
|
||||
maxlength="120"
|
||||
/>
|
||||
<span class="field__hint">8자 이상으로 설정하면 더 안전하게 사용할 수 있어요.</span>
|
||||
<span class="field__hint">6~120자 입력 가능 · {{ password.length }}/120자</span>
|
||||
</label>
|
||||
|
||||
<label v-if="mode === 'signup'" class="field">
|
||||
@@ -103,8 +104,9 @@ async function submit() {
|
||||
type="password"
|
||||
placeholder="********"
|
||||
autocomplete="new-password"
|
||||
maxlength="120"
|
||||
/>
|
||||
<span class="field__hint">같은 비밀번호를 한 번 더 입력해주세요.</span>
|
||||
<span class="field__hint">같은 비밀번호를 한 번 더 입력해주세요. {{ passwordConfirm.length }}/120자</span>
|
||||
</label>
|
||||
|
||||
<div v-if="!hasUsers" class="roleBadge">첫 회원가입 계정은 자동으로 관리자 권한이 부여됩니다.</div>
|
||||
|
||||
@@ -156,8 +156,8 @@ async function logout() {
|
||||
<div class="settingsFields">
|
||||
<label class="field">
|
||||
<span class="field__label">닉네임</span>
|
||||
<input v-model="nickname" class="field__input" placeholder="작성자 닉네임" />
|
||||
<span class="field__hint">티어표 작성자 이름으로 표시됩니다.</span>
|
||||
<input v-model="nickname" class="field__input" maxlength="40" placeholder="작성자 닉네임" />
|
||||
<span class="field__hint">티어표 작성자 이름으로 표시됩니다. {{ nickname.length }}/40자</span>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
|
||||
@@ -794,10 +794,12 @@ onUnmounted(() => {
|
||||
<label class="templateRequestDraft__field">
|
||||
<span class="templateRequestDraft__label">요청 제목</span>
|
||||
<input v-model="templateRequestDraftTitle" class="templateRequestDraft__input" maxlength="80" placeholder="예: 템플릿 등록 요청" />
|
||||
<span class="templateRequestDraft__hint">{{ templateRequestDraftTitle.length }}/80자</span>
|
||||
</label>
|
||||
<label class="templateRequestDraft__field">
|
||||
<span class="templateRequestDraft__label">요청 설명</span>
|
||||
<textarea v-model="templateRequestDraftDescription" class="templateRequestDraft__input templateRequestDraft__textarea" maxlength="240" placeholder="예: 여름 이벤트 한정 캐릭터 추가용으로 신규 템플릿이 필요합니다." />
|
||||
<span class="templateRequestDraft__hint">{{ templateRequestDraftDescription.length }}/240자</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="modalCard__actions">
|
||||
@@ -823,10 +825,12 @@ onUnmounted(() => {
|
||||
<label class="templateRequestDraft__field">
|
||||
<span class="templateRequestDraft__label">요청 제목</span>
|
||||
<input v-model="templateRequestDraftTitle" class="templateRequestDraft__input" maxlength="80" placeholder="예: 템플릿 업데이트 요청" />
|
||||
<span class="templateRequestDraft__hint">{{ templateRequestDraftTitle.length }}/80자</span>
|
||||
</label>
|
||||
<label class="templateRequestDraft__field">
|
||||
<span class="templateRequestDraft__label">요청 설명</span>
|
||||
<textarea v-model="templateRequestDraftDescription" class="templateRequestDraft__input templateRequestDraft__textarea" maxlength="240" placeholder="예: 여름 이벤트 한정 캐릭터 추가" />
|
||||
<span class="templateRequestDraft__hint">{{ templateRequestDraftDescription.length }}/240자</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="modalCard__actions">
|
||||
@@ -928,7 +932,7 @@ onUnmounted(() => {
|
||||
열 삭제
|
||||
</button>
|
||||
<span class="grab" title="드래그로 순서 변경" data-group-handle>↕</span>
|
||||
<input v-model="g.name" class="groupName" :readonly="!canEdit" />
|
||||
<input v-model="g.name" class="groupName" maxlength="16" :readonly="!canEdit" />
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
@@ -1004,7 +1008,8 @@ onUnmounted(() => {
|
||||
<template v-if="globalRightRailOpen">
|
||||
<div class="editorSidebar__section">
|
||||
<div class="editorSidebar__label">Title</div>
|
||||
<input v-model="title" class="editorSidebar__input" placeholder="Title Text" :readonly="!canEdit" />
|
||||
<input v-model="title" class="editorSidebar__input" maxlength="120" placeholder="Title Text" :readonly="!canEdit" />
|
||||
<div class="editorSidebar__hint">{{ title.length }}/120자</div>
|
||||
<div v-if="untitledWarning" class="editorSidebar__hint editorSidebar__hint--warn">{{ untitledWarning }}</div>
|
||||
</div>
|
||||
|
||||
@@ -1014,8 +1019,10 @@ onUnmounted(() => {
|
||||
v-model="description"
|
||||
class="editorSidebar__textarea"
|
||||
placeholder="Description Text"
|
||||
maxlength="1000"
|
||||
:readonly="!canEdit"
|
||||
></textarea>
|
||||
<div class="editorSidebar__hint">{{ description.length }}/1000자</div>
|
||||
</div>
|
||||
|
||||
<div class="editorSidebar__section">
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user