From 744202077f0da8c7ca4dc60154342df8710363a1 Mon Sep 17 00:00:00 2001 From: zenn Date: Thu, 23 Apr 2026 13:48:40 +0900 Subject: [PATCH] =?UTF-8?q?v0.1.46=20-=20=EB=B9=84=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EB=8D=B0=EB=AA=A8=20=ED=99=94=EB=A9=B4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HANDOFF.md | 1 + TODO.md | 1 + src/App.vue | 183 +++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 183 insertions(+), 2 deletions(-) diff --git a/HANDOFF.md b/HANDOFF.md index 444f0c5..47e13a4 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -227,6 +227,7 @@ - 비로그인 랜딩은 모바일에서 `카드 안 카드`처럼 보이지 않도록 기능 설명 카드를 얇은 리스트로 단순화했고, `LOGIN` / `SIGN UP` 버튼은 같은 너비와 높이로 맞췄다. 로그인/회원가입 모달도 하단 전환 영역을 별도 카드 대신 구분선 형태로 정리했다. - 로그인 모달에 `로그인 유지` 체크박스를 추가했다. 기본값은 OFF이며, OFF 상태에서는 인증 토큰을 `sessionStorage`에 저장해 브라우저 세션이 끝나면 사라지고, ON 상태에서만 `localStorage`에 저장한다. - 현재 로그아웃은 프론트 저장 토큰을 지우는 수준이다. 개인 기록 서비스 성격을 고려하면 다음 단계에서 서버 세션 폐기 API와 미사용 자동 로그아웃 옵션을 추가하는 편이 좋다. +- 비로그인 랜딩에 `DEMO VIEW`를 추가했다. 데모는 실제 저장/로그인 상태와 분리된 읽기 전용 샘플이며, 어제/오늘/내일 3일치 플래너를 전환해서 제품 감각을 먼저 볼 수 있다. ## 갱신 규칙 diff --git a/TODO.md b/TODO.md index 6dc1674..5c2d150 100644 --- a/TODO.md +++ b/TODO.md @@ -90,6 +90,7 @@ - [x] 백엔드 기본 스캐폴딩을 추가한다. - [x] PostgreSQL 전환 초안을 적용한다. - [x] 로그인 화면 문구와 관리자 정보 노출 지점을 일반 사용자 기준으로 정리한다. +- [x] 비로그인 사용자가 저장 없이 볼 수 있는 3일치 샘플 데모 화면을 추가한다. - [x] 미완료 항목을 다음 날짜 빈칸으로 이월하는 버튼을 추가한다. - [ ] 이메일 인증 플로우를 설계하고 구현한다. - [ ] 비밀번호 찾기 / 재설정 토큰 흐름을 추가한다. diff --git a/src/App.vue b/src/App.vue index 77a7fff..0aee679 100644 --- a/src/App.vue +++ b/src/App.vue @@ -34,6 +34,8 @@ const DDAY_DISABLED_STORAGE_KEY = 'ten-minute-dday-disabled-dates' const screenMode = ref('planner') const viewMode = ref('focus') const printLayout = ref('single') +const demoMode = ref(false) +const demoDayOffset = ref(0) const authDialogOpen = ref(false) const authMode = ref('login') const authBusy = ref(false) @@ -330,6 +332,86 @@ function buildFallbackRecord(date) { } } +function createDemoDate(offset) { + const date = new Date() + date.setDate(date.getDate() + offset) + return date +} + +function createDemoRecord(offset) { + const variants = { + '-1': { + comment: '어제는 오전 집중이 좋았고, 오후에는 미뤄둔 메모를 정리했다. 오늘은 남은 작업을 가볍게 이어가기.', + tasks: [ + ['01', '주간 목표 다시 읽기', true], + ['02', '블로그 초안 20분 정리', true], + ['03', '메일 답장 3개 처리', true], + ['04', '내일 첫 작업 후보 적기', false], + ['05', '잠들기 전 회고 한 줄 남기기', false], + ], + memo: [ + ['NOTE', '오전에는 알림을 끄면 집중이 훨씬 오래 간다.'], + ['IDEA', '할 일은 적게 쓰고, 시간표는 솔직하게 남기기.'], + ['NEXT', '내일은 첫 칸에 가장 부담 없는 작업을 넣어두기.'], + ], + timetable: createTimetableFromRanges([[6, 11], [24, 29], [78, 83]]), + }, + 0: { + comment: '오늘은 큰 계획보다 10분 단위로 시작하는 감각을 유지한다. 끝내지 못한 일은 내일 첫 작업으로 넘긴다.', + tasks: [ + ['01', '가장 작은 작업 하나 먼저 끝내기', true], + ['02', '집중 시간 30분 확보하기', true], + ['03', '플래너 코멘트 한 줄 남기기', false], + ['04', '저녁 전에 미완료 항목 정리', false], + ['05', '내일 첫 작업 예약하기', false], + ], + memo: [ + ['FOCUS', '완벽한 하루보다 다시 이어갈 수 있는 하루가 목표.'], + ['RULE', '10분만 시작하고, 더 할 수 있으면 한 칸 더 칠하기.'], + ['MOOD', '오늘은 가볍게, 하지만 기록은 남기기.'], + ], + timetable: createTimetableFromRanges([[12, 17], [36, 44], [90, 95]]), + }, + 1: { + comment: '내일은 아침 첫 칸에 부담 없는 작업을 놓고 시작한다. 오늘 남긴 흐름을 끊지 않는 것이 목표.', + tasks: [ + ['01', '오늘 미완료 항목 하나 이어서 보기', false], + ['02', '오전 집중 블록 2칸 채우기', false], + ['03', '중요하지 않은 일 1개 덜어내기', false], + ['04', '짧은 회고 문장 남기기', false], + ['05', '다음날 첫 작업 적어두기', false], + ], + memo: [ + ['PLAN', '시작 작업은 작게, 마감 작업은 분명하게.'], + ['CHECK', '완료보다 흐름 유지가 우선인 날.'], + ['SPACE', '비워둔 칸은 실패가 아니라 여유로 보기.'], + ], + timetable: createTimetableFromRanges([[8, 13], [48, 53]]), + }, + } + const variant = variants[offset] ?? variants[0] + const record = buildFallbackRecord(createDemoDate(offset)) + + record.comment = variant.comment + record.tasks = record.tasks.map((task, index) => { + const sample = variant.tasks[index] + + return sample + ? { label: sample[0], title: sample[1], checked: Boolean(sample[2]) } + : task + }) + record.memo = record.memo.map((item, index) => { + const sample = variant.memo[index] + + return sample + ? { label: sample[0], text: sample[1] } + : item + }) + record.timetable = variant.timetable + + return record +} + function normalizeRecord(record) { return { ...record, @@ -405,6 +487,22 @@ function getDateDisplay(date) { const selectedDateDisplay = computed(() => getDateDisplay(selectedDate.value)) const secondaryDateDisplay = computed(() => getDateDisplay(secondaryDate.value)) +const demoDate = computed(() => createDemoDate(demoDayOffset.value)) +const demoDateDisplay = computed(() => getDateDisplay(demoDate.value)) +const demoPlanner = computed(() => createDemoRecord(demoDayOffset.value)) +const demoDday = computed(() => { + const offset = demoDayOffset.value + + if (offset < 0) { + return 'D-8 나만의 루틴 만들기' + } + + if (offset > 0) { + return 'D-6 나만의 루틴 만들기' + } + + return 'D-7 나만의 루틴 만들기' +}) const monthLabel = computed(() => `${calendarViewDate.value.getMonth() + 1}`.padStart(2, '0'), @@ -2014,7 +2112,7 @@ onBeforeUnmount(() => {
+ +