v0.0.1
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
|
||||||
37
AGENTS.md
Normal file
37
AGENTS.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Codex Working Rules
|
||||||
|
|
||||||
|
This file defines repo-specific rules that CODEX AI and future helpers should follow.
|
||||||
|
|
||||||
|
## Product Rules
|
||||||
|
|
||||||
|
- Preserve the planner identity as a `10-minute planner diary`.
|
||||||
|
- Keep the default primary experience as `1 page + right-side information panel`.
|
||||||
|
- Keep `2 page spread` mode available unless the user explicitly removes it.
|
||||||
|
- Prefer calm paper-like layouts over generic dashboard styling.
|
||||||
|
|
||||||
|
## Technical Rules
|
||||||
|
|
||||||
|
- Use Vue for implementation.
|
||||||
|
- Use TailwindCSS for styling.
|
||||||
|
- Prefer reusable Vue components over large monolithic templates.
|
||||||
|
- Keep mock data easy to replace with real data sources later.
|
||||||
|
- Preserve responsive behavior for desktop and mobile.
|
||||||
|
|
||||||
|
## Workflow Rules
|
||||||
|
|
||||||
|
- Use local Git versioning continuously during development.
|
||||||
|
- Record meaningful product or technical notes in `HANDOFF.md`.
|
||||||
|
- Do not remove user-authored notes from `HANDOFF.md` unless they are outdated and replaced.
|
||||||
|
- When a major change is made, update both code and handoff context together.
|
||||||
|
|
||||||
|
## Commit Rules
|
||||||
|
|
||||||
|
- Use semantic version style commits starting from `v0.0.1` when the user asks for versioned checkpoints.
|
||||||
|
- Prefer small, understandable checkpoints over large ambiguous commits.
|
||||||
|
|
||||||
|
## Design Implementation Rules
|
||||||
|
|
||||||
|
- Use the provided Figma files as the visual source of truth.
|
||||||
|
- Match the overall proportions, line rhythm, and typography feel of the diary layout.
|
||||||
|
- Add new UI around the planner only when it clearly supports navigation, planning, or review.
|
||||||
|
|
||||||
46
HANDOFF.md
Normal file
46
HANDOFF.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Handoff Notes
|
||||||
|
|
||||||
|
## Project Summary
|
||||||
|
|
||||||
|
- Project: 10 Minute Planner web UI
|
||||||
|
- Stack: Vue 3 + Vite + TailwindCSS + TypeScript
|
||||||
|
- Current version baseline: `v0.0.1`
|
||||||
|
|
||||||
|
## Source Design
|
||||||
|
|
||||||
|
- Figma spread view: `https://www.figma.com/design/ZgIAmg2YlVWpABD7JVLPzY/Untitled?node-id=1-36&m=dev`
|
||||||
|
- Figma focus view with side info: `https://www.figma.com/design/ZgIAmg2YlVWpABD7JVLPzY/Untitled?node-id=1-2472&m=dev`
|
||||||
|
|
||||||
|
## Current Product Direction
|
||||||
|
|
||||||
|
- Default UX direction is `1 page + extra information panel`.
|
||||||
|
- `2 page spread` view is still implemented as an alternate mode for comparison.
|
||||||
|
- The UI should feel like a paper diary, but interactions should still feel like an app.
|
||||||
|
|
||||||
|
## Current Implementation
|
||||||
|
|
||||||
|
- Main shell: `src/App.vue`
|
||||||
|
- Planner paper layout: `src/components/PlannerPage.vue`
|
||||||
|
- Right-side calendar: `src/components/MiniCalendar.vue`
|
||||||
|
- Tailwind setup is in place and should remain the styling system for this project.
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
- Vue was chosen over static HTML because the planner needs stateful interactions:
|
||||||
|
date switching, mode toggling, sidebar summaries, and future data persistence.
|
||||||
|
- TailwindCSS is the required styling approach even when using Vue.
|
||||||
|
- The current data is mock data for layout and interaction verification.
|
||||||
|
|
||||||
|
## Next Recommended Steps
|
||||||
|
|
||||||
|
- Connect planner data to persistent storage or local state management.
|
||||||
|
- Make task checkbox state editable.
|
||||||
|
- Add timetable interaction for selecting or painting focused time blocks.
|
||||||
|
- Decide whether the right panel should prioritize calendar, stats, or next-day planning on mobile.
|
||||||
|
- Add print/export styling if the diary-like output needs physical printing.
|
||||||
|
|
||||||
|
## Update Rule
|
||||||
|
|
||||||
|
- When an important decision, constraint, bug, or workflow change happens, append it here.
|
||||||
|
- Keep this file concise and practical so the next helper can continue without re-discovery.
|
||||||
|
|
||||||
12
index.html
Normal file
12
index.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>10분 플래너</title>
|
||||||
|
</head>
|
||||||
|
<body class="bg-stone-100">
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2586
package-lock.json
generated
Normal file
2586
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
package.json
Normal file
23
package.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "ten-minute-planner",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vue-tsc --noEmit && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"vue": "^3.5.13"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"postcss": "^8.4.49",
|
||||||
|
"tailwindcss": "^3.4.17",
|
||||||
|
"typescript": "^5.7.2",
|
||||||
|
"vite": "^6.0.5",
|
||||||
|
"vue-tsc": "^2.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
337
src/App.vue
Normal file
337
src/App.vue
Normal 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>
|
||||||
47
src/components/MiniCalendar.vue
Normal file
47
src/components/MiniCalendar.vue
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
type DayCell = {
|
||||||
|
key: string
|
||||||
|
label: number
|
||||||
|
date: Date
|
||||||
|
isCurrentMonth: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
monthLabel: string
|
||||||
|
days: DayCell[]
|
||||||
|
selectedKey: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
select: [date: Date]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="border border-stone-200 bg-white/80 p-5">
|
||||||
|
<div class="mb-4 flex items-center justify-between">
|
||||||
|
<h2 class="text-[11px] font-bold tracking-[0.22em] text-ink">CALENDAR</h2>
|
||||||
|
<span class="text-[11px] font-semibold tracking-[0.16em] text-stone-500">{{ monthLabel }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3 grid grid-cols-7 gap-2 text-center text-[10px] font-bold tracking-[0.12em] text-stone-400">
|
||||||
|
<span v-for="weekday in ['S', 'M', 'T', 'W', 'T', 'F', 'S']" :key="weekday">{{ weekday }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-7 gap-2">
|
||||||
|
<button
|
||||||
|
v-for="day in days"
|
||||||
|
:key="day.key"
|
||||||
|
type="button"
|
||||||
|
class="aspect-square rounded-full border text-[11px] font-semibold transition"
|
||||||
|
:class="[
|
||||||
|
day.key === selectedKey
|
||||||
|
? 'border-ink bg-ink text-white'
|
||||||
|
: 'border-stone-200 bg-stone-50 text-stone-700 hover:border-stone-400 hover:bg-white',
|
||||||
|
day.isCurrentMonth ? '' : 'opacity-35',
|
||||||
|
]"
|
||||||
|
@click="emit('select', day.date)"
|
||||||
|
>
|
||||||
|
{{ day.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
124
src/components/PlannerPage.vue
Normal file
124
src/components/PlannerPage.vue
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
type PlannerTask = {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
checked?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlannerProps = {
|
||||||
|
dateLabel: string
|
||||||
|
dday: string
|
||||||
|
comment: string
|
||||||
|
totalTime: string
|
||||||
|
tasks: PlannerTask[]
|
||||||
|
memo: string[]
|
||||||
|
hours: string[]
|
||||||
|
brand?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<PlannerProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<article
|
||||||
|
class="flex w-full max-w-[762px] flex-col gap-3 bg-paper px-6 py-6 text-[10px] font-bold tracking-[0.16em] text-ink shadow-paper sm:px-12 sm:py-12"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-4 py-[18px]">
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<div class="relative h-[90px] w-[394px] flex-1 border-t border-ink px-[10px] pt-[10px]">
|
||||||
|
<span class="absolute -top-2 left-0 bg-paper px-[2px] text-muted">YEAR / MONTH / DAY</span>
|
||||||
|
<p class="pt-6 text-xs tracking-[0.24em] text-ink sm:text-sm">{{ dateLabel }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="relative h-[90px] w-[210px] border-t border-ink px-[10px] pt-[10px]">
|
||||||
|
<span class="absolute -top-2 left-0 bg-paper px-[2px] text-muted">D-DAY</span>
|
||||||
|
<p class="pt-6 text-xs tracking-[0.24em] text-ink sm:text-sm">{{ dday }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-4 border-b border-ink pb-[18px]">
|
||||||
|
<div class="relative h-[90px] w-[394px] flex-1 border-t border-ink px-[10px] pt-[10px]">
|
||||||
|
<span class="absolute -top-2 left-0 bg-paper px-[2px] text-muted">COMMENT</span>
|
||||||
|
<p class="pt-6 text-[11px] font-semibold normal-case tracking-[0.08em] text-stone-700 sm:text-xs">
|
||||||
|
{{ comment }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="relative h-[90px] w-[210px] border-t border-ink px-[10px] pt-[10px]">
|
||||||
|
<span class="absolute -top-2 left-0 bg-paper px-[2px] text-muted">TOTAL TIME</span>
|
||||||
|
<p class="pt-6 text-xs tracking-[0.24em] text-ink sm:text-sm">{{ totalTime }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-4 py-[10px]">
|
||||||
|
<div class="flex w-[394px] flex-1 flex-col gap-9">
|
||||||
|
<section class="relative">
|
||||||
|
<div class="absolute -top-[9px] left-0 bg-paper px-[2px] text-muted">TASKS</div>
|
||||||
|
<div class="border-t border-ink">
|
||||||
|
<div
|
||||||
|
v-for="(task, index) in tasks"
|
||||||
|
:key="task.id"
|
||||||
|
class="flex h-[38px] items-center border-b"
|
||||||
|
:class="index % 5 === 4 || index === tasks.length - 1 ? 'border-ink' : 'border-line'"
|
||||||
|
>
|
||||||
|
<div class="h-full w-[62px] border-r border-dashed border-ink px-2 py-2 text-[9px] text-stone-500">
|
||||||
|
{{ task.id }}
|
||||||
|
</div>
|
||||||
|
<div class="flex min-w-0 flex-1 items-center px-3">
|
||||||
|
<span class="truncate text-[11px] font-semibold normal-case tracking-[0.06em] text-stone-800">
|
||||||
|
{{ task.title }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex h-full w-[42px] items-center justify-center p-[10px]">
|
||||||
|
<span
|
||||||
|
class="block h-full w-full border border-dashed"
|
||||||
|
:class="task.checked ? 'border-ink bg-stone-100' : 'border-ink/60'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="relative">
|
||||||
|
<div class="absolute -top-2 left-0 bg-paper px-[2px] text-muted">MEMO</div>
|
||||||
|
<div class="border-t border-ink">
|
||||||
|
<div
|
||||||
|
v-for="(memoItem, index) in memo"
|
||||||
|
:key="`${memoItem}-${index}`"
|
||||||
|
class="flex h-[38px] items-center border-b"
|
||||||
|
:class="index === memo.length - 1 ? 'border-ink' : 'border-line'"
|
||||||
|
>
|
||||||
|
<div class="h-full w-[62px] border-r border-dashed border-ink" />
|
||||||
|
<div class="flex-1 px-3 text-[11px] font-semibold normal-case tracking-[0.06em] text-stone-700">
|
||||||
|
{{ memoItem }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="relative w-[210px] shrink-0">
|
||||||
|
<div class="absolute -top-2 left-0 bg-paper px-[2px] text-muted">TIME TABLE</div>
|
||||||
|
<div class="border-t border-ink">
|
||||||
|
<div
|
||||||
|
v-for="(hour, index) in hours"
|
||||||
|
:key="`${hour}-${index}`"
|
||||||
|
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">
|
||||||
|
{{ hour }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="quarter in 6"
|
||||||
|
:key="quarter"
|
||||||
|
class="h-full w-[30px] border-r border-dashed border-line last:border-r-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<p class="text-[10px] tracking-[0.18em] text-ink">{{ brand ?? 'SORI.STUDIO' }}</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
5
src/main.ts
Normal file
5
src/main.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import './style.css'
|
||||||
|
|
||||||
|
createApp(App).mount('#app')
|
||||||
22
src/style.css
Normal file
22
src/style.css
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
html {
|
||||||
|
font-family: "Inter", "Pretendard", ui-sans-serif, system-ui, sans-serif;
|
||||||
|
color: #111111;
|
||||||
|
background: #f3f0ea;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top, rgba(255, 255, 255, 0.9), transparent 32%),
|
||||||
|
linear-gradient(180deg, #f6f2ea 0%, #efe8de 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
@apply outline-none;
|
||||||
|
}
|
||||||
|
}
|
||||||
18
tailwind.config.js
Normal file
18
tailwind.config.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: ['./index.html', './src/**/*.{vue,ts}'],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
ink: '#111111',
|
||||||
|
line: '#c9c9c9',
|
||||||
|
muted: '#b4b1c1',
|
||||||
|
paper: '#fffdf9',
|
||||||
|
},
|
||||||
|
boxShadow: {
|
||||||
|
paper: '0 24px 64px rgba(17, 17, 17, 0.08)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"strict": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"types": ["vite/client"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.vue"]
|
||||||
|
}
|
||||||
6
vite.config.ts
Normal file
6
vite.config.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user