diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..70526dd --- /dev/null +++ b/.prettierrc @@ -0,0 +1,11 @@ +{ + "semi": false, + "singleQuote": true, + "trailingComma": "es5", + "bracketSpacing": true, + "arrowParens": "always", + "useTabs": false, + "tabWidth": 2, + "printWidth": 80, + "endOfLine": "lf" +} diff --git a/README.md b/README.md index 34a3ed5..e4ae826 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,41 @@ -# 1주차 과제: Vanilla Todo +# [CEOS 23rd Week1] - Vanilla Todo -# 서론 +캘린더를 기준으로 날짜별 할 일을 관리하고, 오늘과 이번 달 진행 상황을 함께 확인할 수 있는 [Vanilla Todo](https://vanilla-todo-23rd.vercel.app/) 프로젝트입니다. -안녕하세요 🙌🏻 23기 프론트엔드 운영진 **원채영**입니다. +HTML, CSS, JavaScript만으로 캘린더 UI와 Todo 상태 관리를 직접 구현했습니다. -이번 미션은 개발 환경 구축과 스터디 진행 방식에 익숙해지실 수 있도록 간단한 **to-do list** 만들기를 진행합니다. 무작정 첫 스터디부터 React를 다루는 것보다는 왜 React가 필요한지, React가 없으면 무엇이 불편한지 느껴 보고 본격적인 스터디에 들어가는 것이 React를 이해하는 데 더 많은 도움이 될 것이라 생각합니다. +## 주요 기능 -비교적 가벼운 미션인 만큼 코드를 짜는 데 있어 여러분의 ”**창의성**”을 충분히 발휘해 보시기 바랍니다. 작동하기만 하면 되는 것보다 같은 코드를 짜는 여러가지 방식과 패턴에 대해 고민해 보시고, 본인이 생각한 가장 창의적인 방법으로 코드를 작성해 주세요. 여러분이 미션을 수행하는 과정에서 겪는 고민과 생각의 깊이만큼 스터디에서 더 많은 것을 얻어가실 수 있을 것입니다. +- 날짜 선택 기반 Todo 추가, 완료, 삭제 +- 월간 캘린더에서 날짜 이동 및 선택 +- 오늘/월별 Todo 통계 제공 +- `localStorage` 기반 데이터 저장 +- 모바일과 데스크톱에 대응하는 반응형 UI -막히는 부분이 있더라도 우선은 스스로 공부하고 찾아보는 방법을 권고드리지만, 운영진의 도움이 필요하시다면 얼마든지 프론트엔드 카톡방에 편하게 질문을 남겨 주세요! +## 폴더 구조 -# 과제 +```text +vanilla-todo-23rd/ +├─ index.html # 앱의 마크업 구조 +├─ script.js # 캘린더, Todo, 통계 로직 +└─ style.css # 전체 UI 스타일 및 반응형 스타일 +``` -## 목표 +## 기술 스택 -- VSCode, Prettier를 이용하여 개발 환경을 관리합니다. -- HTML/CSS의 기초를 이해합니다. -- JavaScript를 이용한 DOM 조작을 이해합니다. -- Vanilla Js를 이용한 어플리케이션 상태 관리 방법을 이해합니다. +| 구분 | 기술 | 사용 이유 | +| ---------- | -------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | +| Markup | | 시맨틱한 구조로 캘린더, 통계, Todo 입력/목록 UI를 구성하기 위해 사용 | +| Styling | | 레이아웃, 상태별 스타일, 반응형 UI를 직접 제어하기 위해 사용 | +| Language | | 캘린더 렌더링, Todo CRUD, 통계 계산 등 동적인 동작을 구현하기 위해 사용 | +| Formatting | | 코드 포맷을 일관되게 유지해 가독성과 유지보수성을 높이기 위해 사용 | +| Deploy | | 정적 웹 앱을 배포하고 실제 동작을 빠르게 확인하기 위해 사용 | -## 기한 +## 실행 방법 -- 2026년 3월 14일 토요일 23:59까지 +```bash +git clone -b waldls https://github.com/waldls/vanilla-todo-23rd.git +cd vanilla-todo-23rd +``` -## Review Questions - -- DOM은 무엇인가요? -- 이벤트 흐름 제어(버블링 & 캡처링)이 무엇인가요? -- 클로저와 스코프가 무엇인가요? - -## 필수 요건 - -- [결과 화면](https://vanilla-todo-21th-jet.vercel.app/)의 기능을 구현합니다. (날짜, 요일별 todo 개수) -- 결과 링크의 화면 디자인 그대로 구현해도 좋고, 자신만의 디자인을 적용해도 좋습니다. -- CSS의 Flexbox를 이용하여 레이아웃을 구성합니다. -- JQuery, React, Bootstrap 등 외부 라이브러리를 사용하지 않습니다. -- 함수와 변수의 이름은 lowerCamelCase로 짓습니다. -- 코딩의 단위를 기능별로 나누어 Commit 메세지를 작성합니다. -- Semantic tag를 활용하여 HTML 구조를 완성합니다. - -## 선택 요건 - -- 외부 폰트 Pretendard를 적용합니다. -- 브라우저의 `localStorage` 혹은 `sessionStorage`를 이용하여 다음 번 접속 시에 기존의 투두 데이터를 불러옵니다. -- 미디어쿼리를 이용해서 반응형을 적용합니다. -- 이 외에도 추가하고 싶은 기능이 있다면 마음껏 추가하셔도 됩니다. - -# 링크 및 참고자료 - -- [HTML/CSS 기초](https://heropy.blog/2019/04/24/html-css-starter/) -- [HTML 태그](https://heropy.blog/2019/05/26/html-elements/) -- [FlexBox 가이드](https://heropy.blog/2018/11/24/css-flexible-box/) -- [JS를 통한 DOM 조작](https://velog.io/@bining/javascript-DOM-%EC%A1%B0%EC%9E%91%ED%95%98%EA%B8%B0#append) -- [localStorage, sessionStorage](https://www.daleseo.com/js-web-storage/) -- [git 사용법](https://wayhome25.github.io/git/2017/07/08/git-first-pull-request-story/) -- [좋은 코드리뷰 방법](https://tech.kakao.com/2022/03/17/2022-newkrew-onboarding-codereview/) -- [MDN 공식문서-createElement()](https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement) -- [MDN 공식문서-appendChild()](https://developer.mozilla.org/ko/docs/Web/API/Node/appendChild) -- [DOM 개념,HTML 요소 조작](https://poiemaweb.com/js-dom#3-dom-query--traversing-%EC%9A%94%EC%86%8C%EC%97%90%EC%9D%98-%EC%A0%91%EA%B7%BC) \ No newline at end of file +이후 `index.html`을 브라우저에서 열면 별도의 빌드 과정 없이 바로 실행할 수 있습니다. diff --git a/index.html b/index.html new file mode 100644 index 0000000..be113b0 --- /dev/null +++ b/index.html @@ -0,0 +1,86 @@ + + + + + + Vanilla Todo + + + +
+
+

Vanilla Todo

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

+
+
    +
    +
    + + +
    +
    +
    +
    + + + diff --git a/script.js b/script.js new file mode 100644 index 0000000..43485d9 --- /dev/null +++ b/script.js @@ -0,0 +1,250 @@ +// 상태 +const today = new Date() +let currentYear = today.getFullYear() +let currentMonth = today.getMonth() +let selectedDate = new Date(today) +let store = loadStoreFromStorage() + +// 유틸 +const dateToKey = (date) => { + const y = date.getFullYear() + const m = String(date.getMonth() + 1).padStart(2, '0') + const d = String(date.getDate()).padStart(2, '0') + return `${y}-${m}-${d}` +} + +const formatDateTitle = (date) => + `${date.getFullYear()}년 ${date.getMonth() + 1}월 ${date.getDate()}일` + +const isSameDate = (a, b) => + a.getFullYear() === b.getFullYear() && + a.getMonth() === b.getMonth() && + a.getDate() === b.getDate() + +const getTodosByDate = (date) => store[dateToKey(date)] ?? [] + +// 스토리지 +function loadStoreFromStorage() { + try { + const raw = localStorage.getItem('todo-store') + return raw ? JSON.parse(raw) : {} + } catch { + return {} + } +} + +const saveStoreToStorage = () => { + localStorage.setItem('todo-store', JSON.stringify(store)) +} + +// 렌더링 +const renderTodayStatsSection = () => { + const textEl = document.getElementById('todayStatsText') + const percentEl = document.getElementById('todayStatsPercent') + const fillEl = document.getElementById('todayProgressBarFill') + const trackEl = document.getElementById('todayProgressBarTrack') + const todos = getTodosByDate(today) + const total = todos.length + const done = todos.filter((t) => t.done).length + const percent = total === 0 ? 0 : Math.round((done / total) * 100) + + textEl.textContent = + total === 0 ? '오늘 할 일이 없어요' : `오늘 ${done} / ${total}개 완료` + percentEl.textContent = total === 0 ? '' : `${percent}%` + fillEl.style.width = `${percent}%` + trackEl.setAttribute('aria-valuenow', percent) +} + +const renderMonthStatsSection = () => { + const statsEl = document.getElementById('statsText') + const fillEl = document.getElementById('progressBarFill') + const percentEl = document.getElementById('statsPercent') + const daysInMonth = new Date(currentYear, currentMonth + 1, 0).getDate() + let total = 0 + let done = 0 + + for (let i = 1; i <= daysInMonth; i++) { + const todos = getTodosByDate(new Date(currentYear, currentMonth, i)) + total += todos.length + done += todos.filter((t) => t.done).length + } + + const percent = total === 0 ? 0 : Math.round((done / total) * 100) + statsEl.textContent = + total === 0 ? '이번 달 할 일이 없어요' : `이번 달 ${done} / ${total}개 완료` + percentEl.textContent = total === 0 ? '' : `${percent}%` + fillEl.style.width = `${percent}%` +} + +const renderCalendarGrid = () => { + const titleEl = document.getElementById('calendarTitle') + const daysEl = document.getElementById('calendarDays') + titleEl.textContent = `${currentYear}년 ${currentMonth + 1}월` + daysEl.innerHTML = '' + + const firstDay = new Date(currentYear, currentMonth, 1).getDay() + const lastDate = new Date(currentYear, currentMonth + 1, 0).getDate() + const prevLastDate = new Date(currentYear, currentMonth, 0).getDate() + + for (let i = firstDay - 1; i >= 0; i--) { + const d = new Date(currentYear, currentMonth - 1, prevLastDate - i) + daysEl.appendChild(createCalendarDayButton(d, true)) + } + + for (let i = 1; i <= lastDate; i++) { + const d = new Date(currentYear, currentMonth, i) + daysEl.appendChild(createCalendarDayButton(d, false)) + } + + const total = firstDay + lastDate + const remaining = total % 7 === 0 ? 0 : 7 - (total % 7) + for (let i = 1; i <= remaining; i++) { + const d = new Date(currentYear, currentMonth + 1, i) + daysEl.appendChild(createCalendarDayButton(d, true)) + } + + renderTodayStatsSection() + renderMonthStatsSection() +} + +const createCalendarDayButton = (date, otherMonth) => { + const btn = document.createElement('button') + btn.className = 'day' + btn.textContent = String(date.getDate()) + btn.dataset.date = dateToKey(date) + + if (otherMonth) btn.classList.add('other-month') + if (isSameDate(date, today)) btn.classList.add('today') + if (isSameDate(date, selectedDate)) btn.classList.add('selected') + if (getTodosByDate(date).length > 0) btn.classList.add('has-todos') + + btn.addEventListener('click', () => { + selectedDate = date + if (otherMonth) { + currentYear = date.getFullYear() + currentMonth = date.getMonth() + renderCalendarGrid() + } else { + document + .querySelectorAll('.day') + .forEach((el) => el.classList.remove('selected')) + btn.classList.add('selected') + } + renderTodoListSection() + }) + + return btn +} + +const renderTodoListSection = () => { + const titleEl = document.getElementById('selectedDateTitle') + const listEl = document.getElementById('todoList') + titleEl.textContent = formatDateTitle(selectedDate) + listEl.innerHTML = '' + + const todos = getTodosByDate(selectedDate) + + todos.forEach((todo) => { + const li = document.createElement('li') + li.className = 'todoItem' + (todo.done ? ' done' : '') + li.dataset.id = String(todo.id) + + const textEl = document.createElement('p') + textEl.className = 'todoText' + textEl.textContent = todo.text + + const actions = document.createElement('div') + actions.className = 'todoActions' + + const doneBtn = document.createElement('button') + doneBtn.className = 'doneBtn' + doneBtn.textContent = todo.done ? '취소' : '완료' + doneBtn.addEventListener('click', () => toggleTodoDone(todo.id)) + + const deleteBtn = document.createElement('button') + deleteBtn.className = 'deleteBtn' + deleteBtn.textContent = '삭제' + deleteBtn.addEventListener('click', () => deleteTodoItem(todo.id)) + + actions.appendChild(doneBtn) + actions.appendChild(deleteBtn) + li.appendChild(textEl) + li.appendChild(actions) + listEl.appendChild(li) + }) +} + +// CRUD +const addTodoItem = (text) => { + const key = dateToKey(selectedDate) + if (!store[key]) store[key] = [] + store[key].push({ id: Date.now(), text: text.trim(), done: false }) + saveStoreToStorage() + renderTodoListSection() + renderCalendarGrid() +} + +const toggleTodoDone = (id) => { + const key = dateToKey(selectedDate) + const todo = store[key]?.find((t) => t.id === id) + if (todo) { + todo.done = !todo.done + saveStoreToStorage() + renderTodoListSection() + renderTodayStatsSection() + renderMonthStatsSection() + } +} + +const deleteTodoItem = (id) => { + const key = dateToKey(selectedDate) + if (!store[key]) return + store[key] = store[key].filter((t) => t.id !== id) + if (store[key].length === 0) delete store[key] + saveStoreToStorage() + renderTodoListSection() + renderCalendarGrid() +} + +// 이벤트 +const handleTodoAdd = () => { + const input = document.getElementById('todoInput') + const text = input.value.trim() + if (!text) return + addTodoItem(text) + input.value = '' + input.focus() +} + +const bindUIEvents = () => { + document.getElementById('prevMonthBtn').addEventListener('click', () => { + currentMonth-- + if (currentMonth < 0) { + currentMonth = 11 + currentYear-- + } + renderCalendarGrid() + }) + + document.getElementById('nextMonthBtn').addEventListener('click', () => { + currentMonth++ + if (currentMonth > 11) { + currentMonth = 0 + currentYear++ + } + renderCalendarGrid() + }) + + document.getElementById('todoAddBtn').addEventListener('click', handleTodoAdd) + + document.getElementById('todoInput').addEventListener('keydown', (e) => { + if (e.key === 'Enter' && !e.isComposing) handleTodoAdd() + }) +} + +// 초기화 +document.addEventListener('DOMContentLoaded', () => { + bindUIEvents() + renderCalendarGrid() + renderTodoListSection() +}) diff --git a/style.css b/style.css new file mode 100644 index 0000000..beac36d --- /dev/null +++ b/style.css @@ -0,0 +1,529 @@ +@import url('https://cdn.jsdelivr.net/npm/pretendard/dist/web/static/pretendard.css'); + +/* 디자인 토큰 */ +:root { + /* 배경 */ + --color-bg: #eef4f9; + --color-surface: #f7fafd; + --color-surface-alt: #e8f1f8; + + /* 테두리 */ + --color-border: #c8daea; + --color-border-soft: #daeaf5; + + /* 텍스트 */ + --color-text-primary: #1e3040; + --color-text-secondary: #5a7a90; + --color-text-muted: #96b4c8; + + /* 포인트 컬러 */ + --color-blue: #3a8fc7; + --color-blue-soft: #d0e8f5; + --color-green: #4aaa7f; + --color-green-soft: #d0f0e4; + --color-red: #d05a5a; + --color-red-soft: #fae0e0; + --color-orange: #e8a020; + + /* 반경 */ + --radius-sm: 6px; + --radius-md: 10px; + --radius-lg: 16px; + + /* 그림자 */ + --shadow-card: 0 2px 16px rgba(58, 100, 140, 0.08); + --shadow-inset: inset 0 1px 3px rgba(58, 100, 140, 0.07); + + /* 폰트 */ + --font: 'Pretendard', sans-serif; + + /* 타이포그래피 */ + --text-xs: 0.73rem; + --text-sm: 0.82rem; + --text-md: 0.88rem; + --text-lg: 1.05rem; + --text-xl: 1.45rem; +} + +/* 리셋 */ +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +/* 레이아웃 */ +html, +body { + height: 100%; +} + +body { + font-family: var(--font); + background-color: var(--color-bg); + color: var(--color-text-primary); + height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 32px 24px; + background-image: + radial-gradient( + ellipse at 15% 60%, + rgba(180, 215, 240, 0.35) 0%, + transparent 55% + ), + radial-gradient( + ellipse at 85% 15%, + rgba(200, 225, 245, 0.25) 0%, + transparent 45% + ); +} + +#app { + width: 100%; + max-width: 1100px; + height: calc(100vh - 120px); + display: flex; + flex-direction: column; + background: var(--color-surface); + border: 1.5px solid var(--color-border); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-card); + overflow: hidden; +} + +#appHeader { + border-bottom: 1.5px solid var(--color-border); + padding: 20px 36px; + text-align: center; + background: var(--color-surface); + flex-shrink: 0; +} + +#appHeader h1 { + font-size: var(--text-xl); + font-weight: 600; + letter-spacing: 0.02em; + color: var(--color-text-primary); +} + +#appMain { + display: flex; + flex: 1; + overflow: hidden; +} + +/* 달력 섹션 */ +#calendarSection { + width: 420px; + flex-shrink: 0; + display: flex; + flex-direction: column; + border-right: 1.5px solid var(--color-border); + padding: 28px; + background: var(--color-surface-alt); + overflow-y: auto; +} + +#calendar { + display: flex; + flex-direction: column; + gap: 16px; +} + +#calendarHeader { + display: flex; + align-items: center; + justify-content: space-between; +} + +#calendarTitle { + font-size: var(--text-lg); + font-weight: 600; + color: var(--color-text-primary); +} + +#prevMonthBtn, +#nextMonthBtn { + background: none; + border: none; + cursor: pointer; + color: var(--color-text-secondary); + font-size: var(--text-xs); + padding: 6px 9px; + border-radius: var(--radius-sm); +} + +#prevMonthBtn:hover, +#nextMonthBtn:hover { + background: var(--color-border-soft); +} + +#calendarWeekdays { + display: grid; + grid-template-columns: repeat(7, 1fr); + text-align: center; +} + +#calendarWeekdays span { + font-size: var(--text-sm); + font-weight: 500; + color: var(--color-text-muted); + padding: 6px 0; +} + +#calendarDays { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 3px; +} + +.day { + background: none; + border: none; + cursor: pointer; + font-family: var(--font); + font-size: var(--text-md); + color: var(--color-text-primary); + padding: 9px 2px; + border-radius: var(--radius-sm); + text-align: center; + position: relative; +} + +.day:hover { + background: var(--color-border-soft); +} + +.day.other-month { + color: var(--color-text-muted); + font-weight: 300; +} + +.day.today { + color: var(--color-orange); + font-weight: 600; +} + +.day.today::after { + content: ''; + position: absolute; + bottom: 3px; + left: 50%; + transform: translateX(-50%); + width: 4px; + height: 4px; + border-radius: 50%; + background: var(--color-orange); +} + +.day.selected { + background: var(--color-blue) !important; + color: #fff !important; + font-weight: 600; +} + +.day.selected::after { + display: none; +} + +.day.has-todos::before { + content: ''; + position: absolute; + top: 3px; + right: 4px; + width: 4px; + height: 4px; + border-radius: 50%; + background: var(--color-green); +} + +/* 통계 */ +#todayStats { + margin-top: 20px; + padding: 14px 16px; + background: var(--color-surface); + border: 1px solid var(--color-border-soft); + border-radius: var(--radius-md); + display: flex; + flex-direction: column; + gap: 10px; +} + +#todayStatsHeader { + display: flex; + justify-content: space-between; + align-items: center; +} + +#todayStatsText { + font-size: var(--text-sm); + color: var(--color-text-secondary); +} + +#todayStatsPercent { + font-size: var(--text-sm); + font-weight: 600; + color: var(--color-blue); +} + +#todayProgressBarTrack { + width: 100%; + height: 6px; + background: var(--color-border-soft); + border-radius: 99px; + overflow: hidden; +} + +#todayProgressBarFill { + height: 100%; + width: 0%; + background: var(--color-blue); + border-radius: 99px; + transition: width 0.4s ease; +} + +#monthStats { + margin-top: 12px; + padding: 14px 16px; + background: var(--color-surface); + border: 1px solid var(--color-border-soft); + border-radius: var(--radius-md); + display: flex; + flex-direction: column; + gap: 10px; +} + +#statsHeader { + display: flex; + justify-content: space-between; + align-items: center; +} + +#statsText { + font-size: var(--text-sm); + color: var(--color-text-secondary); +} + +#statsPercent { + font-size: var(--text-sm); + font-weight: 600; + color: var(--color-blue); +} + +#progressBarTrack { + width: 100%; + height: 6px; + background: var(--color-border-soft); + border-radius: 99px; + overflow: hidden; +} + +#progressBarFill { + height: 100%; + width: 0%; + background: var(--color-blue); + border-radius: 99px; + transition: width 0.4s ease; +} + +/* ── 투두 섹션 ── */ +#todoSection { + flex: 1; + display: flex; + flex-direction: column; + padding: 28px 32px; + gap: 14px; + overflow: hidden; +} + +#selectedDateTitle { + font-size: var(--text-lg); + font-weight: 600; + color: var(--color-text-primary); + padding-bottom: 12px; + border-bottom: 1px solid var(--color-border-soft); + flex-shrink: 0; +} + +#todoListWrapper { + flex: 1; + overflow-y: auto; + padding-right: 4px; + scrollbar-width: thin; + scrollbar-color: var(--color-border) transparent; +} + +#todoListWrapper::-webkit-scrollbar { + width: 5px; +} +#todoListWrapper::-webkit-scrollbar-track { + background: transparent; +} +#todoListWrapper::-webkit-scrollbar-thumb { + background: var(--color-border); + border-radius: 99px; +} + +#todoList { + list-style: none; + display: flex; + flex-direction: column; + gap: 8px; +} + +.todoItem { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 11px 14px; + background: var(--color-surface-alt); + border: 1px solid var(--color-border-soft); + border-radius: var(--radius-md); +} + +.todoItem.done { + opacity: 0.5; +} + +.todoItem.done .todoText { + text-decoration: line-through; + color: var(--color-text-muted); +} + +.todoText { + flex: 1; + font-size: var(--text-md); + color: var(--color-text-primary); + line-height: 1.5; + word-break: break-all; +} + +.todoActions { + display: flex; + gap: 6px; + flex-shrink: 0; +} + +.doneBtn, +.deleteBtn { + border: 1px solid; + border-radius: var(--radius-sm); + font-family: var(--font); + font-size: var(--text-xs); + font-weight: 500; + padding: 4px 10px; + cursor: pointer; + white-space: nowrap; +} + +.doneBtn { + background: var(--color-green-soft); + border-color: var(--color-green); + color: var(--color-green); +} + +.deleteBtn { + background: var(--color-red-soft); + border-color: var(--color-red); + color: var(--color-red); +} + +#todoList:empty::after { + content: '할 일이 없어요'; + display: block; + text-align: center; + color: var(--color-text-muted); + font-size: var(--text-sm); + padding: 36px 0; +} + +/* ── 입력창 ── */ +#todoInputArea { + display: flex; + gap: 8px; + flex-shrink: 0; + padding-top: 12px; + border-top: 1px solid var(--color-border-soft); +} + +#todoInput { + flex: 1; + border: 1.5px solid var(--color-border); + border-radius: var(--radius-md); + background: var(--color-surface-alt); + box-shadow: var(--shadow-inset); + font-family: var(--font); + font-size: var(--text-md); + color: var(--color-text-primary); + padding: 10px 14px; + outline: none; + transition: border-color 0.15s; +} + +#todoInput::placeholder { + color: var(--color-text-muted); +} + +#todoInput:focus { + border-color: var(--color-blue); +} + +#todoAddBtn { + background: var(--color-blue-soft); + border: 1px solid var(--color-blue); + border-radius: var(--radius-md); + font-family: var(--font); + font-size: var(--text-md); + font-weight: 600; + padding: 10px 20px; + color: var(--color-blue); + cursor: pointer; + white-space: nowrap; +} + +/* 반응형 */ +@media (max-width: 900px) { + body { + padding: 0; + align-items: flex-start; + } + #app { + border-radius: 0; + border: none; + box-shadow: none; + min-height: 100vh; + } + #appMain { + flex-direction: column; + } + #calendarSection { + width: 100%; + border-right: none; + border-bottom: 1.5px solid var(--color-border); + padding: 16px; + } + .day { + padding: 6px 2px; + font-size: var(--text-sm); + } + #calendarSection { + display: grid; + grid-template-columns: 1fr 1fr; + } + #calendar { + grid-column: 1 / -1; + } + #todayStats { + margin-top: 12px; + margin-right: 6px; + } + #monthStats { + margin-top: 12px; + margin-left: 6px; + } +}