This commit is contained in:
2026-04-21 12:30:44 +09:00
commit 6ae64d2a13
15 changed files with 3292 additions and 0 deletions

337
src/App.vue Normal file
View File

@@ -0,0 +1,337 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import MiniCalendar from './components/MiniCalendar.vue'
import PlannerPage from './components/PlannerPage.vue'
type PlannerTask = {
id: string
title: string
checked?: boolean
}
type PlannerRecord = {
dday: string
comment: string
totalTime: string
tasks: PlannerTask[]
memo: string[]
prevSummary: string[]
nextFocus: string[]
}
type ViewMode = 'focus' | 'spread'
const viewMode = ref<ViewMode>('focus')
const selectedDate = ref(new Date(2026, 3, 21))
const hours = [
'6', '7', '8', '9', '10', '11', '12',
'1', '2', '3', '4', '5',
'6', '7', '8', '9', '10', '11', '12',
'1', '2', '3', '4', '5',
]
const plannerSeed: Record<string, PlannerRecord> = {
'2026-04-21': {
dday: 'D-12 LAUNCH',
comment: '집중 작업 3개만 남기고, 10분 단위로 흐름을 끊지 않기.',
totalTime: '09H 40M',
tasks: [
{ id: '01', title: '홈 화면 정보 구조 정리', checked: true },
{ id: '02', title: '플래너 페이지 헤더 상태 연결', checked: true },
{ id: '03', title: '타임테이블 액티브 블록 설계' },
{ id: '04', title: '우측 캘린더 빠른 이동 구현' },
{ id: '05', title: '전날 요약 영역 문구 다듬기' },
{ id: '06', title: '다음날 할 일 자동 제안 시나리오' },
{ id: '07', title: '통계 카드 레이아웃 정리' },
{ id: '08', title: '모바일 스택 레이아웃 점검' },
{ id: '09', title: '체크박스 인터랙션 연결' },
{ id: '10', title: '페이지 넘김 애니메이션 방향 검토' },
{ id: '11', title: 'Tailwind 토큰 정리' },
{ id: '12', title: 'Vue 컴포넌트 분리 기준 정하기' },
{ id: '13', title: '빈 상태 문구 작성' },
{ id: '14', title: '주간 리듬 회고 메모 추가' },
{ id: '15', title: '디자인 QA 체크리스트 정리' },
],
memo: [
'오른쪽 패널은 정보 확인과 이동이 한 번에 되도록 유지.',
'2페이지 보기는 비교용으로 남기고 기본값은 집중 보기로 사용.',
'작업 흐름이 끊기지 않도록 클릭 포인트를 줄이기.',
],
prevSummary: [
'어제 완료한 핵심 3개 보기',
'이전 체크 누적률 78%',
'막힌 작업 메모 바로 이어보기',
],
nextFocus: [
'내일 첫 집중 블록 예약',
'다음날 핵심 3개 자동 복사',
'미완료 작업만 재정렬',
],
},
'2026-04-22': {
dday: 'D-11 LAUNCH',
comment: '초반 90분은 구현, 후반은 문장과 사용성 정리에 사용.',
totalTime: '08H 20M',
tasks: [
{ id: '01', title: '통계 섹션 수치 실제 계산 연결', checked: true },
{ id: '02', title: '날짜 선택 시 상태 동기화' },
{ id: '03', title: '다음날 할 일 템플릿 개선' },
{ id: '04', title: '2페이지 보기 비교 카피 작성' },
{ id: '05', title: '주간 흐름 카드 추가' },
{ id: '06', title: '메모 영역 편집 UX 개선' },
{ id: '07', title: '프린트 스타일 초안 점검' },
{ id: '08', title: '색 대비 보정' },
{ id: '09', title: '우측 패널 축약 버전 검토' },
{ id: '10', title: '마감용 체크리스트 분리' },
{ id: '11', title: '키보드 탐색 흐름 다듬기' },
{ id: '12', title: '데이터 구조 문서화' },
{ id: '13', title: '테스트 날짜 더미 세트 늘리기' },
{ id: '14', title: '브랜드 푸터 위치 확정' },
{ id: '15', title: '빈 상태 일러스트 여부 결정' },
],
memo: [
'한 화면에 모든 정보를 모으되 시선 이동은 짧게.',
'캘린더는 검색보다 빠른 이동 장치로 동작해야 함.',
'실행 화면은 다이어리 같고, 인터랙션은 앱처럼.',
],
prevSummary: [
'이전날 메모 3줄 이어보기',
'전날 타임블록 밀도 확인',
'완료율 기준 다음 일정 추천',
],
nextFocus: [
'다음날 오전 블록 추천',
'오늘 미완료 작업 넘기기',
'이번 주 누적 시간 요약',
],
},
}
const dateFormatter = new Intl.DateTimeFormat('ko-KR', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
weekday: 'short',
})
function toKey(date: Date) {
const year = date.getFullYear()
const month = `${date.getMonth() + 1}`.padStart(2, '0')
const day = `${date.getDate()}`.padStart(2, '0')
return `${year}-${month}-${day}`
}
function buildFallbackRecord(date: Date): PlannerRecord {
return {
dday: 'D-00 FOCUS',
comment: `${date.getMonth() + 1}${date.getDate()}일 플래너를 위한 빈 페이지입니다.`,
totalTime: '00H 00M',
tasks: Array.from({ length: 15 }, (_, index) => ({
id: `${index + 1}`.padStart(2, '0'),
title: index < 3 ? '새 작업을 추가해 주세요.' : '',
})),
memo: ['메모를 남겨 두세요.', '중요한 문장을 짧게 적어 보세요.', '내일로 넘길 내용을 적어 두세요.'],
prevSummary: ['이전 기록 없음', '새로운 흐름 시작', ''],
nextFocus: ['다음 집중 블록 준비', '내일의 핵심 3개 정하기', ''],
}
}
const planner = computed(() => plannerSeed[toKey(selectedDate.value)] ?? buildFallbackRecord(selectedDate.value))
const secondaryDate = computed(() => {
const next = new Date(selectedDate.value)
next.setDate(next.getDate() + 1)
return next
})
const secondaryPlanner = computed(() => plannerSeed[toKey(secondaryDate.value)] ?? buildFallbackRecord(secondaryDate.value))
const monthLabel = computed(() =>
`${selectedDate.value.getFullYear()}.${`${selectedDate.value.getMonth() + 1}`.padStart(2, '0')}`,
)
const calendarDays = computed(() => {
const base = selectedDate.value
const first = new Date(base.getFullYear(), base.getMonth(), 1)
const start = new Date(first)
start.setDate(first.getDate() - first.getDay())
return Array.from({ length: 35 }, (_, index) => {
const date = new Date(start)
date.setDate(start.getDate() + index)
return {
key: toKey(date),
label: date.getDate(),
date,
isCurrentMonth: date.getMonth() === base.getMonth(),
}
})
})
const completedTasks = computed(() => planner.value.tasks.filter((task) => task.checked).length)
const completionRate = computed(() => Math.round((completedTasks.value / planner.value.tasks.length) * 100))
function shiftDate(amount: number) {
const next = new Date(selectedDate.value)
next.setDate(next.getDate() + amount)
selectedDate.value = next
}
</script>
<template>
<main class="min-h-screen px-4 py-6 text-ink sm:px-6 lg:px-10">
<div class="mx-auto flex max-w-[1680px] flex-col gap-6">
<header class="flex flex-col gap-4 rounded-[28px] border border-white/60 bg-white/70 p-5 backdrop-blur sm:p-6">
<div class="flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between">
<div class="space-y-2">
<p class="text-[11px] font-bold uppercase tracking-[0.28em] text-stone-500">10 Minute Planner</p>
<h1 class="text-2xl font-semibold tracking-[-0.04em] text-stone-900 sm:text-4xl">
다이어리처럼 보이되, 앱답게 빠르게 이동하는 플래너
</h1>
<p class="max-w-3xl text-sm font-medium leading-6 text-stone-600">
기본 모드는 Figma의 1페이지 + 보조 정보 패널 구성을 따르고, 비교용으로 2페이지 펼침 보기도 함께 제공합니다.
</p>
</div>
<div class="flex flex-wrap items-center gap-3">
<div class="inline-flex rounded-full border border-stone-200 bg-stone-100 p-1">
<button
type="button"
class="rounded-full px-4 py-2 text-xs font-bold tracking-[0.14em] transition"
:class="viewMode === 'focus' ? 'bg-white text-ink shadow-sm' : 'text-stone-500'"
@click="viewMode = 'focus'"
>
1 PAGE + INFO
</button>
<button
type="button"
class="rounded-full px-4 py-2 text-xs font-bold tracking-[0.14em] transition"
:class="viewMode === 'spread' ? 'bg-white text-ink shadow-sm' : 'text-stone-500'"
@click="viewMode = 'spread'"
>
2 PAGE SPREAD
</button>
</div>
<div class="inline-flex items-center gap-2 rounded-full border border-stone-200 bg-white px-2 py-2">
<button
type="button"
class="rounded-full border border-stone-200 px-3 py-2 text-xs font-bold tracking-[0.14em] text-stone-600 transition hover:border-stone-400 hover:text-ink"
@click="shiftDate(-1)"
>
PREV DAY
</button>
<button
type="button"
class="rounded-full border border-stone-200 px-3 py-2 text-xs font-bold tracking-[0.14em] text-stone-600 transition hover:border-stone-400 hover:text-ink"
@click="shiftDate(1)"
>
NEXT DAY
</button>
</div>
</div>
</div>
</header>
<section
v-if="viewMode === 'focus'"
class="grid gap-6 xl:grid-cols-[minmax(0,1fr)_320px]"
>
<PlannerPage
:date-label="dateFormatter.format(selectedDate)"
:dday="planner.dday"
:comment="planner.comment"
:total-time="planner.totalTime"
:tasks="planner.tasks"
:memo="planner.memo"
:hours="hours"
/>
<aside class="flex flex-col gap-4">
<section class="border border-stone-200 bg-white/80 p-5">
<p class="mb-4 text-[11px] font-bold tracking-[0.22em] text-ink">PREV SNAPSHOT</p>
<div class="space-y-3">
<p
v-for="item in planner.prevSummary"
:key="item"
class="border-b border-stone-200 pb-3 text-[11px] font-semibold tracking-[0.08em] text-stone-700 last:border-b-0 last:pb-0"
>
{{ item || ' ' }}
</p>
</div>
</section>
<section class="border border-stone-200 bg-white/80 p-5">
<p class="mb-4 text-[11px] font-bold tracking-[0.22em] text-ink">READ NEXT</p>
<div class="space-y-3">
<p
v-for="item in planner.nextFocus"
:key="item"
class="border-b border-stone-200 pb-3 text-[11px] font-semibold tracking-[0.08em] text-stone-700 last:border-b-0 last:pb-0"
>
{{ item || ' ' }}
</p>
</div>
</section>
<MiniCalendar
:month-label="monthLabel"
:days="calendarDays"
:selected-key="toKey(selectedDate)"
@select="selectedDate = $event"
/>
<section class="grid grid-cols-2 gap-4">
<article class="border border-stone-200 bg-white/80 p-5">
<p class="text-[11px] font-bold tracking-[0.22em] text-ink">STATS</p>
<div class="mt-5 space-y-4">
<div>
<p class="text-[28px] font-semibold tracking-[-0.05em] text-stone-900">{{ completionRate }}%</p>
<p class="text-[11px] font-semibold tracking-[0.08em] text-stone-500">TASK COMPLETION</p>
</div>
<div>
<p class="text-[22px] font-semibold tracking-[-0.04em] text-stone-900">{{ planner.totalTime }}</p>
<p class="text-[11px] font-semibold tracking-[0.08em] text-stone-500">FOCUSED TIME</p>
</div>
</div>
</article>
<article class="border border-stone-200 bg-white/80 p-5">
<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">
{{ dateFormatter.format(secondaryDate) }}
</p>
<p class="text-[11px] font-semibold tracking-[0.08em] text-stone-600">
내일의 작업은 "{{ secondaryPlanner.tasks[0]?.title || '새 작업 추가' }}" 시작합니다.
</p>
</div>
</article>
</section>
</aside>
</section>
<section v-else class="overflow-x-auto rounded-[32px] border border-white/60 bg-white/40 p-4 sm:p-6">
<div class="flex min-w-[1260px] gap-6">
<PlannerPage
:date-label="dateFormatter.format(selectedDate)"
:dday="planner.dday"
:comment="planner.comment"
:total-time="planner.totalTime"
:tasks="planner.tasks"
:memo="planner.memo"
:hours="hours"
/>
<PlannerPage
:date-label="dateFormatter.format(secondaryDate)"
:dday="secondaryPlanner.dday"
:comment="secondaryPlanner.comment"
:total-time="secondaryPlanner.totalTime"
:tasks="secondaryPlanner.tasks"
:memo="secondaryPlanner.memo"
:hours="hours"
/>
</div>
</section>
</div>
</main>
</template>