feat: 할 일 완료 항목 정리 기능 추가
오래 쓴 라벨에 완료 항목이 계속 쌓여 카드가 길어지는 문제를 해소한다. - 목록 상단에 '완료 항목 숨기기' 토글 — 화면에서만 걸러내고 데이터는 유지 - 라벨 메뉴에 '완료 항목 N개 삭제' — 확인 후 일괄 삭제(DELETE labels/:id/done-items) - 완료 항목이 없으면 두 진입점 모두 노출하지 않는다 - 숨김 상태에서도 진행 집계는 전체 기준을 유지한다 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@ const emit = defineEmits<{
|
||||
(e: 'remove', item: ApiTodoItem): void
|
||||
(e: 'edit-label'): void
|
||||
(e: 'delete-label'): void
|
||||
(e: 'clear-done'): void
|
||||
}>()
|
||||
|
||||
// 라벨 메뉴(편집/삭제) 드롭다운
|
||||
@@ -155,6 +156,14 @@ function cancelEdit(): void {
|
||||
>
|
||||
라벨 편집
|
||||
</button>
|
||||
<!-- 완료 항목이 있을 때만 노출 — 쌓인 완료 항목을 정리하는 용도 -->
|
||||
<button
|
||||
v-if="props.label.doneCount > 0"
|
||||
type="button"
|
||||
@click="menuOpen = false; emit('clear-done')"
|
||||
>
|
||||
완료 항목 {{ props.label.doneCount }}개 삭제
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="danger"
|
||||
|
||||
@@ -28,6 +28,13 @@ export function useTodo() {
|
||||
await api.delete(`/todos/labels/${id}`)
|
||||
}
|
||||
|
||||
// 라벨의 완료 항목 일괄 삭제 — 변경된 라벨을 돌려받아 카드만 교체한다
|
||||
async function clearDoneItems(labelId: string): Promise<ApiTodoLabel> {
|
||||
return (await api.delete(
|
||||
`/todos/labels/${labelId}/done-items`,
|
||||
)) as unknown as ApiTodoLabel
|
||||
}
|
||||
|
||||
// 업무 → 할 일 생성. 같은 업무로 만든 라벨이 있으면 서버가 항목을 병합해 준다
|
||||
async function createFromTask(
|
||||
payload: TodoFromTaskPayload,
|
||||
@@ -62,6 +69,7 @@ export function useTodo() {
|
||||
createLabel,
|
||||
updateLabel,
|
||||
deleteLabel,
|
||||
clearDoneItems,
|
||||
createFromTask,
|
||||
createItem,
|
||||
updateItem,
|
||||
|
||||
@@ -48,6 +48,15 @@ async function onDeleteLabel(label: ApiTodoLabel): Promise<void> {
|
||||
await store.deleteLabel(label.id)
|
||||
}
|
||||
|
||||
async function onClearDone(label: ApiTodoLabel): Promise<void> {
|
||||
const ok = await dialog.confirm(
|
||||
`'${label.name}' 라벨의 완료된 할 일 ${label.doneCount}건을 삭제할까요?`,
|
||||
{ title: '완료 항목 삭제', confirmText: '삭제', variant: 'danger' },
|
||||
)
|
||||
if (!ok) return
|
||||
swallow(store.clearDoneItems(label.id))
|
||||
}
|
||||
|
||||
// ----- 할 일 -----
|
||||
// 오류 안내는 API 인터셉터가 토스트로 처리하므로 여기서는 거부만 삼킨다.
|
||||
// (삼키지 않으면 템플릿 핸들러에서 처리되지 않은 Promise 거부가 남는다)
|
||||
@@ -112,6 +121,15 @@ async function onRemoveItem(item: ApiTodoItem): Promise<void> {
|
||||
placeholder="라벨·할 일 검색…"
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
v-if="store.hasDoneItems"
|
||||
type="button"
|
||||
class="btn hide-done"
|
||||
:aria-pressed="store.hideDone"
|
||||
@click="store.hideDone = !store.hideDone"
|
||||
>
|
||||
{{ store.hideDone ? '완료 항목 보기' : '완료 항목 숨기기' }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn primary new-label"
|
||||
@@ -151,6 +169,7 @@ async function onRemoveItem(item: ApiTodoItem): Promise<void> {
|
||||
@remove="onRemoveItem"
|
||||
@edit-label="openEditLabel(label)"
|
||||
@delete-label="onDeleteLabel(label)"
|
||||
@clear-done="onClearDone(label)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -183,6 +202,13 @@ async function onRemoveItem(item: ApiTodoItem): Promise<void> {
|
||||
margin: 0.25rem 0 1.125rem;
|
||||
}
|
||||
/* 회의 로비와 동일하게 주요 버튼을 우측으로 밀어 배치 */
|
||||
/* 완료 숨김 토글은 주요 버튼 왼쪽에 붙인다 */
|
||||
.hide-done {
|
||||
margin-left: auto;
|
||||
}
|
||||
.hide-done + .new-label {
|
||||
margin-left: 0;
|
||||
}
|
||||
.new-label {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@@ -11,14 +11,24 @@ export const useTodoStore = defineStore('todo', () => {
|
||||
const labels = ref<ApiTodoLabel[]>([])
|
||||
const keyword = ref('')
|
||||
const loading = ref(false)
|
||||
// 완료 항목 숨기기 — 화면에서만 걸러내며 데이터는 그대로 둔다
|
||||
const hideDone = ref(false)
|
||||
|
||||
// 검색 — 라벨 이름 또는 할 일 내용에 걸리면 남긴다.
|
||||
// 라벨 이름이 걸린 경우는 항목을 그대로 두고, 항목만 걸린 경우는 걸린 항목만 보여준다.
|
||||
const displayLabels = computed<ApiTodoLabel[]>(() => {
|
||||
const kw = keyword.value.trim().toLowerCase()
|
||||
if (!kw) return labels.value
|
||||
// 진행 집계(doneCount/totalCount)는 걸러내도 원본 수치를 유지한다 —
|
||||
// '2/5 중 완료 숨김' 상태에서도 전체 진행률을 알 수 있어야 한다
|
||||
const source = hideDone.value
|
||||
? labels.value.map((l) => ({
|
||||
...l,
|
||||
items: l.items.filter((i) => !i.done),
|
||||
}))
|
||||
: labels.value
|
||||
if (!kw) return source
|
||||
const result: ApiTodoLabel[] = []
|
||||
for (const l of labels.value) {
|
||||
for (const l of source) {
|
||||
if (l.name.toLowerCase().includes(kw)) {
|
||||
result.push(l)
|
||||
continue
|
||||
@@ -29,6 +39,11 @@ export const useTodoStore = defineStore('todo', () => {
|
||||
return result
|
||||
})
|
||||
|
||||
// 완료 항목이 하나라도 있는지 — '완료 항목 정리' 노출 판단에 쓴다
|
||||
const hasDoneItems = computed(() =>
|
||||
labels.value.some((l) => l.doneCount > 0),
|
||||
)
|
||||
|
||||
const isFiltered = computed(() => keyword.value.trim() !== '')
|
||||
// 전체 할 일 수(헤더 count-pill 표기용)
|
||||
const totalCount = computed(() =>
|
||||
@@ -84,6 +99,9 @@ export const useTodoStore = defineStore('todo', () => {
|
||||
await api.deleteLabel(id)
|
||||
labels.value = labels.value.filter((l) => l.id !== id)
|
||||
}
|
||||
async function clearDoneItems(id: string): Promise<void> {
|
||||
replaceLabel(await api.clearDoneItems(id))
|
||||
}
|
||||
|
||||
// --- 항목 ---
|
||||
async function addItem(labelId: string, title: string): Promise<void> {
|
||||
@@ -128,12 +146,15 @@ export const useTodoStore = defineStore('todo', () => {
|
||||
displayLabels,
|
||||
keyword,
|
||||
loading,
|
||||
hideDone,
|
||||
isFiltered,
|
||||
hasDoneItems,
|
||||
totalCount,
|
||||
load,
|
||||
createLabel,
|
||||
updateLabel,
|
||||
deleteLabel,
|
||||
clearDoneItems,
|
||||
addItem,
|
||||
toggleItem,
|
||||
renameItem,
|
||||
|
||||
Reference in New Issue
Block a user