@jcjeon/integration-cli 0.2.0

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.
Files changed (264) hide show
  1. package/.gitignore +23 -0
  2. package/.npmignore +21 -0
  3. package/.prettierignore +6 -0
  4. package/.prettierrc +26 -0
  5. package/AGENTS.md +10 -0
  6. package/CLAUDE.md +10 -0
  7. package/README.md +384 -0
  8. package/apps/server/README.md +294 -0
  9. package/apps/server/eslint.config.mjs +20 -0
  10. package/apps/server/nest-cli.json +8 -0
  11. package/apps/server/package.json +89 -0
  12. package/apps/server/scripts/postinstall.js +53 -0
  13. package/apps/server/src/__mocks__/glob.js +6 -0
  14. package/apps/server/src/__mocks__/uuid.js +5 -0
  15. package/apps/server/src/app.controller.spec.ts +24 -0
  16. package/apps/server/src/app.controller.ts +13 -0
  17. package/apps/server/src/app.module.ts +18 -0
  18. package/apps/server/src/app.service.ts +8 -0
  19. package/apps/server/src/common/ji-paths.ts +41 -0
  20. package/apps/server/src/database/database.module.ts +27 -0
  21. package/apps/server/src/database/entities/agent-changelog.entity.ts +39 -0
  22. package/apps/server/src/database/entities/agent-session.entity.ts +29 -0
  23. package/apps/server/src/database/entities/conversation.entity.ts +41 -0
  24. package/apps/server/src/database/entities/session.entity.ts +16 -0
  25. package/apps/server/src/database/entities/task-agent-run.entity.ts +40 -0
  26. package/apps/server/src/database/entities/task-agent.entity.ts +42 -0
  27. package/apps/server/src/database/entities/task-requirement.entity.ts +27 -0
  28. package/apps/server/src/database/entities/task-run.entity.ts +41 -0
  29. package/apps/server/src/database/entities/task.entity.ts +44 -0
  30. package/apps/server/src/main.ts +65 -0
  31. package/apps/server/src/modules/agents/agent-model-settings.spec.ts +80 -0
  32. package/apps/server/src/modules/agents/agents.module.ts +11 -0
  33. package/apps/server/src/modules/agents/claude/claude-auth.manager.ts +83 -0
  34. package/apps/server/src/modules/agents/claude/claude-pty.manager.ts +380 -0
  35. package/apps/server/src/modules/agents/claude/claude.controller.ts +85 -0
  36. package/apps/server/src/modules/agents/claude/claude.gateway.ts +158 -0
  37. package/apps/server/src/modules/agents/claude/claude.module.ts +18 -0
  38. package/apps/server/src/modules/agents/claude/claude.service.ts +67 -0
  39. package/apps/server/src/modules/agents/claude/dto/create-session.dto.ts +24 -0
  40. package/apps/server/src/modules/agents/claude/dto/resize-session.dto.ts +13 -0
  41. package/apps/server/src/modules/agents/claude/dto/send-input.dto.ts +9 -0
  42. package/apps/server/src/modules/agents/claude/interfaces/claude-session.interface.ts +26 -0
  43. package/apps/server/src/modules/agents/claude/interfaces/pty-event.interface.ts +10 -0
  44. package/apps/server/src/modules/agents/claude/interfaces/stream-event.interface.ts +61 -0
  45. package/apps/server/src/modules/agents/codex/codex-auth.manager.ts +107 -0
  46. package/apps/server/src/modules/agents/codex/codex-session.manager.ts +357 -0
  47. package/apps/server/src/modules/agents/codex/codex.controller.ts +64 -0
  48. package/apps/server/src/modules/agents/codex/codex.gateway.ts +97 -0
  49. package/apps/server/src/modules/agents/codex/codex.module.ts +17 -0
  50. package/apps/server/src/modules/agents/codex/dto/configure-auth.dto.ts +7 -0
  51. package/apps/server/src/modules/agents/gemini/dto/configure-auth.dto.ts +15 -0
  52. package/apps/server/src/modules/agents/gemini/dto/create-session.dto.ts +9 -0
  53. package/apps/server/src/modules/agents/gemini/dto/send-input.dto.ts +9 -0
  54. package/apps/server/src/modules/agents/gemini/gemini-auth.manager.ts +157 -0
  55. package/apps/server/src/modules/agents/gemini/gemini-session.manager.ts +287 -0
  56. package/apps/server/src/modules/agents/gemini/gemini.controller.ts +93 -0
  57. package/apps/server/src/modules/agents/gemini/gemini.gateway.ts +149 -0
  58. package/apps/server/src/modules/agents/gemini/gemini.module.ts +17 -0
  59. package/apps/server/src/modules/agents/gemini/interfaces/gemini-session.interface.ts +18 -0
  60. package/apps/server/src/modules/agents/gemini/interfaces/stream-event.interface.ts +14 -0
  61. package/apps/server/src/modules/agents/session-termination.spec.ts +103 -0
  62. package/apps/server/src/modules/changelog/changelog.controller.ts +20 -0
  63. package/apps/server/src/modules/changelog/changelog.module.ts +14 -0
  64. package/apps/server/src/modules/changelog/changelog.service.spec.ts +531 -0
  65. package/apps/server/src/modules/changelog/changelog.service.ts +690 -0
  66. package/apps/server/src/modules/conversations/conversation.controller.spec.ts +106 -0
  67. package/apps/server/src/modules/conversations/conversation.controller.ts +60 -0
  68. package/apps/server/src/modules/conversations/conversation.module.ts +14 -0
  69. package/apps/server/src/modules/conversations/conversation.service.spec.ts +176 -0
  70. package/apps/server/src/modules/conversations/conversation.service.ts +54 -0
  71. package/apps/server/src/modules/conversations/dto/create-conversation.dto.ts +37 -0
  72. package/apps/server/src/modules/conversations/enums/conversation.enum.ts +13 -0
  73. package/apps/server/src/modules/fs/fs.controller.ts +29 -0
  74. package/apps/server/src/modules/fs/fs.module.ts +8 -0
  75. package/apps/server/src/modules/harness/dto/save-harness.dto.ts +9 -0
  76. package/apps/server/src/modules/harness/harness.controller.spec.ts +95 -0
  77. package/apps/server/src/modules/harness/harness.controller.ts +35 -0
  78. package/apps/server/src/modules/harness/harness.module.ts +11 -0
  79. package/apps/server/src/modules/harness/harness.service.spec.ts +217 -0
  80. package/apps/server/src/modules/harness/harness.service.ts +112 -0
  81. package/apps/server/src/modules/sessions/session.controller.spec.ts +68 -0
  82. package/apps/server/src/modules/sessions/session.controller.ts +43 -0
  83. package/apps/server/src/modules/sessions/session.module.ts +14 -0
  84. package/apps/server/src/modules/sessions/session.service.spec.ts +106 -0
  85. package/apps/server/src/modules/sessions/session.service.ts +35 -0
  86. package/apps/server/src/modules/tasks/dto/create-task.dto.ts +54 -0
  87. package/apps/server/src/modules/tasks/dto/execute-task.dto.ts +22 -0
  88. package/apps/server/src/modules/tasks/dto/merge-file.dto.ts +7 -0
  89. package/apps/server/src/modules/tasks/dto/rerun-task.dto.ts +14 -0
  90. package/apps/server/src/modules/tasks/dto/update-task.dto.ts +55 -0
  91. package/apps/server/src/modules/tasks/task-execution.service.ts +978 -0
  92. package/apps/server/src/modules/tasks/task.gateway.ts +140 -0
  93. package/apps/server/src/modules/tasks/tasks.controller.spec.ts +210 -0
  94. package/apps/server/src/modules/tasks/tasks.controller.ts +139 -0
  95. package/apps/server/src/modules/tasks/tasks.module.ts +30 -0
  96. package/apps/server/src/modules/tasks/tasks.service.spec.ts +552 -0
  97. package/apps/server/src/modules/tasks/tasks.service.ts +333 -0
  98. package/apps/server/test/app.e2e-spec.ts +28 -0
  99. package/apps/server/test/jest-e2e.json +9 -0
  100. package/apps/server/tsconfig.build.json +4 -0
  101. package/apps/server/tsconfig.json +13 -0
  102. package/apps/web/AGENTS.md +7 -0
  103. package/apps/web/CLAUDE.md +1 -0
  104. package/apps/web/README.md +36 -0
  105. package/apps/web/eslint.config.mjs +21 -0
  106. package/apps/web/next-env.d.ts +6 -0
  107. package/apps/web/next.config.ts +7 -0
  108. package/apps/web/package.json +49 -0
  109. package/apps/web/postcss.config.mjs +7 -0
  110. package/apps/web/public/file.svg +1 -0
  111. package/apps/web/public/globe.svg +1 -0
  112. package/apps/web/public/next.svg +1 -0
  113. package/apps/web/public/vercel.svg +1 -0
  114. package/apps/web/public/window.svg +1 -0
  115. package/apps/web/src/app/claude/page.tsx +5 -0
  116. package/apps/web/src/app/codex/page.tsx +126 -0
  117. package/apps/web/src/app/favicon.ico +0 -0
  118. package/apps/web/src/app/gemini/page.tsx +130 -0
  119. package/apps/web/src/app/globals.css +149 -0
  120. package/apps/web/src/app/layout.tsx +40 -0
  121. package/apps/web/src/app/login/page.tsx +67 -0
  122. package/apps/web/src/app/page.tsx +497 -0
  123. package/apps/web/src/app/task/[id]/page.tsx +11 -0
  124. package/apps/web/src/app/test/page.tsx +298 -0
  125. package/apps/web/src/components/ui/Modal.tsx +78 -0
  126. package/apps/web/src/components/ui/WorkingDirPicker.tsx +195 -0
  127. package/apps/web/src/components/ui/__tests__/Modal.test.tsx +68 -0
  128. package/apps/web/src/features/auth/api/__tests__/auth.api.test.ts +83 -0
  129. package/apps/web/src/features/auth/api/auth.api.ts +81 -0
  130. package/apps/web/src/features/auth/hooks/__tests__/useClaudeAuth.test.ts +166 -0
  131. package/apps/web/src/features/auth/hooks/__tests__/useCodexAuth.test.ts +127 -0
  132. package/apps/web/src/features/auth/hooks/__tests__/useGeminiAuth.test.ts +120 -0
  133. package/apps/web/src/features/auth/hooks/useClaudeAuth.ts +88 -0
  134. package/apps/web/src/features/auth/hooks/useCodexAuth.ts +149 -0
  135. package/apps/web/src/features/auth/hooks/useGeminiAuth.ts +125 -0
  136. package/apps/web/src/features/auth/ui/CodexLoginPanel.tsx +302 -0
  137. package/apps/web/src/features/auth/ui/GeminiLoginPanel.tsx +316 -0
  138. package/apps/web/src/features/auth/ui/LoginForm.tsx +190 -0
  139. package/apps/web/src/features/auth/ui/LoginPanel.tsx +114 -0
  140. package/apps/web/src/features/auth/ui/__tests__/LoginPanel.test.tsx +105 -0
  141. package/apps/web/src/features/chat/api/__tests__/sessions.api.test.ts +187 -0
  142. package/apps/web/src/features/chat/api/sessions.api.ts +161 -0
  143. package/apps/web/src/features/chat/container/ClaudePageContainer.tsx +152 -0
  144. package/apps/web/src/features/chat/hooks/__tests__/useCodexSessions.test.ts +131 -0
  145. package/apps/web/src/features/chat/hooks/__tests__/useGeminiSessions.test.ts +130 -0
  146. package/apps/web/src/features/chat/hooks/useAgentModelSettings.ts +54 -0
  147. package/apps/web/src/features/chat/hooks/useClaudeSessions.ts +323 -0
  148. package/apps/web/src/features/chat/hooks/useCodexSessions.ts +275 -0
  149. package/apps/web/src/features/chat/hooks/useGeminiSessions.ts +255 -0
  150. package/apps/web/src/features/chat/hooks/useSessionCommand.ts +66 -0
  151. package/apps/web/src/features/chat/hooks/useSessionRename.ts +61 -0
  152. package/apps/web/src/features/chat/hooks/useSessionWorkingDirectories.ts +34 -0
  153. package/apps/web/src/features/chat/hooks/useUnifiedSessions.ts +156 -0
  154. package/apps/web/src/features/chat/lib/agentModelOptions.ts +72 -0
  155. package/apps/web/src/features/chat/ui/AgentModelPicker.tsx +134 -0
  156. package/apps/web/src/features/chat/ui/AgentSelectModal.tsx +236 -0
  157. package/apps/web/src/features/chat/ui/ChatInput.tsx +162 -0
  158. package/apps/web/src/features/chat/ui/ChatMessage.tsx +204 -0
  159. package/apps/web/src/features/chat/ui/ChatWorkspace.tsx +207 -0
  160. package/apps/web/src/features/chat/ui/CheckingSkeleton.tsx +44 -0
  161. package/apps/web/src/features/chat/ui/ClaudeLoginView.tsx +44 -0
  162. package/apps/web/src/features/chat/ui/PermissionCard.tsx +37 -0
  163. package/apps/web/src/features/chat/ui/SessionSidebar.tsx +280 -0
  164. package/apps/web/src/features/chat/ui/__tests__/AgentSelectModal.test.tsx +58 -0
  165. package/apps/web/src/features/chat/ui/__tests__/ChatInput.test.tsx +134 -0
  166. package/apps/web/src/features/chat/ui/__tests__/ChatMessage.test.tsx +106 -0
  167. package/apps/web/src/features/chat/ui/__tests__/ChatWorkspace.test.tsx +66 -0
  168. package/apps/web/src/features/diff/ui/DiffFileRow.tsx +73 -0
  169. package/apps/web/src/features/diff/ui/DiffHunk.tsx +61 -0
  170. package/apps/web/src/features/diff/ui/FileChangeBadge.tsx +23 -0
  171. package/apps/web/src/features/diff/ui/__tests__/DiffFileRow.test.tsx +40 -0
  172. package/apps/web/src/features/diff/ui/__tests__/DiffHunk.test.tsx +24 -0
  173. package/apps/web/src/features/diff/ui/__tests__/FileChangeBadge.test.tsx +16 -0
  174. package/apps/web/src/features/fs/api/fs.api.ts +14 -0
  175. package/apps/web/src/features/fs/hooks/useDirBrowser.ts +50 -0
  176. package/apps/web/src/features/harness/api/__tests__/harness.api.test.ts +73 -0
  177. package/apps/web/src/features/harness/api/harness.api.ts +46 -0
  178. package/apps/web/src/features/harness/hooks/__tests__/useHarness.test.ts +65 -0
  179. package/apps/web/src/features/harness/hooks/useHarness.ts +66 -0
  180. package/apps/web/src/features/harness/ui/HarnessModal.tsx +171 -0
  181. package/apps/web/src/features/harness/ui/__tests__/HarnessModal.test.tsx +46 -0
  182. package/apps/web/src/features/status/ui/AgentStatusModal.tsx +267 -0
  183. package/apps/web/src/features/status/ui/__tests__/AgentStatusModal.test.tsx +71 -0
  184. package/apps/web/src/features/tasks/api/__tests__/changelog.api.test.ts +89 -0
  185. package/apps/web/src/features/tasks/api/__tests__/tasks.api.test.ts +282 -0
  186. package/apps/web/src/features/tasks/api/changelog.api.ts +52 -0
  187. package/apps/web/src/features/tasks/api/tasks.api.ts +175 -0
  188. package/apps/web/src/features/tasks/container/TaskDetailPageContainer.tsx +69 -0
  189. package/apps/web/src/features/tasks/hooks/__tests__/useChangelogCodeCopy.test.ts +48 -0
  190. package/apps/web/src/features/tasks/hooks/__tests__/useTaskChangelog.test.ts +48 -0
  191. package/apps/web/src/features/tasks/hooks/__tests__/useTaskCreate.test.ts +217 -0
  192. package/apps/web/src/features/tasks/hooks/__tests__/useTaskEdit.test.ts +152 -0
  193. package/apps/web/src/features/tasks/hooks/__tests__/useTaskExecution.test.ts +143 -0
  194. package/apps/web/src/features/tasks/hooks/__tests__/useTaskList.test.ts +168 -0
  195. package/apps/web/src/features/tasks/hooks/__tests__/useTaskNotification.test.ts +125 -0
  196. package/apps/web/src/features/tasks/hooks/__tests__/useTaskRuns.test.ts +51 -0
  197. package/apps/web/src/features/tasks/hooks/useChangelogCodeCopy.ts +52 -0
  198. package/apps/web/src/features/tasks/hooks/useCopyToClipboard.ts +47 -0
  199. package/apps/web/src/features/tasks/hooks/useTaskChangelog.ts +32 -0
  200. package/apps/web/src/features/tasks/hooks/useTaskCreate.ts +137 -0
  201. package/apps/web/src/features/tasks/hooks/useTaskDetail.ts +217 -0
  202. package/apps/web/src/features/tasks/hooks/useTaskEdit.ts +130 -0
  203. package/apps/web/src/features/tasks/hooks/useTaskExecution.ts +137 -0
  204. package/apps/web/src/features/tasks/hooks/useTaskList.ts +159 -0
  205. package/apps/web/src/features/tasks/hooks/useTaskNotification.ts +80 -0
  206. package/apps/web/src/features/tasks/hooks/useTaskRuns.ts +32 -0
  207. package/apps/web/src/features/tasks/ui/AgentOutputPanel.tsx +203 -0
  208. package/apps/web/src/features/tasks/ui/AgentRoleSelect.tsx +97 -0
  209. package/apps/web/src/features/tasks/ui/ChangelogPanel.tsx +321 -0
  210. package/apps/web/src/features/tasks/ui/RunHistoryPanel.tsx +193 -0
  211. package/apps/web/src/features/tasks/ui/TaskCreateModal.tsx +205 -0
  212. package/apps/web/src/features/tasks/ui/TaskDetailView.tsx +413 -0
  213. package/apps/web/src/features/tasks/ui/TaskEditModal.tsx +165 -0
  214. package/apps/web/src/features/tasks/ui/TaskListModal.tsx +591 -0
  215. package/apps/web/src/features/tasks/ui/__tests__/AgentRoleSelect.test.tsx +91 -0
  216. package/apps/web/src/features/tasks/ui/__tests__/ChangelogPanel.test.tsx +94 -0
  217. package/apps/web/src/features/tasks/ui/__tests__/RunHistoryPanel.test.tsx +71 -0
  218. package/apps/web/src/features/tasks/ui/__tests__/TaskCreateModal.test.tsx +153 -0
  219. package/apps/web/src/features/tasks/ui/__tests__/TaskEditModal.test.tsx +75 -0
  220. package/apps/web/src/features/tasks/ui/__tests__/TaskListModal.test.tsx +243 -0
  221. package/apps/web/src/hooks/useWorkingDir.ts +28 -0
  222. package/apps/web/src/lib/__tests__/ansi.test.ts +88 -0
  223. package/apps/web/src/lib/ansi.ts +105 -0
  224. package/apps/web/src/lib/constants.ts +4 -0
  225. package/apps/web/src/lib/quota.ts +22 -0
  226. package/apps/web/src/lib/theme.tsx +78 -0
  227. package/apps/web/src/lib/toast.tsx +175 -0
  228. package/apps/web/src/store/agentStatusStore.ts +38 -0
  229. package/apps/web/tsconfig.json +18 -0
  230. package/apps/web/vitest.config.ts +25 -0
  231. package/apps/web/vitest.setup.ts +10 -0
  232. package/package.json +85 -0
  233. package/packages/cli/dist/commands/check.d.ts +1 -0
  234. package/packages/cli/dist/commands/check.js +89 -0
  235. package/packages/cli/dist/commands/init.d.ts +5 -0
  236. package/packages/cli/dist/commands/init.js +183 -0
  237. package/packages/cli/dist/commands/start.d.ts +4 -0
  238. package/packages/cli/dist/commands/start.js +188 -0
  239. package/packages/cli/dist/index.d.ts +2 -0
  240. package/packages/cli/dist/index.js +71 -0
  241. package/packages/cli/dist/utils/agent-tools.d.ts +28 -0
  242. package/packages/cli/dist/utils/agent-tools.js +193 -0
  243. package/packages/cli/dist/utils/project-init.d.ts +12 -0
  244. package/packages/cli/dist/utils/project-init.js +258 -0
  245. package/packages/cli/dist/utils/proxy.d.ts +8 -0
  246. package/packages/cli/dist/utils/proxy.js +138 -0
  247. package/packages/cli/package.json +30 -0
  248. package/packages/cli/src/commands/check.ts +77 -0
  249. package/packages/cli/src/commands/init.ts +209 -0
  250. package/packages/cli/src/commands/start.ts +183 -0
  251. package/packages/cli/src/index.ts +91 -0
  252. package/packages/cli/src/utils/agent-tools.ts +201 -0
  253. package/packages/cli/src/utils/project-init.ts +252 -0
  254. package/packages/cli/src/utils/proxy.ts +123 -0
  255. package/packages/cli/tsconfig.json +14 -0
  256. package/packages/eslint-config/base.mjs +31 -0
  257. package/packages/eslint-config/nest.mjs +55 -0
  258. package/packages/eslint-config/next.mjs +23 -0
  259. package/packages/eslint-config/package.json +20 -0
  260. package/packages/typescript-config/base.json +16 -0
  261. package/packages/typescript-config/nestjs.json +17 -0
  262. package/packages/typescript-config/nextjs.json +15 -0
  263. package/packages/typescript-config/package.json +11 -0
  264. package/turbo.json +28 -0
@@ -0,0 +1,497 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { useEffect, useState } from "react";
5
+
6
+ import { useClaudeAuth } from "@/features/auth/hooks/useClaudeAuth";
7
+ import { useGeminiAuth } from "@/features/auth/hooks/useGeminiAuth";
8
+ import { useCodexAuth } from "@/features/auth/hooks/useCodexAuth";
9
+ import { LoginPanel } from "@/features/auth/ui/LoginPanel";
10
+ import { GeminiLoginPanel } from "@/features/auth/ui/GeminiLoginPanel";
11
+ import { CodexLoginPanel } from "@/features/auth/ui/CodexLoginPanel";
12
+ import { Modal } from "@/components/ui/Modal";
13
+ import { ThemeToggle } from "@/lib/theme";
14
+
15
+ type AgentKey = "claude" | "gemini" | "codex";
16
+ type AuthBadge = "loading" | "authenticated" | "unauthenticated" | "unavailable";
17
+
18
+ // ─── 에이전트 인증 상태 → Badge 변환 ─────────────────────────────────────────
19
+
20
+ function toBadge(state: string): AuthBadge {
21
+ if (state === "checking") return "loading";
22
+ if (state === "authenticated") return "authenticated";
23
+ if (state === "not-installed") return "unavailable";
24
+ return "unauthenticated";
25
+ }
26
+
27
+ // ─── Badge ────────────────────────────────────────────────────────────────────
28
+
29
+ const BADGE_CONFIG: Record<
30
+ AuthBadge,
31
+ { border: string; bg: string; text: string; dot: string; label: string }
32
+ > = {
33
+ loading: {
34
+ border: "border-gray-900/[0.07] dark:border-white/[0.07]",
35
+ bg: "bg-gray-900/[0.04] dark:bg-white/[0.04]",
36
+ text: "text-transparent",
37
+ dot: "bg-gray-900/20 dark:bg-white/20",
38
+ label: "확인 중",
39
+ },
40
+ authenticated: {
41
+ border: "border-emerald-500/25",
42
+ bg: "bg-emerald-500/[0.08]",
43
+ text: "text-emerald-600 dark:text-emerald-400",
44
+ dot: "bg-emerald-500 shadow-[0_0_5px_#34d399] dark:bg-emerald-400",
45
+ label: "사용 가능",
46
+ },
47
+ unauthenticated: {
48
+ border: "border-amber-500/25",
49
+ bg: "bg-amber-500/[0.08]",
50
+ text: "text-amber-600 dark:text-amber-400",
51
+ dot: "bg-amber-500 dark:bg-amber-400",
52
+ label: "로그인 필요",
53
+ },
54
+ unavailable: {
55
+ border: "border-gray-900/[0.06] dark:border-white/[0.06]",
56
+ bg: "bg-gray-900/[0.03] dark:bg-white/[0.03]",
57
+ text: "text-gray-900/30 dark:text-white/30",
58
+ dot: "bg-gray-900/20 dark:bg-white/20",
59
+ label: "미설치",
60
+ },
61
+ };
62
+
63
+ function Badge({ badge }: { badge: AuthBadge }) {
64
+ const { border, bg, text, dot, label } = BADGE_CONFIG[badge];
65
+ const isPulsing = badge === "loading" || badge === "authenticated";
66
+ return (
67
+ <span
68
+ className={[
69
+ "inline-flex min-w-[86px] shrink-0 items-center justify-center gap-1.5",
70
+ "rounded-full border px-3 py-[5px] text-xs font-medium",
71
+ "transition-all duration-500",
72
+ border, bg, text,
73
+ ].join(" ")}
74
+ >
75
+ <span className={["h-1.5 w-1.5 rounded-full", dot, isPulsing ? "animate-pulse" : ""].join(" ")} />
76
+ {label}
77
+ </span>
78
+ );
79
+ }
80
+
81
+ // ─── 에이전트 아이콘 ──────────────────────────────────────────────────────────
82
+
83
+ const CLAUDE_ICON = (
84
+ <svg viewBox="0 0 24 24" fill="currentColor" className="h-5 w-5 text-[#D97757]" aria-hidden="true">
85
+ <path d="M13.827 3.816L20.05 20.2h-3.672l-1.234-3.365H8.856L7.622 20.2H3.95L10.173 3.816h3.654zm-1.827 4.91l-1.989 5.453h3.978l-1.989-5.453z" />
86
+ </svg>
87
+ );
88
+
89
+ const GEMINI_ICON = (
90
+ <svg viewBox="0 0 24 24" className="h-5 w-5" aria-hidden="true">
91
+ <path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4" />
92
+ <path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853" />
93
+ <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05" />
94
+ <path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335" />
95
+ </svg>
96
+ );
97
+
98
+ const CODEX_ICON = (
99
+ <svg viewBox="0 0 24 24" fill="currentColor" className="h-5 w-5 text-gray-900/70 dark:text-white/70" aria-hidden="true">
100
+ <path d="M22.282 9.821a5.985 5.985 0 0 0-.516-4.91 6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a5.985 5.985 0 0 0-3.998 2.9 6.046 6.046 0 0 0 .743 7.097 5.98 5.98 0 0 0 .51 4.911 6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206 5.99 5.99 0 0 0 3.997-2.9 6.056 6.056 0 0 0-.747-7.073zM13.26 22.43a4.476 4.476 0 0 1-2.876-1.04l.141-.081 4.779-2.758a.795.795 0 0 0 .392-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494zM3.6 18.304a4.47 4.47 0 0 1-.535-3.014l.142.085 4.783 2.759a.771.771 0 0 0 .78 0l5.843-3.369v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.5 4.5 0 0 1-6.14-1.646zM2.34 7.896a4.485 4.485 0 0 1 2.366-1.973V11.6a.766.766 0 0 0 .388.676l5.815 3.355-2.02 1.168a.076.076 0 0 1-.071 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.872zm16.597 3.855l-5.833-3.387L15.119 7.2a.076.076 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667zm2.01-3.023l-.141-.085-4.774-2.782a.776.776 0 0 0-.785 0L9.409 9.23V6.897a.066.066 0 0 1 .028-.061l4.83-2.787a4.5 4.5 0 0 1 6.68 4.66zm-12.64 4.135l-2.02-1.164a.08.08 0 0 1-.038-.057V6.075a4.5 4.5 0 0 1 7.375-3.453l-.142.08L8.704 5.46a.795.795 0 0 0-.393.681zm1.097-2.365l2.602-1.5 2.607 1.5v2.999l-2.597 1.5-2.607-1.5z" />
101
+ </svg>
102
+ );
103
+
104
+ const AGENT_META: Record<
105
+ AgentKey,
106
+ { name: string; description: string; icon: React.ReactNode; iconBg: string; accentFrom: string; accentVia: string }
107
+ > = {
108
+ claude: {
109
+ name: "Claude CLI",
110
+ description: "Anthropic Claude Code",
111
+ icon: CLAUDE_ICON,
112
+ iconBg: "bg-orange-500/[0.10]",
113
+ accentFrom: "from-orange-500/[0.10]",
114
+ accentVia: "via-orange-400/50",
115
+ },
116
+ gemini: {
117
+ name: "Gemini CLI",
118
+ description: "Google Gemini",
119
+ icon: GEMINI_ICON,
120
+ iconBg: "bg-blue-500/[0.08]",
121
+ accentFrom: "from-blue-500/[0.10]",
122
+ accentVia: "via-blue-400/40",
123
+ },
124
+ codex: {
125
+ name: "Codex CLI",
126
+ description: "OpenAI Codex",
127
+ icon: CODEX_ICON,
128
+ iconBg: "bg-gray-900/[0.05] dark:bg-white/[0.05]",
129
+ accentFrom: "from-gray-500/[0.07]",
130
+ accentVia: "via-gray-400/25",
131
+ },
132
+ };
133
+
134
+ // ─── 에이전트 인증 카드 ───────────────────────────────────────────────────────
135
+
136
+ interface AgentAuthCardProps {
137
+ agentKey: AgentKey;
138
+ badge: AuthBadge;
139
+ onAuth: () => void;
140
+ index: number;
141
+ }
142
+
143
+ function AgentAuthCard({ agentKey, badge, onAuth, index }: AgentAuthCardProps) {
144
+ const meta = AGENT_META[agentKey];
145
+ const canAuth = badge === "unauthenticated" || badge === "loading";
146
+ const isAuth = badge === "authenticated";
147
+
148
+ return (
149
+ <div
150
+ className={[
151
+ "animate-fade-in-up relative overflow-hidden rounded-2xl",
152
+ "border border-gray-900/[0.07] dark:border-white/[0.07]",
153
+ "bg-gray-900/[0.025] dark:bg-white/[0.025]",
154
+ "p-4 transition-all duration-300",
155
+ ].join(" ")}
156
+ style={{ animationDelay: `${index * 60}ms` }}
157
+ >
158
+ {/* 상단 accent 라인 */}
159
+ <div
160
+ className={[
161
+ "absolute inset-x-0 top-0 h-px",
162
+ "bg-gradient-to-r from-transparent to-transparent",
163
+ meta.accentVia,
164
+ ].join(" ")}
165
+ />
166
+ {/* 상단 ambient tint */}
167
+ <div
168
+ className={[
169
+ "pointer-events-none absolute inset-x-0 top-0 h-20",
170
+ `bg-gradient-to-b ${meta.accentFrom} to-transparent opacity-50`,
171
+ ].join(" ")}
172
+ />
173
+
174
+ <div className="relative flex items-center justify-between gap-3">
175
+ {/* 왼쪽: 아이콘 + 이름 */}
176
+ <div className="flex items-center gap-3 min-w-0">
177
+ <span className={["flex h-9 w-9 shrink-0 items-center justify-center rounded-xl", meta.iconBg].join(" ")}>
178
+ {meta.icon}
179
+ </span>
180
+ <div className="min-w-0">
181
+ <p className="text-[14px] font-semibold text-gray-900/85 dark:text-white/85">{meta.name}</p>
182
+ <p className="text-[12px] text-gray-900/35 dark:text-white/35">{meta.description}</p>
183
+ </div>
184
+ </div>
185
+
186
+ {/* 오른쪽: 배지 + 인증 버튼 */}
187
+ <div className="flex shrink-0 items-center gap-2">
188
+ <Badge badge={badge} />
189
+ {!isAuth && badge !== "unavailable" && (
190
+ <button
191
+ onClick={onAuth}
192
+ disabled={!canAuth}
193
+ className={[
194
+ "rounded-lg border px-3 py-1.5 text-xs font-medium transition-all",
195
+ canAuth
196
+ ? "border-gray-900/[0.10] bg-gray-900/[0.04] text-gray-900/60 hover:border-gray-900/[0.18] hover:bg-gray-900/[0.08] hover:text-gray-900/90 dark:border-white/[0.10] dark:bg-white/[0.04] dark:text-white/60 dark:hover:border-white/[0.18] dark:hover:bg-white/[0.08] dark:hover:text-white/90"
197
+ : "cursor-not-allowed border-gray-900/[0.06] bg-gray-900/[0.02] text-gray-900/25 dark:border-white/[0.06] dark:bg-white/[0.02] dark:text-white/25",
198
+ ].join(" ")}
199
+ >
200
+ 인증하기
201
+ </button>
202
+ )}
203
+ {isAuth && (
204
+ <span className="flex h-7 w-7 items-center justify-center rounded-full bg-emerald-500/[0.10]">
205
+ <svg viewBox="0 0 16 16" fill="currentColor" className="h-3.5 w-3.5 text-emerald-500 dark:text-emerald-400">
206
+ <path fillRule="evenodd" d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z" clipRule="evenodd" />
207
+ </svg>
208
+ </span>
209
+ )}
210
+ </div>
211
+ </div>
212
+ </div>
213
+ );
214
+ }
215
+
216
+ // ─── Claude 페이지 진입 버튼 ─────────────────────────────────────────────────
217
+
218
+ function ClaudeEntryButton({ canEnter, isLoading }: { canEnter: boolean; isLoading: boolean }) {
219
+ if (isLoading) {
220
+ return (
221
+ <div className="animate-fade-in-up flex h-[60px] w-full max-w-[460px] items-center justify-center rounded-2xl border border-gray-900/[0.07] bg-gray-900/[0.025] dark:border-white/[0.07] dark:bg-white/[0.025]">
222
+ <span className="h-4 w-4 animate-spin rounded-full border-2 border-gray-900/[0.08] border-t-orange-500 dark:border-white/[0.08]" />
223
+ </div>
224
+ );
225
+ }
226
+
227
+ if (canEnter) {
228
+ return (
229
+ <Link
230
+ href="/claude"
231
+ className={[
232
+ "animate-fade-in-up group relative flex w-full max-w-[460px] items-center justify-between overflow-hidden",
233
+ "rounded-2xl px-6 py-4 transition-all duration-300",
234
+ "bg-gradient-to-r from-orange-600 to-orange-500",
235
+ "shadow-[0_4px_20px_-2px_rgba(234,88,12,0.30)] hover:shadow-[0_8px_28px_-2px_rgba(234,88,12,0.40)]",
236
+ "hover:from-orange-500 hover:to-orange-400",
237
+ ].join(" ")}
238
+ style={{ animationDelay: "180ms" }}
239
+ >
240
+ {/* 배경 반짝임 */}
241
+ <div className="pointer-events-none absolute inset-0 bg-gradient-to-tr from-white/[0.07] to-transparent" />
242
+
243
+ <div className="relative flex items-center gap-3">
244
+ <span className="flex h-8 w-8 items-center justify-center rounded-lg bg-white/[0.18]">
245
+ <svg viewBox="0 0 24 24" fill="currentColor" className="h-4.5 w-4.5 text-white">
246
+ <path d="M13.827 3.816L20.05 20.2h-3.672l-1.234-3.365H8.856L7.622 20.2H3.95L10.173 3.816h3.654zm-1.827 4.91l-1.989 5.453h3.978l-1.989-5.453z" />
247
+ </svg>
248
+ </span>
249
+ <div>
250
+ <p className="text-[15px] font-bold tracking-tight text-white">JI CLI 시작하기</p>
251
+ <p className="text-[11px] text-white/60">멀티 에이전트 채팅 시작</p>
252
+ </div>
253
+ </div>
254
+
255
+ <div className="relative flex h-7 w-7 items-center justify-center rounded-lg bg-white/[0.15] transition-transform duration-200 group-hover:translate-x-0.5">
256
+ <svg viewBox="0 0 16 16" fill="currentColor" className="h-4 w-4 text-white">
257
+ <path fillRule="evenodd" d="M6.22 4.22a.75.75 0 011.06 0l3.25 3.25a.75.75 0 010 1.06l-3.25 3.25a.75.75 0 01-1.06-1.06L8.94 8 6.22 5.28a.75.75 0 010-1.06z" clipRule="evenodd" />
258
+ </svg>
259
+ </div>
260
+ </Link>
261
+ );
262
+ }
263
+
264
+ return (
265
+ <div className="animate-fade-in-up flex w-full max-w-[460px] flex-col items-center gap-2" style={{ animationDelay: "180ms" }}>
266
+ <div
267
+ className={[
268
+ "relative flex w-full items-center justify-between overflow-hidden",
269
+ "rounded-2xl border border-gray-900/[0.07] bg-gray-900/[0.025] px-6 py-4",
270
+ "dark:border-white/[0.07] dark:bg-white/[0.025]",
271
+ "opacity-40 cursor-not-allowed",
272
+ ].join(" ")}
273
+ >
274
+ <div className="flex items-center gap-3">
275
+ <span className="flex h-8 w-8 items-center justify-center rounded-lg border border-gray-900/[0.08] bg-gray-900/[0.04] dark:border-white/[0.08] dark:bg-white/[0.04]">
276
+ <svg viewBox="0 0 16 16" fill="currentColor" className="h-4 w-4 text-gray-900/40 dark:text-white/40">
277
+ <path fillRule="evenodd" d="M8 1a3.5 3.5 0 00-3.5 3.5V7A1.5 1.5 0 003 8.5v5A1.5 1.5 0 004.5 15h7a1.5 1.5 0 001.5-1.5v-5A1.5 1.5 0 0011.5 7V4.5A3.5 3.5 0 008 1zm2 6V4.5a2 2 0 10-4 0V7h4z" clipRule="evenodd" />
278
+ </svg>
279
+ </span>
280
+ <div>
281
+ <p className="text-[15px] font-bold tracking-tight text-gray-900/60 dark:text-white/60">JI Claude 시작하기</p>
282
+ <p className="text-[11px] text-gray-900/30 dark:text-white/30">멀티 에이전트 채팅 시작</p>
283
+ </div>
284
+ </div>
285
+ <div className="flex h-7 w-7 items-center justify-center rounded-lg border border-gray-900/[0.08] bg-gray-900/[0.04] dark:border-white/[0.08] dark:bg-white/[0.04]">
286
+ <svg viewBox="0 0 16 16" fill="currentColor" className="h-3.5 w-3.5 text-gray-900/30 dark:text-white/30">
287
+ <path fillRule="evenodd" d="M6.22 4.22a.75.75 0 011.06 0l3.25 3.25a.75.75 0 010 1.06l-3.25 3.25a.75.75 0 01-1.06-1.06L8.94 8 6.22 5.28a.75.75 0 010-1.06z" clipRule="evenodd" />
288
+ </svg>
289
+ </div>
290
+ </div>
291
+ <p className="text-[12px] text-gray-900/30 dark:text-white/30">
292
+ 에이전트를 하나 이상 인증해야 시작할 수 있습니다
293
+ </p>
294
+ </div>
295
+ );
296
+ }
297
+
298
+ // ─── Page ─────────────────────────────────────────────────────────────────────
299
+
300
+ export default function Home() {
301
+ const claude = useClaudeAuth();
302
+ const gemini = useGeminiAuth();
303
+ const codex = useCodexAuth();
304
+
305
+ const [modalOpen, setModalOpen] = useState<AgentKey | null>(null);
306
+
307
+ // 인증 완료 시 모달 자동 닫기 (1.2초 후)
308
+ useEffect(() => {
309
+ if (modalOpen === "claude" && claude.loginState === "done") {
310
+ const t = setTimeout(() => setModalOpen(null), 1200);
311
+ return () => clearTimeout(t);
312
+ }
313
+ }, [claude.loginState, modalOpen]);
314
+
315
+ useEffect(() => {
316
+ if (modalOpen === "gemini" && gemini.loginState === "done") {
317
+ const t = setTimeout(() => setModalOpen(null), 1200);
318
+ return () => clearTimeout(t);
319
+ }
320
+ }, [gemini.loginState, modalOpen]);
321
+
322
+ useEffect(() => {
323
+ if (
324
+ modalOpen === "codex" &&
325
+ (codex.loginState === "done" || codex.apiKeyLoginState === "done")
326
+ ) {
327
+ const t = setTimeout(() => setModalOpen(null), 1200);
328
+ return () => clearTimeout(t);
329
+ }
330
+ }, [codex.loginState, codex.apiKeyLoginState, modalOpen]);
331
+
332
+ // 모달 닫기 핸들러 (진행 중인 인증 취소 포함)
333
+ const handleCloseModal = () => {
334
+ if (modalOpen === "claude") claude.cancelLogin();
335
+ if (modalOpen === "gemini") gemini.cancelLogin();
336
+ if (modalOpen === "codex") codex.cancelDeviceLogin();
337
+ setModalOpen(null);
338
+ };
339
+
340
+ const claudeBadge = toBadge(claude.authState);
341
+ const geminiBadge = toBadge(gemini.authState);
342
+ const codexBadge = toBadge(codex.authState);
343
+
344
+ const isAnyAuthenticated =
345
+ claudeBadge === "authenticated" ||
346
+ geminiBadge === "authenticated" ||
347
+ codexBadge === "authenticated";
348
+
349
+ const isChecking =
350
+ claudeBadge === "loading" ||
351
+ geminiBadge === "loading" ||
352
+ codexBadge === "loading";
353
+
354
+ return (
355
+ <div className="relative flex min-h-screen flex-col items-center justify-center overflow-hidden bg-[#faf8f5] px-6 py-12 dark:bg-[#07090e]">
356
+
357
+ {/* 테마 토글 */}
358
+ <div className="absolute right-4 top-4">
359
+ <ThemeToggle />
360
+ </div>
361
+
362
+ {/* 배경 glow */}
363
+ <div className="pointer-events-none absolute inset-0 overflow-hidden">
364
+ <div className="absolute left-1/2 top-[40%] h-[600px] w-[800px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-orange-500/[0.04] blur-[120px] dark:bg-orange-500/[0.035]" />
365
+ <div className="absolute -left-24 bottom-1/4 h-[350px] w-[450px] rounded-full bg-blue-600/[0.03] blur-[100px] dark:bg-blue-600/[0.025]" />
366
+ <div className="absolute -right-24 top-1/4 h-[300px] w-[400px] rounded-full bg-purple-600/[0.03] blur-[100px] dark:bg-purple-600/[0.025]" />
367
+ </div>
368
+
369
+ {/* dot-grid */}
370
+ <div
371
+ className="pointer-events-none absolute inset-0 opacity-60"
372
+ style={{
373
+ backgroundImage: "radial-gradient(circle, var(--dot-color) 1px, transparent 1px)",
374
+ backgroundSize: "28px 28px",
375
+ maskImage: "radial-gradient(ellipse 75% 65% at 50% 50%, black 30%, transparent 100%)",
376
+ WebkitMaskImage: "radial-gradient(ellipse 75% 65% at 50% 50%, black 30%, transparent 100%)",
377
+ }}
378
+ />
379
+
380
+ {/* 헤더 */}
381
+ <header className="animate-fade-in-up relative mb-10 flex flex-col items-center gap-5 text-center">
382
+ <div className="relative">
383
+ <div className="flex h-12 w-80 items-center justify-center rounded-[16px] border border-gray-900/[0.10] bg-gray-900/[0.05] shadow-[inset_0_1px_0_rgba(0,0,0,0.06)] backdrop-blur-sm dark:border-white/[0.10] dark:bg-white/[0.05] dark:shadow-[inset_0_1px_0_rgba(255,255,255,0.08)]">
384
+ <span className="font-mono text-sm font-bold tracking-widest text-gray-900/65 dark:text-white/65">
385
+ INTEGRATION-CLI
386
+ </span>
387
+ </div>
388
+ <div className="absolute -inset-2 rounded-[20px] bg-orange-500/10 blur-xl" />
389
+ </div>
390
+ <div>
391
+ <h1
392
+ className="text-[2.75rem] font-bold leading-none tracking-[-0.03em]"
393
+ style={{
394
+ background: "var(--heading-gradient)",
395
+ WebkitBackgroundClip: "text",
396
+ WebkitTextFillColor: "transparent",
397
+ backgroundClip: "text",
398
+ }}
399
+ >
400
+ JI-CLI
401
+ </h1>
402
+ <p className="mt-2.5 text-[13px] font-medium uppercase tracking-[0.06em] text-gray-900/28 dark:text-white/28">
403
+ 하나의 플랫폼에서 모든 AI CLI를 제어합니다
404
+ </p>
405
+ </div>
406
+ </header>
407
+
408
+ {/* 본문 */}
409
+ <div className="relative flex w-full max-w-[460px] flex-col gap-3">
410
+
411
+ {/* ── 에이전트 인증 섹션 ──────────────────────────────────────────── */}
412
+ <div className="animate-fade-in-up mb-1 flex items-center gap-2" style={{ animationDelay: "60ms" }}>
413
+ <span className="text-[11px] font-semibold uppercase tracking-[0.07em] text-gray-900/35 dark:text-white/35">
414
+ 에이전트 인증
415
+ </span>
416
+ <div className="h-px flex-1 bg-gray-900/[0.06] dark:bg-white/[0.06]" />
417
+ </div>
418
+
419
+ <AgentAuthCard
420
+ agentKey="claude"
421
+ badge={claudeBadge}
422
+ onAuth={() => setModalOpen("claude")}
423
+ index={0}
424
+ />
425
+ <AgentAuthCard
426
+ agentKey="gemini"
427
+ badge={geminiBadge}
428
+ onAuth={() => setModalOpen("gemini")}
429
+ index={1}
430
+ />
431
+ <AgentAuthCard
432
+ agentKey="codex"
433
+ badge={codexBadge}
434
+ onAuth={() => setModalOpen("codex")}
435
+ index={2}
436
+ />
437
+
438
+ {/* ── 구분선 ──────────────────────────────────────────────────────── */}
439
+ <div className="animate-fade-in-up my-1 flex items-center gap-2" style={{ animationDelay: "150ms" }}>
440
+ <div className="h-px flex-1 bg-gray-900/[0.06] dark:bg-white/[0.06]" />
441
+ <span className="text-[11px] font-semibold uppercase tracking-[0.07em] text-gray-900/35 dark:text-white/35">
442
+ 시작하기
443
+ </span>
444
+ <div className="h-px flex-1 bg-gray-900/[0.06] dark:bg-white/[0.06]" />
445
+ </div>
446
+
447
+ {/* ── Claude 페이지 진입 버튼 ─────────────────────────────────────── */}
448
+ <ClaudeEntryButton canEnter={isAnyAuthenticated} isLoading={isChecking} />
449
+ </div>
450
+
451
+ {/* 하단 rule */}
452
+ <div className="pointer-events-none absolute inset-x-0 bottom-0 h-px bg-gradient-to-r from-transparent via-gray-900/[0.05] to-transparent dark:via-white/[0.05]" />
453
+
454
+ {/* ── Claude 인증 모달 ────────────────────────────────────────────────── */}
455
+ <Modal open={modalOpen === "claude"} onClose={handleCloseModal} title="Claude CLI 인증" maxWidth="max-w-lg">
456
+ <LoginPanel
457
+ loginState={claude.loginState}
458
+ loginOutput={claude.loginOutput}
459
+ loginUrls={claude.loginUrls}
460
+ onStart={claude.startLogin}
461
+ onCancel={() => { claude.cancelLogin(); setModalOpen(null); }}
462
+ />
463
+ </Modal>
464
+
465
+ {/* ── Gemini 인증 모달 ────────────────────────────────────────────────── */}
466
+ <Modal open={modalOpen === "gemini"} onClose={handleCloseModal} title="Gemini CLI 인증" maxWidth="max-w-lg">
467
+ <GeminiLoginPanel
468
+ loginState={gemini.loginState}
469
+ loginOutput={gemini.loginOutput}
470
+ loginUrls={gemini.loginUrls}
471
+ configError={gemini.configError}
472
+ onSaveApiKey={gemini.saveApiKey}
473
+ onStartGca={gemini.startGcaLogin}
474
+ onCancel={() => { gemini.cancelLogin(); setModalOpen(null); }}
475
+ onReset={gemini.resetLogin}
476
+ />
477
+ </Modal>
478
+
479
+ {/* ── Codex 인증 모달 ─────────────────────────────────────────────────── */}
480
+ <Modal open={modalOpen === "codex"} onClose={handleCloseModal} title="Codex CLI 인증" maxWidth="max-w-lg">
481
+ <CodexLoginPanel
482
+ loginMethod={codex.loginMethod}
483
+ onMethodChange={codex.setLoginMethod}
484
+ loginState={codex.loginState}
485
+ loginOutput={codex.loginOutput}
486
+ loginUrls={codex.loginUrls}
487
+ deviceCode={codex.deviceCode}
488
+ onStartDeviceLogin={codex.startDeviceLogin}
489
+ onCancelDeviceLogin={() => { codex.cancelDeviceLogin(); setModalOpen(null); }}
490
+ apiKeyLoginState={codex.apiKeyLoginState}
491
+ configError={codex.configError}
492
+ onSaveApiKey={codex.saveApiKey}
493
+ />
494
+ </Modal>
495
+ </div>
496
+ );
497
+ }
@@ -0,0 +1,11 @@
1
+ "use client";
2
+
3
+ import { useParams } from "next/navigation";
4
+
5
+ import { TaskDetailPageContainer } from "@/features/tasks/container/TaskDetailPageContainer";
6
+
7
+ export default function TaskDetailPage() {
8
+ const params = useParams<{ id: string }>();
9
+
10
+ return <TaskDetailPageContainer taskId={params.id} />;
11
+ }