v0.1.46 - 비로그인 데모 화면 추가

This commit is contained in:
2026-04-23 13:48:40 +09:00
parent e847ddd227
commit 744202077f
3 changed files with 183 additions and 2 deletions

View File

@@ -227,6 +227,7 @@
- 비로그인 랜딩은 모바일에서 `카드 안 카드`처럼 보이지 않도록 기능 설명 카드를 얇은 리스트로 단순화했고, `LOGIN` / `SIGN UP` 버튼은 같은 너비와 높이로 맞췄다. 로그인/회원가입 모달도 하단 전환 영역을 별도 카드 대신 구분선 형태로 정리했다.
- 로그인 모달에 `로그인 유지` 체크박스를 추가했다. 기본값은 OFF이며, OFF 상태에서는 인증 토큰을 `sessionStorage`에 저장해 브라우저 세션이 끝나면 사라지고, ON 상태에서만 `localStorage`에 저장한다.
- 현재 로그아웃은 프론트 저장 토큰을 지우는 수준이다. 개인 기록 서비스 성격을 고려하면 다음 단계에서 서버 세션 폐기 API와 미사용 자동 로그아웃 옵션을 추가하는 편이 좋다.
- 비로그인 랜딩에 `DEMO VIEW`를 추가했다. 데모는 실제 저장/로그인 상태와 분리된 읽기 전용 샘플이며, 어제/오늘/내일 3일치 플래너를 전환해서 제품 감각을 먼저 볼 수 있다.
## 갱신 규칙

View File

@@ -90,6 +90,7 @@
- [x] 백엔드 기본 스캐폴딩을 추가한다.
- [x] PostgreSQL 전환 초안을 적용한다.
- [x] 로그인 화면 문구와 관리자 정보 노출 지점을 일반 사용자 기준으로 정리한다.
- [x] 비로그인 사용자가 저장 없이 볼 수 있는 3일치 샘플 데모 화면을 추가한다.
- [x] 미완료 항목을 다음 날짜 빈칸으로 이월하는 버튼을 추가한다.
- [ ] 이메일 인증 플로우를 설계하고 구현한다.
- [ ] 비밀번호 찾기 / 재설정 토큰 흐름을 추가한다.

View File

@@ -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(() => {
<div class="min-w-0 space-y-6" :class="isAuthenticated ? 'xl:h-full xl:overflow-hidden' : 'w-full'">
<section
v-if="!isAuthenticated"
v-if="!isAuthenticated && !demoMode"
class="scrollbar-hide print-hidden mx-auto w-full max-w-3xl rounded-[30px] border border-white/70 bg-white/72 px-5 py-7 shadow-[0_24px_80px_rgba(28,25,23,0.08)] sm:px-8 sm:py-9"
>
<div class="mx-auto flex max-w-2xl flex-col gap-6 text-center">
@@ -2044,7 +2142,14 @@ onBeforeUnmount(() => {
</p>
</div>
<div class="grid w-full gap-3 sm:mx-auto sm:max-w-md sm:grid-cols-2">
<div class="grid w-full gap-3 sm:mx-auto sm:max-w-2xl sm:grid-cols-3">
<button
type="button"
class="h-12 w-full rounded-full border border-stone-300 bg-white px-6 text-xs font-bold tracking-[0.18em] text-stone-700 transition hover:border-stone-500 hover:text-stone-900"
@click="demoMode = true; demoDayOffset = 0"
>
DEMO VIEW
</button>
<button
type="button"
class="h-12 w-full rounded-full border border-stone-300 bg-white px-6 text-xs font-bold tracking-[0.18em] text-stone-700 transition hover:border-stone-500 hover:text-stone-900"
@@ -2063,6 +2168,80 @@ onBeforeUnmount(() => {
</div>
</section>
<section
v-else-if="!isAuthenticated && demoMode"
class="print-hidden mx-auto grid w-full max-w-6xl gap-5 rounded-[30px] border border-white/70 bg-white/55 p-4 shadow-[0_24px_80px_rgba(28,25,23,0.08)] sm:p-6 xl:grid-cols-[minmax(0,1fr)_280px] xl:items-start"
>
<div class="overflow-x-auto rounded-[26px] border border-white/70 bg-white/45 p-3 sm:p-4">
<div class="pointer-events-none mx-auto w-[762px] max-w-none">
<PlannerPage
:date-main="demoDateDisplay.main"
:date-weekday="demoDateDisplay.weekday"
:date-weekday-tone="demoDateDisplay.weekdayTone"
:dday="demoDday"
:show-dday="true"
:comment="demoPlanner.comment"
:total-time="formatTotalTimeKorean(demoPlanner)"
:tasks="demoPlanner.tasks"
:memo="demoPlanner.memo"
:hours="hours"
:timetable="demoPlanner.timetable"
/>
</div>
</div>
<aside class="rounded-[26px] border border-white/70 bg-[#f7f2ea]/95 p-4">
<p class="text-[10px] font-bold uppercase tracking-[0.24em] text-stone-500">DEMO PAGE</p>
<h2 class="mt-3 text-2xl font-semibold tracking-[-0.05em] text-stone-900">
3일치 샘플로<br>흐름을 먼저 보기
</h2>
<p class="mt-3 text-sm font-semibold leading-6 text-stone-600">
화면은 저장되지 않는 샘플입니다. 어제, 오늘, 내일 기록이 어떻게 이어지는지 가볍게 확인해보세요.
</p>
<div class="mt-5 grid gap-2">
<button
v-for="item in [
{ label: '어제', offset: -1 },
{ label: '오늘', offset: 0 },
{ label: '내일', offset: 1 },
]"
:key="item.offset"
type="button"
class="rounded-2xl border px-4 py-3 text-left text-xs font-bold tracking-[0.14em] transition"
:class="demoDayOffset === item.offset ? 'border-stone-900 bg-stone-900 text-white' : 'border-stone-200 bg-white text-stone-600 hover:border-stone-400 hover:text-stone-900'"
@click="demoDayOffset = item.offset"
>
{{ item.label }}
</button>
</div>
<div class="mt-5 grid gap-2 border-t border-stone-200 pt-5">
<button
type="button"
class="rounded-full border border-stone-900 bg-stone-900 px-5 py-3 text-xs font-bold tracking-[0.16em] text-white transition hover:bg-stone-700"
@click="openAuthDialog('signup')"
>
SIGN UP
</button>
<button
type="button"
class="rounded-full border border-stone-300 bg-white px-5 py-3 text-xs font-bold tracking-[0.16em] text-stone-700 transition hover:border-stone-500 hover:text-stone-900"
@click="openAuthDialog('login')"
>
LOGIN
</button>
<button
type="button"
class="rounded-full px-5 py-3 text-xs font-bold tracking-[0.16em] text-stone-500 transition hover:text-stone-900"
@click="demoMode = false"
>
BACK
</button>
</div>
</aside>
</section>
<section
v-else-if="screenMode === 'planner' && viewMode === 'focus'"
class="print-hidden relative grid gap-6 xl:h-full xl:min-h-0"