fix(todo): 모바일 최적화 — 터치 타겟 44px, 라벨 버튼, 확인 시트, 탭 인디케이터

- 카드 액션 버튼 36px→44px + 아이콘+텍스트 라벨 (모바일)
- 날짜 필터/입력 터치 타겟 36px min-height로 확대
- 빈 상태 메시지 모바일 적절하게 변경 ("드래그하여 이동"→"아직 항목이 없습니다")
- 완료 비우기 MobileSheet 확인 다이얼로그 (모바일)
- 완료 탭 내 "비우기" 버튼 추가
- SwipeableView 활성 탭 하단 인디케이터 + 44px 높이
- 폼 라벨 14px, 입력 16px (iOS 줌 방지)
- 모바일 컬럼/패널 배경·보더 제거로 공간 절약

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-27 13:39:09 +09:00
parent 6cbdf95596
commit bebd55874c
4 changed files with 231 additions and 14 deletions

View File

@@ -233,6 +233,7 @@
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
padding: 0;
line-height: 1;
-webkit-tap-highlight-color: transparent;
}
.todo-card__btn:hover {
@@ -288,6 +289,24 @@
opacity: 0.6;
}
.todo-done-panel__clear-btn {
font-size: 11px;
padding: 4px 10px;
border-radius: 6px;
border: 1px solid rgba(239, 68, 68, 0.3);
background: rgba(239, 68, 68, 0.08);
color: #ef4444;
cursor: pointer;
font-family: inherit;
-webkit-tap-highlight-color: transparent;
transition: background 0.15s ease;
min-height: 32px;
}
.todo-done-panel__clear-btn:active {
background: rgba(239, 68, 68, 0.2);
}
/* ── 날짜 필터 ────────────────────────────────────────────────────────── */
.todo-done-panel__filter {
@@ -311,6 +330,7 @@
white-space: nowrap;
font-family: inherit;
line-height: 1.6;
-webkit-tap-highlight-color: transparent;
}
.todo-date-btn:hover {
@@ -387,23 +407,166 @@
display: block;
}
.todo-toolbar {
display: none;
}
.todo-col {
min-height: 120px;
border: none;
background: transparent;
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
.todo-col__head {
padding: 10px 4px;
border-bottom: none;
}
.todo-col__body {
padding: 4px 0;
}
/* 카드 버튼 44px 터치 타겟 */
.todo-card__btn {
width: 44px;
height: 44px;
font-size: 14px;
border-radius: 10px;
}
.todo-card__actions {
gap: 6px;
}
.todo-card__title {
font-size: 15px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* 날짜 필터 버튼 44px 터치 타겟 */
.todo-date-btn {
font-size: 12px;
padding: 8px 14px;
min-height: 36px;
}
.todo-date-input {
font-size: 13px;
padding: 8px 12px;
height: 36px;
min-height: 36px;
}
.todo-done-panel {
border: none;
background: transparent;
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
.todo-done-panel__head {
flex-direction: column;
align-items: flex-start;
gap: 10px;
padding: 10px 4px;
border-bottom: none;
}
.todo-done-panel__filter {
justify-content: flex-start;
gap: 8px;
}
.todo-done-panel__body {
grid-template-columns: 1fr;
padding: 4px 0;
}
/* 폼 라벨 가독성 */
.todo-form__field {
font-size: 14px;
gap: 8px;
}
.todo-form__field input,
.todo-form__field textarea {
font-size: 16px;
padding: 12px 14px;
}
.todo-form__actions .button {
width: 100%;
min-height: 48px;
font-size: 15px;
}
/* 빈 상태 메시지 */
.todo-col__empty {
font-size: 13px;
padding: 32px 16px;
}
/* 라벨 버튼 (모바일) */
.todo-card__btn--labeled {
width: auto;
height: 38px;
padding: 0 12px;
gap: 4px;
font-size: 12px;
}
.todo-card__btn-label {
font-size: 11px;
font-weight: 500;
}
.todo-card__actions {
flex-wrap: wrap;
}
}
/* ── 확인 시트 (모바일) ─────────────────────────────────────────────── */
.todo-confirm-sheet {
display: flex;
flex-direction: column;
gap: 20px;
padding: 8px 0;
}
.todo-confirm-sheet__msg {
margin: 0;
font-size: 15px;
color: var(--text);
line-height: 1.6;
text-align: center;
}
.todo-confirm-sheet__actions {
display: flex;
gap: 10px;
}
.todo-confirm-sheet__actions .button {
flex: 1;
min-height: 48px;
font-size: 15px;
justify-content: center;
}
.button.danger {
background: rgba(239, 68, 68, 0.15);
border: 1px solid rgba(239, 68, 68, 0.4);
color: #ef4444;
}
.button.danger:hover {
background: rgba(239, 68, 68, 0.25);
}
@media (max-width: 480px) {

View File

@@ -35,6 +35,7 @@ const Todo = () => {
const [dragging, setDragging] = useState(null);
const [dragOver, setDragOver] = useState(null);
const [doneDate, setDoneDate] = useState(''); // '' = 전체
const [confirmClear, setConfirmClear] = useState(false);
const dragItem = useRef(null);
const load = useCallback(async () => {
@@ -93,6 +94,7 @@ const Todo = () => {
};
const handleClear = async () => {
setConfirmClear(false);
try {
await clearTodos();
setTodos((prev) => prev.filter((t) => t.status !== 'done'));
@@ -172,20 +174,22 @@ const Todo = () => {
<button
key={c.id}
type="button"
className="todo-card__btn"
className={`todo-card__btn${isMobile ? ' todo-card__btn--labeled' : ''}`}
title={`${c.label}으로 이동`}
onClick={() => handleMove(todo.id, c.id)}
>
{c.id === 'todo' ? '↩' : c.id === 'in_progress' ? '▶' : '✓'}
<span className="todo-card__btn-icon">{c.id === 'todo' ? '↩' : c.id === 'in_progress' ? '▶' : '✓'}</span>
{isMobile && <span className="todo-card__btn-label">{c.label}</span>}
</button>
))}
<button
type="button"
className="todo-card__btn todo-card__btn--danger"
className={`todo-card__btn todo-card__btn--danger${isMobile ? ' todo-card__btn--labeled' : ''}`}
title="삭제"
onClick={() => handleDelete(todo.id)}
>
<span className="todo-card__btn-icon"></span>
{isMobile && <span className="todo-card__btn-label">삭제</span>}
</button>
</div>
</div>
@@ -208,7 +212,9 @@ const Todo = () => {
</div>
<div className="todo-col__body">
{items.length === 0 && (
<p className="todo-col__empty">드래그하여 이동</p>
<p className="todo-col__empty">
{isMobile ? '아직 항목이 없습니다' : '드래그하여 이동'}
</p>
)}
{items.map((todo) => renderCard(todo, col.id))}
</div>
@@ -265,7 +271,11 @@ const Todo = () => {
<button
type="button"
className="button ghost"
onClick={handleClear}
onClick={() => {
const doneCount = todos.filter(t => t.status === 'done').length;
if (doneCount === 0) return;
if (isMobile) { setConfirmClear(true); } else { handleClear(); }
}}
>
완료 비우기
</button>
@@ -285,15 +295,20 @@ const Todo = () => {
{ key: 'todo', label: '할 일', content: renderColumn({ id: 'todo', label: '할 일' }) },
{ key: 'in_progress', label: '진행 중', content: renderColumn({ id: 'in_progress', label: '진행 중' }) },
{ key: 'done', label: '완료', content: (
<div
className={`todo-done-panel${dragOver === 'done' ? ' is-drag-over' : ''}`}
onDragOver={(e) => onDragOver(e, 'done')}
onDrop={(e) => onDrop(e, 'done')}
>
<div className="todo-done-panel">
<div className="todo-done-panel__head">
<div className="todo-done-panel__title-row">
<span className="todo-col__title">완료</span>
<span className="todo-col__count">{doneTodos.length}</span>
{doneTodos.length > 0 && (
<button
type="button"
className="todo-done-panel__clear-btn"
onClick={() => setConfirmClear(true)}
>
비우기
</button>
)}
</div>
<div className="todo-done-panel__filter">
<button type="button" className={`todo-date-btn${doneDate === '' ? ' is-active' : ''}`} onClick={() => setDoneDate('')}>전체</button>
@@ -306,7 +321,7 @@ const Todo = () => {
</div>
<div className="todo-done-panel__body">
{doneTodos.length === 0 ? (
<p className="todo-col__empty">{doneDate ? '해당 날짜에 완료된 항목이 없습니다' : '드래그하여 이동'}</p>
<p className="todo-col__empty">{doneDate ? '해당 날짜에 완료된 항목이 없습니다' : '완료된 항목이 없습니다'}</p>
) : (
doneTodos.map((todo) => renderCard(todo, 'done'))
)}
@@ -369,7 +384,7 @@ const Todo = () => {
<div className="todo-done-panel__body">
{doneTodos.length === 0 ? (
<p className="todo-col__empty">
{doneDate ? '해당 날짜에 완료된 항목이 없습니다' : '드래그하여 이동'}
{doneDate ? '해당 날짜에 완료된 항목이 없습니다' : (isMobile ? '완료된 항목이 없습니다' : '드래그하여 이동')}
</p>
) : (
doneTodos.map((todo) => renderCard(todo, 'done'))
@@ -388,6 +403,24 @@ const Todo = () => {
{addForm}
</MobileSheet>
{/* 모바일: 완료 비우기 확인 시트 */}
<MobileSheet
open={confirmClear}
onClose={() => setConfirmClear(false)}
title="완료 항목 비우기"
snap="half"
>
<div className="todo-confirm-sheet">
<p className="todo-confirm-sheet__msg">
완료된 항목 {todos.filter(t => t.status === 'done').length}건을 모두 삭제합니다.
</p>
<div className="todo-confirm-sheet__actions">
<button type="button" className="button ghost" onClick={() => setConfirmClear(false)}>취소</button>
<button type="button" className="button danger" onClick={handleClear}>삭제</button>
</div>
</div>
</MobileSheet>
<FAB onClick={() => setAddSheetOpen(true)} label="할일 추가" />
</div>
</PullToRefresh>