@cartanova/qgrid-cli 2.6.1 → 2.6.4

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 (81) hide show
  1. package/README.ko.md +2 -0
  2. package/README.md +2 -0
  3. package/bundle/dist/application/qgrid/oauth.js +21 -3
  4. package/bundle/dist/application/qgrid/qgrid.dispatcher.js +40 -6
  5. package/bundle/dist/application/qgrid/qgrid.frame.js +47 -19
  6. package/bundle/dist/application/qgrid/qgrid.types.js +13 -3
  7. package/bundle/dist/application/qgrid/token-death.js +77 -0
  8. package/bundle/dist/application/queries.generated.js +3 -2
  9. package/bundle/dist/application/request-log/request-log.model.js +105 -31
  10. package/bundle/dist/application/request-log-step/request-log-step.model.js +1 -1
  11. package/bundle/dist/application/sonamu.generated.js +60 -5
  12. package/bundle/dist/application/sonamu.generated.sso.js +18 -2
  13. package/bundle/dist/application/token/token.model.js +53 -2
  14. package/bundle/dist/i18n/en.js +3 -1
  15. package/bundle/dist/i18n/ko.js +3 -1
  16. package/bundle/dist/i18n/sd.generated.js +18 -1
  17. package/bundle/dist/migrations/20260807133553_manual__backfill_request_log_cost.js +49 -0
  18. package/bundle/dist/migrations/20260807145144_alter_request_logs_alter1.js +13 -0
  19. package/bundle/dist/migrations/20260807164057_alter_request_logs_alter1.js +13 -0
  20. package/bundle/dist/server-error-handler.js +5 -3
  21. package/bundle/dist/sonamu.config.js +5 -1
  22. package/bundle/dist/utils/providers/openai/openai-refresh.js +12 -1
  23. package/bundle/dist/utils/slack-notify.js +65 -0
  24. package/bundle/src/application/qgrid/oauth.ts +20 -1
  25. package/bundle/src/application/qgrid/qgrid-oauth-redirect.test.ts +15 -3
  26. package/bundle/src/application/qgrid/qgrid.dispatcher.test.ts +30 -2
  27. package/bundle/src/application/qgrid/qgrid.dispatcher.ts +44 -6
  28. package/bundle/src/application/qgrid/qgrid.frame.ts +67 -47
  29. package/bundle/src/application/qgrid/qgrid.types.ts +10 -0
  30. package/bundle/src/application/qgrid/token-death.test.ts +150 -0
  31. package/bundle/src/application/qgrid/token-death.ts +96 -0
  32. package/bundle/src/application/queries.generated.ts +5 -2
  33. package/bundle/src/application/request-log/request-log.entity.json +46 -7
  34. package/bundle/src/application/request-log/request-log.model.test.ts +153 -42
  35. package/bundle/src/application/request-log/request-log.model.ts +136 -50
  36. package/bundle/src/application/sonamu.generated.http +19 -1
  37. package/bundle/src/application/sonamu.generated.sso.ts +16 -0
  38. package/bundle/src/application/sonamu.generated.ts +58 -3
  39. package/bundle/src/application/token/token.model.test.ts +49 -0
  40. package/bundle/src/application/token/token.model.ts +75 -0
  41. package/bundle/src/i18n/en.ts +3 -0
  42. package/bundle/src/i18n/ko.ts +3 -0
  43. package/bundle/src/i18n/sd.generated.ts +17 -0
  44. package/bundle/src/migrations/20260807133553_manual__backfill_request_log_cost.ts +110 -0
  45. package/bundle/src/migrations/20260807145144_alter_request_logs_alter1.ts +13 -0
  46. package/bundle/src/migrations/20260807164057_alter_request_logs_alter1.ts +13 -0
  47. package/bundle/src/server-error-handler.test.ts +58 -0
  48. package/bundle/src/server-error-handler.ts +16 -5
  49. package/bundle/src/sonamu.config.ts +6 -0
  50. package/bundle/src/utils/providers/openai/openai-refresh.ts +17 -0
  51. package/bundle/src/utils/slack-notify.test.ts +97 -0
  52. package/bundle/src/utils/slack-notify.ts +74 -0
  53. package/bundle/web-dist/client/assets/index-BGQSGMcK.css +1 -0
  54. package/bundle/web-dist/client/assets/index-CnNT0mdq.js +78 -0
  55. package/bundle/web-dist/client/assets/logs-DCY81UWb.js +1 -0
  56. package/bundle/web-dist/client/assets/{monit-CyqMmy1G.js → monit-D5OoAday.js} +1 -1
  57. package/bundle/web-dist/client/assets/routes-B4D9C86u.js +5 -0
  58. package/bundle/web-dist/client/assets/{sd.generated-CjW3rfJ8.js → sd.generated-DYdwSB2I.js} +1 -1
  59. package/bundle/web-dist/client/assets/{services.generated-BuT6Gp8V.js → services.generated-BsAvotjG.js} +1 -1
  60. package/bundle/web-dist/client/assets/{show-C5WxfoAt.js → show-BSX8ii7Z.js} +3 -3
  61. package/bundle/web-dist/client/assets/tokens-Gapk41cz.js +1 -0
  62. package/bundle/web-dist/client/assets/{use-update-token-mutation-DBkxL29Y.js → use-update-token-mutation-DqEpK7It.js} +1 -1
  63. package/bundle/web-dist/client/index.html +3 -3
  64. package/bundle/web-dist/server/assets/{lazyRouteComponent-PRxXWW3t.js → lazyRouteComponent-BaZaveNc.js} +1 -1
  65. package/bundle/web-dist/server/assets/{logs-D0z1Ghj1.js → logs-DMOw3SjD.js} +91 -42
  66. package/bundle/web-dist/server/assets/{monit-dBXxnAyI.js → monit-UmcqHroX.js} +2 -2
  67. package/bundle/web-dist/server/assets/{routes-B3p7fuwk.js → routes-Sv_4FjtH.js} +52 -46
  68. package/bundle/web-dist/server/assets/{sd.generated-ek-tz-7H.js → sd.generated-B0xqnHEo.js} +21 -0
  69. package/bundle/web-dist/server/assets/{services.generated-BNuxWs7K.js → services.generated-DqAKskAS.js} +23 -8
  70. package/bundle/web-dist/server/assets/{show-0iNioJ2I.js → show-C-oSdzHX.js} +31 -25
  71. package/bundle/web-dist/server/assets/{tokens-CvytQ9Xy.js → tokens-h-2A3wfD.js} +292 -191
  72. package/bundle/web-dist/server/assets/{use-update-token-mutation-BBCC2GKM.js → use-update-token-mutation-DHCaa7Ku.js} +1 -1
  73. package/bundle/web-dist/server/entry-server.generated.js +70 -29
  74. package/package.json +1 -1
  75. package/skills/qgrid/references/cli-env-and-server-boot.md +8 -0
  76. package/skills/qgrid/references/token-auth-quota-lifecycle.md +11 -0
  77. package/bundle/web-dist/client/assets/index-BGGBFmzE.js +0 -78
  78. package/bundle/web-dist/client/assets/index-GA9b-zy4.css +0 -1
  79. package/bundle/web-dist/client/assets/logs-Dh19u6Tk.js +0 -1
  80. package/bundle/web-dist/client/assets/routes-Cy0gCq8c.js +0 -5
  81. package/bundle/web-dist/client/assets/tokens-Ciolqw4l.js +0 -1
@@ -1,7 +1,7 @@
1
- import { Z as __toESM, _ as require_jsx_runtime, g as useQueryClient, v as require_react } from "./sd.generated-ek-tz-7H.js";
2
- import { a as TokenService, n as QgridService } from "./services.generated-BNuxWs7K.js";
1
+ import { Z as __toESM, _ as require_jsx_runtime, g as useQueryClient, v as require_react } from "./sd.generated-B0xqnHEo.js";
2
+ import { a as TokenService, n as QgridService } from "./services.generated-DqAKskAS.js";
3
3
  import { i as Q, o as lucideKeyRound } from "../entry-server.generated.js";
4
- import { t as useUpdateTokenMutation } from "./use-update-token-mutation-BBCC2GKM.js";
4
+ import { t as useUpdateTokenMutation } from "./use-update-token-mutation-DHCaa7Ku.js";
5
5
  //#region ~icons/lucide/plus.jsx
6
6
  var import_react = /* @__PURE__ */ __toESM(require_react());
7
7
  var import_jsx_runtime = require_jsx_runtime();
@@ -20,77 +20,168 @@ var lucidePlus = (props) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", {
20
20
  })
21
21
  });
22
22
  //#endregion
23
- //#region src/components/qgrid/AddTokenModal.tsx
24
- function AddTokenModal() {
25
- const [open, setOpen] = (0, import_react.useState)(false);
26
- const [oauthLoading, setOauthLoading] = (0, import_react.useState)(null);
27
- const [name, setName] = (0, import_react.useState)("");
28
- const [codeEntry, setCodeEntry] = (0, import_react.useState)(false);
23
+ //#region src/components/qgrid/OAuthCodeEntry.tsx
24
+ /**
25
+ * 원격 접속(code 모드) Claude 로그인의 코드 입력 단계.
26
+ * Add Token 모달과 재로그인이 같은 문구·에러·재시작 동선을 쓰도록 한 곳에 둔다.
27
+ */
28
+ function OAuthCodeEntry({ isPending, isError, onSubmit, onRestart }) {
29
29
  const [pastedCode, setPastedCode] = (0, import_react.useState)("");
30
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
31
+ className: "space-y-2",
32
+ children: [
33
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
34
+ className: "text-[12px] text-sand-600 leading-relaxed",
35
+ children: "새 탭에서 Claude 로그인을 완료하면 코드가 표시됩니다. 코드 전체를 아래에 붙여넣으세요."
36
+ }),
37
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Q, {
38
+ value: pastedCode,
39
+ onChange: (e) => setPastedCode(e.target.value),
40
+ placeholder: "code#state",
41
+ className: "w-full border border-sand-200 rounded-md px-3 py-2 text-sm font-mono text-sand-900 bg-white placeholder:text-sand-300 focus:outline-none focus:border-sienna-300"
42
+ }),
43
+ isError && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
44
+ className: "text-[11px] text-red-500",
45
+ children: "코드 확인에 실패했습니다 — 표시된 코드 전체를 다시 붙여넣거나 로그인을 다시 시작하세요."
46
+ }),
47
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
48
+ type: "button",
49
+ className: "w-full py-2.5 text-sm font-medium rounded-md bg-sand-900 text-white hover:bg-sand-800 disabled:opacity-50 transition-colors duration-150 flex items-center justify-center gap-2",
50
+ disabled: !pastedCode.trim() || isPending,
51
+ onClick: () => onSubmit(pastedCode),
52
+ children: isPending ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
53
+ className: "flex items-center gap-1.5",
54
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin" }), "Verifying..."]
55
+ }) : "Complete login"
56
+ }),
57
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
58
+ type: "button",
59
+ className: "w-full py-1.5 text-[12px] text-sand-500 hover:text-sand-700 transition-colors duration-150",
60
+ onClick: () => {
61
+ setPastedCode("");
62
+ onRestart();
63
+ },
64
+ children: "처음부터 다시 시작"
65
+ })
66
+ ]
67
+ });
68
+ }
69
+ //#endregion
70
+ //#region src/components/qgrid/use-oauth-login-flow.ts
71
+ var OPENAI_POLL_INTERVAL_MS = 3e3;
72
+ var OPENAI_POLL_TIMEOUT_MS = 3e5;
73
+ /** 캐시에 적재된 토큰 행 수. 폴링 조기 종료 판정에만 쓰는 근사치다. */
74
+ function countTokens(queryClient) {
75
+ let total = 0;
76
+ for (const query of queryClient.getQueryCache().findAll({ queryKey: ["Token"] })) {
77
+ const rows = query.state.data?.rows;
78
+ if (Array.isArray(rows)) total = Math.max(total, rows.length);
79
+ }
80
+ return total;
81
+ }
82
+ /**
83
+ * provider별 OAuth 로그인 시작과 코드 제출을 한 곳에 모은다.
84
+ * Add Token 모달과 tokens 목록의 재로그인 버튼이 같은 플로우를 쓴다 — 두 진입점이
85
+ * redirect/code 분기나 폴링 처리를 각자 구현하면 원격 접속 경험이 갈라진다.
86
+ */
87
+ function useOAuthLoginFlow() {
88
+ const [loadingProvider, setLoadingProvider] = (0, import_react.useState)(null);
89
+ const [codeEntry, setCodeEntry] = (0, import_react.useState)(false);
30
90
  const queryClient = useQueryClient();
31
91
  const oauthStartMutation = QgridService.useOauthStartMutation();
32
92
  const oauthStartOpenAIMutation = QgridService.useOauthStartOpenAIMutation();
33
93
  const oauthCompleteMutation = QgridService.useOauthCompleteMutation();
34
- const handleOAuthLogin = async (provider) => {
35
- if (!name.trim()) return;
36
- if (provider === "openai") {
37
- const popup = window.open("about:blank", "_blank");
38
- setOauthLoading("openai");
39
- try {
40
- const { authUrl } = await oauthStartOpenAIMutation.mutateAsync({ name: name.trim() });
94
+ const pollTimers = (0, import_react.useRef)({});
95
+ const stopPolling = () => {
96
+ if (pollTimers.current.interval !== void 0) clearInterval(pollTimers.current.interval);
97
+ if (pollTimers.current.timeout !== void 0) clearTimeout(pollTimers.current.timeout);
98
+ pollTimers.current = {};
99
+ };
100
+ (0, import_react.useEffect)(() => stopPolling, []);
101
+ const invalidateTokens = () => Promise.all([queryClient.invalidateQueries({ queryKey: ["Token"] }), queryClient.invalidateQueries({ queryKey: ["Qgrid"] })]);
102
+ const start = async (provider, name) => {
103
+ const trimmed = name.trim();
104
+ if (!trimmed) return;
105
+ const popup = window.open("about:blank", "_blank");
106
+ setLoadingProvider(provider);
107
+ try {
108
+ if (provider === "openai") {
109
+ const { authUrl } = await oauthStartOpenAIMutation.mutateAsync({ name: trimmed });
41
110
  if (popup) popup.location.href = authUrl;
42
111
  else window.open(authUrl, "_blank");
43
- const poll = setInterval(async () => {
44
- await queryClient.invalidateQueries({ queryKey: ["Token"] });
45
- await queryClient.invalidateQueries({ queryKey: ["Qgrid"] });
46
- }, 3e3);
47
- setTimeout(() => {
48
- clearInterval(poll);
49
- setOauthLoading(null);
50
- }, 3e5);
51
- } catch (e) {
52
- console.error("OAuth start failed:", e);
53
- setOauthLoading(null);
112
+ const before = countTokens(queryClient);
113
+ stopPolling();
114
+ pollTimers.current.interval = window.setInterval(() => {
115
+ invalidateTokens().then(() => {
116
+ if (countTokens(queryClient) > before) {
117
+ stopPolling();
118
+ setLoadingProvider(null);
119
+ }
120
+ });
121
+ }, OPENAI_POLL_INTERVAL_MS);
122
+ pollTimers.current.timeout = window.setTimeout(() => {
123
+ stopPolling();
124
+ setLoadingProvider(null);
125
+ }, OPENAI_POLL_TIMEOUT_MS);
126
+ return;
54
127
  }
55
- } else {
56
- const popup = window.open("about:blank", "_blank");
57
- setOauthLoading("anthropic");
58
- try {
59
- const { authUrl, mode } = await oauthStartMutation.mutateAsync({ name: name.trim() });
60
- if (mode === "code") {
61
- if (popup) popup.location.href = authUrl;
62
- else window.open(authUrl, "_blank");
63
- setCodeEntry(true);
64
- setOauthLoading(null);
65
- } else {
66
- popup?.close();
67
- window.location.href = authUrl;
68
- }
69
- } catch (e) {
128
+ const { authUrl, mode } = await oauthStartMutation.mutateAsync({ name: trimmed });
129
+ if (mode === "code") {
130
+ if (popup) popup.location.href = authUrl;
131
+ else window.open(authUrl, "_blank");
132
+ setCodeEntry(true);
133
+ setLoadingProvider(null);
134
+ } else {
70
135
  popup?.close();
71
- console.error("OAuth start failed:", e);
72
- setOauthLoading(null);
136
+ window.location.href = authUrl;
73
137
  }
138
+ } catch (e) {
139
+ popup?.close();
140
+ console.error("OAuth start failed:", e);
141
+ setLoadingProvider(null);
74
142
  }
75
143
  };
76
- const submitPastedCode = async () => {
77
- if (!pastedCode.trim()) return;
144
+ /** @returns 교환 성공 여부. 실패 시 `completeMutation.isError` 로 안내 문구를 띄운다. */
145
+ const submitCode = async (pastedCode) => {
146
+ const trimmed = pastedCode.trim();
147
+ if (!trimmed) return false;
78
148
  try {
79
- await oauthCompleteMutation.mutateAsync({ pastedCode: pastedCode.trim() });
149
+ await oauthCompleteMutation.mutateAsync({ pastedCode: trimmed });
80
150
  } catch (e) {
81
151
  console.error("OAuth complete failed:", e);
82
- return;
152
+ return false;
83
153
  }
84
- await Promise.all([queryClient.invalidateQueries({ queryKey: ["Token"] }), queryClient.invalidateQueries({ queryKey: ["Qgrid"] })]);
85
- close();
154
+ await invalidateTokens();
155
+ return true;
156
+ };
157
+ const reset = () => {
158
+ stopPolling();
159
+ setLoadingProvider(null);
160
+ setCodeEntry(false);
161
+ oauthCompleteMutation.reset();
162
+ };
163
+ return {
164
+ loadingProvider,
165
+ codeEntry,
166
+ start,
167
+ submitCode,
168
+ reset,
169
+ completeMutation: oauthCompleteMutation
86
170
  };
171
+ }
172
+ //#endregion
173
+ //#region src/components/qgrid/AddTokenModal.tsx
174
+ function AddTokenModal() {
175
+ const [open, setOpen] = (0, import_react.useState)(false);
176
+ const [name, setName] = (0, import_react.useState)("");
177
+ const oauth = useOAuthLoginFlow();
87
178
  const close = () => {
88
179
  setOpen(false);
89
180
  setName("");
90
- setOauthLoading(null);
91
- setCodeEntry(false);
92
- setPastedCode("");
93
- oauthCompleteMutation.reset();
181
+ oauth.reset();
182
+ };
183
+ const submitPastedCode = async (pastedCode) => {
184
+ if (await oauth.submitCode(pastedCode)) close();
94
185
  };
95
186
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
96
187
  type: "button",
@@ -124,53 +215,20 @@ function AddTokenModal() {
124
215
  onChange: (e) => setName(e.target.value),
125
216
  placeholder: "e.g. your-token-name",
126
217
  className: "mt-1 w-full border border-sand-200 rounded-md px-3 py-2 text-sm text-sand-900 bg-white placeholder:text-sand-300 focus:outline-none focus:border-sienna-300"
127
- })] }), codeEntry ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
128
- className: "space-y-2",
129
- children: [
130
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
131
- className: "text-[12px] text-sand-600 leading-relaxed",
132
- children: "새 탭에서 Claude 로그인을 완료하면 코드가 표시됩니다. 코드 전체를 아래에 붙여넣으세요."
133
- }),
134
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Q, {
135
- value: pastedCode,
136
- onChange: (e) => setPastedCode(e.target.value),
137
- placeholder: "code#state",
138
- className: "w-full border border-sand-200 rounded-md px-3 py-2 text-sm font-mono text-sand-900 bg-white placeholder:text-sand-300 focus:outline-none focus:border-sienna-300"
139
- }),
140
- oauthCompleteMutation.isError && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
141
- className: "text-[11px] text-red-500",
142
- children: "코드 확인에 실패했습니다 — 표시된 코드 전체를 다시 붙여넣거나 로그인을 다시 시작하세요."
143
- }),
144
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
145
- type: "button",
146
- className: "w-full py-2.5 text-sm font-medium rounded-md bg-sand-900 text-white hover:bg-sand-800 disabled:opacity-50 transition-colors duration-150 flex items-center justify-center gap-2",
147
- disabled: !pastedCode.trim() || oauthCompleteMutation.isPending,
148
- onClick: submitPastedCode,
149
- children: oauthCompleteMutation.isPending ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
150
- className: "flex items-center gap-1.5",
151
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin" }), "Verifying..."]
152
- }) : "Complete login"
153
- }),
154
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
155
- type: "button",
156
- className: "w-full py-1.5 text-[12px] text-sand-500 hover:text-sand-700 transition-colors duration-150",
157
- onClick: () => {
158
- setCodeEntry(false);
159
- setPastedCode("");
160
- oauthCompleteMutation.reset();
161
- },
162
- children: "처음부터 다시 시작"
163
- })
164
- ]
218
+ })] }), oauth.codeEntry ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(OAuthCodeEntry, {
219
+ isPending: oauth.completeMutation.isPending,
220
+ isError: oauth.completeMutation.isError,
221
+ onSubmit: (code) => void submitPastedCode(code),
222
+ onRestart: oauth.reset
165
223
  }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
166
224
  className: "space-y-2",
167
225
  children: [
168
226
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
169
227
  type: "button",
170
228
  className: "w-full py-2.5 text-sm font-medium rounded-md bg-sand-900 text-white hover:bg-sand-800 disabled:opacity-50 transition-colors duration-150 flex items-center justify-center gap-2",
171
- disabled: !name.trim() || oauthLoading !== null,
172
- onClick: () => handleOAuthLogin("openai"),
173
- children: oauthLoading === "openai" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
229
+ disabled: !name.trim() || oauth.loadingProvider !== null,
230
+ onClick: () => void oauth.start("openai", name),
231
+ children: oauth.loadingProvider === "openai" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
174
232
  className: "flex items-center gap-1.5",
175
233
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin" }), "Waiting for login..."]
176
234
  }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(lucideKeyRound, { className: "size-4" }), "Login with OpenAI"] })
@@ -189,9 +247,9 @@ function AddTokenModal() {
189
247
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
190
248
  type: "button",
191
249
  className: "w-full py-2.5 text-sm font-medium rounded-md border border-sand-300 text-sand-700 hover:bg-sand-100 disabled:opacity-50 transition-colors duration-150 flex items-center justify-center gap-2",
192
- disabled: !name.trim() || oauthLoading !== null,
193
- onClick: () => handleOAuthLogin("anthropic"),
194
- children: oauthLoading === "anthropic" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
250
+ disabled: !name.trim() || oauth.loadingProvider !== null,
251
+ onClick: () => void oauth.start("anthropic", name),
252
+ children: oauth.loadingProvider === "anthropic" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
195
253
  className: "flex items-center gap-1.5",
196
254
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "w-3.5 h-3.5 border-2 border-sand-400 border-t-transparent rounded-full animate-spin" }), "Waiting for login..."]
197
255
  }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(lucideKeyRound, { className: "size-4" }), "Login with Claude"] })
@@ -287,6 +345,7 @@ function TokenTable({ data, isLoading }) {
287
345
  const removeMutation = QgridService.useRemoveTokenMutation();
288
346
  const updateMutation = useUpdateTokenMutation();
289
347
  const toggleMutation = QgridService.useToggleTokenMutation();
348
+ const oauth = useOAuthLoginFlow();
290
349
  const invalidate = () => Promise.all([queryClient.invalidateQueries({ queryKey: ["Token"] }), queryClient.invalidateQueries({ queryKey: ["Qgrid"] })]);
291
350
  const handleToggle = async (token) => {
292
351
  await toggleMutation.mutateAsync({ id: token.id });
@@ -298,6 +357,9 @@ function TokenTable({ data, isLoading }) {
298
357
  await invalidate();
299
358
  setDeleteTarget(null);
300
359
  };
360
+ const submitReloginCode = async (pastedCode) => {
361
+ if (await oauth.submitCode(pastedCode)) oauth.reset();
362
+ };
301
363
  const openEdit = (token) => {
302
364
  setEditTarget(token);
303
365
  setEditName(token.name ?? "");
@@ -330,103 +392,142 @@ function TokenTable({ data, isLoading }) {
330
392
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
331
393
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
332
394
  className: "panel overflow-hidden",
333
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("table", {
334
- className: "w-full text-[13px]",
335
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", {
336
- className: "panel-header",
337
- children: [
338
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
339
- className: "text-left px-5 py-2.5 text-[10px] uppercase tracking-wider text-sand-400 font-medium",
340
- children: "Provider"
341
- }),
342
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
343
- className: "text-left px-5 py-2.5 text-[10px] uppercase tracking-wider text-sand-400 font-medium",
344
- children: "Name"
345
- }),
346
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
347
- className: "text-left px-5 py-2.5 text-[10px] uppercase tracking-wider text-sand-400 font-medium",
348
- children: "Token"
349
- }),
350
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
351
- className: "text-left px-5 py-2.5 text-[10px] uppercase tracking-wider text-sand-400 font-medium",
352
- children: "Status"
353
- }),
354
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
355
- className: "text-left px-5 py-2.5 text-[10px] uppercase tracking-wider text-sand-400 font-medium",
356
- children: "Expires"
357
- }),
358
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { className: "w-20 px-3 py-2.5" })
359
- ]
360
- }) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tbody", {
361
- className: "divide-y divide-sand-100/80",
362
- children: tokens.map((token) => {
363
- const expiry = formatExpiry(getExpiresAt(token));
364
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", {
365
- className: `transition-colors duration-150 hover:bg-sand-100/60 ${token.active ? "" : "opacity-50"}`,
366
- children: [
367
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
368
- className: "px-5 py-3",
369
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
370
- className: "text-xs font-medium text-sand-500 uppercase",
371
- children: token.provider
372
- })
373
- }),
374
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
375
- className: "px-5 py-3",
376
- children: token.name ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
377
- className: "text-sm text-sand-700",
378
- children: token.name
379
- }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
380
- className: "text-[11px] text-sand-400",
381
- children: "—"
382
- })
383
- }),
384
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
385
- className: "px-5 py-3",
386
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", {
387
- className: "text-[13px] font-mono text-sand-800",
388
- children: maskAccessToken(token)
389
- })
390
- }),
391
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
392
- className: "px-5 py-3",
393
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
394
- type: "button",
395
- className: `relative inline-flex h-5 w-9 items-center rounded-full transition-colors duration-200 ${token.active ? "bg-sage-400" : "bg-sand-300"}`,
396
- onClick: () => handleToggle(token),
397
- disabled: toggleMutation.isPending,
398
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `inline-block h-3.5 w-3.5 rounded-full bg-white transition-transform duration-200 ${token.active ? "translate-x-4.5" : "translate-x-0.5"}` })
399
- })
400
- }),
401
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
402
- className: "px-5 py-3",
403
- children: expiry.text && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
404
- className: `text-xs ${expiry.color}`,
405
- children: expiry.text
406
- })
407
- }),
408
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
409
- className: "px-3 py-3",
410
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
411
- className: "flex items-center gap-1",
412
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
413
- type: "button",
414
- className: "p-1 rounded text-sand-400 hover:text-sienna-500 transition-colors duration-150",
415
- onClick: () => openEdit(token),
416
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(lucidePencil, { className: "size-4" })
417
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
395
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
396
+ className: "overflow-x-auto",
397
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("table", {
398
+ className: "w-full min-w-[640px] text-[13px]",
399
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", {
400
+ className: "panel-header",
401
+ children: [
402
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
403
+ className: "text-left px-5 py-2.5 text-[10px] uppercase tracking-wider text-sand-400 font-medium",
404
+ children: "Provider"
405
+ }),
406
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
407
+ className: "text-left px-5 py-2.5 text-[10px] uppercase tracking-wider text-sand-400 font-medium",
408
+ children: "Name"
409
+ }),
410
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
411
+ className: "text-left px-5 py-2.5 text-[10px] uppercase tracking-wider text-sand-400 font-medium",
412
+ children: "Token"
413
+ }),
414
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
415
+ className: "text-left px-5 py-2.5 text-[10px] uppercase tracking-wider text-sand-400 font-medium",
416
+ children: "Status"
417
+ }),
418
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
419
+ className: "text-left px-5 py-2.5 text-[10px] uppercase tracking-wider text-sand-400 font-medium",
420
+ children: "Expires"
421
+ }),
422
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { className: "w-20 px-3 py-2.5" })
423
+ ]
424
+ }) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tbody", {
425
+ className: "divide-y divide-sand-100/80",
426
+ children: tokens.map((token) => {
427
+ const expiry = formatExpiry(getExpiresAt(token));
428
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", {
429
+ className: `transition-colors duration-150 hover:bg-sand-100/60 ${token.active ? "" : "opacity-50"}`,
430
+ children: [
431
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
432
+ className: "px-5 py-3",
433
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
434
+ className: "text-xs font-medium text-sand-500 uppercase",
435
+ children: token.provider
436
+ })
437
+ }),
438
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
439
+ className: "px-5 py-3",
440
+ children: token.name ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
441
+ className: "text-sm text-sand-700",
442
+ children: token.name
443
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
444
+ className: "text-[11px] text-sand-400",
445
+ children: "—"
446
+ })
447
+ }),
448
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
449
+ className: "px-5 py-3",
450
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", {
451
+ className: "text-[13px] font-mono text-sand-800",
452
+ children: maskAccessToken(token)
453
+ })
454
+ }),
455
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
456
+ className: "px-5 py-3",
457
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
418
458
  type: "button",
419
- className: "p-1 rounded text-sand-400 hover:text-red-500 transition-colors duration-150",
420
- onClick: () => setDeleteTarget(token),
421
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(lucideTrash2, { className: "size-4" })
422
- })]
459
+ className: `relative inline-flex h-5 w-9 items-center rounded-full transition-colors duration-200 ${token.active ? "bg-sage-400" : "bg-sand-300"}`,
460
+ onClick: () => handleToggle(token),
461
+ disabled: toggleMutation.isPending,
462
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `inline-block h-3.5 w-3.5 rounded-full bg-white transition-transform duration-200 ${token.active ? "translate-x-4.5" : "translate-x-0.5"}` })
463
+ })
464
+ }),
465
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
466
+ className: "px-5 py-3",
467
+ children: expiry.text && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
468
+ className: `text-xs ${expiry.color}`,
469
+ children: expiry.text
470
+ })
471
+ }),
472
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
473
+ className: "px-3 py-3",
474
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
475
+ className: "flex items-center gap-1",
476
+ children: [
477
+ !token.active && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
478
+ type: "button",
479
+ title: "기존 이름으로 다시 로그인",
480
+ className: "px-2 py-0.5 mr-1 text-[11px] font-medium rounded-md border border-sand-300 text-sand-600 hover:bg-sand-100 disabled:opacity-50 transition-colors duration-150 inline-flex items-center gap-1",
481
+ disabled: oauth.loadingProvider !== null,
482
+ onClick: () => void oauth.start(token.provider, token.name ?? ""),
483
+ children: oauth.loadingProvider === token.provider ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "w-3 h-3 border-2 border-sand-400 border-t-transparent rounded-full animate-spin" }), "로그인 대기"] }) : "재로그인"
484
+ }),
485
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
486
+ type: "button",
487
+ className: "p-1 rounded text-sand-400 hover:text-sienna-500 transition-colors duration-150",
488
+ onClick: () => openEdit(token),
489
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(lucidePencil, { className: "size-4" })
490
+ }),
491
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
492
+ type: "button",
493
+ className: "p-1 rounded text-sand-400 hover:text-red-500 transition-colors duration-150",
494
+ onClick: () => setDeleteTarget(token),
495
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(lucideTrash2, { className: "size-4" })
496
+ })
497
+ ]
498
+ })
423
499
  })
424
- })
425
- ]
426
- }, token.id);
500
+ ]
501
+ }, token.id);
502
+ })
503
+ })]
504
+ })
505
+ })
506
+ }),
507
+ oauth.codeEntry && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
508
+ className: "fixed inset-0 z-50 flex items-start justify-center pt-[15vh]",
509
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
510
+ className: "absolute inset-0 bg-sand-900/8 backdrop-blur-sm",
511
+ onClick: oauth.reset,
512
+ onKeyDown: () => {}
513
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
514
+ className: "relative panel shadow-xl w-full max-w-md mx-4",
515
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
516
+ className: "px-5 py-4 border-b border-sand-100/60",
517
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", {
518
+ className: "text-base font-medium text-sand-900",
519
+ children: "재로그인"
520
+ })
521
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
522
+ className: "px-5 py-4",
523
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(OAuthCodeEntry, {
524
+ isPending: oauth.completeMutation.isPending,
525
+ isError: oauth.completeMutation.isError,
526
+ onSubmit: (code) => void submitReloginCode(code),
527
+ onRestart: oauth.reset
427
528
  })
428
529
  })]
429
- })
530
+ })]
430
531
  }),
431
532
  editTarget && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
432
533
  className: "fixed inset-0 z-50 flex items-start justify-center pt-[15vh]",
@@ -1,4 +1,4 @@
1
- import { n as QgridService, o as useMutation } from "./services.generated-BNuxWs7K.js";
1
+ import { n as QgridService, o as useMutation } from "./services.generated-DqAKskAS.js";
2
2
  //#region src/services/token/use-update-token-mutation.ts
3
3
  function useUpdateTokenMutation() {
4
4
  return useMutation({ mutationFn: ({ id, name, quotaThreshold, weight }) => QgridService.updateToken(id, name, quotaThreshold, weight) });