This commit is contained in:
2026-04-21 14:21:08 +09:00
parent 85d21f5842
commit 0206cfebf8
7 changed files with 113 additions and 25 deletions

View File

@@ -9,6 +9,8 @@ const screenMode = ref('planner')
const viewMode = ref('focus')
const selectedDate = ref(new Date())
const calendarViewDate = ref(new Date(selectedDate.value))
const statsRangeStart = ref(toKey(new Date(new Date().setDate(new Date().getDate() - 6))))
const statsRangeEnd = ref(toKey(new Date()))
const hours = [
'6', '7', '8', '9', '10', '11', '12',
@@ -136,6 +138,10 @@ function toDateValue(value, fallback = new Date()) {
return Number.isNaN(nextDate.getTime()) ? new Date(fallback) : nextDate
}
function startOfDay(date) {
return new Date(date.getFullYear(), date.getMonth(), date.getDate())
}
function buildFallbackRecord(date) {
return {
dday: 'D-00 FOCUS',
@@ -221,10 +227,17 @@ function restoreDateState() {
if (savedState.calendarViewDate) {
calendarViewDate.value = toDateValue(savedState.calendarViewDate, selectedDate.value)
return
} else {
calendarViewDate.value = new Date(selectedDate.value)
}
calendarViewDate.value = new Date(selectedDate.value)
if (savedState.statsRangeStart) {
statsRangeStart.value = savedState.statsRangeStart
}
if (savedState.statsRangeEnd) {
statsRangeEnd.value = savedState.statsRangeEnd
}
} catch (error) {
console.warn('저장된 날짜 상태를 불러오지 못했습니다.', error)
}
@@ -404,19 +417,37 @@ const plannerEntries = computed(() =>
.sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey)),
)
const normalizedStatsRange = computed(() => {
const todayKey = toKey(new Date())
const startKey = statsRangeStart.value || todayKey
const endKey = statsRangeEnd.value || todayKey
if (startKey <= endKey) {
return { startKey, endKey }
}
return { startKey: endKey, endKey: startKey }
})
const rangeEntries = computed(() =>
plannerEntries.value.filter(([key]) =>
key >= normalizedStatsRange.value.startKey && key <= normalizedStatsRange.value.endKey,
),
)
const totalFocusedMinutes = computed(() =>
plannerEntries.value.reduce((total, [, record]) => total + getFocusedMinutes(record), 0),
rangeEntries.value.reduce((total, [, record]) => total + getFocusedMinutes(record), 0),
)
const totalRecordedTasks = computed(() =>
plannerEntries.value.reduce(
rangeEntries.value.reduce(
(total, [, record]) => total + record.tasks.filter((task) => task.title.trim()).length,
0,
),
)
const completedRecordedTasks = computed(() =>
plannerEntries.value.reduce(
rangeEntries.value.reduce(
(total, [, record]) => total + record.tasks.filter((task) => task.title.trim() && task.checked).length,
0,
),
@@ -443,7 +474,7 @@ const overviewCards = computed(() => [
},
{
label: 'RECORDED DAYS',
value: `${plannerEntries.value.length}`,
value: `${rangeEntries.value.length}`,
caption: '기록이 남아 있는 날짜 수',
},
{
@@ -466,21 +497,23 @@ function createDateLabel(dateKey) {
}
const weeklyRecords = computed(() => {
const entries = Array.from({ length: 7 }, (_, index) => {
const date = new Date(selectedDate.value)
date.setDate(selectedDate.value.getDate() - (6 - index))
const record = getPlannerRecord(date)
const focusedMinutes = getFocusedMinutes(record)
const entries = rangeEntries.value.map(([key, record]) => {
const date = toDateValue(key)
const weekdayShort = ['일', '월', '화', '수', '목', '금', '토'][date.getDay()]
const focusedMinutes = getFocusedMinutes(record)
return {
key: toKey(date),
key,
weekday: weekdayShort,
focusedMinutes,
focusedTime: formatMinutes(focusedMinutes),
}
})
if (entries.length === 0) {
return []
}
const maxMinutes = Math.max(...entries.map((entry) => entry.focusedMinutes), 60)
return entries.map((entry) => ({
@@ -490,7 +523,7 @@ const weeklyRecords = computed(() => {
})
const recentRecords = computed(() =>
[...plannerEntries.value]
[...rangeEntries.value]
.sort(([leftKey], [rightKey]) => rightKey.localeCompare(leftKey))
.slice(0, 5)
.map(([key, record]) => ({
@@ -503,11 +536,11 @@ const recentRecords = computed(() =>
)
const bestDay = computed(() => {
if (plannerEntries.value.length === 0) {
if (rangeEntries.value.length === 0) {
return null
}
const [bestKey, bestRecord] = [...plannerEntries.value].reduce((bestEntry, currentEntry) => {
const [bestKey, bestRecord] = [...rangeEntries.value].reduce((bestEntry, currentEntry) => {
const [, bestRecordValue] = bestEntry
const [, currentRecordValue] = currentEntry
@@ -523,7 +556,7 @@ const bestDay = computed(() => {
})
watch(
[plannerRecords, selectedDate, calendarViewDate],
[plannerRecords, selectedDate, calendarViewDate, statsRangeStart, statsRangeEnd],
() => {
if (typeof window === 'undefined') {
return
@@ -546,6 +579,8 @@ watch(
JSON.stringify({
selectedDate: selectedDate.value.toISOString(),
calendarViewDate: calendarViewDate.value.toISOString(),
statsRangeStart: normalizedStatsRange.value.startKey,
statsRangeEnd: normalizedStatsRange.value.endKey,
records: serializableRecords,
}),
)
@@ -742,7 +777,8 @@ function clearTaskLabels(record) {
<p class="text-[11px] font-bold tracking-[0.22em] text-ink">NEXT DAY</p>
<div class="mt-5 space-y-3">
<p class="text-lg font-semibold tracking-[-0.04em] text-stone-900">
{{ secondaryDateDisplay.main }} {{ secondaryDateDisplay.weekday }}
<span>{{ secondaryDateDisplay.main }}</span>
<span class="ml-1" :class="secondaryDateDisplay.weekdayTone">{{ secondaryDateDisplay.weekday }}</span>
</p>
<p class="text-[11px] font-semibold tracking-[0.08em] text-stone-600">
내일의 작업은 "{{ secondaryPlanner.tasks[0]?.title || '새 작업 추가' }}" 시작합니다.
@@ -806,6 +842,10 @@ function clearTaskLabels(record) {
:recent-records="recentRecords"
:best-day="bestDay"
:selected-date-label="`${selectedDateDisplay.main} ${selectedDateDisplay.weekday}`"
:range-start="normalizedStatsRange.startKey"
:range-end="normalizedStatsRange.endKey"
@update:range-start="statsRangeStart = $event"
@update:range-end="statsRangeEnd = $event"
/>
</div>
</main>

View File

@@ -230,7 +230,10 @@ onBeforeUnmount(() => {
class="flex h-[30px] border-b"
:class="index === hours.length - 1 ? 'border-ink' : 'border-line'"
>
<div class="flex h-full w-[30px] items-center justify-center border-r border-ink text-[9px] text-ink">
<div
class="flex h-full w-[30px] touch-none select-none items-center justify-center border-r border-ink text-[9px] text-ink"
@pointerdown.prevent
>
{{ hour }}
</div>
<div

View File

@@ -12,6 +12,14 @@ defineProps({
type: Array,
required: true,
},
rangeStart: {
type: String,
required: true,
},
rangeEnd: {
type: String,
required: true,
},
bestDay: {
type: Object,
default: null,
@@ -21,10 +29,43 @@ defineProps({
required: true,
},
})
const emit = defineEmits(['update:range-start', 'update:range-end'])
</script>
<template>
<section class="grid gap-6">
<article class="rounded-[28px] border border-white/60 bg-white/80 p-5 shadow-paper backdrop-blur">
<div class="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<div>
<p class="text-[11px] font-bold tracking-[0.22em] text-stone-500">RANGE</p>
<h2 class="mt-2 text-2xl font-semibold tracking-[-0.05em] text-stone-900">
원하는 기간 기준으로 통계 보기
</h2>
</div>
<div class="grid gap-3 sm:grid-cols-2">
<label class="flex flex-col gap-2 text-[11px] font-bold tracking-[0.14em] text-stone-500">
START DATE
<input
:value="rangeStart"
type="date"
class="rounded-2xl border border-stone-200 bg-white px-4 py-3 text-sm font-semibold text-stone-800 outline-none transition focus:border-stone-400"
@input="emit('update:range-start', $event.target.value)"
/>
</label>
<label class="flex flex-col gap-2 text-[11px] font-bold tracking-[0.14em] text-stone-500">
END DATE
<input
:value="rangeEnd"
type="date"
class="rounded-2xl border border-stone-200 bg-white px-4 py-3 text-sm font-semibold text-stone-800 outline-none transition focus:border-stone-400"
@input="emit('update:range-end', $event.target.value)"
/>
</label>
</div>
</div>
</article>
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<article
v-for="card in overviewCards"
@@ -43,7 +84,7 @@ defineProps({
<div>
<p class="text-[11px] font-bold tracking-[0.22em] text-stone-500">WEEKLY FLOW</p>
<h2 class="mt-2 text-2xl font-semibold tracking-[-0.05em] text-stone-900">
최근 7 기록 흐름
선택 기간 기록 흐름
</h2>
</div>
<p class="text-[11px] font-semibold tracking-[0.06em] text-stone-500">
@@ -51,11 +92,11 @@ defineProps({
</p>
</div>
<div class="mt-8 grid grid-cols-7 gap-3">
<div class="mt-8 flex gap-3 overflow-x-auto pb-2">
<div
v-for="record in weeklyRecords"
:key="record.key"
class="flex flex-col items-center gap-3"
class="flex min-w-[56px] flex-col items-center gap-3"
>
<div class="flex h-40 items-end">
<div