@kmgeon/taskflow 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +374 -0
- package/bin/task-mcp.mjs +19 -0
- package/bin/task.mjs +19 -0
- package/docs/clean-code.md +29 -0
- package/docs/git.md +36 -0
- package/docs/guideline.md +25 -0
- package/docs/security.md +32 -0
- package/docs/step-by-step.md +29 -0
- package/docs/superpowers/specs/2026-03-21-cli-advisor-design.md +383 -0
- package/docs/superpowers/specs/2026-03-21-init-redesign-design.md +429 -0
- package/docs/superpowers/specs/2026-03-21-skill-architecture-design.md +362 -0
- package/docs/superpowers/specs/2026-03-23-t-create-task-run-design.md +40 -0
- package/docs/superpowers/specs/2026-03-23-task-run-design.md +44 -0
- package/docs/tdd.md +41 -0
- package/package.json +114 -0
- package/src/app/(protected)/dashboard/page.tsx +7 -0
- package/src/app/(protected)/layout.tsx +10 -0
- package/src/app/api/[[...hono]]/route.ts +13 -0
- package/src/app/example/page.tsx +11 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +168 -0
- package/src/app/layout.tsx +35 -0
- package/src/app/page.tsx +5 -0
- package/src/app/providers.tsx +57 -0
- package/src/backend/config/index.ts +36 -0
- package/src/backend/hono/app.ts +32 -0
- package/src/backend/hono/context.ts +38 -0
- package/src/backend/http/response.ts +64 -0
- package/src/backend/middleware/context.ts +23 -0
- package/src/backend/middleware/error.ts +31 -0
- package/src/backend/middleware/supabase.ts +23 -0
- package/src/backend/supabase/client.ts +17 -0
- package/src/cli/commands/__tests__/task-commands.test.ts +170 -0
- package/src/cli/commands/advisor.ts +45 -0
- package/src/cli/commands/ask.ts +50 -0
- package/src/cli/commands/board.ts +72 -0
- package/src/cli/commands/init.ts +184 -0
- package/src/cli/commands/list.ts +138 -0
- package/src/cli/commands/run.ts +143 -0
- package/src/cli/commands/set-status.ts +50 -0
- package/src/cli/commands/show.ts +28 -0
- package/src/cli/commands/tree.ts +72 -0
- package/src/cli/index.ts +38 -0
- package/src/cli/lib/__tests__/formatter.test.ts +123 -0
- package/src/cli/lib/error-boundary.test.ts +135 -0
- package/src/cli/lib/error-boundary.ts +70 -0
- package/src/cli/lib/formatter.ts +764 -0
- package/src/cli/lib/trd.ts +33 -0
- package/src/cli/lib/validate.test.ts +89 -0
- package/src/cli/lib/validate.ts +43 -0
- package/src/cli/prompts/task-run.md +25 -0
- package/src/components/layout/AppLayout.tsx +15 -0
- package/src/components/layout/Sidebar.tsx +124 -0
- package/src/components/ui/accordion.tsx +58 -0
- package/src/components/ui/avatar.tsx +50 -0
- package/src/components/ui/badge.tsx +36 -0
- package/src/components/ui/button.tsx +56 -0
- package/src/components/ui/card.tsx +79 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/dialog.tsx +122 -0
- package/src/components/ui/dropdown-menu.tsx +200 -0
- package/src/components/ui/file-upload.tsx +50 -0
- package/src/components/ui/form.tsx +179 -0
- package/src/components/ui/input.tsx +25 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/scroll-area.tsx +48 -0
- package/src/components/ui/select.tsx +160 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/sheet.tsx +140 -0
- package/src/components/ui/textarea.tsx +22 -0
- package/src/components/ui/toast.tsx +129 -0
- package/src/components/ui/toaster.tsx +35 -0
- package/src/core/ai/claude-client.ts +79 -0
- package/src/core/claude-runner/flag-builder.ts +57 -0
- package/src/core/claude-runner/index.ts +2 -0
- package/src/core/claude-runner/spawner.ts +86 -0
- package/src/core/prd/__tests__/auto-analyzer.test.ts +35 -0
- package/src/core/prd/__tests__/generator.test.ts +26 -0
- package/src/core/prd/__tests__/scanner.test.ts +35 -0
- package/src/core/prd/auto-analyzer.ts +9 -0
- package/src/core/prd/generator.ts +8 -0
- package/src/core/prd/scanner.ts +117 -0
- package/src/core/project/__tests__/claude-setup.test.ts +133 -0
- package/src/core/project/__tests__/config.test.ts +30 -0
- package/src/core/project/__tests__/init.test.ts +37 -0
- package/src/core/project/__tests__/skill-setup.test.ts +62 -0
- package/src/core/project/claude-setup.ts +224 -0
- package/src/core/project/config.ts +34 -0
- package/src/core/project/docs-setup.ts +26 -0
- package/src/core/project/docs-templates.ts +205 -0
- package/src/core/project/init.ts +40 -0
- package/src/core/project/skill-setup.ts +32 -0
- package/src/core/project/skill-templates.ts +277 -0
- package/src/core/task/index.ts +16 -0
- package/src/core/types.ts +58 -0
- package/src/features/example/backend/error.ts +9 -0
- package/src/features/example/backend/route.ts +52 -0
- package/src/features/example/backend/schema.ts +25 -0
- package/src/features/example/backend/service.ts +73 -0
- package/src/features/example/components/example-status.test.tsx +97 -0
- package/src/features/example/components/example-status.tsx +160 -0
- package/src/features/example/hooks/useExampleQuery.ts +23 -0
- package/src/features/example/lib/dto.test.ts +57 -0
- package/src/features/example/lib/dto.ts +5 -0
- package/src/features/kanban/backend/__tests__/sse-broadcaster.test.ts +137 -0
- package/src/features/kanban/backend/__tests__/sse-event-format.test.ts +55 -0
- package/src/features/kanban/backend/route.ts +55 -0
- package/src/features/kanban/backend/sse-broadcaster.ts +142 -0
- package/src/features/kanban/backend/sse-route.ts +43 -0
- package/src/features/kanban/components/KanbanBoard.tsx +105 -0
- package/src/features/kanban/components/KanbanColumn.tsx +51 -0
- package/src/features/kanban/components/KanbanError.tsx +29 -0
- package/src/features/kanban/components/KanbanSkeleton.tsx +46 -0
- package/src/features/kanban/components/ProgressCard.tsx +42 -0
- package/src/features/kanban/components/TaskCard.tsx +76 -0
- package/src/features/kanban/components/__tests__/kanban-components.test.tsx +86 -0
- package/src/features/kanban/hooks/useTaskSse.ts +66 -0
- package/src/features/kanban/hooks/useTasksQuery.ts +52 -0
- package/src/features/kanban/lib/__tests__/kanban-utils.test.ts +97 -0
- package/src/features/kanban/lib/kanban-utils.ts +37 -0
- package/src/features/taskflow/constants.ts +54 -0
- package/src/features/taskflow/index.ts +27 -0
- package/src/features/taskflow/lib/__tests__/filter.test.ts +89 -0
- package/src/features/taskflow/lib/__tests__/graph.test.ts +247 -0
- package/src/features/taskflow/lib/__tests__/repository.test.ts +233 -0
- package/src/features/taskflow/lib/__tests__/serializer.test.ts +98 -0
- package/src/features/taskflow/lib/advisor/__tests__/advisor-integration.test.ts +98 -0
- package/src/features/taskflow/lib/advisor/ai-advisor.test.ts +40 -0
- package/src/features/taskflow/lib/advisor/ai-advisor.ts +20 -0
- package/src/features/taskflow/lib/advisor/context-builder.test.ts +73 -0
- package/src/features/taskflow/lib/advisor/context-builder.ts +151 -0
- package/src/features/taskflow/lib/advisor/db.test.ts +106 -0
- package/src/features/taskflow/lib/advisor/db.ts +185 -0
- package/src/features/taskflow/lib/advisor/local-summary.test.ts +53 -0
- package/src/features/taskflow/lib/advisor/local-summary.ts +72 -0
- package/src/features/taskflow/lib/advisor/prompts.ts +86 -0
- package/src/features/taskflow/lib/filter.ts +54 -0
- package/src/features/taskflow/lib/fs-utils.ts +50 -0
- package/src/features/taskflow/lib/graph.ts +148 -0
- package/src/features/taskflow/lib/index-builder.ts +42 -0
- package/src/features/taskflow/lib/repository.ts +168 -0
- package/src/features/taskflow/lib/serializer.ts +62 -0
- package/src/features/taskflow/lib/watcher.ts +40 -0
- package/src/features/taskflow/types.ts +71 -0
- package/src/hooks/use-toast.ts +194 -0
- package/src/lib/remote/api-client.ts +40 -0
- package/src/lib/supabase/client.ts +8 -0
- package/src/lib/supabase/server.ts +46 -0
- package/src/lib/supabase/types.ts +3 -0
- package/src/lib/utils.ts +6 -0
- package/src/mcp/index.ts +7 -0
- package/src/mcp/server.ts +21 -0
- package/src/mcp/tools/brainstorm.ts +48 -0
- package/src/mcp/tools/prd.ts +71 -0
- package/src/mcp/tools/project.ts +39 -0
- package/src/mcp/tools/task-status.ts +40 -0
- package/src/mcp/tools/task.ts +82 -0
- package/src/mcp/util.ts +6 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface TrdFile {
|
|
5
|
+
name: string;
|
|
6
|
+
fileName: string;
|
|
7
|
+
filePath: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function findTrdFiles(projectRoot: string): TrdFile[] {
|
|
11
|
+
const taskflowDir = path.join(projectRoot, ".taskflow");
|
|
12
|
+
if (!fs.existsSync(taskflowDir)) return [];
|
|
13
|
+
|
|
14
|
+
return fs
|
|
15
|
+
.readdirSync(taskflowDir)
|
|
16
|
+
.filter((f) => f.startsWith("trd-") && f.endsWith(".md"))
|
|
17
|
+
.map((fileName) => {
|
|
18
|
+
const name = fileName
|
|
19
|
+
.replace(/^trd-/, "")
|
|
20
|
+
.replace(/\.md$/, "")
|
|
21
|
+
.replace(/-/g, " ");
|
|
22
|
+
return {
|
|
23
|
+
name,
|
|
24
|
+
fileName,
|
|
25
|
+
filePath: path.join(taskflowDir, fileName),
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** TRD 파일에서 그룹 이름 목록 반환 */
|
|
31
|
+
export function getTrdGroupNames(projectRoot: string): string[] {
|
|
32
|
+
return findTrdFiles(projectRoot).map((t) => t.name);
|
|
33
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
validateRequired,
|
|
4
|
+
validateName,
|
|
5
|
+
validateCommaList,
|
|
6
|
+
validateOptional,
|
|
7
|
+
} from "./validate.js";
|
|
8
|
+
|
|
9
|
+
describe("validateRequired", () => {
|
|
10
|
+
const validate = validateRequired("프로젝트명");
|
|
11
|
+
|
|
12
|
+
it("값이 있으면 true를 반환해야 한다", () => {
|
|
13
|
+
expect(validate("TaskFlow")).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("빈 값이면 에러 메시지를 반환해야 한다", () => {
|
|
17
|
+
expect(validate("")).toContain("필수 입력");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("공백만 있으면 에러 메시지를 반환해야 한다", () => {
|
|
21
|
+
expect(validate(" ")).toContain("필수 입력");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("에러 메시지에 라벨이 포함되어야 한다", () => {
|
|
25
|
+
expect(validate("")).toContain("프로젝트명");
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("validateName", () => {
|
|
30
|
+
const validate = validateName("기능명");
|
|
31
|
+
|
|
32
|
+
it("일반 이름은 통과해야 한다", () => {
|
|
33
|
+
expect(validate("사용자-인증")).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("빈 값이면 에러를 반환해야 한다", () => {
|
|
37
|
+
expect(validate("")).toContain("필수 입력");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("100자 초과 시 에러를 반환해야 한다", () => {
|
|
41
|
+
const long = "a".repeat(101);
|
|
42
|
+
expect(validate(long)).toContain("100자 이내");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("100자 이하는 통과해야 한다", () => {
|
|
46
|
+
expect(validate("a".repeat(100))).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("금지 문자 포함 시 에러를 반환해야 한다", () => {
|
|
50
|
+
expect(validate("name<bad>")).toContain("사용할 수 없는 문자");
|
|
51
|
+
expect(validate('name"bad')).toContain("사용할 수 없는 문자");
|
|
52
|
+
expect(validate("name|bad")).toContain("사용할 수 없는 문자");
|
|
53
|
+
expect(validate("name?bad")).toContain("사용할 수 없는 문자");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("허용 문자는 통과해야 한다", () => {
|
|
57
|
+
expect(validate("auth-module_v2")).toBe(true);
|
|
58
|
+
expect(validate("한글기능")).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("validateCommaList", () => {
|
|
63
|
+
const validate = validateCommaList("요구사항");
|
|
64
|
+
|
|
65
|
+
it("값이 있으면 통과해야 한다", () => {
|
|
66
|
+
expect(validate("항목1, 항목2")).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("빈 값이면 에러를 반환해야 한다", () => {
|
|
70
|
+
expect(validate("")).toContain("최소 하나");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("500자 초과 시 에러를 반환해야 한다", () => {
|
|
74
|
+
const long = "a".repeat(501);
|
|
75
|
+
expect(validate(long)).toContain("500자 이내");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("validateOptional", () => {
|
|
80
|
+
const validate = validateOptional();
|
|
81
|
+
|
|
82
|
+
it("빈 값도 통과해야 한다", () => {
|
|
83
|
+
expect(validate("")).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("값이 있어도 통과해야 한다", () => {
|
|
87
|
+
expect(validate("something")).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const MAX_INPUT_LENGTH = 500;
|
|
2
|
+
const MAX_NAME_LENGTH = 100;
|
|
3
|
+
const FORBIDDEN_CHARS = /[<>:"/\\|?*\x00-\x1f]/;
|
|
4
|
+
|
|
5
|
+
export function validateRequired(label: string) {
|
|
6
|
+
return (value: string): true | string => {
|
|
7
|
+
if (!value.trim()) {
|
|
8
|
+
return `${label}은(는) 필수 입력입니다.`;
|
|
9
|
+
}
|
|
10
|
+
return true;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function validateName(label: string) {
|
|
15
|
+
return (value: string): true | string => {
|
|
16
|
+
if (!value.trim()) {
|
|
17
|
+
return `${label}은(는) 필수 입력입니다.`;
|
|
18
|
+
}
|
|
19
|
+
if (value.length > MAX_NAME_LENGTH) {
|
|
20
|
+
return `${label}은(는) ${MAX_NAME_LENGTH}자 이내로 입력해주세요.`;
|
|
21
|
+
}
|
|
22
|
+
if (FORBIDDEN_CHARS.test(value)) {
|
|
23
|
+
return `${label}에 사용할 수 없는 문자가 포함되어 있습니다. (< > : " / \\ | ? *)`;
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function validateCommaList(label: string) {
|
|
30
|
+
return (value: string): true | string => {
|
|
31
|
+
if (!value.trim()) {
|
|
32
|
+
return `최소 하나의 ${label}을(를) 입력하세요.`;
|
|
33
|
+
}
|
|
34
|
+
if (value.length > MAX_INPUT_LENGTH) {
|
|
35
|
+
return `입력이 너무 깁니다. ${MAX_INPUT_LENGTH}자 이내로 입력해주세요.`;
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function validateOptional() {
|
|
42
|
+
return (_value: string): true => true;
|
|
43
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
당신은 소프트웨어 구현 전문 에이전트입니다.
|
|
2
|
+
|
|
3
|
+
## 임무
|
|
4
|
+
주어진 그룹의 태스크를 순서대로 모두 구현하세요.
|
|
5
|
+
|
|
6
|
+
## 워크플로우
|
|
7
|
+
다음 루프를 그룹 내 모든 태스크가 완료될 때까지 반복하세요:
|
|
8
|
+
|
|
9
|
+
1. MCP `get_next_task`를 호출하여 다음 태스크를 가져옵니다 (group 파라미터 사용)
|
|
10
|
+
2. 태스크가 없으면 종료합니다
|
|
11
|
+
3. MCP `set_task_status`로 상태를 `InProgress`로 변경합니다
|
|
12
|
+
4. 태스크의 요구사항에 따라 구현합니다:
|
|
13
|
+
- 코드 작성/수정
|
|
14
|
+
- 테스트 작성 및 실행
|
|
15
|
+
- 기존 코드 패턴을 따르세요
|
|
16
|
+
5. 구현 완료 후 MCP `set_task_status`로 상태를 `Done`으로 변경합니다
|
|
17
|
+
6. 다음 태스크로 이동합니다
|
|
18
|
+
|
|
19
|
+
## 규칙
|
|
20
|
+
- 한 번에 하나의 태스크만 처리합니다
|
|
21
|
+
- 태스크를 건너뛰지 마세요
|
|
22
|
+
- 기존 코드 스타일과 패턴을 따르세요
|
|
23
|
+
- 테스트가 있으면 반드시 실행하세요
|
|
24
|
+
- 막히면 해당 태스크를 `Blocked`로 설정하고 다음 태스크로 넘어가세요
|
|
25
|
+
- 모든 태스크 완료 시 작업 요약을 출력하세요
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { Sidebar } from "./Sidebar";
|
|
3
|
+
|
|
4
|
+
type AppLayoutProps = {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function AppLayout({ children }: AppLayoutProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div className="flex h-screen overflow-hidden bg-background">
|
|
11
|
+
<Sidebar />
|
|
12
|
+
<main className="flex-1 overflow-auto">{children}</main>
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { usePathname } from "next/navigation";
|
|
5
|
+
import {
|
|
6
|
+
LayoutGrid,
|
|
7
|
+
GitFork,
|
|
8
|
+
Clock,
|
|
9
|
+
Settings,
|
|
10
|
+
Search,
|
|
11
|
+
ChevronDown,
|
|
12
|
+
} from "lucide-react";
|
|
13
|
+
import { cn } from "@/lib/utils";
|
|
14
|
+
import { Separator } from "@/components/ui/separator";
|
|
15
|
+
|
|
16
|
+
type NavItem = {
|
|
17
|
+
label: string;
|
|
18
|
+
href: string;
|
|
19
|
+
icon: React.ReactNode;
|
|
20
|
+
shortcut?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const navItems: NavItem[] = [
|
|
24
|
+
{
|
|
25
|
+
label: "칸반 보드",
|
|
26
|
+
href: "/dashboard",
|
|
27
|
+
icon: <LayoutGrid className="h-4 w-4" />,
|
|
28
|
+
shortcut: "G K",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
label: "의존성 그래프",
|
|
32
|
+
href: "/dashboard/graph",
|
|
33
|
+
icon: <GitFork className="h-4 w-4" />,
|
|
34
|
+
shortcut: "G D",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
label: "타임라인",
|
|
38
|
+
href: "/dashboard/timeline",
|
|
39
|
+
icon: <Clock className="h-4 w-4" />,
|
|
40
|
+
shortcut: "G T",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
label: "설정",
|
|
44
|
+
href: "/dashboard/settings",
|
|
45
|
+
icon: <Settings className="h-4 w-4" />,
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
export function Sidebar() {
|
|
50
|
+
const pathname = usePathname();
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<aside className="flex h-screen w-60 shrink-0 flex-col border-r border-border bg-secondary/50">
|
|
54
|
+
{/* Project selector */}
|
|
55
|
+
<div className="flex h-14 items-center gap-2 px-4">
|
|
56
|
+
<div className="flex h-7 w-7 items-center justify-center rounded-md bg-accent text-xs font-bold text-accent-foreground">
|
|
57
|
+
TF
|
|
58
|
+
</div>
|
|
59
|
+
<div className="flex flex-1 items-center gap-1">
|
|
60
|
+
<span className="text-sm font-semibold text-foreground">
|
|
61
|
+
TaskFlow
|
|
62
|
+
</span>
|
|
63
|
+
<ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<Separator />
|
|
68
|
+
|
|
69
|
+
{/* Command palette trigger */}
|
|
70
|
+
<div className="px-3 py-3">
|
|
71
|
+
<button
|
|
72
|
+
type="button"
|
|
73
|
+
className="flex w-full items-center gap-2 rounded-md border border-border bg-background px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:border-foreground/20"
|
|
74
|
+
>
|
|
75
|
+
<Search className="h-3.5 w-3.5" />
|
|
76
|
+
<span className="flex-1 text-left">검색...</span>
|
|
77
|
+
<kbd className="rounded border border-border bg-muted px-1.5 py-0.5 text-[10px] font-medium">
|
|
78
|
+
/
|
|
79
|
+
</kbd>
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{/* Navigation */}
|
|
84
|
+
<nav className="flex-1 space-y-1 px-3">
|
|
85
|
+
{navItems.map((item) => {
|
|
86
|
+
const isActive = pathname === item.href;
|
|
87
|
+
return (
|
|
88
|
+
<Link
|
|
89
|
+
key={item.href}
|
|
90
|
+
href={item.href}
|
|
91
|
+
className={cn(
|
|
92
|
+
"flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors",
|
|
93
|
+
isActive
|
|
94
|
+
? "bg-accent/15 font-medium text-accent"
|
|
95
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
96
|
+
)}
|
|
97
|
+
>
|
|
98
|
+
{item.icon}
|
|
99
|
+
<span className="flex-1">{item.label}</span>
|
|
100
|
+
{item.shortcut && (
|
|
101
|
+
<span className="text-[10px] text-muted-foreground/60">
|
|
102
|
+
{item.shortcut}
|
|
103
|
+
</span>
|
|
104
|
+
)}
|
|
105
|
+
</Link>
|
|
106
|
+
);
|
|
107
|
+
})}
|
|
108
|
+
</nav>
|
|
109
|
+
|
|
110
|
+
<Separator />
|
|
111
|
+
|
|
112
|
+
{/* Bottom status */}
|
|
113
|
+
<div className="px-4 py-3">
|
|
114
|
+
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
115
|
+
<div className="h-2 w-2 rounded-full bg-green-500" />
|
|
116
|
+
<span>로컬 서버 연결됨</span>
|
|
117
|
+
</div>
|
|
118
|
+
<p className="mt-1 text-[10px] text-muted-foreground/60">
|
|
119
|
+
v0.1.0 · 3 태스크 진행 중
|
|
120
|
+
</p>
|
|
121
|
+
</div>
|
|
122
|
+
</aside>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
|
5
|
+
import { ChevronDown } from "lucide-react"
|
|
6
|
+
|
|
7
|
+
import { cn } from "@/lib/utils"
|
|
8
|
+
|
|
9
|
+
const Accordion = AccordionPrimitive.Root
|
|
10
|
+
|
|
11
|
+
const AccordionItem = React.forwardRef<
|
|
12
|
+
React.ElementRef<typeof AccordionPrimitive.Item>,
|
|
13
|
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
|
14
|
+
>(({ className, ...props }, ref) => (
|
|
15
|
+
<AccordionPrimitive.Item
|
|
16
|
+
ref={ref}
|
|
17
|
+
className={cn("border-b", className)}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
))
|
|
21
|
+
AccordionItem.displayName = "AccordionItem"
|
|
22
|
+
|
|
23
|
+
const AccordionTrigger = React.forwardRef<
|
|
24
|
+
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
|
25
|
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
|
26
|
+
>(({ className, children, ...props }, ref) => (
|
|
27
|
+
<AccordionPrimitive.Header className="flex">
|
|
28
|
+
<AccordionPrimitive.Trigger
|
|
29
|
+
ref={ref}
|
|
30
|
+
className={cn(
|
|
31
|
+
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
|
32
|
+
className
|
|
33
|
+
)}
|
|
34
|
+
{...props}
|
|
35
|
+
>
|
|
36
|
+
{children}
|
|
37
|
+
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
|
38
|
+
</AccordionPrimitive.Trigger>
|
|
39
|
+
</AccordionPrimitive.Header>
|
|
40
|
+
))
|
|
41
|
+
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
|
42
|
+
|
|
43
|
+
const AccordionContent = React.forwardRef<
|
|
44
|
+
React.ElementRef<typeof AccordionPrimitive.Content>,
|
|
45
|
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
|
46
|
+
>(({ className, children, ...props }, ref) => (
|
|
47
|
+
<AccordionPrimitive.Content
|
|
48
|
+
ref={ref}
|
|
49
|
+
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
|
50
|
+
{...props}
|
|
51
|
+
>
|
|
52
|
+
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
|
53
|
+
</AccordionPrimitive.Content>
|
|
54
|
+
))
|
|
55
|
+
|
|
56
|
+
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
|
57
|
+
|
|
58
|
+
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
|
|
8
|
+
const Avatar = React.forwardRef<
|
|
9
|
+
React.ElementRef<typeof AvatarPrimitive.Root>,
|
|
10
|
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
|
11
|
+
>(({ className, ...props }, ref) => (
|
|
12
|
+
<AvatarPrimitive.Root
|
|
13
|
+
ref={ref}
|
|
14
|
+
className={cn(
|
|
15
|
+
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
|
16
|
+
className
|
|
17
|
+
)}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
))
|
|
21
|
+
Avatar.displayName = AvatarPrimitive.Root.displayName
|
|
22
|
+
|
|
23
|
+
const AvatarImage = React.forwardRef<
|
|
24
|
+
React.ElementRef<typeof AvatarPrimitive.Image>,
|
|
25
|
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
|
26
|
+
>(({ className, ...props }, ref) => (
|
|
27
|
+
<AvatarPrimitive.Image
|
|
28
|
+
ref={ref}
|
|
29
|
+
className={cn("aspect-square h-full w-full", className)}
|
|
30
|
+
{...props}
|
|
31
|
+
/>
|
|
32
|
+
))
|
|
33
|
+
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
|
34
|
+
|
|
35
|
+
const AvatarFallback = React.forwardRef<
|
|
36
|
+
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
|
37
|
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
|
38
|
+
>(({ className, ...props }, ref) => (
|
|
39
|
+
<AvatarPrimitive.Fallback
|
|
40
|
+
ref={ref}
|
|
41
|
+
className={cn(
|
|
42
|
+
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
|
43
|
+
className
|
|
44
|
+
)}
|
|
45
|
+
{...props}
|
|
46
|
+
/>
|
|
47
|
+
))
|
|
48
|
+
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
|
49
|
+
|
|
50
|
+
export { Avatar, AvatarImage, AvatarFallback }
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
|
|
6
|
+
const badgeVariants = cva(
|
|
7
|
+
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default:
|
|
12
|
+
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
|
13
|
+
secondary:
|
|
14
|
+
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
15
|
+
destructive:
|
|
16
|
+
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
|
17
|
+
outline: "text-foreground",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
defaultVariants: {
|
|
21
|
+
variant: "default",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
export interface BadgeProps
|
|
27
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
28
|
+
VariantProps<typeof badgeVariants> {}
|
|
29
|
+
|
|
30
|
+
function Badge({ className, variant, ...props }: BadgeProps) {
|
|
31
|
+
return (
|
|
32
|
+
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { Badge, badgeVariants };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
|
|
7
|
+
const buttonVariants = cva(
|
|
8
|
+
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
13
|
+
destructive:
|
|
14
|
+
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
15
|
+
outline:
|
|
16
|
+
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
|
17
|
+
secondary:
|
|
18
|
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
19
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
20
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
21
|
+
},
|
|
22
|
+
size: {
|
|
23
|
+
default: "h-10 px-4 py-2",
|
|
24
|
+
sm: "h-9 rounded-md px-3",
|
|
25
|
+
lg: "h-11 rounded-md px-8",
|
|
26
|
+
icon: "h-10 w-10",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
defaultVariants: {
|
|
30
|
+
variant: "default",
|
|
31
|
+
size: "default",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
export interface ButtonProps
|
|
37
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
38
|
+
VariantProps<typeof buttonVariants> {
|
|
39
|
+
asChild?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
43
|
+
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
44
|
+
const Comp = asChild ? Slot : "button";
|
|
45
|
+
return (
|
|
46
|
+
<Comp
|
|
47
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
48
|
+
ref={ref}
|
|
49
|
+
{...props}
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
Button.displayName = "Button";
|
|
55
|
+
|
|
56
|
+
export { Button, buttonVariants };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
const Card = React.forwardRef<
|
|
6
|
+
HTMLDivElement,
|
|
7
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
8
|
+
>(({ className, ...props }, ref) => (
|
|
9
|
+
<div
|
|
10
|
+
ref={ref}
|
|
11
|
+
className={cn(
|
|
12
|
+
"rounded-lg border bg-card text-card-foreground shadow-xs",
|
|
13
|
+
className
|
|
14
|
+
)}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
))
|
|
18
|
+
Card.displayName = "Card"
|
|
19
|
+
|
|
20
|
+
const CardHeader = React.forwardRef<
|
|
21
|
+
HTMLDivElement,
|
|
22
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
23
|
+
>(({ className, ...props }, ref) => (
|
|
24
|
+
<div
|
|
25
|
+
ref={ref}
|
|
26
|
+
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
|
27
|
+
{...props}
|
|
28
|
+
/>
|
|
29
|
+
))
|
|
30
|
+
CardHeader.displayName = "CardHeader"
|
|
31
|
+
|
|
32
|
+
const CardTitle = React.forwardRef<
|
|
33
|
+
HTMLDivElement,
|
|
34
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
35
|
+
>(({ className, ...props }, ref) => (
|
|
36
|
+
<div
|
|
37
|
+
ref={ref}
|
|
38
|
+
className={cn(
|
|
39
|
+
"text-2xl font-semibold leading-none tracking-tight",
|
|
40
|
+
className
|
|
41
|
+
)}
|
|
42
|
+
{...props}
|
|
43
|
+
/>
|
|
44
|
+
))
|
|
45
|
+
CardTitle.displayName = "CardTitle"
|
|
46
|
+
|
|
47
|
+
const CardDescription = React.forwardRef<
|
|
48
|
+
HTMLDivElement,
|
|
49
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
50
|
+
>(({ className, ...props }, ref) => (
|
|
51
|
+
<div
|
|
52
|
+
ref={ref}
|
|
53
|
+
className={cn("text-sm text-muted-foreground", className)}
|
|
54
|
+
{...props}
|
|
55
|
+
/>
|
|
56
|
+
))
|
|
57
|
+
CardDescription.displayName = "CardDescription"
|
|
58
|
+
|
|
59
|
+
const CardContent = React.forwardRef<
|
|
60
|
+
HTMLDivElement,
|
|
61
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
62
|
+
>(({ className, ...props }, ref) => (
|
|
63
|
+
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
|
64
|
+
))
|
|
65
|
+
CardContent.displayName = "CardContent"
|
|
66
|
+
|
|
67
|
+
const CardFooter = React.forwardRef<
|
|
68
|
+
HTMLDivElement,
|
|
69
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
70
|
+
>(({ className, ...props }, ref) => (
|
|
71
|
+
<div
|
|
72
|
+
ref={ref}
|
|
73
|
+
className={cn("flex items-center p-6 pt-0", className)}
|
|
74
|
+
{...props}
|
|
75
|
+
/>
|
|
76
|
+
))
|
|
77
|
+
CardFooter.displayName = "CardFooter"
|
|
78
|
+
|
|
79
|
+
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
|
5
|
+
import { Check } from "lucide-react";
|
|
6
|
+
|
|
7
|
+
import { cn } from "@/lib/utils";
|
|
8
|
+
|
|
9
|
+
const Checkbox = React.forwardRef<
|
|
10
|
+
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
|
11
|
+
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
|
12
|
+
>(({ className, ...props }, ref) => (
|
|
13
|
+
<CheckboxPrimitive.Root
|
|
14
|
+
ref={ref}
|
|
15
|
+
className={cn(
|
|
16
|
+
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
|
17
|
+
className,
|
|
18
|
+
)}
|
|
19
|
+
{...props}
|
|
20
|
+
>
|
|
21
|
+
<CheckboxPrimitive.Indicator
|
|
22
|
+
className={cn("flex items-center justify-center text-current")}
|
|
23
|
+
>
|
|
24
|
+
<Check className="h-4 w-4" />
|
|
25
|
+
</CheckboxPrimitive.Indicator>
|
|
26
|
+
</CheckboxPrimitive.Root>
|
|
27
|
+
));
|
|
28
|
+
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
|
29
|
+
|
|
30
|
+
export { Checkbox };
|