From c1c7288127fec1db4c7ebfaba3875330668acbd6 Mon Sep 17 00:00:00 2001 From: zenn Date: Thu, 23 Apr 2026 16:23:27 +0900 Subject: [PATCH] =?UTF-8?q?v0.1.55=20-=20=EA=B8=B0=EA=B0=84=20=EC=9D=B8?= =?UTF-8?q?=EC=87=84=20=EB=AA=A8=EB=8B=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HANDOFF.md | 1 + src/App.vue | 333 +++++++++++++++++++++++++++++++++++++------------- src/style.css | 26 ++-- 3 files changed, 264 insertions(+), 96 deletions(-) diff --git a/HANDOFF.md b/HANDOFF.md index 78f4aa9..8a982d6 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -219,6 +219,7 @@ - 플래너 본문 시간 라벨은 `총 시간`에서 `FOCUSED TIME`으로 바꿨다. 인쇄 CSS에서 COMMENT/FOCUSED TIME 라벨이 잘리지 않도록 부모 overflow를 열고, COMMENT는 남는 폭을 채우며 FOCUSED TIME은 오른쪽 210px 칸에 붙도록 조정했다. - Docker 배포용 루트 `.env`와 개발용 `.env.dev`를 분리했다. `docker-compose.yml`은 운영 `.env`, `docker-compose.dev.yml`은 기존 dev Postgres 볼륨과 맞는 `.env.dev`를 읽는다. - STATS의 `RANGE FLOW`는 항목 수에 따라 막대 폭과 간격을 조정한다. 1주일 내외는 넓은 막대로 카드 폭을 채우고, 1달 내외는 좁은 막대와 요약 라벨로 한 화면에서 흐름을 보며, 세부 날짜와 집중 시간은 막대 hover 팝업으로 확인한다. +- 왼쪽 사이드바의 인쇄 영역은 `PRINT` 버튼 하나로 줄이고, 클릭 시 모달에서 시작일/종료일과 `1페이지씩` 또는 `2페이지씩` 출력 방식을 선택한다. 인쇄 전용 렌더링은 선택 기간의 날짜를 순서대로 여러 장 생성하며, 2페이지씩 출력에서 홀수 날짜가 남으면 오른쪽은 빈 페이지 프레임으로 둔다. - `5173` 포트가 직접 `npm run dev` 없이 열려 있으면 대부분 `ten-minute-frontend-dev` 컨테이너가 떠 있는 상태다. 개발용 Docker를 끄려면 `docker compose -f docker-compose.dev.yml down`을 사용한다. - 현재 환경에서는 Docker 데몬이 꺼져 있어서 `docker compose build` 실검증은 하지 못했고, 데몬 시작 후 다시 확인이 필요하다. - 이미지 저장 기능은 추후 `print-only` 또는 별도 export 전용 레이아웃을 기준으로 구현하면 화면/인쇄/공유 결과를 맞추기 쉽다. diff --git a/src/App.vue b/src/App.vue index 8005c08..9a26fe6 100644 --- a/src/App.vue +++ b/src/App.vue @@ -36,6 +36,7 @@ const CARRYOVER_CHECK_POLICIES = ['ask', 'all', 'current'] const screenMode = ref('planner') const viewMode = ref('focus') const printLayout = ref('single') +const printDialogOpen = ref(false) const demoMode = ref(false) const demoDayOffset = ref(0) const authDialogOpen = ref(false) @@ -59,6 +60,9 @@ const leftPanelOpen = ref(false) const rightPanelOpen = ref(false) const statsRangeStart = ref(toKey(new Date(new Date().setDate(new Date().getDate() - 6)))) const statsRangeEnd = ref(toKey(new Date())) +const printRangeStart = ref(toKey(new Date())) +const printRangeEnd = ref(toKey(new Date())) +const printRangeLayout = ref('single') const authForm = reactive({ nickname: '', email: '', @@ -550,6 +554,56 @@ const calendarDays = computed(() => { }) }) +const normalizedPrintRange = computed(() => { + const startKey = printRangeStart.value || toKey(selectedDate.value) + const endKey = printRangeEnd.value || startKey + + if (startKey <= endKey) { + return { startKey, endKey } + } + + return { startKey: endKey, endKey: startKey } +}) + +const printDateKeys = computed(() => { + const dateKeys = [] + const currentDate = startOfDay(toDateValue(normalizedPrintRange.value.startKey)) + const endDate = startOfDay(toDateValue(normalizedPrintRange.value.endKey)) + + while (currentDate <= endDate) { + dateKeys.push(toKey(currentDate)) + currentDate.setDate(currentDate.getDate() + 1) + } + + return dateKeys +}) + +const printPages = computed(() => + printDateKeys.value.map((dateKey) => createPrintPage(dateKey)), +) + +const printPapers = computed(() => { + if (printLayout.value === 'single') { + return printPages.value.map((page) => [page]) + } + + const papers = [] + + for (let index = 0; index < printPages.value.length; index += 2) { + papers.push(printPages.value.slice(index, index + 2)) + } + + return papers +}) + +const printPageCountLabel = computed(() => { + const dayCount = printDateKeys.value.length + const paperCount = printRangeLayout.value === 'double' ? Math.ceil(dayCount / 2) : dayCount + const layoutLabel = printRangeLayout.value === 'double' ? '2페이지씩' : '1페이지씩' + + return `${dayCount}일치 / ${paperCount}장 (${layoutLabel})` +}) + const markedDateKeys = computed(() => Object.entries(plannerRecords) .filter(([, record]) => hasPlannerContent(record)) @@ -801,6 +855,12 @@ function closeCarryoverCheckPrompt() { } function handleGlobalKeydown(event) { + if (event.key === 'Escape' && printDialogOpen.value) { + event.preventDefault() + closePrintDialog() + return + } + if (event.key === 'Escape' && carryoverCheckPrompt.value) { event.preventDefault() closeCarryoverCheckPrompt() @@ -971,6 +1031,56 @@ function createDateLabel(dateKey) { return `${display.main} ${display.weekday}` } +function findPlannerGoalForDate(dateKey) { + const activeGoals = goals.value + .filter((goal) => { + if (!goal.activeFrom || !goal.activeUntil) { + return false + } + + return dateKey >= goal.activeFrom && dateKey <= goal.activeUntil + }) + .sort((left, right) => { + const currentDate = startOfDay(toDateValue(dateKey)) + const leftDistance = Math.abs(startOfDay(toDateValue(left.targetDate)).getTime() - currentDate.getTime()) + const rightDistance = Math.abs(startOfDay(toDateValue(right.targetDate)).getTime() - currentDate.getTime()) + return leftDistance - rightDistance + }) + + return activeGoals[0] ?? null +} + +function createPlannerDdayForDate(dateKey) { + if (isDdayDisabledForDate(dateKey)) { + return '' + } + + const goal = findPlannerGoalForDate(dateKey) + + if (!goal) { + return '' + } + + const targetDate = startOfDay(toDateValue(goal.targetDate)) + const currentDate = startOfDay(toDateValue(dateKey)) + const diffDays = Math.round((targetDate.getTime() - currentDate.getTime()) / (24 * 60 * 60 * 1000)) + const badge = + diffDays === 0 ? 'D-DAY' : diffDays > 0 ? `D-${diffDays}` : `D+${Math.abs(diffDays)}` + + return `${badge} ${goal.title}` +} + +function createPrintPage(dateKey) { + const date = toDateValue(dateKey) + + return { + key: dateKey, + display: getDateDisplay(date), + record: getPlannerRecord(date), + dday: createPlannerDdayForDate(dateKey), + } +} + const weeklyRecords = computed(() => { const entries = rangeEntries.value.map(([key, record]) => { const date = toDateValue(key) @@ -1211,6 +1321,19 @@ function closeRightPanel() { rightPanelOpen.value = false } +function openPrintDialog() { + const selectedKey = toKey(selectedDate.value) + printRangeStart.value = selectedKey + printRangeEnd.value = selectedKey + printRangeLayout.value = viewMode.value === 'spread' ? 'double' : 'single' + printDialogOpen.value = true + closeLeftPanel() +} + +function closePrintDialog() { + printDialogOpen.value = false +} + function applyStatsQuickRange(days) { const endDate = new Date() const startDate = new Date(endDate) @@ -1845,6 +1968,13 @@ async function printSelectedPlanner(layout = 'single') { window.print() } +async function printPlannerRange() { + printLayout.value = printRangeLayout.value + applyPrintPageStyle(printRangeLayout.value) + await nextTick() + window.print() +} + onMounted(() => { resetGoalForm() setSyncFeedback('local', '로그인 후 클라우드 저장을 사용할 수 있습니다.', { @@ -2058,16 +2188,9 @@ onBeforeUnmount(() => { - @@ -2226,16 +2349,9 @@ onBeforeUnmount(() => { - @@ -2832,79 +2948,35 @@ onBeforeUnmount(() => { /> @@ -2922,6 +2994,93 @@ onBeforeUnmount(() => { @update:field="updateAuthField" /> +
+
+
+
+

Print Planner

+

+ 출력할 날짜 범위 선택 +

+

+ 선택한 기간을 1페이지씩 또는 2페이지씩 묶어서 바로 출력합니다. +

+
+ +
+ +
+ + +
+ +
+ + +
+ +
+

출력 예정

+

{{ printPageCountLabel }}

+
+ +
+ + +
+
+
+