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:
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user