v0.1.43 - 플래너 라벨과 가이드 동작 정리
This commit is contained in:
523
src/App.vue
523
src/App.vue
@@ -2,6 +2,7 @@
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
import AdminDashboard from './components/AdminDashboard.vue'
|
||||
import AuthDialog from './components/AuthDialog.vue'
|
||||
import GuideTooltip from './components/GuideTooltip.vue'
|
||||
import GoalsDashboard from './components/GoalsDashboard.vue'
|
||||
import MiniCalendar from './components/MiniCalendar.vue'
|
||||
import PlannerPage from './components/PlannerPage.vue'
|
||||
@@ -28,6 +29,8 @@ import {
|
||||
restorePlannerUiState,
|
||||
} from './lib/plannerStorage'
|
||||
|
||||
const GUIDE_TOOLTIP_STORAGE_KEY = 'ten-minute-guide-tooltips-hidden'
|
||||
const DDAY_DISABLED_STORAGE_KEY = 'ten-minute-dday-disabled-dates'
|
||||
const screenMode = ref('planner')
|
||||
const viewMode = ref('focus')
|
||||
const printLayout = ref('single')
|
||||
@@ -76,6 +79,10 @@ const profileBusy = ref(false)
|
||||
const passwordBusy = ref(false)
|
||||
const profileMessage = ref('')
|
||||
const passwordMessage = ref('')
|
||||
const carryoverMessage = ref('')
|
||||
const guideTooltipResetMessage = ref('')
|
||||
const hiddenGuideTooltips = ref(readHiddenGuideTooltips())
|
||||
const ddayDisabledDateKeys = ref(readDdayDisabledDateKeys())
|
||||
const adminBusy = ref(false)
|
||||
const adminMessage = ref('')
|
||||
const adminOverview = ref({
|
||||
@@ -103,6 +110,84 @@ let isHydratingRemoteRecords = false
|
||||
const syncTimers = new Map()
|
||||
let syncToastTimer = null
|
||||
|
||||
function readHiddenGuideTooltips() {
|
||||
if (typeof window === 'undefined') {
|
||||
return []
|
||||
}
|
||||
|
||||
try {
|
||||
const value = JSON.parse(window.localStorage.getItem(GUIDE_TOOLTIP_STORAGE_KEY) ?? '[]')
|
||||
return Array.isArray(value) ? value : []
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function persistHiddenGuideTooltips() {
|
||||
if (typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
window.localStorage.setItem(GUIDE_TOOLTIP_STORAGE_KEY, JSON.stringify(hiddenGuideTooltips.value))
|
||||
}
|
||||
|
||||
function isGuideTooltipVisible(id) {
|
||||
return !hiddenGuideTooltips.value.includes(id)
|
||||
}
|
||||
|
||||
function dismissGuideTooltip(id) {
|
||||
if (hiddenGuideTooltips.value.includes(id)) {
|
||||
return
|
||||
}
|
||||
|
||||
hiddenGuideTooltips.value = [...hiddenGuideTooltips.value, id]
|
||||
guideTooltipResetMessage.value = ''
|
||||
persistHiddenGuideTooltips()
|
||||
}
|
||||
|
||||
function resetGuideTooltips() {
|
||||
hiddenGuideTooltips.value = []
|
||||
guideTooltipResetMessage.value = '가이드 툴팁을 다시 볼 수 있게 했습니다.'
|
||||
persistHiddenGuideTooltips()
|
||||
}
|
||||
|
||||
function readDdayDisabledDateKeys() {
|
||||
if (typeof window === 'undefined') {
|
||||
return []
|
||||
}
|
||||
|
||||
try {
|
||||
const value = JSON.parse(window.localStorage.getItem(DDAY_DISABLED_STORAGE_KEY) ?? '[]')
|
||||
return Array.isArray(value) ? value : []
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function persistDdayDisabledDateKeys() {
|
||||
if (typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
window.localStorage.setItem(DDAY_DISABLED_STORAGE_KEY, JSON.stringify(ddayDisabledDateKeys.value))
|
||||
}
|
||||
|
||||
function isDdayDisabledForDate(dateKey) {
|
||||
return ddayDisabledDateKeys.value.includes(dateKey)
|
||||
}
|
||||
|
||||
function setDdayDisabledForDate(dateKey, disabled) {
|
||||
if (disabled) {
|
||||
if (!ddayDisabledDateKeys.value.includes(dateKey)) {
|
||||
ddayDisabledDateKeys.value = [...ddayDisabledDateKeys.value, dateKey]
|
||||
}
|
||||
} else {
|
||||
ddayDisabledDateKeys.value = ddayDisabledDateKeys.value.filter((key) => key !== dateKey)
|
||||
}
|
||||
|
||||
persistDdayDisabledDateKeys()
|
||||
}
|
||||
|
||||
function createEmptyTimetable() {
|
||||
return Array.from({ length: timetableCellCount }, () => false)
|
||||
}
|
||||
@@ -379,9 +464,12 @@ const activePlannerGoals = computed(() =>
|
||||
}),
|
||||
)
|
||||
const plannerGoal = computed(() => activePlannerGoals.value[0] ?? null)
|
||||
const plannerGoalToggleOn = computed(() => Boolean(plannerGoal.value) && planner.value.goalEnabled)
|
||||
const selectedDateKey = computed(() => toKey(selectedDate.value))
|
||||
const plannerGoalToggleOn = computed(() =>
|
||||
Boolean(plannerGoal.value) && !isDdayDisabledForDate(selectedDateKey.value),
|
||||
)
|
||||
const plannerDday = computed(() => {
|
||||
if (!planner.value.goalEnabled || !plannerGoal.value) {
|
||||
if (!plannerGoalToggleOn.value || !plannerGoal.value) {
|
||||
return ''
|
||||
}
|
||||
|
||||
@@ -394,7 +482,7 @@ const plannerDday = computed(() => {
|
||||
return `${badge} ${plannerGoal.value.title}`
|
||||
})
|
||||
const showPlannerDday = computed(() =>
|
||||
planner.value.goalEnabled && Boolean(plannerGoal.value),
|
||||
plannerGoalToggleOn.value && Boolean(plannerGoal.value),
|
||||
)
|
||||
const hasActiveGoalForSelectedDate = computed(() => Boolean(plannerGoal.value))
|
||||
|
||||
@@ -404,6 +492,9 @@ const filledTasks = computed(() =>
|
||||
const completedTasks = computed(() =>
|
||||
filledTasks.value.filter((task) => task.checked).length,
|
||||
)
|
||||
const incompleteTasks = computed(() =>
|
||||
planner.value.tasks.filter((task) => task.title.trim() && !task.checked),
|
||||
)
|
||||
const completionRate = computed(() => {
|
||||
if (filledTasks.value.length === 0) {
|
||||
return 0
|
||||
@@ -451,6 +542,7 @@ function shiftDate(amount) {
|
||||
next.setDate(next.getDate() + amount)
|
||||
selectedDate.value = next
|
||||
calendarViewDate.value = new Date(next)
|
||||
carryoverMessage.value = ''
|
||||
}
|
||||
|
||||
function shiftCalendarMonth(amount) {
|
||||
@@ -468,6 +560,7 @@ function shiftCalendarYear(amount) {
|
||||
function selectDate(date) {
|
||||
selectedDate.value = new Date(date)
|
||||
calendarViewDate.value = new Date(date)
|
||||
carryoverMessage.value = ''
|
||||
}
|
||||
|
||||
function updateComment(record, value) {
|
||||
@@ -480,8 +573,12 @@ function updateGoalEnabled(record, value) {
|
||||
return
|
||||
}
|
||||
|
||||
setDdayDisabledForDate(selectedDateKey.value, !value)
|
||||
record.goalEnabled = value
|
||||
schedulePlannerSyncForRecord(record)
|
||||
|
||||
if (hasPlannerContent(record)) {
|
||||
schedulePlannerSyncForRecord(record)
|
||||
}
|
||||
}
|
||||
|
||||
function updateTaskLabel(record, { index, value }) {
|
||||
@@ -499,6 +596,59 @@ function toggleTask(record, index) {
|
||||
schedulePlannerSyncForRecord(record)
|
||||
}
|
||||
|
||||
function clearTasks(record, indexes) {
|
||||
indexes.forEach((index) => {
|
||||
if (!record.tasks[index]) {
|
||||
return
|
||||
}
|
||||
|
||||
record.tasks[index].title = ''
|
||||
record.tasks[index].checked = false
|
||||
})
|
||||
schedulePlannerSyncForRecord(record)
|
||||
}
|
||||
|
||||
function carryIncompleteTasksToNextDay() {
|
||||
const tasksToCarry = incompleteTasks.value.map((task) => ({
|
||||
label: task.label,
|
||||
title: task.title,
|
||||
checked: false,
|
||||
}))
|
||||
|
||||
if (tasksToCarry.length === 0) {
|
||||
carryoverMessage.value = '이월할 미완료 항목이 없습니다.'
|
||||
return
|
||||
}
|
||||
|
||||
const targetRecord = secondaryPlanner.value
|
||||
const emptyIndexes = targetRecord.tasks
|
||||
.map((task, index) => ({ task, index }))
|
||||
.filter(({ task }) => !task.title.trim())
|
||||
.map(({ index }) => index)
|
||||
const copyCount = Math.min(tasksToCarry.length, emptyIndexes.length)
|
||||
|
||||
if (copyCount === 0) {
|
||||
carryoverMessage.value = '다음 날짜에 비어 있는 할 일 칸이 없습니다.'
|
||||
return
|
||||
}
|
||||
|
||||
for (let index = 0; index < copyCount; index += 1) {
|
||||
const task = tasksToCarry[index]
|
||||
targetRecord.tasks[emptyIndexes[index]] = {
|
||||
...task,
|
||||
checked: false,
|
||||
}
|
||||
}
|
||||
|
||||
schedulePlannerSyncForRecord(targetRecord)
|
||||
|
||||
const nextDateLabel = createDateLabel(toKey(secondaryDate.value))
|
||||
carryoverMessage.value =
|
||||
copyCount === tasksToCarry.length
|
||||
? `${nextDateLabel} 빈칸에 미완료 ${copyCount}개를 이월했습니다.`
|
||||
: `${nextDateLabel} 빈칸 ${copyCount}개까지만 이월했습니다.`
|
||||
}
|
||||
|
||||
function updateMemo(record, { index, value }) {
|
||||
record.memo[index].text = value
|
||||
schedulePlannerSyncForRecord(record)
|
||||
@@ -698,9 +848,7 @@ const prevSnapshotItems = computed(() => {
|
||||
|
||||
const readNextItems = computed(() => {
|
||||
const nextDayFirstTask = secondaryPlanner.value.tasks.find((task) => task.title.trim())
|
||||
const incompleteTasks = planner.value.tasks.filter((task) => task.title.trim() && !task.checked)
|
||||
const carryTask = incompleteTasks[0]?.title
|
||||
const todayComment = planner.value.comment.trim()
|
||||
const carryTask = incompleteTasks.value[0]?.title
|
||||
|
||||
return [
|
||||
nextDayFirstTask
|
||||
@@ -708,12 +856,9 @@ const readNextItems = computed(() => {
|
||||
: carryTask
|
||||
? `내일 이어갈 첫 작업: ${carryTask}`
|
||||
: '내일 첫 작업은 아직 비어 있습니다.',
|
||||
incompleteTasks.length > 0
|
||||
? `미완료 ${incompleteTasks.length}개를 이어서 볼 수 있습니다.`
|
||||
: '오늘 미완료 작업은 없습니다.',
|
||||
todayComment
|
||||
? `오늘 코멘트 이어보기: ${todayComment}`
|
||||
: '오늘 코멘트는 아직 비어 있습니다.',
|
||||
incompleteTasks.value.length > 0
|
||||
? `아직 처리되지 않은 할 일이 ${incompleteTasks.value.length}개 있습니다.`
|
||||
: '오늘 할 일은 모두 처리되었습니다.',
|
||||
]
|
||||
})
|
||||
|
||||
@@ -795,8 +940,8 @@ watch(
|
||||
watch(
|
||||
[selectedDate, plannerGoal],
|
||||
() => {
|
||||
if (!plannerGoal.value && planner.value.goalEnabled) {
|
||||
planner.value.goalEnabled = false
|
||||
if (plannerGoal.value && planner.value.goalEnabled === false && !isDdayDisabledForDate(selectedDateKey.value)) {
|
||||
planner.value.goalEnabled = true
|
||||
|
||||
if (hasPlannerContent(planner.value)) {
|
||||
schedulePlannerSyncForRecord(planner.value)
|
||||
@@ -1546,7 +1691,6 @@ onBeforeUnmount(() => {
|
||||
<p class="text-[10px] font-bold tracking-[0.16em] text-stone-500">SIGNED IN</p>
|
||||
<p class="mt-2 text-lg font-semibold tracking-[-0.03em] text-stone-900">{{ currentUser.nickname }}</p>
|
||||
<p class="mt-1 text-sm font-semibold text-stone-500">{{ currentUser.email }}</p>
|
||||
<p class="mt-2 text-[11px] font-bold tracking-[0.16em] text-stone-400">{{ currentUser.role === 'admin' ? 'ADMIN' : 'USER' }}</p>
|
||||
<button
|
||||
type="button"
|
||||
class="mt-4 w-full rounded-full border border-stone-200 px-4 py-3 text-xs font-bold tracking-[0.16em] text-stone-600 transition hover:border-stone-400 hover:text-ink"
|
||||
@@ -1738,7 +1882,6 @@ onBeforeUnmount(() => {
|
||||
<p class="text-[10px] font-bold tracking-[0.16em] text-stone-500">SIGNED IN</p>
|
||||
<p class="mt-2 text-lg font-semibold tracking-[-0.03em] text-stone-900">{{ currentUser.nickname }}</p>
|
||||
<p class="mt-1 text-sm font-semibold text-stone-500">{{ currentUser.email }}</p>
|
||||
<p class="mt-2 text-[11px] font-bold tracking-[0.16em] text-stone-400">{{ currentUser.role === 'admin' ? 'ADMIN' : 'USER' }}</p>
|
||||
<button
|
||||
type="button"
|
||||
class="mt-4 w-full rounded-full border border-stone-200 px-4 py-3 text-xs font-bold tracking-[0.16em] text-stone-600 transition hover:border-stone-400 hover:text-ink"
|
||||
@@ -1872,18 +2015,16 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
<div class="mx-auto flex max-w-3xl flex-col gap-6 text-center">
|
||||
<div class="space-y-3">
|
||||
<p class="text-[11px] font-bold uppercase tracking-[0.28em] text-stone-500">Members Only</p>
|
||||
<p class="text-[11px] font-bold uppercase tracking-[0.28em] text-stone-500">회원 전용 플래너</p>
|
||||
<h2 class="text-3xl font-semibold tracking-[-0.05em] text-stone-900 sm:text-4xl">
|
||||
로그인 후 플래너를 작성하고<br>
|
||||
클라우드에 안전하게 저장하세요
|
||||
10 Minute Planner
|
||||
</h2>
|
||||
<p class="mx-auto max-w-2xl text-sm leading-7 text-stone-600 sm:text-base">
|
||||
이제 플래너는 사용자 계정 기준으로 문서와 통계가 연결됩니다. 로그인하지 않은 상태에서는 데이터가 섞일 수 있어
|
||||
작성 화면을 열지 않도록 변경했습니다.
|
||||
로그인 후 나만의 10분 플래너를 작성하고,<br>날짜별 기록과 통계를 안전하게 이어가세요.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 rounded-[28px] border border-stone-200 bg-[#fbf7f0] p-5 text-left sm:grid-cols-3">
|
||||
<div class="grid gap-4 rounded-[28px] border border-stone-200 bg-[#fbf7f0] p-5 text-left">
|
||||
<article class="rounded-2xl border border-stone-200 bg-white px-4 py-5">
|
||||
<p class="text-[10px] font-bold tracking-[0.18em] text-stone-500">ACCOUNT</p>
|
||||
<p class="mt-3 text-sm font-semibold leading-6 text-stone-800">
|
||||
@@ -1962,6 +2103,7 @@ onBeforeUnmount(() => {
|
||||
@update:task-label="updateTaskLabel(planner, $event)"
|
||||
@update:task-title="updateTaskTitle(planner, $event)"
|
||||
@toggle:task="toggleTask(planner, $event)"
|
||||
@clear:tasks="clearTasks(planner, $event)"
|
||||
@update:memo-label="updateMemoLabel(planner, $event)"
|
||||
@update:memo="updateMemo(planner, $event)"
|
||||
@update:timetable="updateTimetable(planner, $event)"
|
||||
@@ -2000,72 +2142,82 @@ onBeforeUnmount(() => {
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section class="rounded-[24px] border border-stone-200 bg-white/88 p-5 shadow-[0_12px_36px_rgba(28,25,23,0.05)]">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p class="text-[11px] font-bold tracking-[0.22em] text-ink">TASK LABELS</p>
|
||||
<p class="mt-2 text-[11px] font-semibold leading-5 tracking-[0.06em] text-stone-600">
|
||||
번호가 필요한 날만 빠르게 채우고, 필요 없으면 바로 비울 수 있습니다.
|
||||
</p>
|
||||
<section class="rounded-[24px] border border-stone-200 bg-white/88 p-4 shadow-[0_12px_36px_rgba(28,25,23,0.05)]">
|
||||
<div class="grid gap-3">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="text-[11px] font-bold tracking-[0.22em] text-ink">TASK LABELS</p>
|
||||
<GuideTooltip
|
||||
title="Task Labels"
|
||||
description="번호가 필요한 날만 빠르게 채우고, 필요 없으면 바로 비울 수 있습니다."
|
||||
:visible="isGuideTooltipVisible('task-labels')"
|
||||
@dismiss="dismissGuideTooltip('task-labels')"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="relative h-8 w-16 shrink-0 rounded-full transition-colors duration-300 ease-out"
|
||||
:class="areTaskLabelsNumbered(planner) ? 'bg-stone-900' : 'bg-stone-300'"
|
||||
@click="areTaskLabelsNumbered(planner) ? clearTaskLabels(planner) : fillTaskLabelsWithNumbers(planner)"
|
||||
>
|
||||
<span
|
||||
class="absolute left-1 top-1 h-6 w-6 transform-gpu rounded-full bg-white shadow-sm transition-transform duration-300 ease-out will-change-transform"
|
||||
:class="areTaskLabelsNumbered(planner) ? 'translate-x-8' : 'translate-x-0'"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-3 border-t border-stone-200 pt-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="text-[11px] font-bold tracking-[0.22em] text-ink">D-DAY 사용</p>
|
||||
<GuideTooltip
|
||||
title="D-Day"
|
||||
description="현재 날짜에 적용된 목표가 있을 때만 본문 D-DAY 표시를 켜고 끌 수 있습니다. 목표는 GOALS 화면에서 표시 기간을 지정하면 자동으로 연결됩니다."
|
||||
:visible="isGuideTooltipVisible('planner-dday')"
|
||||
@dismiss="dismissGuideTooltip('planner-dday')"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="relative h-8 w-16 shrink-0 rounded-full transition-colors duration-300 ease-out disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:class="plannerGoalToggleOn ? 'bg-stone-900' : 'bg-stone-300'"
|
||||
:disabled="!hasActiveGoalForSelectedDate"
|
||||
@click="updateGoalEnabled(planner, !plannerGoalToggleOn)"
|
||||
>
|
||||
<span
|
||||
class="absolute left-1 top-1 h-6 w-6 transform-gpu rounded-full bg-white shadow-sm transition-transform duration-300 ease-out will-change-transform"
|
||||
:class="plannerGoalToggleOn ? 'translate-x-8' : 'translate-x-0'"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="relative h-8 w-16 shrink-0 rounded-full transition-colors duration-300 ease-out"
|
||||
:class="areTaskLabelsNumbered(planner) ? 'bg-stone-900' : 'bg-stone-300'"
|
||||
@click="areTaskLabelsNumbered(planner) ? clearTaskLabels(planner) : fillTaskLabelsWithNumbers(planner)"
|
||||
>
|
||||
<span
|
||||
class="absolute left-1 top-1 h-6 w-6 transform-gpu rounded-full bg-white shadow-sm transition-transform duration-300 ease-out will-change-transform"
|
||||
:class="areTaskLabelsNumbered(planner) ? 'translate-x-8' : 'translate-x-0'"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-[24px] border border-stone-200 bg-white/88 p-5 shadow-[0_12px_36px_rgba(28,25,23,0.05)]">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<p class="text-[11px] font-bold tracking-[0.22em] text-ink">D-DAY 사용</p>
|
||||
<p class="text-[11px] font-bold tracking-[0.22em] text-ink">미완료 항목 이월</p>
|
||||
<p class="mt-2 text-[11px] font-semibold leading-5 tracking-[0.06em] text-stone-600">
|
||||
현재 날짜에 적용된 목표가 있을 때만 본문 D-DAY 표시를 켜고 끌 수 있습니다.
|
||||
체크하지 못한 할 일을 다음 날짜의 빈칸에 순서대로 복사합니다.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="relative h-8 w-16 shrink-0 rounded-full transition-colors duration-300 ease-out disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:class="plannerGoalToggleOn ? 'bg-stone-900' : 'bg-stone-300'"
|
||||
:disabled="!hasActiveGoalForSelectedDate"
|
||||
@click="updateGoalEnabled(planner, !planner.goalEnabled)"
|
||||
class="w-full rounded-full border border-stone-900 bg-stone-900 px-4 py-3 text-xs font-bold tracking-[0.14em] text-white transition hover:bg-stone-700 disabled:cursor-not-allowed disabled:border-stone-200 disabled:bg-stone-200 disabled:text-stone-500"
|
||||
:disabled="incompleteTasks.length === 0"
|
||||
@click="carryIncompleteTasksToNextDay"
|
||||
>
|
||||
<span
|
||||
class="absolute left-1 top-1 h-6 w-6 transform-gpu rounded-full bg-white shadow-sm transition-transform duration-300 ease-out will-change-transform"
|
||||
:class="plannerGoalToggleOn ? 'translate-x-8' : 'translate-x-0'"
|
||||
/>
|
||||
다음날로 이월하기
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 space-y-3">
|
||||
<div v-if="plannerGoal" class="rounded-2xl border border-stone-200 bg-white px-4 py-3">
|
||||
<p class="text-[10px] font-bold tracking-[0.16em] text-stone-500">현재 목표</p>
|
||||
<p class="mt-2 text-sm font-semibold tracking-[0.02em] text-stone-900">{{ plannerGoal.title }}</p>
|
||||
<p class="mt-1 text-[11px] font-semibold tracking-[0.06em] text-stone-500">
|
||||
목표일 {{ plannerGoal.targetDate }} / 적용 {{ plannerGoal.activeFrom }} ~ {{ plannerGoal.activeUntil }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="plannerGoal" class="rounded-2xl border border-stone-200 bg-[#fbf7f0] px-4 py-4">
|
||||
<p class="text-[11px] font-semibold leading-5 tracking-[0.06em] text-stone-600">
|
||||
이 날짜에는 이미 적용된 목표가 있으므로 토글만으로 표시 여부를 빠르게 조절할 수 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="rounded-2xl border border-dashed border-stone-300 bg-white/70 px-4 py-4">
|
||||
<p class="text-[11px] font-semibold leading-5 tracking-[0.06em] text-stone-500">
|
||||
현재 날짜에 적용된 목표가 없습니다. GOALS 화면에서 표시 기간을 지정하면 여기 토글이 자동으로 활성화됩니다.
|
||||
</p>
|
||||
</div>
|
||||
<p
|
||||
v-if="carryoverMessage"
|
||||
class="rounded-2xl border border-stone-200 bg-[#fbf7f0] px-4 py-3 text-[11px] font-semibold leading-5 tracking-[0.06em] text-stone-600"
|
||||
>
|
||||
{{ carryoverMessage }}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4" :class="isWideFocusSidebar ? '' : 'max-w-[360px]'">
|
||||
@@ -2097,35 +2249,35 @@ onBeforeUnmount(() => {
|
||||
{{ nextDaySummary }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 border-t border-stone-200 pt-5">
|
||||
<p class="text-[11px] font-bold tracking-[0.22em] text-ink">READ NEXT</p>
|
||||
<div class="mt-3 grid gap-3">
|
||||
<p
|
||||
v-for="item in readNextItems"
|
||||
:key="item"
|
||||
class="rounded-2xl border border-stone-200 bg-[#fbf7f0] px-4 py-4 text-[11px] font-semibold leading-5 tracking-[0.06em] text-stone-700"
|
||||
>
|
||||
{{ item || ' ' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 border-t border-stone-200 pt-5">
|
||||
<p class="text-[11px] font-bold tracking-[0.22em] text-ink">PREV SNAPSHOT</p>
|
||||
<div class="mt-3 grid gap-3">
|
||||
<p
|
||||
v-for="item in prevSnapshotItems"
|
||||
:key="item"
|
||||
class="rounded-2xl border border-stone-200 bg-white px-4 py-4 text-[11px] font-semibold leading-5 tracking-[0.06em] text-stone-700"
|
||||
>
|
||||
{{ item || ' ' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="rounded-[24px] border border-stone-200 bg-white/82 p-5 shadow-[0_12px_36px_rgba(28,25,23,0.05)]" :class="isWideFocusSidebar ? '2xl:col-span-2' : ''">
|
||||
<p class="mb-4 text-[11px] font-bold tracking-[0.22em] text-ink">READ NEXT</p>
|
||||
<div class="grid gap-3" :class="isWideFocusSidebar ? 'sm:grid-cols-3 2xl:grid-cols-3' : 'grid-cols-1'">
|
||||
<p
|
||||
v-for="item in readNextItems"
|
||||
:key="item"
|
||||
class="rounded-2xl border border-stone-200 bg-[#fbf7f0] px-4 py-4 text-[11px] font-semibold leading-5 tracking-[0.06em] text-stone-700"
|
||||
>
|
||||
{{ item || ' ' }}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-[24px] border border-stone-200 bg-white/82 p-5 shadow-[0_12px_36px_rgba(28,25,23,0.05)]" :class="isWideFocusSidebar ? '2xl:col-span-2' : ''">
|
||||
<p class="mb-4 text-[11px] font-bold tracking-[0.22em] text-ink">PREV SNAPSHOT</p>
|
||||
<div class="grid gap-3" :class="isWideFocusSidebar ? 'sm:grid-cols-3 2xl:grid-cols-3' : 'grid-cols-1'">
|
||||
<p
|
||||
v-for="item in prevSnapshotItems"
|
||||
:key="item"
|
||||
class="rounded-2xl border border-stone-200 bg-white px-4 py-4 text-[11px] font-semibold leading-5 tracking-[0.06em] text-stone-700"
|
||||
>
|
||||
{{ item || ' ' }}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -2161,72 +2313,82 @@ onBeforeUnmount(() => {
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section class="rounded-[24px] border border-stone-200 bg-white/88 p-5 shadow-[0_12px_36px_rgba(28,25,23,0.05)]">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p class="text-[11px] font-bold tracking-[0.22em] text-ink">TASK LABELS</p>
|
||||
<p class="mt-2 text-[11px] font-semibold leading-5 tracking-[0.06em] text-stone-600">
|
||||
번호가 필요한 날만 빠르게 채우고, 필요 없으면 바로 비울 수 있습니다.
|
||||
</p>
|
||||
<section class="rounded-[24px] border border-stone-200 bg-white/88 p-4 shadow-[0_12px_36px_rgba(28,25,23,0.05)]">
|
||||
<div class="grid gap-3">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="text-[11px] font-bold tracking-[0.22em] text-ink">TASK LABELS</p>
|
||||
<GuideTooltip
|
||||
title="Task Labels"
|
||||
description="번호가 필요한 날만 빠르게 채우고, 필요 없으면 바로 비울 수 있습니다."
|
||||
:visible="isGuideTooltipVisible('task-labels')"
|
||||
@dismiss="dismissGuideTooltip('task-labels')"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="relative h-8 w-16 shrink-0 rounded-full transition-colors duration-300 ease-out"
|
||||
:class="areTaskLabelsNumbered(planner) ? 'bg-stone-900' : 'bg-stone-300'"
|
||||
@click="areTaskLabelsNumbered(planner) ? clearTaskLabels(planner) : fillTaskLabelsWithNumbers(planner)"
|
||||
>
|
||||
<span
|
||||
class="absolute left-1 top-1 h-6 w-6 transform-gpu rounded-full bg-white shadow-sm transition-transform duration-300 ease-out will-change-transform"
|
||||
:class="areTaskLabelsNumbered(planner) ? 'translate-x-8' : 'translate-x-0'"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-3 border-t border-stone-200 pt-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="text-[11px] font-bold tracking-[0.22em] text-ink">D-DAY 사용</p>
|
||||
<GuideTooltip
|
||||
title="D-Day"
|
||||
description="현재 날짜에 적용된 목표가 있을 때만 본문 D-DAY 표시를 켜고 끌 수 있습니다. 목표는 GOALS 화면에서 표시 기간을 지정하면 자동으로 연결됩니다."
|
||||
:visible="isGuideTooltipVisible('planner-dday')"
|
||||
@dismiss="dismissGuideTooltip('planner-dday')"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="relative h-8 w-16 shrink-0 rounded-full transition-colors duration-300 ease-out disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:class="plannerGoalToggleOn ? 'bg-stone-900' : 'bg-stone-300'"
|
||||
:disabled="!hasActiveGoalForSelectedDate"
|
||||
@click="updateGoalEnabled(planner, !plannerGoalToggleOn)"
|
||||
>
|
||||
<span
|
||||
class="absolute left-1 top-1 h-6 w-6 transform-gpu rounded-full bg-white shadow-sm transition-transform duration-300 ease-out will-change-transform"
|
||||
:class="plannerGoalToggleOn ? 'translate-x-8' : 'translate-x-0'"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="relative h-8 w-16 shrink-0 rounded-full transition-colors duration-300 ease-out"
|
||||
:class="areTaskLabelsNumbered(planner) ? 'bg-stone-900' : 'bg-stone-300'"
|
||||
@click="areTaskLabelsNumbered(planner) ? clearTaskLabels(planner) : fillTaskLabelsWithNumbers(planner)"
|
||||
>
|
||||
<span
|
||||
class="absolute left-1 top-1 h-6 w-6 transform-gpu rounded-full bg-white shadow-sm transition-transform duration-300 ease-out will-change-transform"
|
||||
:class="areTaskLabelsNumbered(planner) ? 'translate-x-8' : 'translate-x-0'"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-[24px] border border-stone-200 bg-white/88 p-5 shadow-[0_12px_36px_rgba(28,25,23,0.05)]">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<p class="text-[11px] font-bold tracking-[0.22em] text-ink">D-DAY 사용</p>
|
||||
<p class="text-[11px] font-bold tracking-[0.22em] text-ink">미완료 항목 이월</p>
|
||||
<p class="mt-2 text-[11px] font-semibold leading-5 tracking-[0.06em] text-stone-600">
|
||||
현재 날짜에 적용된 목표가 있을 때만 본문 D-DAY 표시를 켜고 끌 수 있습니다.
|
||||
체크하지 못한 할 일을 다음 날짜의 빈칸에 순서대로 복사합니다.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="relative h-8 w-16 shrink-0 rounded-full transition-colors duration-300 ease-out disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:class="plannerGoalToggleOn ? 'bg-stone-900' : 'bg-stone-300'"
|
||||
:disabled="!hasActiveGoalForSelectedDate"
|
||||
@click="updateGoalEnabled(planner, !planner.goalEnabled)"
|
||||
class="w-full rounded-full border border-stone-900 bg-stone-900 px-4 py-3 text-xs font-bold tracking-[0.14em] text-white transition hover:bg-stone-700 disabled:cursor-not-allowed disabled:border-stone-200 disabled:bg-stone-200 disabled:text-stone-500"
|
||||
:disabled="incompleteTasks.length === 0"
|
||||
@click="carryIncompleteTasksToNextDay"
|
||||
>
|
||||
<span
|
||||
class="absolute left-1 top-1 h-6 w-6 transform-gpu rounded-full bg-white shadow-sm transition-transform duration-300 ease-out will-change-transform"
|
||||
:class="plannerGoalToggleOn ? 'translate-x-8' : 'translate-x-0'"
|
||||
/>
|
||||
다음날로 이월하기
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 space-y-3">
|
||||
<div v-if="plannerGoal" class="rounded-2xl border border-stone-200 bg-white px-4 py-3">
|
||||
<p class="text-[10px] font-bold tracking-[0.16em] text-stone-500">현재 목표</p>
|
||||
<p class="mt-2 text-sm font-semibold tracking-[0.02em] text-stone-900">{{ plannerGoal.title }}</p>
|
||||
<p class="mt-1 text-[11px] font-semibold tracking-[0.06em] text-stone-500">
|
||||
목표일 {{ plannerGoal.targetDate }} / 적용 {{ plannerGoal.activeFrom }} ~ {{ plannerGoal.activeUntil }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="plannerGoal" class="rounded-2xl border border-stone-200 bg-[#fbf7f0] px-4 py-4">
|
||||
<p class="text-[11px] font-semibold leading-5 tracking-[0.06em] text-stone-600">
|
||||
이 날짜에는 이미 적용된 목표가 있으므로 토글만으로 표시 여부를 빠르게 조절할 수 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="rounded-2xl border border-dashed border-stone-300 bg-white/70 px-4 py-4">
|
||||
<p class="text-[11px] font-semibold leading-5 tracking-[0.06em] text-stone-500">
|
||||
현재 날짜에 적용된 목표가 없습니다. GOALS 화면에서 표시 기간을 지정하면 여기 토글이 자동으로 활성화됩니다.
|
||||
</p>
|
||||
</div>
|
||||
<p
|
||||
v-if="carryoverMessage"
|
||||
class="rounded-2xl border border-stone-200 bg-[#fbf7f0] px-4 py-3 text-[11px] font-semibold leading-5 tracking-[0.06em] text-stone-600"
|
||||
>
|
||||
{{ carryoverMessage }}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="grid max-w-[360px] gap-4">
|
||||
@@ -2258,35 +2420,35 @@ onBeforeUnmount(() => {
|
||||
{{ nextDaySummary }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 border-t border-stone-200 pt-5">
|
||||
<p class="text-[11px] font-bold tracking-[0.22em] text-ink">READ NEXT</p>
|
||||
<div class="mt-3 grid gap-3">
|
||||
<p
|
||||
v-for="item in readNextItems"
|
||||
:key="item"
|
||||
class="rounded-2xl border border-stone-200 bg-[#fbf7f0] px-4 py-4 text-[11px] font-semibold leading-5 tracking-[0.06em] text-stone-700"
|
||||
>
|
||||
{{ item || ' ' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 border-t border-stone-200 pt-5">
|
||||
<p class="text-[11px] font-bold tracking-[0.22em] text-ink">PREV SNAPSHOT</p>
|
||||
<div class="mt-3 grid gap-3">
|
||||
<p
|
||||
v-for="item in prevSnapshotItems"
|
||||
:key="item"
|
||||
class="rounded-2xl border border-stone-200 bg-white px-4 py-4 text-[11px] font-semibold leading-5 tracking-[0.06em] text-stone-700"
|
||||
>
|
||||
{{ item || ' ' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="rounded-[24px] border border-stone-200 bg-white/82 p-5 shadow-[0_12px_36px_rgba(28,25,23,0.05)]">
|
||||
<p class="mb-4 text-[11px] font-bold tracking-[0.22em] text-ink">READ NEXT</p>
|
||||
<div class="grid gap-3 grid-cols-1">
|
||||
<p
|
||||
v-for="item in readNextItems"
|
||||
:key="item"
|
||||
class="rounded-2xl border border-stone-200 bg-[#fbf7f0] px-4 py-4 text-[11px] font-semibold leading-5 tracking-[0.06em] text-stone-700"
|
||||
>
|
||||
{{ item || ' ' }}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-[24px] border border-stone-200 bg-white/82 p-5 shadow-[0_12px_36px_rgba(28,25,23,0.05)]">
|
||||
<p class="mb-4 text-[11px] font-bold tracking-[0.22em] text-ink">PREV SNAPSHOT</p>
|
||||
<div class="grid gap-3 grid-cols-1">
|
||||
<p
|
||||
v-for="item in prevSnapshotItems"
|
||||
:key="item"
|
||||
class="rounded-2xl border border-stone-200 bg-white px-4 py-4 text-[11px] font-semibold leading-5 tracking-[0.06em] text-stone-700"
|
||||
>
|
||||
{{ item || ' ' }}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</aside>
|
||||
</Transition>
|
||||
@@ -2315,6 +2477,7 @@ onBeforeUnmount(() => {
|
||||
@update:task-label="updateTaskLabel(planner, $event)"
|
||||
@update:task-title="updateTaskTitle(planner, $event)"
|
||||
@toggle:task="toggleTask(planner, $event)"
|
||||
@clear:tasks="clearTasks(planner, $event)"
|
||||
@update:memo-label="updateMemoLabel(planner, $event)"
|
||||
@update:memo="updateMemo(planner, $event)"
|
||||
@update:timetable="updateTimetable(planner, $event)"
|
||||
@@ -2338,6 +2501,7 @@ onBeforeUnmount(() => {
|
||||
@update:task-label="updateTaskLabel(secondaryPlanner, $event)"
|
||||
@update:task-title="updateTaskTitle(secondaryPlanner, $event)"
|
||||
@toggle:task="toggleTask(secondaryPlanner, $event)"
|
||||
@clear:tasks="clearTasks(secondaryPlanner, $event)"
|
||||
@update:memo-label="updateMemoLabel(secondaryPlanner, $event)"
|
||||
@update:memo="updateMemo(secondaryPlanner, $event)"
|
||||
@update:timetable="updateTimetable(secondaryPlanner, $event)"
|
||||
@@ -2375,10 +2539,12 @@ onBeforeUnmount(() => {
|
||||
:password-busy="passwordBusy"
|
||||
:profile-message="profileMessage"
|
||||
:password-message="passwordMessage"
|
||||
:guide-tooltip-reset-message="guideTooltipResetMessage"
|
||||
@update:profile-field="updateProfileField"
|
||||
@update:password-field="updatePasswordField"
|
||||
@submit:profile="submitProfileForm"
|
||||
@submit:password="submitPasswordForm"
|
||||
@reset-guide-tooltips="resetGuideTooltips"
|
||||
/>
|
||||
|
||||
<AdminDashboard
|
||||
@@ -2424,6 +2590,7 @@ onBeforeUnmount(() => {
|
||||
@update:task-label="updateTaskLabel(planner, $event)"
|
||||
@update:task-title="updateTaskTitle(planner, $event)"
|
||||
@toggle:task="toggleTask(planner, $event)"
|
||||
@clear:tasks="clearTasks(planner, $event)"
|
||||
@update:memo-label="updateMemoLabel(planner, $event)"
|
||||
@update:memo="updateMemo(planner, $event)"
|
||||
@update:timetable="updateTimetable(planner, $event)"
|
||||
@@ -2449,6 +2616,7 @@ onBeforeUnmount(() => {
|
||||
@update:task-label="updateTaskLabel(planner, $event)"
|
||||
@update:task-title="updateTaskTitle(planner, $event)"
|
||||
@toggle:task="toggleTask(planner, $event)"
|
||||
@clear:tasks="clearTasks(planner, $event)"
|
||||
@update:memo-label="updateMemoLabel(planner, $event)"
|
||||
@update:memo="updateMemo(planner, $event)"
|
||||
@update:timetable="updateTimetable(planner, $event)"
|
||||
@@ -2471,6 +2639,7 @@ onBeforeUnmount(() => {
|
||||
@update:task-label="updateTaskLabel(secondaryPlanner, $event)"
|
||||
@update:task-title="updateTaskTitle(secondaryPlanner, $event)"
|
||||
@toggle:task="toggleTask(secondaryPlanner, $event)"
|
||||
@clear:tasks="clearTasks(secondaryPlanner, $event)"
|
||||
@update:memo-label="updateMemoLabel(secondaryPlanner, $event)"
|
||||
@update:memo="updateMemo(secondaryPlanner, $event)"
|
||||
@update:timetable="updateTimetable(secondaryPlanner, $event)"
|
||||
|
||||
Reference in New Issue
Block a user