@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.
- package/.gitignore +23 -0
- package/.npmignore +21 -0
- package/.prettierignore +6 -0
- package/.prettierrc +26 -0
- package/AGENTS.md +10 -0
- package/CLAUDE.md +10 -0
- package/README.md +384 -0
- package/apps/server/README.md +294 -0
- package/apps/server/eslint.config.mjs +20 -0
- package/apps/server/nest-cli.json +8 -0
- package/apps/server/package.json +89 -0
- package/apps/server/scripts/postinstall.js +53 -0
- package/apps/server/src/__mocks__/glob.js +6 -0
- package/apps/server/src/__mocks__/uuid.js +5 -0
- package/apps/server/src/app.controller.spec.ts +24 -0
- package/apps/server/src/app.controller.ts +13 -0
- package/apps/server/src/app.module.ts +18 -0
- package/apps/server/src/app.service.ts +8 -0
- package/apps/server/src/common/ji-paths.ts +41 -0
- package/apps/server/src/database/database.module.ts +27 -0
- package/apps/server/src/database/entities/agent-changelog.entity.ts +39 -0
- package/apps/server/src/database/entities/agent-session.entity.ts +29 -0
- package/apps/server/src/database/entities/conversation.entity.ts +41 -0
- package/apps/server/src/database/entities/session.entity.ts +16 -0
- package/apps/server/src/database/entities/task-agent-run.entity.ts +40 -0
- package/apps/server/src/database/entities/task-agent.entity.ts +42 -0
- package/apps/server/src/database/entities/task-requirement.entity.ts +27 -0
- package/apps/server/src/database/entities/task-run.entity.ts +41 -0
- package/apps/server/src/database/entities/task.entity.ts +44 -0
- package/apps/server/src/main.ts +65 -0
- package/apps/server/src/modules/agents/agent-model-settings.spec.ts +80 -0
- package/apps/server/src/modules/agents/agents.module.ts +11 -0
- package/apps/server/src/modules/agents/claude/claude-auth.manager.ts +83 -0
- package/apps/server/src/modules/agents/claude/claude-pty.manager.ts +380 -0
- package/apps/server/src/modules/agents/claude/claude.controller.ts +85 -0
- package/apps/server/src/modules/agents/claude/claude.gateway.ts +158 -0
- package/apps/server/src/modules/agents/claude/claude.module.ts +18 -0
- package/apps/server/src/modules/agents/claude/claude.service.ts +67 -0
- package/apps/server/src/modules/agents/claude/dto/create-session.dto.ts +24 -0
- package/apps/server/src/modules/agents/claude/dto/resize-session.dto.ts +13 -0
- package/apps/server/src/modules/agents/claude/dto/send-input.dto.ts +9 -0
- package/apps/server/src/modules/agents/claude/interfaces/claude-session.interface.ts +26 -0
- package/apps/server/src/modules/agents/claude/interfaces/pty-event.interface.ts +10 -0
- package/apps/server/src/modules/agents/claude/interfaces/stream-event.interface.ts +61 -0
- package/apps/server/src/modules/agents/codex/codex-auth.manager.ts +107 -0
- package/apps/server/src/modules/agents/codex/codex-session.manager.ts +357 -0
- package/apps/server/src/modules/agents/codex/codex.controller.ts +64 -0
- package/apps/server/src/modules/agents/codex/codex.gateway.ts +97 -0
- package/apps/server/src/modules/agents/codex/codex.module.ts +17 -0
- package/apps/server/src/modules/agents/codex/dto/configure-auth.dto.ts +7 -0
- package/apps/server/src/modules/agents/gemini/dto/configure-auth.dto.ts +15 -0
- package/apps/server/src/modules/agents/gemini/dto/create-session.dto.ts +9 -0
- package/apps/server/src/modules/agents/gemini/dto/send-input.dto.ts +9 -0
- package/apps/server/src/modules/agents/gemini/gemini-auth.manager.ts +157 -0
- package/apps/server/src/modules/agents/gemini/gemini-session.manager.ts +287 -0
- package/apps/server/src/modules/agents/gemini/gemini.controller.ts +93 -0
- package/apps/server/src/modules/agents/gemini/gemini.gateway.ts +149 -0
- package/apps/server/src/modules/agents/gemini/gemini.module.ts +17 -0
- package/apps/server/src/modules/agents/gemini/interfaces/gemini-session.interface.ts +18 -0
- package/apps/server/src/modules/agents/gemini/interfaces/stream-event.interface.ts +14 -0
- package/apps/server/src/modules/agents/session-termination.spec.ts +103 -0
- package/apps/server/src/modules/changelog/changelog.controller.ts +20 -0
- package/apps/server/src/modules/changelog/changelog.module.ts +14 -0
- package/apps/server/src/modules/changelog/changelog.service.spec.ts +531 -0
- package/apps/server/src/modules/changelog/changelog.service.ts +690 -0
- package/apps/server/src/modules/conversations/conversation.controller.spec.ts +106 -0
- package/apps/server/src/modules/conversations/conversation.controller.ts +60 -0
- package/apps/server/src/modules/conversations/conversation.module.ts +14 -0
- package/apps/server/src/modules/conversations/conversation.service.spec.ts +176 -0
- package/apps/server/src/modules/conversations/conversation.service.ts +54 -0
- package/apps/server/src/modules/conversations/dto/create-conversation.dto.ts +37 -0
- package/apps/server/src/modules/conversations/enums/conversation.enum.ts +13 -0
- package/apps/server/src/modules/fs/fs.controller.ts +29 -0
- package/apps/server/src/modules/fs/fs.module.ts +8 -0
- package/apps/server/src/modules/harness/dto/save-harness.dto.ts +9 -0
- package/apps/server/src/modules/harness/harness.controller.spec.ts +95 -0
- package/apps/server/src/modules/harness/harness.controller.ts +35 -0
- package/apps/server/src/modules/harness/harness.module.ts +11 -0
- package/apps/server/src/modules/harness/harness.service.spec.ts +217 -0
- package/apps/server/src/modules/harness/harness.service.ts +112 -0
- package/apps/server/src/modules/sessions/session.controller.spec.ts +68 -0
- package/apps/server/src/modules/sessions/session.controller.ts +43 -0
- package/apps/server/src/modules/sessions/session.module.ts +14 -0
- package/apps/server/src/modules/sessions/session.service.spec.ts +106 -0
- package/apps/server/src/modules/sessions/session.service.ts +35 -0
- package/apps/server/src/modules/tasks/dto/create-task.dto.ts +54 -0
- package/apps/server/src/modules/tasks/dto/execute-task.dto.ts +22 -0
- package/apps/server/src/modules/tasks/dto/merge-file.dto.ts +7 -0
- package/apps/server/src/modules/tasks/dto/rerun-task.dto.ts +14 -0
- package/apps/server/src/modules/tasks/dto/update-task.dto.ts +55 -0
- package/apps/server/src/modules/tasks/task-execution.service.ts +978 -0
- package/apps/server/src/modules/tasks/task.gateway.ts +140 -0
- package/apps/server/src/modules/tasks/tasks.controller.spec.ts +210 -0
- package/apps/server/src/modules/tasks/tasks.controller.ts +139 -0
- package/apps/server/src/modules/tasks/tasks.module.ts +30 -0
- package/apps/server/src/modules/tasks/tasks.service.spec.ts +552 -0
- package/apps/server/src/modules/tasks/tasks.service.ts +333 -0
- package/apps/server/test/app.e2e-spec.ts +28 -0
- package/apps/server/test/jest-e2e.json +9 -0
- package/apps/server/tsconfig.build.json +4 -0
- package/apps/server/tsconfig.json +13 -0
- package/apps/web/AGENTS.md +7 -0
- package/apps/web/CLAUDE.md +1 -0
- package/apps/web/README.md +36 -0
- package/apps/web/eslint.config.mjs +21 -0
- package/apps/web/next-env.d.ts +6 -0
- package/apps/web/next.config.ts +7 -0
- package/apps/web/package.json +49 -0
- package/apps/web/postcss.config.mjs +7 -0
- package/apps/web/public/file.svg +1 -0
- package/apps/web/public/globe.svg +1 -0
- package/apps/web/public/next.svg +1 -0
- package/apps/web/public/vercel.svg +1 -0
- package/apps/web/public/window.svg +1 -0
- package/apps/web/src/app/claude/page.tsx +5 -0
- package/apps/web/src/app/codex/page.tsx +126 -0
- package/apps/web/src/app/favicon.ico +0 -0
- package/apps/web/src/app/gemini/page.tsx +130 -0
- package/apps/web/src/app/globals.css +149 -0
- package/apps/web/src/app/layout.tsx +40 -0
- package/apps/web/src/app/login/page.tsx +67 -0
- package/apps/web/src/app/page.tsx +497 -0
- package/apps/web/src/app/task/[id]/page.tsx +11 -0
- package/apps/web/src/app/test/page.tsx +298 -0
- package/apps/web/src/components/ui/Modal.tsx +78 -0
- package/apps/web/src/components/ui/WorkingDirPicker.tsx +195 -0
- package/apps/web/src/components/ui/__tests__/Modal.test.tsx +68 -0
- package/apps/web/src/features/auth/api/__tests__/auth.api.test.ts +83 -0
- package/apps/web/src/features/auth/api/auth.api.ts +81 -0
- package/apps/web/src/features/auth/hooks/__tests__/useClaudeAuth.test.ts +166 -0
- package/apps/web/src/features/auth/hooks/__tests__/useCodexAuth.test.ts +127 -0
- package/apps/web/src/features/auth/hooks/__tests__/useGeminiAuth.test.ts +120 -0
- package/apps/web/src/features/auth/hooks/useClaudeAuth.ts +88 -0
- package/apps/web/src/features/auth/hooks/useCodexAuth.ts +149 -0
- package/apps/web/src/features/auth/hooks/useGeminiAuth.ts +125 -0
- package/apps/web/src/features/auth/ui/CodexLoginPanel.tsx +302 -0
- package/apps/web/src/features/auth/ui/GeminiLoginPanel.tsx +316 -0
- package/apps/web/src/features/auth/ui/LoginForm.tsx +190 -0
- package/apps/web/src/features/auth/ui/LoginPanel.tsx +114 -0
- package/apps/web/src/features/auth/ui/__tests__/LoginPanel.test.tsx +105 -0
- package/apps/web/src/features/chat/api/__tests__/sessions.api.test.ts +187 -0
- package/apps/web/src/features/chat/api/sessions.api.ts +161 -0
- package/apps/web/src/features/chat/container/ClaudePageContainer.tsx +152 -0
- package/apps/web/src/features/chat/hooks/__tests__/useCodexSessions.test.ts +131 -0
- package/apps/web/src/features/chat/hooks/__tests__/useGeminiSessions.test.ts +130 -0
- package/apps/web/src/features/chat/hooks/useAgentModelSettings.ts +54 -0
- package/apps/web/src/features/chat/hooks/useClaudeSessions.ts +323 -0
- package/apps/web/src/features/chat/hooks/useCodexSessions.ts +275 -0
- package/apps/web/src/features/chat/hooks/useGeminiSessions.ts +255 -0
- package/apps/web/src/features/chat/hooks/useSessionCommand.ts +66 -0
- package/apps/web/src/features/chat/hooks/useSessionRename.ts +61 -0
- package/apps/web/src/features/chat/hooks/useSessionWorkingDirectories.ts +34 -0
- package/apps/web/src/features/chat/hooks/useUnifiedSessions.ts +156 -0
- package/apps/web/src/features/chat/lib/agentModelOptions.ts +72 -0
- package/apps/web/src/features/chat/ui/AgentModelPicker.tsx +134 -0
- package/apps/web/src/features/chat/ui/AgentSelectModal.tsx +236 -0
- package/apps/web/src/features/chat/ui/ChatInput.tsx +162 -0
- package/apps/web/src/features/chat/ui/ChatMessage.tsx +204 -0
- package/apps/web/src/features/chat/ui/ChatWorkspace.tsx +207 -0
- package/apps/web/src/features/chat/ui/CheckingSkeleton.tsx +44 -0
- package/apps/web/src/features/chat/ui/ClaudeLoginView.tsx +44 -0
- package/apps/web/src/features/chat/ui/PermissionCard.tsx +37 -0
- package/apps/web/src/features/chat/ui/SessionSidebar.tsx +280 -0
- package/apps/web/src/features/chat/ui/__tests__/AgentSelectModal.test.tsx +58 -0
- package/apps/web/src/features/chat/ui/__tests__/ChatInput.test.tsx +134 -0
- package/apps/web/src/features/chat/ui/__tests__/ChatMessage.test.tsx +106 -0
- package/apps/web/src/features/chat/ui/__tests__/ChatWorkspace.test.tsx +66 -0
- package/apps/web/src/features/diff/ui/DiffFileRow.tsx +73 -0
- package/apps/web/src/features/diff/ui/DiffHunk.tsx +61 -0
- package/apps/web/src/features/diff/ui/FileChangeBadge.tsx +23 -0
- package/apps/web/src/features/diff/ui/__tests__/DiffFileRow.test.tsx +40 -0
- package/apps/web/src/features/diff/ui/__tests__/DiffHunk.test.tsx +24 -0
- package/apps/web/src/features/diff/ui/__tests__/FileChangeBadge.test.tsx +16 -0
- package/apps/web/src/features/fs/api/fs.api.ts +14 -0
- package/apps/web/src/features/fs/hooks/useDirBrowser.ts +50 -0
- package/apps/web/src/features/harness/api/__tests__/harness.api.test.ts +73 -0
- package/apps/web/src/features/harness/api/harness.api.ts +46 -0
- package/apps/web/src/features/harness/hooks/__tests__/useHarness.test.ts +65 -0
- package/apps/web/src/features/harness/hooks/useHarness.ts +66 -0
- package/apps/web/src/features/harness/ui/HarnessModal.tsx +171 -0
- package/apps/web/src/features/harness/ui/__tests__/HarnessModal.test.tsx +46 -0
- package/apps/web/src/features/status/ui/AgentStatusModal.tsx +267 -0
- package/apps/web/src/features/status/ui/__tests__/AgentStatusModal.test.tsx +71 -0
- package/apps/web/src/features/tasks/api/__tests__/changelog.api.test.ts +89 -0
- package/apps/web/src/features/tasks/api/__tests__/tasks.api.test.ts +282 -0
- package/apps/web/src/features/tasks/api/changelog.api.ts +52 -0
- package/apps/web/src/features/tasks/api/tasks.api.ts +175 -0
- package/apps/web/src/features/tasks/container/TaskDetailPageContainer.tsx +69 -0
- package/apps/web/src/features/tasks/hooks/__tests__/useChangelogCodeCopy.test.ts +48 -0
- package/apps/web/src/features/tasks/hooks/__tests__/useTaskChangelog.test.ts +48 -0
- package/apps/web/src/features/tasks/hooks/__tests__/useTaskCreate.test.ts +217 -0
- package/apps/web/src/features/tasks/hooks/__tests__/useTaskEdit.test.ts +152 -0
- package/apps/web/src/features/tasks/hooks/__tests__/useTaskExecution.test.ts +143 -0
- package/apps/web/src/features/tasks/hooks/__tests__/useTaskList.test.ts +168 -0
- package/apps/web/src/features/tasks/hooks/__tests__/useTaskNotification.test.ts +125 -0
- package/apps/web/src/features/tasks/hooks/__tests__/useTaskRuns.test.ts +51 -0
- package/apps/web/src/features/tasks/hooks/useChangelogCodeCopy.ts +52 -0
- package/apps/web/src/features/tasks/hooks/useCopyToClipboard.ts +47 -0
- package/apps/web/src/features/tasks/hooks/useTaskChangelog.ts +32 -0
- package/apps/web/src/features/tasks/hooks/useTaskCreate.ts +137 -0
- package/apps/web/src/features/tasks/hooks/useTaskDetail.ts +217 -0
- package/apps/web/src/features/tasks/hooks/useTaskEdit.ts +130 -0
- package/apps/web/src/features/tasks/hooks/useTaskExecution.ts +137 -0
- package/apps/web/src/features/tasks/hooks/useTaskList.ts +159 -0
- package/apps/web/src/features/tasks/hooks/useTaskNotification.ts +80 -0
- package/apps/web/src/features/tasks/hooks/useTaskRuns.ts +32 -0
- package/apps/web/src/features/tasks/ui/AgentOutputPanel.tsx +203 -0
- package/apps/web/src/features/tasks/ui/AgentRoleSelect.tsx +97 -0
- package/apps/web/src/features/tasks/ui/ChangelogPanel.tsx +321 -0
- package/apps/web/src/features/tasks/ui/RunHistoryPanel.tsx +193 -0
- package/apps/web/src/features/tasks/ui/TaskCreateModal.tsx +205 -0
- package/apps/web/src/features/tasks/ui/TaskDetailView.tsx +413 -0
- package/apps/web/src/features/tasks/ui/TaskEditModal.tsx +165 -0
- package/apps/web/src/features/tasks/ui/TaskListModal.tsx +591 -0
- package/apps/web/src/features/tasks/ui/__tests__/AgentRoleSelect.test.tsx +91 -0
- package/apps/web/src/features/tasks/ui/__tests__/ChangelogPanel.test.tsx +94 -0
- package/apps/web/src/features/tasks/ui/__tests__/RunHistoryPanel.test.tsx +71 -0
- package/apps/web/src/features/tasks/ui/__tests__/TaskCreateModal.test.tsx +153 -0
- package/apps/web/src/features/tasks/ui/__tests__/TaskEditModal.test.tsx +75 -0
- package/apps/web/src/features/tasks/ui/__tests__/TaskListModal.test.tsx +243 -0
- package/apps/web/src/hooks/useWorkingDir.ts +28 -0
- package/apps/web/src/lib/__tests__/ansi.test.ts +88 -0
- package/apps/web/src/lib/ansi.ts +105 -0
- package/apps/web/src/lib/constants.ts +4 -0
- package/apps/web/src/lib/quota.ts +22 -0
- package/apps/web/src/lib/theme.tsx +78 -0
- package/apps/web/src/lib/toast.tsx +175 -0
- package/apps/web/src/store/agentStatusStore.ts +38 -0
- package/apps/web/tsconfig.json +18 -0
- package/apps/web/vitest.config.ts +25 -0
- package/apps/web/vitest.setup.ts +10 -0
- package/package.json +85 -0
- package/packages/cli/dist/commands/check.d.ts +1 -0
- package/packages/cli/dist/commands/check.js +89 -0
- package/packages/cli/dist/commands/init.d.ts +5 -0
- package/packages/cli/dist/commands/init.js +183 -0
- package/packages/cli/dist/commands/start.d.ts +4 -0
- package/packages/cli/dist/commands/start.js +188 -0
- package/packages/cli/dist/index.d.ts +2 -0
- package/packages/cli/dist/index.js +71 -0
- package/packages/cli/dist/utils/agent-tools.d.ts +28 -0
- package/packages/cli/dist/utils/agent-tools.js +193 -0
- package/packages/cli/dist/utils/project-init.d.ts +12 -0
- package/packages/cli/dist/utils/project-init.js +258 -0
- package/packages/cli/dist/utils/proxy.d.ts +8 -0
- package/packages/cli/dist/utils/proxy.js +138 -0
- package/packages/cli/package.json +30 -0
- package/packages/cli/src/commands/check.ts +77 -0
- package/packages/cli/src/commands/init.ts +209 -0
- package/packages/cli/src/commands/start.ts +183 -0
- package/packages/cli/src/index.ts +91 -0
- package/packages/cli/src/utils/agent-tools.ts +201 -0
- package/packages/cli/src/utils/project-init.ts +252 -0
- package/packages/cli/src/utils/proxy.ts +123 -0
- package/packages/cli/tsconfig.json +14 -0
- package/packages/eslint-config/base.mjs +31 -0
- package/packages/eslint-config/nest.mjs +55 -0
- package/packages/eslint-config/next.mjs +23 -0
- package/packages/eslint-config/package.json +20 -0
- package/packages/typescript-config/base.json +16 -0
- package/packages/typescript-config/nestjs.json +17 -0
- package/packages/typescript-config/nextjs.json +15 -0
- package/packages/typescript-config/package.json +11 -0
- package/turbo.json +28 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRef, useState } from "react";
|
|
4
|
+
|
|
5
|
+
import type { GeminiAuthMethod, GeminiLoginState } from "../hooks/useGeminiAuth";
|
|
6
|
+
|
|
7
|
+
interface GeminiLoginPanelProps {
|
|
8
|
+
loginState: GeminiLoginState;
|
|
9
|
+
loginOutput: string;
|
|
10
|
+
loginUrls: string[];
|
|
11
|
+
configError: string;
|
|
12
|
+
onSaveApiKey: (key: string) => void;
|
|
13
|
+
onStartGca: () => void;
|
|
14
|
+
onCancel: () => void;
|
|
15
|
+
onReset: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const GOOGLE_LOGO = (
|
|
19
|
+
<svg viewBox="0 0 24 24" className="h-5 w-5 shrink-0" aria-hidden="true">
|
|
20
|
+
<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" />
|
|
21
|
+
<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" />
|
|
22
|
+
<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" />
|
|
23
|
+
<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" />
|
|
24
|
+
</svg>
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// ─── Method Select ────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
interface MethodSelectProps {
|
|
30
|
+
onSelect: (method: GeminiAuthMethod) => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function MethodSelect({ onSelect }: MethodSelectProps) {
|
|
34
|
+
return (
|
|
35
|
+
<div className="flex w-full max-w-sm flex-col gap-3">
|
|
36
|
+
<button
|
|
37
|
+
onClick={() => onSelect("api-key")}
|
|
38
|
+
className="flex items-center gap-3 rounded-xl border border-gray-900/[0.08] bg-gray-900/[0.02] px-5 py-4 text-left transition-all hover:border-blue-500/40 hover:bg-blue-500/[0.04] dark:border-white/[0.08] dark:bg-white/[0.02] dark:hover:border-blue-400/40 dark:hover:bg-blue-400/[0.04]"
|
|
39
|
+
>
|
|
40
|
+
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-blue-500/[0.10]">
|
|
41
|
+
<svg viewBox="0 0 20 20" fill="currentColor" className="h-4.5 w-4.5 text-blue-500 dark:text-blue-400">
|
|
42
|
+
<path fillRule="evenodd" d="M8 7a5 5 0 113.61 4.804l-1.903 1.903A1 1 0 019 14H8v1a1 1 0 01-1 1H6v1a1 1 0 01-1 1H3a1 1 0 01-1-1v-2a1 1 0 01.293-.707L8.196 8.39A5.002 5.002 0 018 7zm5-3a.75.75 0 000 1.5A1.5 1.5 0 0114.5 7 .75.75 0 0016 7a3 3 0 00-3-3z" clipRule="evenodd" />
|
|
43
|
+
</svg>
|
|
44
|
+
</span>
|
|
45
|
+
<div>
|
|
46
|
+
<p className="text-sm font-semibold text-gray-900/85 dark:text-white/85">API 키</p>
|
|
47
|
+
<p className="text-xs text-gray-900/35 dark:text-white/35">Google AI Studio에서 발급한 키 입력</p>
|
|
48
|
+
</div>
|
|
49
|
+
</button>
|
|
50
|
+
|
|
51
|
+
<button
|
|
52
|
+
onClick={() => onSelect("gca")}
|
|
53
|
+
className="flex items-center gap-3 rounded-xl border border-gray-900/[0.08] bg-gray-900/[0.02] px-5 py-4 text-left transition-all hover:border-blue-500/40 hover:bg-blue-500/[0.04] dark:border-white/[0.08] dark:bg-white/[0.02] dark:hover:border-blue-400/40 dark:hover:bg-blue-400/[0.04]"
|
|
54
|
+
>
|
|
55
|
+
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-blue-500/[0.10]">
|
|
56
|
+
{GOOGLE_LOGO}
|
|
57
|
+
</span>
|
|
58
|
+
<div>
|
|
59
|
+
<p className="text-sm font-semibold text-gray-900/85 dark:text-white/85">Google Cloud (GCA)</p>
|
|
60
|
+
<p className="text-xs text-gray-900/35 dark:text-white/35">gcloud CLI로 Application Default Credentials 설정</p>
|
|
61
|
+
</div>
|
|
62
|
+
</button>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ─── API Key Form ─────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
interface ApiKeyFormProps {
|
|
70
|
+
loginState: GeminiLoginState;
|
|
71
|
+
configError: string;
|
|
72
|
+
onSubmit: (key: string) => void;
|
|
73
|
+
onBack: () => void;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function ApiKeyForm({ loginState, configError, onSubmit, onBack }: ApiKeyFormProps) {
|
|
77
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
78
|
+
|
|
79
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
const key = inputRef.current?.value.trim() ?? "";
|
|
82
|
+
if (key) onSubmit(key);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
if (loginState === "done") return <SuccessView />;
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<form onSubmit={handleSubmit} className="flex w-full max-w-sm flex-col gap-4">
|
|
89
|
+
<div className="flex flex-col gap-1.5">
|
|
90
|
+
<label className="text-xs font-medium text-gray-900/50 dark:text-white/50">
|
|
91
|
+
Gemini API 키
|
|
92
|
+
</label>
|
|
93
|
+
<input
|
|
94
|
+
ref={inputRef}
|
|
95
|
+
type="password"
|
|
96
|
+
placeholder="AIza..."
|
|
97
|
+
autoFocus
|
|
98
|
+
required
|
|
99
|
+
className="rounded-lg border border-gray-900/[0.10] bg-gray-900/[0.03] px-3.5 py-2.5 font-mono text-sm text-gray-900/85 placeholder-gray-900/20 outline-none transition-colors focus:border-blue-500/50 focus:ring-2 focus:ring-blue-500/10 dark:border-white/[0.10] dark:bg-white/[0.03] dark:text-white/85 dark:placeholder-white/20 dark:focus:border-blue-400/50 dark:focus:ring-blue-400/10"
|
|
100
|
+
/>
|
|
101
|
+
<p className="text-[11px] text-gray-900/30 dark:text-white/30">
|
|
102
|
+
<a
|
|
103
|
+
href="https://aistudio.google.com/app/apikey"
|
|
104
|
+
target="_blank"
|
|
105
|
+
rel="noopener noreferrer"
|
|
106
|
+
className="text-blue-500 hover:underline dark:text-blue-400"
|
|
107
|
+
>
|
|
108
|
+
Google AI Studio
|
|
109
|
+
</a>
|
|
110
|
+
에서 API 키를 발급받을 수 있습니다.
|
|
111
|
+
</p>
|
|
112
|
+
{configError && (
|
|
113
|
+
<p className="text-xs text-red-500 dark:text-red-400">{configError}</p>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div className="flex flex-col gap-2">
|
|
118
|
+
<button
|
|
119
|
+
type="submit"
|
|
120
|
+
disabled={loginState === "pending"}
|
|
121
|
+
className="flex items-center justify-center gap-2 rounded-xl bg-blue-600 px-6 py-3 text-sm font-semibold text-white transition-colors hover:bg-blue-500 disabled:opacity-50"
|
|
122
|
+
>
|
|
123
|
+
{loginState === "pending" && (
|
|
124
|
+
<span className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
|
|
125
|
+
)}
|
|
126
|
+
{loginState === "pending" ? "저장 중…" : "저장"}
|
|
127
|
+
</button>
|
|
128
|
+
<button
|
|
129
|
+
type="button"
|
|
130
|
+
onClick={onBack}
|
|
131
|
+
className="text-xs text-gray-900/25 transition-colors hover:text-gray-900/50 dark:text-white/25 dark:hover:text-white/50"
|
|
132
|
+
>
|
|
133
|
+
← 뒤로
|
|
134
|
+
</button>
|
|
135
|
+
</div>
|
|
136
|
+
</form>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ─── GCA Form ─────────────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
interface GcaFormProps {
|
|
143
|
+
loginState: GeminiLoginState;
|
|
144
|
+
loginOutput: string;
|
|
145
|
+
loginUrls: string[];
|
|
146
|
+
onStart: () => void;
|
|
147
|
+
onCancel: () => void;
|
|
148
|
+
onBack: () => void;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function GcaForm({ loginState, loginOutput, loginUrls, onStart, onCancel, onBack }: GcaFormProps) {
|
|
152
|
+
if (loginState === "done") return <SuccessView />;
|
|
153
|
+
|
|
154
|
+
const isPending = loginState === "pending";
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<div className="flex w-full max-w-lg flex-col gap-4">
|
|
158
|
+
{loginState === "idle" || loginState === "error" ? (
|
|
159
|
+
<>
|
|
160
|
+
<div className="flex flex-col gap-2 rounded-xl border border-gray-900/[0.07] bg-gray-900/[0.025] p-4 dark:border-white/[0.07] dark:bg-white/[0.025]">
|
|
161
|
+
<p className="text-sm font-medium text-gray-900/70 dark:text-white/70">
|
|
162
|
+
gcloud CLI가 필요합니다
|
|
163
|
+
</p>
|
|
164
|
+
<p className="text-xs text-gray-900/40 dark:text-white/40">
|
|
165
|
+
먼저{" "}
|
|
166
|
+
<a
|
|
167
|
+
href="https://cloud.google.com/sdk/docs/install"
|
|
168
|
+
target="_blank"
|
|
169
|
+
rel="noopener noreferrer"
|
|
170
|
+
className="text-blue-500 hover:underline dark:text-blue-400"
|
|
171
|
+
>
|
|
172
|
+
Google Cloud SDK
|
|
173
|
+
</a>
|
|
174
|
+
를 설치한 후 아래 버튼을 클릭하세요.
|
|
175
|
+
</p>
|
|
176
|
+
<code className="mt-1 rounded-md bg-gray-900/[0.04] px-2.5 py-1.5 font-mono text-xs text-gray-900/50 dark:bg-white/[0.04] dark:text-white/50">
|
|
177
|
+
gcloud auth application-default login
|
|
178
|
+
</code>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
{loginState === "error" && loginOutput && (
|
|
182
|
+
<pre className="max-h-32 overflow-y-auto rounded-lg border border-red-200 bg-red-50 px-4 py-3 font-mono text-xs whitespace-pre-wrap text-red-600 dark:border-red-900/50 dark:bg-red-950/40 dark:text-red-400">
|
|
183
|
+
{loginOutput}
|
|
184
|
+
</pre>
|
|
185
|
+
)}
|
|
186
|
+
|
|
187
|
+
<div className="flex flex-col gap-2">
|
|
188
|
+
<button
|
|
189
|
+
onClick={onStart}
|
|
190
|
+
className="flex items-center justify-center gap-2 rounded-xl bg-blue-600 px-6 py-3 text-sm font-semibold text-white transition-colors hover:bg-blue-500"
|
|
191
|
+
>
|
|
192
|
+
{GOOGLE_LOGO}
|
|
193
|
+
Google 계정으로 로그인
|
|
194
|
+
</button>
|
|
195
|
+
<button
|
|
196
|
+
onClick={onBack}
|
|
197
|
+
className="text-xs text-gray-900/25 transition-colors hover:text-gray-900/50 dark:text-white/25 dark:hover:text-white/50"
|
|
198
|
+
>
|
|
199
|
+
← 뒤로
|
|
200
|
+
</button>
|
|
201
|
+
</div>
|
|
202
|
+
</>
|
|
203
|
+
) : null}
|
|
204
|
+
|
|
205
|
+
{isPending && (
|
|
206
|
+
<>
|
|
207
|
+
{loginUrls.length > 0 && (
|
|
208
|
+
<div className="flex flex-col gap-2">
|
|
209
|
+
<p className="text-xs font-medium text-gray-900/35 dark:text-white/35">브라우저에서 아래 링크를 열어 인증을 완료하세요:</p>
|
|
210
|
+
{loginUrls.map((url) => (
|
|
211
|
+
<a
|
|
212
|
+
key={url}
|
|
213
|
+
href={url}
|
|
214
|
+
target="_blank"
|
|
215
|
+
rel="noopener noreferrer"
|
|
216
|
+
className="break-all rounded-lg border border-blue-700/50 bg-blue-950/30 px-4 py-3 text-xs font-mono text-blue-700 transition-colors hover:border-blue-500 hover:text-blue-600 dark:text-blue-300 dark:hover:text-blue-200"
|
|
217
|
+
>
|
|
218
|
+
{url}
|
|
219
|
+
</a>
|
|
220
|
+
))}
|
|
221
|
+
</div>
|
|
222
|
+
)}
|
|
223
|
+
{loginOutput && (
|
|
224
|
+
<pre className="max-h-48 overflow-y-auto rounded-lg border border-gray-900/[0.07] bg-gray-900/[0.02] px-4 py-3 font-mono text-xs leading-relaxed whitespace-pre-wrap text-gray-900/45 dark:border-white/[0.07] dark:bg-white/[0.02] dark:text-white/45">
|
|
225
|
+
{loginOutput}
|
|
226
|
+
</pre>
|
|
227
|
+
)}
|
|
228
|
+
{loginUrls.length === 0 && (
|
|
229
|
+
<div className="flex items-center justify-center gap-2 text-sm text-gray-900/30 dark:text-white/30">
|
|
230
|
+
<span className="h-4 w-4 animate-spin rounded-full border-2 border-gray-900/[0.08] border-t-blue-500 dark:border-white/[0.08]" />
|
|
231
|
+
gcloud 인증 진행 중…
|
|
232
|
+
</div>
|
|
233
|
+
)}
|
|
234
|
+
<button onClick={onCancel} className="text-xs text-gray-900/25 transition-colors hover:text-gray-900/50 dark:text-white/25 dark:hover:text-white/50">
|
|
235
|
+
취소
|
|
236
|
+
</button>
|
|
237
|
+
</>
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ─── Success ──────────────────────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
function SuccessView() {
|
|
246
|
+
return (
|
|
247
|
+
<div className="flex flex-col items-center gap-2 text-center">
|
|
248
|
+
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-emerald-500/[0.10]">
|
|
249
|
+
<svg viewBox="0 0 20 20" fill="currentColor" className="h-6 w-6 text-emerald-500 dark:text-emerald-400">
|
|
250
|
+
<path fillRule="evenodd" d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" clipRule="evenodd" />
|
|
251
|
+
</svg>
|
|
252
|
+
</div>
|
|
253
|
+
<p className="text-sm font-medium text-emerald-600 dark:text-emerald-400">인증 완료</p>
|
|
254
|
+
</div>
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ─── Panel Root ───────────────────────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
export function GeminiLoginPanel({
|
|
261
|
+
loginState,
|
|
262
|
+
loginOutput,
|
|
263
|
+
loginUrls,
|
|
264
|
+
configError,
|
|
265
|
+
onSaveApiKey,
|
|
266
|
+
onStartGca,
|
|
267
|
+
onCancel,
|
|
268
|
+
onReset,
|
|
269
|
+
}: GeminiLoginPanelProps) {
|
|
270
|
+
const [selectedMethod, setSelectedMethod] = useState<GeminiAuthMethod | null>(null);
|
|
271
|
+
|
|
272
|
+
const handleBack = () => {
|
|
273
|
+
onReset();
|
|
274
|
+
setSelectedMethod(null);
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
return (
|
|
278
|
+
<div className="flex flex-1 flex-col items-center justify-center gap-6 px-6">
|
|
279
|
+
{/* Header */}
|
|
280
|
+
<div className="flex flex-col items-center gap-3 text-center">
|
|
281
|
+
<div className="flex h-16 w-16 items-center justify-center rounded-2xl border border-gray-900/[0.08] bg-blue-500/[0.08] dark:border-white/[0.08]">
|
|
282
|
+
{GOOGLE_LOGO}
|
|
283
|
+
</div>
|
|
284
|
+
<h2 className="text-xl font-semibold text-gray-900/90 dark:text-white/90">Gemini CLI 인증</h2>
|
|
285
|
+
{!selectedMethod && (
|
|
286
|
+
<p className="max-w-sm text-sm text-gray-900/40 dark:text-white/40">
|
|
287
|
+
사용할 인증 방식을 선택해 주세요.
|
|
288
|
+
</p>
|
|
289
|
+
)}
|
|
290
|
+
</div>
|
|
291
|
+
|
|
292
|
+
{/* Content */}
|
|
293
|
+
{!selectedMethod && <MethodSelect onSelect={setSelectedMethod} />}
|
|
294
|
+
|
|
295
|
+
{selectedMethod === "api-key" && (
|
|
296
|
+
<ApiKeyForm
|
|
297
|
+
loginState={loginState}
|
|
298
|
+
configError={configError}
|
|
299
|
+
onSubmit={onSaveApiKey}
|
|
300
|
+
onBack={handleBack}
|
|
301
|
+
/>
|
|
302
|
+
)}
|
|
303
|
+
|
|
304
|
+
{selectedMethod === "gca" && (
|
|
305
|
+
<GcaForm
|
|
306
|
+
loginState={loginState}
|
|
307
|
+
loginOutput={loginOutput}
|
|
308
|
+
loginUrls={loginUrls}
|
|
309
|
+
onStart={onStartGca}
|
|
310
|
+
onCancel={onCancel}
|
|
311
|
+
onBack={handleBack}
|
|
312
|
+
/>
|
|
313
|
+
)}
|
|
314
|
+
</div>
|
|
315
|
+
);
|
|
316
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// ─── Atoms ────────────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
interface LogoMarkProps {
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function LogoMark({ className }: LogoMarkProps) {
|
|
10
|
+
return (
|
|
11
|
+
<div className={`relative ${className ?? ""}`}>
|
|
12
|
+
<div className="flex h-12 w-64 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)]">
|
|
13
|
+
<span className="font-mono text-sm font-bold tracking-widest text-gray-900/65 dark:text-white/65">
|
|
14
|
+
INTEGRATION-CLI
|
|
15
|
+
</span>
|
|
16
|
+
</div>
|
|
17
|
+
<div className="absolute -inset-2 rounded-[20px] bg-orange-500/10 blur-xl" />
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface InputFieldProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
23
|
+
label: string;
|
|
24
|
+
id: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function InputField({ label, id, ...props }: InputFieldProps) {
|
|
28
|
+
return (
|
|
29
|
+
<div className="flex flex-col gap-1.5">
|
|
30
|
+
<label htmlFor={id} className="text-xs font-medium text-gray-900/50 dark:text-white/50">
|
|
31
|
+
{label}
|
|
32
|
+
</label>
|
|
33
|
+
<input
|
|
34
|
+
id={id}
|
|
35
|
+
{...props}
|
|
36
|
+
className="w-full rounded-xl border border-gray-900/[0.10] bg-gray-900/[0.03] px-4 py-3 text-sm text-gray-900/90 placeholder:text-gray-900/25 outline-none ring-0 transition-all duration-200 focus:border-orange-400/60 focus:ring-2 focus:ring-orange-400/15 dark:border-white/[0.10] dark:bg-white/[0.03] dark:text-white/90 dark:placeholder:text-white/25 dark:focus:border-orange-400/50 dark:focus:ring-orange-400/10"
|
|
37
|
+
/>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface SubmitButtonProps {
|
|
43
|
+
label: string;
|
|
44
|
+
loading?: boolean;
|
|
45
|
+
onClick?: () => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function SubmitButton({ label, loading }: SubmitButtonProps) {
|
|
49
|
+
return (
|
|
50
|
+
<button
|
|
51
|
+
type="submit"
|
|
52
|
+
disabled={loading}
|
|
53
|
+
className="relative w-full overflow-hidden rounded-xl bg-orange-600 py-3 text-sm font-semibold text-white transition-all duration-200 hover:bg-orange-500 active:scale-[0.99] disabled:opacity-60"
|
|
54
|
+
>
|
|
55
|
+
{loading ? (
|
|
56
|
+
<span className="flex items-center justify-center gap-2">
|
|
57
|
+
<span className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
|
|
58
|
+
로그인 중…
|
|
59
|
+
</span>
|
|
60
|
+
) : (
|
|
61
|
+
label
|
|
62
|
+
)}
|
|
63
|
+
</button>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface DividerProps {
|
|
68
|
+
label: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function Divider({ label }: DividerProps) {
|
|
72
|
+
return (
|
|
73
|
+
<div className="flex items-center gap-3">
|
|
74
|
+
<div className="h-px flex-1 bg-gray-900/[0.07] dark:bg-white/[0.07]" />
|
|
75
|
+
<span className="text-xs text-gray-900/25 dark:text-white/25">{label}</span>
|
|
76
|
+
<div className="h-px flex-1 bg-gray-900/[0.07] dark:bg-white/[0.07]" />
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ─── Form ─────────────────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
export interface LoginFormProps {
|
|
84
|
+
email: string;
|
|
85
|
+
password: string;
|
|
86
|
+
loading: boolean;
|
|
87
|
+
error: string | null;
|
|
88
|
+
onEmailChange: (value: string) => void;
|
|
89
|
+
onPasswordChange: (value: string) => void;
|
|
90
|
+
onSubmit: (e: React.FormEvent) => void;
|
|
91
|
+
onGuestLogin: () => void;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function LoginForm({
|
|
95
|
+
email,
|
|
96
|
+
password,
|
|
97
|
+
loading,
|
|
98
|
+
error,
|
|
99
|
+
onEmailChange,
|
|
100
|
+
onPasswordChange,
|
|
101
|
+
onSubmit,
|
|
102
|
+
onGuestLogin,
|
|
103
|
+
}: LoginFormProps) {
|
|
104
|
+
return (
|
|
105
|
+
<div className="flex w-full max-w-[400px] flex-col items-center gap-8 animate-fade-in-up">
|
|
106
|
+
{/* Logo */}
|
|
107
|
+
<header className="flex flex-col items-center gap-5 text-center">
|
|
108
|
+
<LogoMark />
|
|
109
|
+
<div>
|
|
110
|
+
<h1
|
|
111
|
+
className="text-[2.25rem] font-bold leading-none tracking-[-0.03em]"
|
|
112
|
+
style={{
|
|
113
|
+
background: "var(--heading-gradient)",
|
|
114
|
+
WebkitBackgroundClip: "text",
|
|
115
|
+
WebkitTextFillColor: "transparent",
|
|
116
|
+
backgroundClip: "text",
|
|
117
|
+
}}
|
|
118
|
+
>
|
|
119
|
+
JC-CLI
|
|
120
|
+
</h1>
|
|
121
|
+
<p className="mt-2 text-[13px] font-medium uppercase tracking-[0.06em] text-gray-900/28 dark:text-white/28">
|
|
122
|
+
하나의 플랫폼에서 모든 AI CLI를 제어합니다
|
|
123
|
+
</p>
|
|
124
|
+
</div>
|
|
125
|
+
</header>
|
|
126
|
+
|
|
127
|
+
{/* Card */}
|
|
128
|
+
<div className="w-full rounded-2xl border border-gray-900/[0.08] bg-gray-900/[0.025] p-6 shadow-[0_1px_3px_rgba(0,0,0,0.04),0_8px_24px_-4px_rgba(0,0,0,0.04)] backdrop-blur-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
|
|
129
|
+
{/* Top accent */}
|
|
130
|
+
<div className="absolute inset-x-0 top-0 h-px rounded-t-2xl bg-gradient-to-r from-transparent via-orange-400/50 to-transparent" />
|
|
131
|
+
|
|
132
|
+
<div className="flex flex-col gap-1.5 mb-6">
|
|
133
|
+
<h2 className="text-[15px] font-semibold text-gray-900/90 dark:text-white/90">
|
|
134
|
+
로그인
|
|
135
|
+
</h2>
|
|
136
|
+
<p className="text-[13px] text-gray-900/35 dark:text-white/35">
|
|
137
|
+
계정에 로그인하여 서비스를 이용하세요.
|
|
138
|
+
</p>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<form onSubmit={onSubmit} className="flex flex-col gap-4">
|
|
142
|
+
<InputField
|
|
143
|
+
id="email"
|
|
144
|
+
label="이메일"
|
|
145
|
+
type="email"
|
|
146
|
+
placeholder="you@example.com"
|
|
147
|
+
autoComplete="email"
|
|
148
|
+
value={email}
|
|
149
|
+
onChange={(e) => onEmailChange(e.target.value)}
|
|
150
|
+
/>
|
|
151
|
+
<InputField
|
|
152
|
+
id="password"
|
|
153
|
+
label="비밀번호"
|
|
154
|
+
type="password"
|
|
155
|
+
placeholder="••••••••"
|
|
156
|
+
autoComplete="current-password"
|
|
157
|
+
value={password}
|
|
158
|
+
onChange={(e) => onPasswordChange(e.target.value)}
|
|
159
|
+
/>
|
|
160
|
+
|
|
161
|
+
{error && (
|
|
162
|
+
<p className="rounded-lg border border-red-500/20 bg-red-500/[0.07] px-3 py-2 text-xs text-red-500 dark:text-red-400">
|
|
163
|
+
{error}
|
|
164
|
+
</p>
|
|
165
|
+
)}
|
|
166
|
+
|
|
167
|
+
<SubmitButton label="로그인" loading={loading} />
|
|
168
|
+
</form>
|
|
169
|
+
|
|
170
|
+
<div className="mt-4 flex flex-col gap-4">
|
|
171
|
+
<Divider label="또는" />
|
|
172
|
+
<button
|
|
173
|
+
type="button"
|
|
174
|
+
onClick={onGuestLogin}
|
|
175
|
+
className="w-full rounded-xl border border-gray-900/[0.09] bg-transparent py-3 text-sm font-medium text-gray-900/60 transition-all duration-200 hover:border-gray-900/[0.14] hover:bg-gray-900/[0.03] hover:text-gray-900/80 dark:border-white/[0.09] dark:text-white/60 dark:hover:border-white/[0.14] dark:hover:bg-white/[0.03] dark:hover:text-white/80"
|
|
176
|
+
>
|
|
177
|
+
게스트로 계속하기
|
|
178
|
+
</button>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<p className="text-xs text-gray-900/25 dark:text-white/25">
|
|
183
|
+
계정이 없으신가요?{" "}
|
|
184
|
+
<button type="button" className="text-orange-600 transition-colors hover:text-orange-500 dark:text-orange-400 dark:hover:text-orange-300">
|
|
185
|
+
회원가입
|
|
186
|
+
</button>
|
|
187
|
+
</p>
|
|
188
|
+
</div>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { LoginState } from "../hooks/useClaudeAuth";
|
|
4
|
+
|
|
5
|
+
interface LoginPanelProps {
|
|
6
|
+
loginState: LoginState;
|
|
7
|
+
loginOutput: string;
|
|
8
|
+
loginUrls: string[];
|
|
9
|
+
onStart: () => void;
|
|
10
|
+
onCancel: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function LoginPanel({ loginState, loginOutput, loginUrls, onStart, onCancel }: LoginPanelProps) {
|
|
14
|
+
const isPending = loginState === "pending";
|
|
15
|
+
const isDone = loginState === "done";
|
|
16
|
+
const isError = loginState === "error";
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="flex flex-1 flex-col items-center justify-center gap-6 px-6">
|
|
20
|
+
<div className="flex flex-col items-center gap-3 text-center">
|
|
21
|
+
<div className="flex h-16 w-16 items-center justify-center rounded-2xl border border-gray-900/[0.08] bg-orange-500/[0.08] dark:border-white/[0.08]">
|
|
22
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="h-8 w-8 text-orange-500 dark:text-orange-400/80">
|
|
23
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" />
|
|
24
|
+
</svg>
|
|
25
|
+
</div>
|
|
26
|
+
<h2 className="text-xl font-semibold text-gray-900/90 dark:text-white/90">Claude Code 로그인 필요</h2>
|
|
27
|
+
<p className="max-w-sm text-sm text-gray-900/40 dark:text-white/40">
|
|
28
|
+
Claude CLI를 사용하려면 Anthropic 계정으로 로그인해야 합니다.
|
|
29
|
+
</p>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
{loginState === "idle" && (
|
|
33
|
+
<button
|
|
34
|
+
onClick={onStart}
|
|
35
|
+
className="rounded-xl bg-orange-600 px-6 py-3 text-sm font-semibold text-white transition-colors hover:bg-orange-500"
|
|
36
|
+
>
|
|
37
|
+
Claude Code 로그인
|
|
38
|
+
</button>
|
|
39
|
+
)}
|
|
40
|
+
|
|
41
|
+
{isPending && (
|
|
42
|
+
<div className="flex w-full max-w-lg flex-col gap-4">
|
|
43
|
+
{loginUrls.length > 0 && (
|
|
44
|
+
<div className="flex flex-col gap-2">
|
|
45
|
+
<p className="text-xs font-medium text-gray-900/35 dark:text-white/35">브라우저에서 아래 링크를 열어 인증을 완료하세요:</p>
|
|
46
|
+
{loginUrls.map((url) => (
|
|
47
|
+
<a
|
|
48
|
+
key={url}
|
|
49
|
+
href={url}
|
|
50
|
+
target="_blank"
|
|
51
|
+
rel="noopener noreferrer"
|
|
52
|
+
className="break-all rounded-lg border border-orange-700/50 bg-orange-950/30 px-4 py-3 text-xs font-mono text-orange-700 transition-colors hover:border-orange-500 hover:text-orange-600 dark:text-orange-300 dark:hover:text-orange-200"
|
|
53
|
+
>
|
|
54
|
+
{url}
|
|
55
|
+
</a>
|
|
56
|
+
))}
|
|
57
|
+
</div>
|
|
58
|
+
)}
|
|
59
|
+
|
|
60
|
+
{loginOutput && (
|
|
61
|
+
<pre className="max-h-48 overflow-y-auto rounded-lg border border-gray-900/[0.07] bg-gray-900/[0.02] px-4 py-3 font-mono text-xs leading-relaxed whitespace-pre-wrap text-gray-900/45 dark:border-white/[0.07] dark:bg-white/[0.02] dark:text-white/45">
|
|
62
|
+
{loginOutput}
|
|
63
|
+
</pre>
|
|
64
|
+
)}
|
|
65
|
+
|
|
66
|
+
{loginUrls.length === 0 && (
|
|
67
|
+
<div className="flex items-center justify-center gap-2 text-sm text-gray-900/30 dark:text-white/30">
|
|
68
|
+
<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]" />
|
|
69
|
+
로그인 프로세스를 시작하는 중…
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
|
|
73
|
+
<button onClick={onCancel} className="text-xs text-gray-900/25 transition-colors hover:text-gray-900/50 dark:text-white/25 dark:hover:text-white/50">
|
|
74
|
+
취소
|
|
75
|
+
</button>
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
|
|
79
|
+
{isDone && (
|
|
80
|
+
<div className="flex flex-col items-center gap-2 text-center">
|
|
81
|
+
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-emerald-500/[0.10]">
|
|
82
|
+
<svg viewBox="0 0 20 20" fill="currentColor" className="h-6 w-6 text-emerald-500 dark:text-emerald-400">
|
|
83
|
+
<path fillRule="evenodd" d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" clipRule="evenodd" />
|
|
84
|
+
</svg>
|
|
85
|
+
</div>
|
|
86
|
+
<p className="text-sm font-medium text-emerald-600 dark:text-emerald-400">로그인 완료</p>
|
|
87
|
+
<p className="text-xs text-gray-900/25 dark:text-white/25">잠시 후 자동으로 이동합니다…</p>
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
90
|
+
|
|
91
|
+
{isError && (
|
|
92
|
+
<div className="flex flex-col items-center gap-3 text-center">
|
|
93
|
+
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-red-500/[0.10]">
|
|
94
|
+
<svg viewBox="0 0 20 20" fill="currentColor" className="h-6 w-6 text-red-500 dark:text-red-400">
|
|
95
|
+
<path fillRule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clipRule="evenodd" />
|
|
96
|
+
</svg>
|
|
97
|
+
</div>
|
|
98
|
+
<p className="text-sm text-red-500 dark:text-red-400">로그인 중 문제가 발생했습니다.</p>
|
|
99
|
+
{loginOutput && (
|
|
100
|
+
<pre className="max-h-32 w-full max-w-lg overflow-y-auto rounded-lg border border-gray-900/[0.07] bg-gray-900/[0.02] px-4 py-3 font-mono text-xs whitespace-pre-wrap text-gray-900/40 dark:border-white/[0.07] dark:bg-white/[0.02] dark:text-white/40">
|
|
101
|
+
{loginOutput}
|
|
102
|
+
</pre>
|
|
103
|
+
)}
|
|
104
|
+
<button
|
|
105
|
+
onClick={onStart}
|
|
106
|
+
className="rounded-xl bg-orange-600 px-6 py-3 text-sm font-semibold text-white transition-colors hover:bg-orange-500"
|
|
107
|
+
>
|
|
108
|
+
다시 시도
|
|
109
|
+
</button>
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|