From c128b19599d256a0646c684d69b43c9b1678ccc5 Mon Sep 17 00:00:00 2001 From: TtiPo Date: Fri, 19 Jun 2026 00:23:01 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EB=AA=A9=EB=A1=9D=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EB=84=A4=EC=9D=B4=EC=85=98=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EA=B3=84=EC=95=BD=20=EB=B6=88=EC=9D=BC=EC=B9=98=20=EB=B0=8F=20?= =?UTF-8?q?=EC=97=85=EB=AC=B4=20=EB=AA=A9=EB=A1=9D=20=EC=BF=BC=EB=A6=AC=20?= =?UTF-8?q?=ED=81=AC=EB=9E=98=EC=8B=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 페이지네이션 도입 후 백엔드 목록 API 가 { items, total, ... } 를 반환하는데 프론트 멤버·업무 소비처가 배열로 기대해 list.map 이 깨지고, 멤버 탭과 업무 목록이 비어 보이던 회귀를 수정. - backend(task): 업무 목록 쿼리에서 to-many 조인 컬럼(checklist.sort_order) 으로 정렬해 페이지네이션 시 TypeORM 이 터지던 문제 제거(task.seq 만 정렬). 목록 응답은 체크리스트 [완료/전체] 수만 쓰므로 항목 순서가 불필요. - frontend(member/task): list 컴포저블이 Paginated 를 반환하도록 맞추고 스토어에서 .items 로 언래핑(저장소·내업무와 동일 계약). Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/src/modules/task/task.service.ts | 7 ++++++- frontend/src/composables/useMember.ts | 13 ++++++++++--- frontend/src/composables/useTask.ts | 10 +++++++--- frontend/src/stores/member.store.ts | 4 ++-- frontend/src/stores/task.store.ts | 4 ++-- 5 files changed, 27 insertions(+), 11 deletions(-) diff --git a/backend/src/modules/task/task.service.ts b/backend/src/modules/task/task.service.ts index 73a93da..ce3dc87 100644 --- a/backend/src/modules/task/task.service.ts +++ b/backend/src/modules/task/task.service.ts @@ -216,7 +216,12 @@ export class TaskService { ); } - qb.orderBy('task.seq', 'DESC').addOrderBy('checklist.sort_order', 'ASC'); + // 정렬은 메인 엔티티(task.seq) 기준만 사용한다. + // skip/take(페이지네이션)와 to-many 조인을 함께 쓰면 TypeORM 이 DISTINCT id 서브쿼리로 + // 페이징하는데, 이때 조인된 to-many 컬럼(checklist.sort_order)으로 ORDER BY 하면 + // 컬럼 메타데이터를 찾지 못해 터진다(createOrderByCombinedWithSelectExpression). + // 목록 응답은 체크리스트의 [완료/전체] 수만 쓰므로 항목 순서가 필요 없다. + qb.orderBy('task.seq', 'DESC'); const [tasks, total] = await qb .skip(pg.skip) diff --git a/frontend/src/composables/useMember.ts b/frontend/src/composables/useMember.ts index 1ad7f75..f64b6b5 100644 --- a/frontend/src/composables/useMember.ts +++ b/frontend/src/composables/useMember.ts @@ -4,15 +4,22 @@ import type { InviteMemberPayload, UpdateMemberPayload, } from '@/types/member' +import type { Paginated } from '@/types/pagination' // 저장소 멤버 도메인 API Composable — 인터셉터가 success/data 를 언래핑 // 경로는 모두 repoId(slugName) 하위: /repos/:repoId/members export function useMember() { const api = useApi() - // 멤버 목록 (멤버 또는 공개 저장소) - async function list(repoId: string): Promise { - return (await api.get(`/repos/${repoId}/members`)) as unknown as ApiRepoMember[] + // 멤버 목록 (멤버 또는 공개 저장소) — 백엔드가 페이지네이션 { items, total, ... } 반환 + async function list( + repoId: string, + page = 1, + size = 20, + ): Promise> { + return (await api.get(`/repos/${repoId}/members`, { + params: { page, size }, + })) as unknown as Paginated } // 멤버 초대 (admin) — 가입된 사용자만 이메일로 초대 diff --git a/frontend/src/composables/useTask.ts b/frontend/src/composables/useTask.ts index f3e5fdc..ceac95c 100644 --- a/frontend/src/composables/useTask.ts +++ b/frontend/src/composables/useTask.ts @@ -10,17 +10,21 @@ import type { UpdateTaskPayload, } from '@/types/task' import type { TaskStatus } from '@/mock/relay.mock' +import type { Paginated } from '@/types/pagination' // 업무 도메인 API Composable — 인터셉터가 success/data 를 언래핑 // 경로는 모두 repoId(slugName) 하위: /repos/:repoId/tasks[/:taskId] export function useTask() { const api = useApi() - // 업무 목록 (상태/담당자 필터 선택) - async function list(repoId: string, query: TaskListQuery = {}): Promise { + // 업무 목록 (상태/담당자 필터 선택) — 백엔드가 페이지네이션 { items, total, ... } 반환 + async function list( + repoId: string, + query: TaskListQuery = {}, + ): Promise> { return (await api.get(`/repos/${repoId}/tasks`, { params: query, - })) as unknown as ApiRepoTask[] + })) as unknown as Paginated } // 업무 단건 상세 (taskId = 저장소 내 순번) diff --git a/frontend/src/stores/member.store.ts b/frontend/src/stores/member.store.ts index 9b70e1a..17037ec 100644 --- a/frontend/src/stores/member.store.ts +++ b/frontend/src/stores/member.store.ts @@ -45,8 +45,8 @@ export const useMemberStore = defineStore('member', () => { async function fetchList(repoId: string): Promise { loading.value = true try { - const list = await memberApi.list(repoId) - members.value = list.map((m) => toMemberView(m, currentUserId())) + const { items } = await memberApi.list(repoId) + members.value = items.map((m) => toMemberView(m, currentUserId())) } finally { loading.value = false } diff --git a/frontend/src/stores/task.store.ts b/frontend/src/stores/task.store.ts index ed91909..1c7b3fb 100644 --- a/frontend/src/stores/task.store.ts +++ b/frontend/src/stores/task.store.ts @@ -211,8 +211,8 @@ export const useTaskStore = defineStore('task', () => { async function fetchList(repoId: string, query: TaskListQuery = {}): Promise { loading.value = true try { - const list = await taskApi.list(repoId, query) - tasks.value = list.map(toRepoTaskView) + const { items } = await taskApi.list(repoId, query) + tasks.value = items.map(toRepoTaskView) } finally { loading.value = false }