v0.1.19 - 사이드 네비게이션과 D-DAY 중복 방지 적용
This commit is contained in:
140
src/App.vue
140
src/App.vue
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user