Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions src/entities/project-column/ui/ProjectColumn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,25 @@ export function ProjectColumn({
</div>
))}

{dragOverIndex === column.tasks.length ? (
<div className="bg-brand h-1 w-full rounded-full" />
) : null}
<div
className={cn(
'mt-1 rounded-[16px] px-3 transition-all',
column.tasks.length === 0 ? 'min-h-[96px]' : 'min-h-[72px]',
dragOverIndex === column.tasks.length
? 'bg-brand/6'
: 'bg-transparent',
)}
onDragOver={(event) => {
event.preventDefault();
event.stopPropagation();
onDragOverTask(column.id, column.tasks.length);
}}
onDrop={(event) => handleDrop(event, column.tasks.length)}
>
{dragOverIndex === column.tasks.length ? (
<div className="bg-brand mt-2 h-1 w-full rounded-full" />
) : null}
</div>
</div>
</section>
);
Expand Down
50 changes: 50 additions & 0 deletions src/entities/task/api/create-task.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use server';

import { getCurrentUserId } from '@/shared/api/supabase/current-user';
import { createSupabaseServerClient } from '@/shared/api/supabase/server';

import { taskTitleSchema } from '../model/task.schema';
import type { UntypedRpcClient } from './rpc-client';

function getTodayIsoDateInKst() {
const parts = new Intl.DateTimeFormat('en-CA', {
timeZone: 'Asia/Seoul',
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).formatToParts(new Date());

const year = parts.find((part) => part.type === 'year')?.value;
const month = parts.find((part) => part.type === 'month')?.value;
const day = parts.find((part) => part.type === 'day')?.value;

if (!year || !month || !day) {
throw new Error('현재 날짜를 계산하지 못했습니다.');
}

return `${year}-${month}-${day}`;
}

export async function createTask(params: { workspaceId: string; title: string }): Promise<void> {
const parsedTitle = taskTitleSchema.safeParse(params.title);

if (!parsedTitle.success) {
throw new Error(parsedTitle.error.issues[0]?.message ?? '입력값이 올바르지 않습니다');
}

const supabase = await createSupabaseServerClient();
const currentUserId = await getCurrentUserId();
const dueDate = getTodayIsoDateInKst();

const { error } = await (supabase as unknown as UntypedRpcClient).rpc('create_task', {
p_workspace_id: params.workspaceId,
p_title: parsedTitle.data,
p_user_id: currentUserId,
p_due_date: dueDate,
});

if (error) {
console.error('[task/createTask] RPC 실패:', error);
throw new Error('업무 생성에 실패했습니다. 잠시 후 다시 시도해주세요.');
}
}
13 changes: 13 additions & 0 deletions src/entities/task/api/delete-task.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
'use server';

import { createSupabaseServerClient } from '@/shared/api/supabase/server';

export async function deleteTask(taskId: string): Promise<void> {
const supabase = await createSupabaseServerClient();
const { error } = await supabase.from('tasks').delete().eq('id', taskId);

if (error) {
console.error('[task/deleteTask] delete 실패:', error);
throw new Error('업무 삭제에 실패했습니다. 잠시 후 다시 시도해주세요.');
}
}
22 changes: 22 additions & 0 deletions src/entities/task/api/get-tasks-by-workspace-id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { getSupabaseBrowserClient } from '@/shared/api/supabase/client';

@coderabbitai coderabbitai Bot Jul 14, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# RLS 정책 정의 파일(마이그레이션) 검색
rg -n "workspace_members|tasks|profiles" -g '*.sql' -C3
fd -e sql | xargs grep -l "ROW LEVEL SECURITY" 2>/dev/null

Repository: TeampleRun/syncly

Length of output: 155


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## Files\n'
git ls-files 'src/entities/task/api/*' 'src/entities/workspace-member/api/*' 'supabase/*' '*.sql' | sed -n '1,200p'

printf '\n## Relevant source snippets\n'
for f in \
  src/entities/task/api/get-tasks-by-workspace-id.ts \
  src/entities/workspace-member/api/get-workspace-members-by-id.client.ts \
  src/entities/task/api/update-task-board.ts \
  src/entities/task/api/use-update-task-board.ts
do
  if [ -f "$f" ]; then
    echo "\n### $f"
    ast-grep outline "$f" --view expanded || true
  fi
done

printf '\n## Search for RLS / policies\n'
rg -n "ROW LEVEL SECURITY|create policy|alter table .* enable row level security|tasks|workspace_members|profiles" -g '*.sql' -g '*.ts' -g '*.tsx' -C 2 | sed -n '1,240p'

Repository: TeampleRun/syncly

Length of output: 2397


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## RLS migration snippets\n'
for f in \
  supabase/migrations/20260709025820_add_auth_rls_policies.sql \
  supabase/migrations/20260709034833_move_rls_helpers_to_private_schema.sql
do
  echo "\n### $f"
  wc -l "$f"
  rg -n "ROW LEVEL SECURITY|create policy|alter table|tasks|workspace_members|profiles" "$f" -C 3
done

printf '\n## Core table definitions (for relation names / keys)\n'
rg -n "create table.*tasks|create table.*workspace_members|create table.*profiles|foreign key|references" \
  supabase/migrations/20260709025327_create_enums_and_core_tables.sql \
  supabase/migrations/20260709032015_add_constraints.sql -C 3

Repository: TeampleRun/syncly

Length of output: 4327


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for f in \
  supabase/migrations/20260709025820_add_auth_rls_policies.sql \
  supabase/migrations/20260709034833_move_rls_helpers_to_private_schema.sql \
  supabase/migrations/20260709025327_create_enums_and_core_tables.sql
do
  echo "\n===== $f ====="
  sed -n '1,260p' "$f"
done

Repository: TeampleRun/syncly

Length of output: 14310


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "### add_auth_rls_policies"
nl -ba supabase/migrations/20260709025820_add_auth_rls_policies.sql | sed -n '1,260p'

echo "\n### create_enums_and_core_tables"
nl -ba supabase/migrations/20260709025327_create_enums_and_core_tables.sql | sed -n '1,260p'

Repository: TeampleRun/syncly

Length of output: 221


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "### src/entities/workspace-member/api/get-workspace-members-by-id.client.ts"
cat -n src/entities/workspace-member/api/get-workspace-members-by-id.client.ts | sed -n '1,220p'

echo
echo "### src/entities/task/api/get-tasks-by-workspace-id.ts"
cat -n src/entities/task/api/get-tasks-by-workspace-id.ts | sed -n '1,220p'

Repository: TeampleRun/syncly

Length of output: 3571


브라우저 조회가 현재는 멤버십으로 제한되지 않습니다. src/entities/task/api/get-tasks-by-workspace-id.tssrc/entities/workspace-member/api/get-workspace-members-by-id.client.ts는 브라우저 Supabase 클라이언트로 직접 조회하지만, supabase/migrations/20260709025327_create_enums_and_core_tables.sqldev_full_access가 남아 있어 tasks/workspace_members/profiles 접근이 사실상 열려 있습니다. 브라우저 경로를 유지하려면 이 정책을 제거하고 멤버십 기반 RLS만 남겨야 합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/entities/task/api/get-tasks-by-workspace-id.ts` at line 1, 브라우저 Supabase
클라이언트로 조회하는 getSupabaseBrowserClient 경로에 멤버십 기반 접근만 적용되도록 수정하세요. dev_full_access
정책을 제거하고 tasks, workspace_members, profiles에 대해 workspace 멤버십을 검증하는 RLS 정책만
유지·적용하세요.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

동의합니다. 현재 브라우저 Supabase 클라이언트에서 tasks, workspace_members, profiles를 직접 조회하고 있으므로, RLS가 실질적인 접근 제어 역할을 해야 합니다. dev_full_access 정책을 제거하고 요청 사용자가 해당 워크스페이스의 멤버인지 검증하는 정책만 유지하도록 보완하면 좋겠습니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "### create_enums_and_core_tables (tasks / workspace_members / profiles table defs)"
sed -n '1,400p' supabase/migrations/20260709025327_create_enums_and_core_tables.sql

echo
echo "### add_auth_rls_policies"
sed -n '1,400p' supabase/migrations/20260709025820_add_auth_rls_policies.sql

echo
echo "### move_rls_helpers_to_private_schema"
sed -n '1,400p' supabase/migrations/20260709034833_move_rls_helpers_to_private_schema.sql

echo
echo "### List all migrations for ordering"
fd -e sql . supabase/migrations | sort

Length of output: 15359


@seongjinss555 네, 확인했습니다. 마이그레이션을 보니 실제로 필요한 멤버십 기반 정책은 이미 존재합니다:

  • profiles: profiles_select(로그인 유저 전체 조회 허용은 의도된 설계), profiles_insert_own, profiles_update_own
  • workspace_members: members_select(private.is_workspace_member 기반), members_insert_self
  • tasks: tasks_member_all(private.is_workspace_member(workspace_id) 기반 CRUD)

문제는 20260709025327_create_enums_and_core_tables.sql에서 모든 public 테이블에 대해 for all using (true) with check (true)dev_full_access 정책이 여전히 남아 있고, RLS는 정책들을 OR로 평가하므로 이 정책 하나가 실질적으로 전체 접근을 허용해버립니다. 즉 is_workspace_member 검증 정책들이 이미 있음에도 dev_full_access가 살아있어 무력화되고 있는 상태입니다.

가장 간단하고 안전한 해결책은 새 마이그레이션을 추가해 모든 public 테이블의 dev_full_access 정책만 drop policy하는 것입니다(테이블 구조나 기존 정책은 그대로 유지). 아래와 같은 형태를 제안합니다.

-- supabase/migrations/<timestamp>_drop_dev_full_access.sql
-- dev_full_access 정책 제거: 실 RLS(auth.uid()/워크스페이스 멤버십 기반) 정책만 유효하도록 강제
do $$
declare t text;
begin
  for t in select tablename from pg_tables where schemaname = 'public' loop
    execute format('drop policy if exists dev_full_access on public.%I', t);
  end loop;
end $$;

이 마이그레이션만 추가하면 브라우저 클라이언트 조회 시(getTasksByWorkspaceId, getWorkspaceMembersByWorkspaceIdClient) private.is_workspace_member(workspace_id) 검증이 실제로 강제됩니다. 원하시면 이 변경을 이번 PR 범위로 반영할지, 별도 후속 이슈로 분리할지 알려주세요. 후속 이슈로 진행을 원하시면 GitHub 이슈를 생성해 드리겠습니다.


✏️ Learnings added
Learnt from: seongjinss555
Repo: TeampleRun/syncly PR: 52
File: src/entities/task/api/get-tasks-by-workspace-id.ts:1-1
Timestamp: 2026-07-14T05:30:47.536Z
Learning: Repository TeampleRun/syncly: `supabase/migrations/20260709025327_create_enums_and_core_tables.sql` applies a temporary `dev_full_access` policy (`for all using (true) with check (true)`) to every public table via a loop, intended as a placeholder until auth was wired up. `supabase/migrations/20260709025820_add_auth_rls_policies.sql` later added real membership-based RLS policies (e.g., `tasks_member_all`, `members_select` using `private.is_workspace_member(workspace_id)`) that coexist with `dev_full_access` via OR evaluation, so `dev_full_access` must be dropped (e.g., via a new migration looping `pg_tables` and running `drop policy if exists dev_full_access on public.%I`) before the membership-based policies actually take effect and browser-side Supabase client queries (like `src/entities/task/api/get-tasks-by-workspace-id.ts` and `src/entities/workspace-member/api/get-workspace-members-by-id.client.ts`) are properly restricted.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영했습니다. 브라우저 직접 조회를 유지하는 대신 RLS를 실제 멤버십 기준으로 맞췄습니다. 새 migration에서 tasks / workspace_members / profiles의 dev_full_access를 제거했고, profiles 조회도 본인 또는 같은 워크스페이스 멤버의 프로필만 보이도록 정책을 좁혔습니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!


import { toTask, type TaskQueryRow } from '../model/task.mapper';
import type { Task } from '../model/task.types';

export async function getTasksByWorkspaceId(workspaceId: string): Promise<Task[]> {
const supabase = getSupabaseBrowserClient();
const { data, error } = await supabase
.from('tasks')
.select(
'id, workspace_id, title, assignee_id, due_date, status, sort_order, assignee_profile:profiles!tasks_assignee_id_fkey(real_name)',
)
.eq('workspace_id', workspaceId)
.is('sprint_id', null)
.order('sort_order');

if (error) {
throw new Error(`업무 조회에 실패했습니다: ${error.message}`);
}

return ((data ?? []) as TaskQueryRow[]).map(toTask);
}
6 changes: 6 additions & 0 deletions src/entities/task/api/rpc-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type UntypedRpcClient = {
rpc: (
fn: string,
args?: Record<string, unknown>,
) => Promise<{ data: unknown; error: { message: string } | null }>;
};
33 changes: 33 additions & 0 deletions src/entities/task/api/update-task-board.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use server';

@coderabbitai coderabbitai Bot Jul 14, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

TaskStatus 리터럴 유니온 중복 정의. 두 파일 모두 'todo' | 'in-progress' | 'done'을 직접 정의하는 대신, barrel(src/entities/task/index.ts)이 이미 export하는 TaskStatus 타입을 재사용해야 합니다. 그대로 두면 상태값이 추가/변경될 때 한쪽만 갱신되어 타입이 어긋날 위험이 있습니다.

  • src/entities/task/api/update-task-board.ts#L8-14: status: 'todo' | 'in-progress' | 'done'status: TaskStatus로 교체하고 TaskStatus../model/task.types에서 import하세요.
  • src/entities/task/api/use-update-task-board.ts#L11-16: UpdateTaskBoardParams.tasks[].status도 동일하게 TaskStatus로 교체하세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/entities/task/api/update-task-board.ts` at line 1, Replace the duplicated
TaskStatus literal unions in the update-task-board API parameter types and the
use-update-task-board tasks status type with TaskStatus. Import TaskStatus from
../model/task.types in update-task-board.ts, and reuse the shared type in
use-update-task-board.ts while preserving the existing parameter shapes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지금 상황에서는 해당 status가 고정이라 큰 상관이 없어보이긴합니다. 다만 이후 코드 리펙토링 혹은 수정 시에 오류가 발생한 가능성이 있어서 고려하면 좋을 거 같습니다

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description image shows the Syncly "프로젝트 관리" (Project Management) kanban board UI: a left sidebar with navigation items (대시보드, 프로젝트 관리 highlighted, 캘린더, 공지, 회의록, 자료실, 채팅, 진행률 차트, 설정) under a "캡스톤 디자인 팀" workspace label, a top header with search, "멤버 초대" button, notification bell, and a profile avatar "김". The main area shows three kanban columns — 대기 (1), 진행 중 (1), 완료 (1) — each with one task card ("대기 테스트", "진행중 테스트", "완료 테스트") showing assignee "JiWoongE" with a blue avatar and due date "7/14". A "+ 새업무" button sits top-right of the board.

@seongjinss555 이해했습니다. 현재는 status 값이 고정되어 있어 실제 리스크는 낮지만, 이후 리팩토링 시 참고할 수 있도록 기록해두겠습니다. 필요할 때 다시 짧게 짚어드리겠습니다. 🐇


✏️ Learnings added
Learnt from: seongjinss555
Repo: TeampleRun/syncly PR: 52
File: src/entities/task/api/update-task-board.ts:1-1
Timestamp: 2026-07-14T05:30:21.421Z
Learning: In `src/entities/task/api/update-task-board.ts` and `src/entities/task/api/use-update-task-board.ts`, the `status: 'todo' | 'in-progress' | 'done'` literal union is intentionally duplicated instead of reusing the shared `TaskStatus` type from `src/entities/task/model/task.types.ts` (per maintainer seongjinss555). This was deemed low risk since the status values are currently fixed, but should be revisited and reused if/when the task domain undergoes refactoring.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영했습니다. update-task-board.ts와 use-update-task-board.ts에서 중복으로 선언하던 status 리터럴 유니온을 제거하고 TaskStatus 타입을 재사용하도록 정리했습니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!


import { createSupabaseServerClient } from '@/shared/api/supabase/server';

import type { TaskStatus } from '../model/task.types';
import { updateTaskBoardSchema } from '../model/task.schema';
import type { UntypedRpcClient } from './rpc-client';

export async function updateTaskBoard(params: {
workspaceId: string;
tasks: Array<{
id: string;
status: TaskStatus;
sortOrder: number;
}>;
}): Promise<void> {
const parsed = updateTaskBoardSchema.safeParse(params);

if (!parsed.success) {
throw new Error(parsed.error.issues[0]?.message ?? '입력값이 올바르지 않습니다');
}

const supabase = await createSupabaseServerClient();
const { error } = await (supabase as unknown as UntypedRpcClient).rpc('update_task_board', {
p_workspace_id: parsed.data.workspaceId,
p_tasks: parsed.data.tasks,
});

if (error) {
console.error('[task/updateTaskBoard] RPC 실패:', error);
throw new Error('업무 정렬 저장에 실패했습니다. 잠시 후 다시 시도해주세요.');
}
}
21 changes: 21 additions & 0 deletions src/entities/task/api/use-create-task.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use client';

import { useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';

import { createTask } from './create-task';
import { tasksByWorkspaceQueryKey } from './use-tasks-by-workspace-id';

export function useCreateTask(workspaceId: string) {
const queryClient = useQueryClient();

return useMutation({
mutationFn: (title: string) => createTask({ workspaceId, title }),
onSettled: () => {
queryClient.invalidateQueries({ queryKey: tasksByWorkspaceQueryKey(workspaceId) });
},
onError: (error) => {
toast.error(error instanceof Error ? error.message : '업무 생성에 실패했습니다');
},
});
}
21 changes: 21 additions & 0 deletions src/entities/task/api/use-delete-task.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use client';

import { useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';

import { deleteTask } from './delete-task';
import { tasksByWorkspaceQueryKey } from './use-tasks-by-workspace-id';

export function useDeleteTask(workspaceId: string) {
const queryClient = useQueryClient();

return useMutation({
mutationFn: (taskId: string) => deleteTask(taskId),
onSettled: () => {
queryClient.invalidateQueries({ queryKey: tasksByWorkspaceQueryKey(workspaceId) });
},
onError: (error) => {
toast.error(error instanceof Error ? error.message : '업무 삭제에 실패했습니다');
},
});
}
15 changes: 15 additions & 0 deletions src/entities/task/api/use-tasks-by-workspace-id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use client';

import { useQuery } from '@tanstack/react-query';

import { getTasksByWorkspaceId } from './get-tasks-by-workspace-id';

export const tasksByWorkspaceQueryKey = (workspaceId: string) =>
['tasks', 'workspace', workspaceId] as const;

export function useTasksByWorkspaceId(workspaceId: string) {
return useQuery({
queryKey: tasksByWorkspaceQueryKey(workspaceId),
queryFn: () => getTasksByWorkspaceId(workspaceId),
});
}
31 changes: 31 additions & 0 deletions src/entities/task/api/use-update-task-board.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use client';

import { useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';

import type { TaskStatus } from '../model/task.types';
import { updateTaskBoard } from './update-task-board';
import { tasksByWorkspaceQueryKey } from './use-tasks-by-workspace-id';

interface UpdateTaskBoardParams {
workspaceId: string;
tasks: Array<{
id: string;
status: TaskStatus;
sortOrder: number;
}>;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

export function useUpdateTaskBoard(workspaceId: string) {
const queryClient = useQueryClient();

return useMutation({
mutationFn: (params: UpdateTaskBoardParams) => updateTaskBoard(params),
onSettled: () => {
queryClient.invalidateQueries({ queryKey: tasksByWorkspaceQueryKey(workspaceId) });
},
onError: (error) => {
toast.error(error instanceof Error ? error.message : '업무 정렬 저장에 실패했습니다');
},
});
}
9 changes: 8 additions & 1 deletion src/entities/task/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
export { getMockTasksByWorkspaceId } from './model/mock-tasks-by-workspace';
export type { Task, TaskStatus } from './model/task.types';
export type { TaskRow, TaskStatusDb } from './model/task.db.types';
export { toTask, toDbTaskStatus, toUiTaskStatus } from './model/task.mapper';
export { taskTitleSchema, taskBoardItemSchema, updateTaskBoardSchema } from './model/task.schema';
export { getTasksByWorkspaceId } from './api/get-tasks-by-workspace-id';
export { useTasksByWorkspaceId, tasksByWorkspaceQueryKey } from './api/use-tasks-by-workspace-id';
export { useCreateTask } from './api/use-create-task';
export { useDeleteTask } from './api/use-delete-task';
export { useUpdateTaskBoard } from './api/use-update-task-board';
export { TaskCard } from './ui/TaskCard';
Loading