v0.1.19 - 사이드 네비게이션과 D-DAY 중복 방지 적용

This commit is contained in:
2026-04-22 10:19:23 +09:00
parent fe538fc88b
commit 9b788406ea
6 changed files with 204 additions and 45 deletions

View File

@@ -210,7 +210,7 @@ function startOfDay(date) {
function buildFallbackRecord(date) {
return {
comment: '',
goalEnabled: false,
goalEnabled: true,
selectedGoalId: null,
tasks: Array.from({ length: 15 }, (_, index) => ({
label: '',
@@ -230,7 +230,7 @@ function buildFallbackRecord(date) {
function normalizeRecord(record) {
return {
...record,
goalEnabled: Boolean(record.goalEnabled),
goalEnabled: record.goalEnabled !== false,
selectedGoalId: record.selectedGoalId ?? null,
tasks: record.tasks.map((task, index) => ({
label: task.label ?? task.id ?? '',
@@ -709,6 +709,28 @@ function setSyncFeedback(status, message, options = {}) {
}
}
function getTodayKey() {
return toKey(new Date())
}
function findOverlappingGoal({ activeFrom, activeUntil, status, excludeGoalId = null }) {
if (!activeFrom || !activeUntil || status !== 'active') {
return null
}
return goals.value.find((goal) => {
if (excludeGoalId && goal.id === excludeGoalId) {
return false
}
if (goal.status !== 'active' || !goal.activeFrom || !goal.activeUntil) {
return false
}
return activeFrom <= goal.activeUntil && activeUntil >= goal.activeFrom
}) ?? null
}
function resetAuthForm() {
authForm.nickname = ''
authForm.email = ''
@@ -718,7 +740,7 @@ function resetAuthForm() {
function resetGoalForm() {
goalForm.title = ''
goalForm.targetDate = ''
goalForm.activeFrom = ''
goalForm.activeFrom = getTodayKey()
goalForm.activeUntil = ''
goalForm.status = 'active'
editingGoalId.value = null
@@ -875,6 +897,17 @@ async function submitGoal() {
return
}
const overlappedGoal = findOverlappingGoal({
activeFrom: goalForm.activeFrom || null,
activeUntil: goalForm.activeUntil || null,
status: goalForm.status,
})
if (overlappedGoal) {
goalMessage.value = `"${overlappedGoal.title}" 목표와 표시 기간이 겹칩니다. D-DAY 기간은 하나만 설정할 수 있습니다.`
return
}
goalBusy.value = true
goalMessage.value = ''
@@ -900,6 +933,14 @@ async function submitGoal() {
function updateGoalFormField({ field, value }) {
goalForm[field] = value
if (field === 'targetDate' && !editingGoalId.value) {
if (!goalForm.activeFrom) {
goalForm.activeFrom = getTodayKey()
}
goalForm.activeUntil = value
}
}
function startGoalEdit(goal) {
@@ -932,6 +973,18 @@ async function saveGoalEdit() {
return
}
const overlappedGoal = findOverlappingGoal({
activeFrom: goalForm.activeFrom || null,
activeUntil: goalForm.activeUntil || null,
status: goalForm.status,
excludeGoalId: editingGoalId.value,
})
if (overlappedGoal) {
goalMessage.value = `"${overlappedGoal.title}" 목표와 표시 기간이 겹칩니다. D-DAY 기간은 하나만 설정할 수 있습니다.`
return
}
goalBusy.value = true
goalMessage.value = ''
@@ -1172,6 +1225,7 @@ async function printSelectedPlanner(layout = 'single') {
}
onMounted(() => {
resetGoalForm()
setSyncFeedback('local', '로그인 후 클라우드 저장을 사용할 수 있습니다.', {
visible: false,
})
@@ -1180,9 +1234,9 @@ onMounted(() => {
</script>
<template>
<main class="min-h-screen px-4 py-6 text-ink sm:px-6 lg:px-10">
<div class="print-root mx-auto flex max-w-[1760px] flex-col gap-6 xl:grid xl:grid-cols-[280px_minmax(0,1fr)] xl:items-start">
<aside class="print-hidden rounded-[28px] border border-white/60 bg-white/70 p-5 backdrop-blur sm:p-6 xl:sticky xl:top-6">
<main class="min-h-screen px-4 py-6 text-ink sm:px-6 lg:px-10 xl:h-screen xl:overflow-hidden">
<div class="print-root mx-auto flex max-w-[1760px] flex-col gap-6 xl:h-[calc(100vh-3rem)] xl:grid xl:grid-cols-[300px_minmax(0,1fr)] xl:items-start">
<aside class="print-hidden rounded-[28px] border border-white/60 bg-white/70 p-5 backdrop-blur sm:p-6 xl:h-full xl:overflow-y-auto">
<div class="space-y-6">
<div class="space-y-2">
<p class="text-[11px] font-bold uppercase tracking-[0.28em] text-stone-500">10 Minute Planner</p>
@@ -1231,40 +1285,44 @@ onMounted(() => {
</template>
</section>
<section v-if="isAuthenticated" class="rounded-[24px] border border-stone-200 bg-white/80 p-4">
<section v-if="isAuthenticated" class="rounded-[24px] border border-stone-200 bg-white/85 p-4 shadow-[0_10px_30px_rgba(28,25,23,0.04)]">
<p class="text-[10px] font-bold tracking-[0.16em] text-stone-500">NAVIGATION</p>
<div class="mt-4 grid grid-cols-2 gap-2">
<div class="mt-4 grid gap-2">
<button
type="button"
class="rounded-2xl px-3 py-3 text-xs font-bold tracking-[0.14em] transition"
:class="screenMode === 'planner' ? 'bg-stone-900 text-white' : 'bg-stone-100 text-stone-500'"
class="rounded-[20px] border px-4 py-4 text-left transition"
:class="screenMode === 'planner' ? 'border-stone-900 bg-stone-900 text-white shadow-[0_12px_24px_rgba(28,25,23,0.18)]' : 'border-stone-200 bg-white text-stone-700 hover:border-stone-400'"
@click="screenMode = 'planner'"
>
PLANNER
<p class="text-xs font-bold tracking-[0.18em]">PLANNER</p>
<p class="mt-1 text-[11px] font-semibold tracking-[0.04em]" :class="screenMode === 'planner' ? 'text-stone-200' : 'text-stone-500'">오늘 문서 작성과 캘린더 이동</p>
</button>
<button
type="button"
class="rounded-2xl px-3 py-3 text-xs font-bold tracking-[0.14em] transition"
:class="screenMode === 'stats' ? 'bg-stone-900 text-white' : 'bg-stone-100 text-stone-500'"
class="rounded-[20px] border px-4 py-4 text-left transition"
:class="screenMode === 'stats' ? 'border-stone-900 bg-stone-900 text-white shadow-[0_12px_24px_rgba(28,25,23,0.18)]' : 'border-stone-200 bg-white text-stone-700 hover:border-stone-400'"
@click="screenMode = 'stats'"
>
STATS
<p class="text-xs font-bold tracking-[0.18em]">STATS</p>
<p class="mt-1 text-[11px] font-semibold tracking-[0.04em]" :class="screenMode === 'stats' ? 'text-stone-200' : 'text-stone-500'">기간별 집중 시간과 완료율</p>
</button>
<button
type="button"
class="rounded-2xl px-3 py-3 text-xs font-bold tracking-[0.14em] transition"
:class="screenMode === 'goals' ? 'bg-stone-900 text-white' : 'bg-stone-100 text-stone-500'"
class="rounded-[20px] border px-4 py-4 text-left transition"
:class="screenMode === 'goals' ? 'border-stone-900 bg-stone-900 text-white shadow-[0_12px_24px_rgba(28,25,23,0.18)]' : 'border-stone-200 bg-white text-stone-700 hover:border-stone-400'"
@click="screenMode = 'goals'"
>
GOALS
<p class="text-xs font-bold tracking-[0.18em]">GOALS</p>
<p class="mt-1 text-[11px] font-semibold tracking-[0.04em]" :class="screenMode === 'goals' ? 'text-stone-200' : 'text-stone-500'">D-DAY 목표와 표시 기간 관리</p>
</button>
<button
type="button"
class="rounded-2xl px-3 py-3 text-xs font-bold tracking-[0.14em] transition"
:class="screenMode === 'settings' ? 'bg-stone-900 text-white' : 'bg-stone-100 text-stone-500'"
class="rounded-[20px] border px-4 py-4 text-left transition"
:class="screenMode === 'settings' ? 'border-stone-900 bg-stone-900 text-white shadow-[0_12px_24px_rgba(28,25,23,0.18)]' : 'border-stone-200 bg-white text-stone-700 hover:border-stone-400'"
@click="screenMode = 'settings'"
>
SETTINGS
<p class="text-xs font-bold tracking-[0.18em]">SETTINGS</p>
<p class="mt-1 text-[11px] font-semibold tracking-[0.04em]" :class="screenMode === 'settings' ? 'text-stone-200' : 'text-stone-500'">계정 정보와 비밀번호 수정</p>
</button>
</div>
</section>
@@ -1333,10 +1391,10 @@ onMounted(() => {
</div>
</aside>
<div class="min-w-0 space-y-6">
<div class="min-w-0 space-y-6 xl:h-full xl:overflow-hidden">
<section
v-if="!isAuthenticated"
class="print-hidden rounded-[32px] border border-white/60 bg-white/65 p-6 shadow-[0_24px_80px_rgba(28,25,23,0.08)] sm:p-8"
class="print-hidden rounded-[28px] border border-white/60 bg-white/65 p-6 shadow-[0_24px_80px_rgba(28,25,23,0.08)] sm:p-8 xl:h-full xl:overflow-y-auto"
>
<div class="mx-auto flex max-w-3xl flex-col gap-6 text-center">
<div class="space-y-3">
@@ -1393,9 +1451,9 @@ onMounted(() => {
<section
v-else-if="screenMode === 'planner' && viewMode === 'focus'"
class="print-hidden grid gap-6 xl:grid-cols-[minmax(0,1fr)_320px]"
class="print-hidden grid gap-6 xl:h-full xl:min-h-0 xl:grid-cols-[minmax(0,1fr)_340px]"
>
<div class="print-target">
<div class="print-target rounded-[28px] border border-white/60 bg-white/45 p-4 shadow-[0_18px_60px_rgba(28,25,23,0.06)] xl:h-full xl:min-h-0 xl:overflow-y-auto xl:pr-3">
<PlannerPage
:date-main="selectedDateDisplay.main"
:date-weekday="selectedDateDisplay.weekday"
@@ -1418,8 +1476,8 @@ onMounted(() => {
/>
</div>
<aside class="print-hidden flex flex-col gap-4">
<section class="border border-stone-200 bg-white/80 p-5">
<aside class="print-hidden flex flex-col gap-4 xl:h-full xl:min-h-0 xl:overflow-y-auto xl:pr-2">
<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="space-y-3">
<p
@@ -1432,7 +1490,7 @@ onMounted(() => {
</div>
</section>
<section class="border border-stone-200 bg-white/80 p-5">
<section class="rounded-[24px] border border-stone-200 bg-white/82 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>
@@ -1442,19 +1500,19 @@ onMounted(() => {
</div>
<button
type="button"
class="relative h-8 w-16 shrink-0 rounded-full transition"
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 top-1 h-6 w-6 rounded-full bg-white shadow-sm transition"
:class="areTaskLabelsNumbered(planner) ? 'left-9' : 'left-1'"
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="border border-stone-200 bg-white/80 p-5">
<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="space-y-3">
<p
@@ -1467,7 +1525,7 @@ onMounted(() => {
</div>
</section>
<section class="border border-stone-200 bg-white/80 p-5">
<section class="rounded-[24px] border border-stone-200 bg-white/82 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">D-DAY 사용</p>
@@ -1477,14 +1535,14 @@ onMounted(() => {
</div>
<button
type="button"
class="relative h-8 w-16 shrink-0 rounded-full transition disabled:cursor-not-allowed disabled:opacity-50"
class="relative h-8 w-16 shrink-0 rounded-full transition-colors duration-300 ease-out disabled:cursor-not-allowed disabled:opacity-50"
:class="planner.goalEnabled ? 'bg-stone-900' : 'bg-stone-300'"
:disabled="!hasActiveGoalForSelectedDate"
@click="updateGoalEnabled(planner, !planner.goalEnabled)"
>
<span
class="absolute top-1 h-6 w-6 rounded-full bg-white shadow-sm transition"
:class="planner.goalEnabled ? 'left-9' : 'left-1'"
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="planner.goalEnabled ? 'translate-x-8' : 'translate-x-0'"
/>
</button>
</div>
@@ -1525,7 +1583,7 @@ onMounted(() => {
/>
<section class="grid grid-cols-2 gap-4">
<article class="border border-stone-200 bg-white/80 p-5">
<article class="rounded-[24px] border border-stone-200 bg-white/82 p-5 shadow-[0_12px_36px_rgba(28,25,23,0.05)]">
<p class="text-[11px] font-bold tracking-[0.22em] text-ink">STATS</p>
<div class="mt-5 space-y-4">
<div>
@@ -1539,7 +1597,7 @@ onMounted(() => {
</div>
</article>
<article class="border border-stone-200 bg-white/80 p-5">
<article class="rounded-[24px] border border-stone-200 bg-white/82 p-5 shadow-[0_12px_36px_rgba(28,25,23,0.05)]">
<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">
@@ -1557,7 +1615,7 @@ onMounted(() => {
<section
v-else-if="screenMode === 'planner'"
class="print-hidden overflow-x-auto rounded-[32px] border border-white/60 bg-white/40 p-4 sm:p-6"
class="print-hidden overflow-x-auto rounded-[28px] border border-white/60 bg-white/45 p-4 shadow-[0_18px_60px_rgba(28,25,23,0.06)] sm:p-6 xl:h-full xl:overflow-y-auto"
>
<div class="flex min-w-[1260px] gap-6">
<div class="print-target">
@@ -1609,7 +1667,7 @@ onMounted(() => {
<GoalsDashboard
v-else-if="screenMode === 'goals'"
class="print-hidden"
class="print-hidden xl:h-full xl:overflow-y-auto"
:goals="filteredGoals"
:query="goalQuery"
:status="goalStatusFilter"
@@ -1629,7 +1687,7 @@ onMounted(() => {
<SettingsDashboard
v-else-if="screenMode === 'settings'"
class="print-hidden"
class="print-hidden xl:h-full xl:overflow-y-auto"
:user="currentUser"
:profile-form="profileForm"
:password-form="passwordForm"
@@ -1645,7 +1703,7 @@ onMounted(() => {
<StatsDashboard
v-else
class="print-hidden"
class="print-hidden xl:h-full xl:overflow-y-auto"
:overview-cards="overviewCards"
:weekly-records="weeklyRecords"
:recent-records="recentRecords"