511 lines
23 KiB
Vue
511 lines
23 KiB
Vue
<script setup>
|
|
import { computed, ref } from 'vue'
|
|
|
|
const props = defineProps({
|
|
summary: {
|
|
type: Object,
|
|
required: true,
|
|
},
|
|
users: {
|
|
type: Array,
|
|
required: true,
|
|
},
|
|
selectedUserId: {
|
|
type: Number,
|
|
default: null,
|
|
},
|
|
userDetail: {
|
|
type: Object,
|
|
default: null,
|
|
},
|
|
recentLogins: {
|
|
type: Array,
|
|
required: true,
|
|
},
|
|
busy: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
message: {
|
|
type: String,
|
|
default: '',
|
|
},
|
|
actionBusyUserId: {
|
|
type: Number,
|
|
default: null,
|
|
},
|
|
detailBusy: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
})
|
|
|
|
const emit = defineEmits([
|
|
'select-user',
|
|
'toggle-user-status',
|
|
'revoke-user-sessions',
|
|
'delete-user',
|
|
])
|
|
|
|
const userSearch = ref('')
|
|
const userStatusFilter = ref('all')
|
|
const userSort = ref('lastLoginDesc')
|
|
|
|
function formatDate(value) {
|
|
if (!value) {
|
|
return '기록 없음'
|
|
}
|
|
|
|
const date = new Date(value)
|
|
|
|
if (Number.isNaN(date.getTime())) {
|
|
return '기록 없음'
|
|
}
|
|
|
|
return new Intl.DateTimeFormat('ko-KR', {
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
}).format(date)
|
|
}
|
|
|
|
function getPlannerSummary(payload) {
|
|
if (!payload || typeof payload !== 'object') {
|
|
return {
|
|
comment: '코멘트 없음',
|
|
taskCount: 0,
|
|
completedCount: 0,
|
|
memoCount: 0,
|
|
}
|
|
}
|
|
|
|
const tasks = Array.isArray(payload.tasks) ? payload.tasks : []
|
|
const memo = Array.isArray(payload.memo) ? payload.memo : []
|
|
const activeTasks = tasks.filter((task) => task?.title?.trim?.())
|
|
|
|
return {
|
|
comment: payload.comment?.trim?.() || '코멘트 없음',
|
|
taskCount: activeTasks.length,
|
|
completedCount: activeTasks.filter((task) => task.checked).length,
|
|
memoCount: memo.filter((item) => item?.text?.trim?.() || item?.label?.trim?.()).length,
|
|
}
|
|
}
|
|
|
|
const filteredUsers = computed(() => {
|
|
const search = userSearch.value.trim().toLowerCase()
|
|
const filtered = props.users.filter((user) => {
|
|
const matchesSearch = !search
|
|
|| String(user.id).includes(search)
|
|
|| user.nickname?.toLowerCase().includes(search)
|
|
|| user.email?.toLowerCase().includes(search)
|
|
|
|
if (!matchesSearch) {
|
|
return false
|
|
}
|
|
|
|
switch (userStatusFilter.value) {
|
|
case 'active':
|
|
return !user.disabledAt && user.isActiveRecently
|
|
case 'disabled':
|
|
return Boolean(user.disabledAt)
|
|
case 'unverified':
|
|
return !user.emailVerifiedAt
|
|
case 'admin':
|
|
return user.role === 'admin'
|
|
case 'member':
|
|
return user.role !== 'admin'
|
|
default:
|
|
return true
|
|
}
|
|
})
|
|
|
|
return [...filtered].sort((left, right) => {
|
|
switch (userSort.value) {
|
|
case 'plannerDesc':
|
|
return right.plannerEntryCount - left.plannerEntryCount || right.id - left.id
|
|
case 'goalDesc':
|
|
return right.goalCount - left.goalCount || right.id - left.id
|
|
case 'createdDesc':
|
|
return new Date(right.createdAt || 0).getTime() - new Date(left.createdAt || 0).getTime() || right.id - left.id
|
|
case 'nicknameAsc':
|
|
return String(left.nickname || '').localeCompare(String(right.nickname || ''), 'ko')
|
|
case 'lastLoginDesc':
|
|
default:
|
|
return new Date(right.lastLoginAt || right.createdAt || 0).getTime()
|
|
- new Date(left.lastLoginAt || left.createdAt || 0).getTime()
|
|
|| right.id - left.id
|
|
}
|
|
})
|
|
})
|
|
|
|
const filteredUsersSummary = computed(() => {
|
|
const total = filteredUsers.value.length
|
|
const disabled = filteredUsers.value.filter((user) => user.disabledAt).length
|
|
const active = filteredUsers.value.filter((user) => !user.disabledAt && user.isActiveRecently).length
|
|
|
|
return { total, disabled, active }
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<section class="grid gap-6">
|
|
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-5">
|
|
<article class="rounded-[28px] border border-white/60 bg-white/80 p-6">
|
|
<p class="text-[11px] font-bold uppercase tracking-[0.24em] text-stone-500">Total Users</p>
|
|
<p class="mt-4 text-4xl font-semibold tracking-[-0.06em] text-stone-900">{{ summary.totalUsers }}</p>
|
|
<p class="mt-2 text-sm font-semibold text-stone-500">현재 가입된 전체 계정 수</p>
|
|
</article>
|
|
|
|
<article class="rounded-[28px] border border-white/60 bg-white/80 p-6">
|
|
<p class="text-[11px] font-bold uppercase tracking-[0.24em] text-stone-500">Active 30 Days</p>
|
|
<p class="mt-4 text-4xl font-semibold tracking-[-0.06em] text-stone-900">{{ summary.activeUsers30d }}</p>
|
|
<p class="mt-2 text-sm font-semibold text-stone-500">최근 30일 안에 접속한 사용자</p>
|
|
</article>
|
|
|
|
<article class="rounded-[28px] border border-white/60 bg-white/80 p-6">
|
|
<p class="text-[11px] font-bold uppercase tracking-[0.24em] text-stone-500">Planner Entries</p>
|
|
<p class="mt-4 text-4xl font-semibold tracking-[-0.06em] text-stone-900">{{ summary.totalPlannerEntries }}</p>
|
|
<p class="mt-2 text-sm font-semibold text-stone-500">전체 작성된 날짜 문서 수</p>
|
|
</article>
|
|
|
|
<article class="rounded-[28px] border border-white/60 bg-white/80 p-6">
|
|
<p class="text-[11px] font-bold uppercase tracking-[0.24em] text-stone-500">Verified / Admin</p>
|
|
<p class="mt-4 text-3xl font-semibold tracking-[-0.06em] text-stone-900">{{ summary.verifiedUsers }} / {{ summary.totalAdmins }}</p>
|
|
<p class="mt-2 text-sm font-semibold text-stone-500">인증 완료 계정 수 / 관리자 수</p>
|
|
</article>
|
|
|
|
<article class="rounded-[28px] border border-white/60 bg-white/80 p-6">
|
|
<p class="text-[11px] font-bold uppercase tracking-[0.24em] text-stone-500">Disabled Users</p>
|
|
<p class="mt-4 text-4xl font-semibold tracking-[-0.06em] text-stone-900">{{ summary.disabledUsers ?? 0 }}</p>
|
|
<p class="mt-2 text-sm font-semibold text-stone-500">관리자가 비활성화한 계정 수</p>
|
|
</article>
|
|
</div>
|
|
|
|
<div class="grid gap-6 xl:grid-cols-[320px_minmax(0,1fr)]">
|
|
<aside class="rounded-[28px] border border-white/60 bg-white/75 p-6">
|
|
<p class="text-[11px] font-bold uppercase tracking-[0.24em] text-stone-500">운영 요약</p>
|
|
<div class="mt-5 space-y-4">
|
|
<div class="rounded-2xl border border-stone-200 bg-white px-4 py-4">
|
|
<p class="text-[10px] font-bold tracking-[0.18em] text-stone-500">최근 7일 신규 가입</p>
|
|
<p class="mt-2 text-2xl font-semibold tracking-[-0.05em] text-stone-900">{{ summary.newUsers7d }}명</p>
|
|
</div>
|
|
<div class="rounded-2xl border border-stone-200 bg-white px-4 py-4">
|
|
<p class="text-[10px] font-bold tracking-[0.18em] text-stone-500">전체 목표 수</p>
|
|
<p class="mt-2 text-2xl font-semibold tracking-[-0.05em] text-stone-900">{{ summary.totalGoals }}개</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-6">
|
|
<p class="text-[11px] font-bold uppercase tracking-[0.24em] text-stone-500">최근 접속</p>
|
|
<div class="mt-4 space-y-3">
|
|
<article
|
|
v-for="user in recentLogins"
|
|
:key="`recent-${user.id}`"
|
|
class="rounded-2xl border border-stone-200 bg-white px-4 py-4"
|
|
>
|
|
<div class="flex items-center justify-between gap-3">
|
|
<p class="text-sm font-semibold text-stone-900">{{ user.nickname }}</p>
|
|
<span class="rounded-full bg-stone-100 px-2 py-1 text-[10px] font-bold tracking-[0.12em] text-stone-600">
|
|
{{ user.role }}
|
|
</span>
|
|
</div>
|
|
<p class="mt-1 text-xs font-semibold text-stone-500">{{ user.email }}</p>
|
|
<p class="mt-3 text-[11px] font-semibold text-stone-600">{{ formatDate(user.lastLoginAt) }}</p>
|
|
</article>
|
|
<p v-if="recentLogins.length === 0" class="rounded-2xl border border-dashed border-stone-300 bg-white/80 px-4 py-4 text-sm font-semibold text-stone-500">
|
|
아직 접속 기록이 없습니다.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<section class="rounded-[28px] border border-white/60 bg-white/75 p-6">
|
|
<div class="flex items-center justify-between gap-4">
|
|
<div>
|
|
<p class="text-[11px] font-bold uppercase tracking-[0.24em] text-stone-500">사용자 목록</p>
|
|
<p class="mt-2 text-sm font-semibold text-stone-500">최종 접속일, 작성 문서 수, 목표 수를 한 번에 볼 수 있습니다.</p>
|
|
</div>
|
|
<div v-if="busy" class="rounded-full bg-stone-900 px-4 py-2 text-[11px] font-bold tracking-[0.16em] text-white">
|
|
불러오는 중...
|
|
</div>
|
|
</div>
|
|
|
|
<p
|
|
v-if="message"
|
|
class="mt-4 rounded-2xl border border-stone-300 bg-white/80 px-4 py-3 text-sm font-semibold leading-6 text-stone-700"
|
|
>
|
|
{{ message }}
|
|
</p>
|
|
|
|
<div class="mt-5 grid gap-3 rounded-[24px] border border-stone-200 bg-[#fcfaf6] p-4 md:grid-cols-[minmax(0,1.2fr)_180px_180px]">
|
|
<label class="grid gap-2">
|
|
<span class="text-[10px] font-bold uppercase tracking-[0.18em] text-stone-500">검색</span>
|
|
<input
|
|
v-model="userSearch"
|
|
type="text"
|
|
placeholder="닉네임, 이메일, ID 검색"
|
|
class="h-11 rounded-2xl border border-stone-200 bg-white px-4 text-sm font-semibold text-stone-700 outline-none transition placeholder:text-stone-400 focus:border-stone-400"
|
|
/>
|
|
</label>
|
|
|
|
<label class="grid gap-2">
|
|
<span class="text-[10px] font-bold uppercase tracking-[0.18em] text-stone-500">상태 필터</span>
|
|
<select
|
|
v-model="userStatusFilter"
|
|
class="h-11 rounded-2xl border border-stone-200 bg-white px-4 text-sm font-semibold text-stone-700 outline-none transition focus:border-stone-400"
|
|
>
|
|
<option value="all">전체 사용자</option>
|
|
<option value="active">최근 활동</option>
|
|
<option value="disabled">비활성화</option>
|
|
<option value="unverified">미인증</option>
|
|
<option value="member">일반 사용자</option>
|
|
<option value="admin">관리자</option>
|
|
</select>
|
|
</label>
|
|
|
|
<label class="grid gap-2">
|
|
<span class="text-[10px] font-bold uppercase tracking-[0.18em] text-stone-500">정렬</span>
|
|
<select
|
|
v-model="userSort"
|
|
class="h-11 rounded-2xl border border-stone-200 bg-white px-4 text-sm font-semibold text-stone-700 outline-none transition focus:border-stone-400"
|
|
>
|
|
<option value="lastLoginDesc">최근 접속순</option>
|
|
<option value="plannerDesc">문서 많은 순</option>
|
|
<option value="goalDesc">목표 많은 순</option>
|
|
<option value="createdDesc">최근 가입순</option>
|
|
<option value="nicknameAsc">이름순</option>
|
|
</select>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="mt-4 flex flex-wrap items-center gap-2 text-[11px] font-bold tracking-[0.12em] text-stone-500">
|
|
<span class="rounded-full bg-white px-3 py-2">표시 {{ filteredUsersSummary.total }}명</span>
|
|
<span class="rounded-full bg-white px-3 py-2">활동 {{ filteredUsersSummary.active }}명</span>
|
|
<span class="rounded-full bg-white px-3 py-2">비활성 {{ filteredUsersSummary.disabled }}명</span>
|
|
</div>
|
|
|
|
<div class="mt-5 overflow-hidden rounded-[24px] border border-stone-200 bg-white">
|
|
<div class="hidden grid-cols-[84px_minmax(0,1.2fr)_110px_150px_90px_90px_150px_190px] gap-3 border-b border-stone-200 bg-[#f8f4ed] px-5 py-4 text-[10px] font-bold uppercase tracking-[0.18em] text-stone-500 xl:grid">
|
|
<span>ID</span>
|
|
<span>사용자</span>
|
|
<span>권한</span>
|
|
<span>최종 접속</span>
|
|
<span>문서 수</span>
|
|
<span>목표 수</span>
|
|
<span>상태</span>
|
|
<span>관리</span>
|
|
</div>
|
|
|
|
<div class="divide-y divide-stone-200">
|
|
<article
|
|
v-for="user in filteredUsers"
|
|
:key="user.id"
|
|
class="px-5 py-4"
|
|
>
|
|
<div class="grid gap-3 xl:grid-cols-[84px_minmax(0,1.2fr)_110px_150px_90px_90px_150px_190px] xl:items-center">
|
|
<p class="text-xs font-bold tracking-[0.14em] text-stone-500">#{{ user.id }}</p>
|
|
<div>
|
|
<p class="text-sm font-semibold text-stone-900">{{ user.nickname }}</p>
|
|
<p class="mt-1 text-xs font-semibold text-stone-500">{{ user.email }}</p>
|
|
<button
|
|
type="button"
|
|
class="mt-2 text-[11px] font-bold tracking-[0.12em] text-stone-500 underline underline-offset-4 transition hover:text-stone-900"
|
|
@click="emit('select-user', user)"
|
|
>
|
|
{{ selectedUserId === user.id ? '상세 닫기' : '상세 보기' }}
|
|
</button>
|
|
</div>
|
|
<p class="text-sm font-semibold text-stone-700">{{ user.role }}</p>
|
|
<p class="text-sm font-semibold text-stone-700">{{ formatDate(user.lastLoginAt) }}</p>
|
|
<p class="text-sm font-semibold text-stone-700">{{ user.plannerEntryCount }}개</p>
|
|
<p class="text-sm font-semibold text-stone-700">{{ user.goalCount }}개</p>
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<span
|
|
class="rounded-full px-3 py-1 text-[10px] font-bold tracking-[0.14em]"
|
|
:class="user.disabledAt ? 'bg-rose-100 text-rose-700' : user.isActiveRecently ? 'bg-emerald-100 text-emerald-700' : 'bg-stone-100 text-stone-500'"
|
|
>
|
|
{{ user.disabledAt ? '비활성화' : user.isActiveRecently ? '활동 중' : '휴면 가능성' }}
|
|
</span>
|
|
<span
|
|
class="rounded-full px-3 py-1 text-[10px] font-bold tracking-[0.14em]"
|
|
:class="user.emailVerifiedAt ? 'bg-sky-100 text-sky-700' : 'bg-amber-100 text-amber-700'"
|
|
>
|
|
{{ user.emailVerifiedAt ? '이메일 인증' : '미인증' }}
|
|
</span>
|
|
<span
|
|
class="rounded-full px-3 py-1 text-[10px] font-bold tracking-[0.14em]"
|
|
:class="user.activeSessionCount > 0 ? 'bg-violet-100 text-violet-700' : 'bg-stone-100 text-stone-500'"
|
|
>
|
|
세션 {{ user.activeSessionCount }}개
|
|
</span>
|
|
</div>
|
|
<div class="flex flex-wrap items-center gap-2 xl:justify-end">
|
|
<button
|
|
v-if="user.role !== 'admin'"
|
|
type="button"
|
|
class="rounded-full border px-3 py-2 text-[10px] font-bold tracking-[0.14em] transition"
|
|
:class="user.disabledAt ? 'border-emerald-300 text-emerald-700 hover:bg-emerald-50' : 'border-amber-300 text-amber-700 hover:bg-amber-50'"
|
|
:disabled="actionBusyUserId === user.id"
|
|
@click="emit('toggle-user-status', user)"
|
|
>
|
|
{{ user.disabledAt ? '다시 허용' : '비활성화' }}
|
|
</button>
|
|
<button
|
|
v-if="user.role !== 'admin'"
|
|
type="button"
|
|
class="rounded-full border border-stone-300 px-3 py-2 text-[10px] font-bold tracking-[0.14em] text-stone-700 transition hover:bg-stone-100"
|
|
:disabled="actionBusyUserId === user.id"
|
|
@click="emit('revoke-user-sessions', user)"
|
|
>
|
|
강제 로그아웃
|
|
</button>
|
|
<button
|
|
v-if="user.role !== 'admin'"
|
|
type="button"
|
|
class="rounded-full border border-rose-300 px-3 py-2 text-[10px] font-bold tracking-[0.14em] text-rose-700 transition hover:bg-rose-50"
|
|
:disabled="actionBusyUserId === user.id"
|
|
@click="emit('delete-user', user)"
|
|
>
|
|
삭제
|
|
</button>
|
|
<span
|
|
v-if="actionBusyUserId === user.id"
|
|
class="text-[10px] font-bold tracking-[0.12em] text-stone-400"
|
|
>
|
|
처리 중...
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
|
|
<div
|
|
v-if="!busy && filteredUsers.length === 0"
|
|
class="px-5 py-10 text-center text-sm font-semibold text-stone-500"
|
|
>
|
|
조건에 맞는 사용자가 없습니다.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<section class="border-t border-stone-200 bg-[#fcfaf6] px-5 py-5">
|
|
<div class="flex items-center justify-between gap-4">
|
|
<div>
|
|
<p class="text-[11px] font-bold uppercase tracking-[0.24em] text-stone-500">사용자 상세</p>
|
|
<p class="mt-2 text-sm font-semibold text-stone-500">선택한 사용자의 최근 플래너 기록과 목표를 확인합니다.</p>
|
|
</div>
|
|
<div
|
|
v-if="detailBusy"
|
|
class="rounded-full bg-stone-900 px-4 py-2 text-[11px] font-bold tracking-[0.16em] text-white"
|
|
>
|
|
불러오는 중...
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-if="userDetail?.user"
|
|
class="mt-5 grid gap-5 xl:grid-cols-[minmax(0,1.15fr)_minmax(320px,0.85fr)]"
|
|
>
|
|
<div class="rounded-[24px] border border-stone-200 bg-white p-5">
|
|
<div class="flex items-center justify-between gap-4">
|
|
<div>
|
|
<p class="text-lg font-semibold text-stone-900">{{ userDetail.user.nickname }}</p>
|
|
<p class="mt-1 text-sm font-semibold text-stone-500">{{ userDetail.user.email }}</p>
|
|
</div>
|
|
<div class="flex flex-wrap justify-end gap-2">
|
|
<span class="rounded-full bg-stone-100 px-3 py-1 text-[10px] font-bold tracking-[0.14em] text-stone-600">{{ userDetail.user.role }}</span>
|
|
<span
|
|
class="rounded-full px-3 py-1 text-[10px] font-bold tracking-[0.14em]"
|
|
:class="userDetail.user.disabledAt ? 'bg-rose-100 text-rose-700' : 'bg-emerald-100 text-emerald-700'"
|
|
>
|
|
{{ userDetail.user.disabledAt ? '비활성화' : '사용 가능' }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-5 grid gap-3 md:grid-cols-3">
|
|
<div class="rounded-2xl border border-stone-200 bg-[#fbf7f0] px-4 py-4">
|
|
<p class="text-[10px] font-bold tracking-[0.16em] text-stone-500">최근 로그인</p>
|
|
<p class="mt-2 text-sm font-semibold text-stone-800">{{ formatDate(userDetail.user.lastLoginAt) }}</p>
|
|
</div>
|
|
<div class="rounded-2xl border border-stone-200 bg-[#fbf7f0] px-4 py-4">
|
|
<p class="text-[10px] font-bold tracking-[0.16em] text-stone-500">문서 / 목표</p>
|
|
<p class="mt-2 text-sm font-semibold text-stone-800">{{ userDetail.user.plannerEntryCount }} / {{ userDetail.user.goalCount }}</p>
|
|
</div>
|
|
<div class="rounded-2xl border border-stone-200 bg-[#fbf7f0] px-4 py-4">
|
|
<p class="text-[10px] font-bold tracking-[0.16em] text-stone-500">활성 세션</p>
|
|
<p class="mt-2 text-sm font-semibold text-stone-800">{{ userDetail.user.activeSessionCount }}개</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-6">
|
|
<p class="text-[11px] font-bold uppercase tracking-[0.2em] text-stone-500">최근 플래너 기록</p>
|
|
<div class="mt-3 space-y-3">
|
|
<article
|
|
v-for="entry in userDetail.plannerEntries"
|
|
:key="entry.entryDate"
|
|
class="rounded-2xl border border-stone-200 bg-[#fffdfa] px-4 py-4"
|
|
>
|
|
<div class="flex items-center justify-between gap-3">
|
|
<p class="text-sm font-semibold text-stone-900">{{ entry.entryDate }}</p>
|
|
<p class="text-[11px] font-semibold text-stone-500">{{ formatDate(entry.updatedAt) }}</p>
|
|
</div>
|
|
<p class="mt-3 text-sm font-semibold leading-6 text-stone-700">{{ getPlannerSummary(entry.payload).comment }}</p>
|
|
<div class="mt-3 flex flex-wrap gap-2 text-[11px] font-bold tracking-[0.12em] text-stone-500">
|
|
<span>작업 {{ getPlannerSummary(entry.payload).taskCount }}개</span>
|
|
<span>완료 {{ getPlannerSummary(entry.payload).completedCount }}개</span>
|
|
<span>메모 {{ getPlannerSummary(entry.payload).memoCount }}개</span>
|
|
</div>
|
|
</article>
|
|
<p
|
|
v-if="userDetail.plannerEntries.length === 0"
|
|
class="rounded-2xl border border-dashed border-stone-300 bg-white px-4 py-4 text-sm font-semibold text-stone-500"
|
|
>
|
|
아직 작성한 플래너 기록이 없습니다.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="rounded-[24px] border border-stone-200 bg-white p-5">
|
|
<p class="text-[11px] font-bold uppercase tracking-[0.2em] text-stone-500">목표 목록</p>
|
|
<div class="mt-3 space-y-3">
|
|
<article
|
|
v-for="goal in userDetail.goals"
|
|
:key="goal.id"
|
|
class="rounded-2xl border border-stone-200 bg-[#fffdfa] px-4 py-4"
|
|
>
|
|
<div class="flex items-center justify-between gap-3">
|
|
<p class="text-sm font-semibold text-stone-900">{{ goal.title }}</p>
|
|
<span class="rounded-full border border-stone-200 px-3 py-1 text-[10px] font-bold tracking-[0.12em] text-stone-500">
|
|
{{ goal.targetDate }}
|
|
</span>
|
|
</div>
|
|
<div class="mt-3 flex flex-wrap gap-2 text-[11px] font-bold tracking-[0.12em] text-stone-500">
|
|
<span>표시 시작 {{ goal.activeFrom || '미설정' }}</span>
|
|
<span>표시 종료 {{ goal.activeUntil || '미설정' }}</span>
|
|
</div>
|
|
</article>
|
|
<p
|
|
v-if="userDetail.goals.length === 0"
|
|
class="rounded-2xl border border-dashed border-stone-300 bg-white px-4 py-4 text-sm font-semibold text-stone-500"
|
|
>
|
|
등록된 목표가 없습니다.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-else
|
|
class="mt-5 rounded-[24px] border border-dashed border-stone-300 bg-white px-5 py-10 text-center text-sm font-semibold text-stone-500"
|
|
>
|
|
사용자 목록에서 `상세 보기`를 눌러 기록과 목표를 확인해 주세요.
|
|
</div>
|
|
</section>
|
|
</section>
|
|
</div>
|
|
</section>
|
|
</template>
|