gencow 0.1.112 → 0.1.114

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/bin/gencow.mjs CHANGED
@@ -29,15 +29,37 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
29
29
 
30
30
  const CREDS_PATH = resolve(homedir(), ".gencow", "credentials.json");
31
31
 
32
+ // ─── Domain Helpers (Dev/Prod 환경 분리) ─────────────────
33
+ // platformUrl에서 기본 도메인을 추출하여 동적 URL 생성.
34
+ // DEV: https://gencow.dev → gencow.dev
35
+ // PROD: https://gencow.app → gencow.app
36
+
37
+ function getBaseDomain(platformUrl) {
38
+ try {
39
+ return new URL(platformUrl).hostname;
40
+ } catch {
41
+ return "gencow.app";
42
+ }
43
+ }
44
+
45
+ function getAppUrl(appName, platformUrl) {
46
+ const domain = getBaseDomain(platformUrl);
47
+ return `https://${appName}.${domain}`;
48
+ }
49
+
50
+ function getWsUrl(appName, platformUrl) {
51
+ const domain = getBaseDomain(platformUrl);
52
+ return `wss://${appName}.${domain}/ws`;
53
+ }
54
+
55
+ function getDashboardUrl(appName, platformUrl) {
56
+ const domain = getBaseDomain(platformUrl);
57
+ return `https://${domain}/apps/${appName}`;
58
+ }
59
+
32
60
  function loadCreds() {
33
61
  try {
34
- const creds = JSON.parse(readFileSync(CREDS_PATH, "utf8"));
35
- // gencow.dev → gencow.app 자동 마이그레이션 (301 리다이렉트 시 POST→GET 변환 방지)
36
- if (creds?.platformUrl?.includes("gencow.dev")) {
37
- creds.platformUrl = creds.platformUrl.replace("gencow.dev", "gencow.app");
38
- try { writeFileSync(CREDS_PATH, JSON.stringify(creds, null, 2)); } catch { }
39
- }
40
- return creds;
62
+ return JSON.parse(readFileSync(CREDS_PATH, "utf8"));
41
63
  } catch {
42
64
  return null;
43
65
  }
@@ -52,8 +74,8 @@ function requireCreds() {
52
74
  // CI/CD 환경: 환경변수 토큰 우선 (Deploy Token 또는 CLI Token)
53
75
  const envToken = process.env.GENCOW_TOKEN || process.env.GENCOW_DEPLOY_TOKEN;
54
76
  if (envToken) {
55
- const platformUrl = process.env.GENCOW_PLATFORM_URL || "https://gencow.app";
56
- return { apiKey: envToken, platformUrl };
77
+ const platformUrl = process.env.GENCOW_PLATFORM_URL || "https://gencow.app"; // 기본값은 PROD
78
+ return { apiKey: envToken, platformUrl: validatePlatformUrl(platformUrl) };
57
79
  }
58
80
 
59
81
  // 로컬: ~/.gencow/credentials.json
@@ -68,9 +90,27 @@ function requireCreds() {
68
90
  }
69
91
  process.exit(1);
70
92
  }
93
+ // 기존 credentials에도 platformUrl 검증 적용
94
+ if (creds.platformUrl) {
95
+ creds.platformUrl = validatePlatformUrl(creds.platformUrl);
96
+ }
71
97
  return creds;
72
98
  }
73
99
 
100
+ /** platformUrl 보안 검증 — https:// 스킴 강제 (localhost 제외) */
101
+ function validatePlatformUrl(url) {
102
+ if (!url) return "https://gencow.app";
103
+ // localhost는 개발용으로 http 허용
104
+ if (url.startsWith("http://localhost")) return url;
105
+ // 그 외는 반드시 https://
106
+ if (!url.startsWith("https://")) {
107
+ error(`platformUrl must use https:// (got: ${url})`);
108
+ info("시크릿이 평문으로 전송될 수 있습니다. GENCOW_PLATFORM_URL을 확인하세요.");
109
+ process.exit(1);
110
+ }
111
+ return url;
112
+ }
113
+
74
114
  async function platformFetch(creds, path, opts = {}) {
75
115
  const url = `${creds.platformUrl}${path}`;
76
116
  const res = await fetch(url, {
@@ -1055,10 +1095,12 @@ ${hasPrompt ? `
1055
1095
  // ── Cloud 모드 (기본) ──
1056
1096
  const gencowJsonPath = resolve(process.cwd(), "gencow.json");
1057
1097
  let appId = null;
1098
+ let seedPlatformUrl = null;
1058
1099
  if (existsSync(gencowJsonPath)) {
1059
1100
  try {
1060
1101
  const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
1061
1102
  appId = gencowJson.appId || gencowJson.appName;
1103
+ seedPlatformUrl = gencowJson.platformUrl || null;
1062
1104
  } catch { /* ignore parse error */ }
1063
1105
  }
1064
1106
 
@@ -1073,11 +1115,12 @@ ${hasPrompt ? `
1073
1115
  log(`\n${BOLD}${CYAN}Gencow DB Seed${RESET} ${DIM}(cloud: ${appId})${RESET}\n`);
1074
1116
  info("Running seed.ts on cloud app...");
1075
1117
 
1076
- const cloudUrl = `https://${appId}.gencow.app/_admin/seed`;
1118
+ const seedAppBaseUrl = getAppUrl(appId, seedPlatformUrl || "https://gencow.app");
1119
+ const cloudUrl = `${seedAppBaseUrl}/_admin/seed`;
1077
1120
 
1078
1121
  try {
1079
1122
  // 앱이 실행 중인지 확인 (status 체크)
1080
- const statusRes = await fetch(`https://${appId}.gencow.app/_admin/status`, {
1123
+ const statusRes = await fetch(`${seedAppBaseUrl}/_admin/status`, {
1081
1124
  signal: AbortSignal.timeout(5000),
1082
1125
  }).catch(() => null);
1083
1126
 
@@ -1110,7 +1153,7 @@ ${hasPrompt ? `
1110
1153
  }
1111
1154
  } catch (e) {
1112
1155
  error(`Cloud app connection failed: ${e.message}`);
1113
- info(`${DIM}앱이 실행 중인지 확인하세요: https://${appId}.gencow.app${RESET}`);
1156
+ info(`${DIM}앱이 실행 중인지 확인하세요: ${seedAppBaseUrl}${RESET}`);
1114
1157
  }
1115
1158
  log("");
1116
1159
  },
@@ -2115,6 +2158,18 @@ ${BOLD}Examples:${RESET}
2115
2158
  const bundleSize = statSync(tmpBundle).size;
2116
2159
  success(`번들 생성: ${(bundleSize / 1024).toFixed(1)} KB`);
2117
2160
 
2161
+ // 2-0. 번들 크기 경고 (Ph2: 리소스 미터링 — 차단 아님, 안내만)
2162
+ try {
2163
+ const { auditBundleSize, formatBundleSizeWarning } = await import("../lib/deploy-auditor.mjs");
2164
+ const sizeResult = auditBundleSize(bundleSize);
2165
+ const sizeMsg = formatBundleSizeWarning(sizeResult);
2166
+ if (sizeMsg) {
2167
+ log(`${YELLOW}${sizeMsg}${RESET}`);
2168
+ }
2169
+ } catch {
2170
+ // 번들 크기 검사 실패 시 무시 (배포 계속)
2171
+ }
2172
+
2118
2173
  // 2. 앱이 없으면 생성 (appId가 없는 경우)
2119
2174
  if (!appId) {
2120
2175
  info("앱 생성 중 (ID 자동 생성)...");
@@ -2396,9 +2451,13 @@ ${BOLD}Examples:${RESET}
2396
2451
  };
2397
2452
  const tsFiles = scanTsFiles(gencowDir);
2398
2453
  // query(, mutation(, httpAction( 호출이 하나라도 있으면 실질적 백엔드
2454
+ // ⚠️ 주석 내 코드 예시를 오탐하지 않도록 주석을 먼저 제거
2399
2455
  const hasApiCalls = tsFiles.some(f => {
2400
2456
  const src = readFileSync(f, "utf8");
2401
- return /\b(query|mutation|httpAction)\s*\(/.test(src);
2457
+ const stripped = src
2458
+ .replace(/\/\/.*$/gm, "") // 한줄 주석 제거
2459
+ .replace(/\/\*[\s\S]*?\*\//g, ""); // 블록 주석 제거
2460
+ return /\b(query|mutation|httpAction)\s*\(/.test(stripped);
2402
2461
  });
2403
2462
  if (!hasApiCalls) {
2404
2463
  isBackendEmpty = true;
@@ -2464,7 +2523,7 @@ ${BOLD}Examples:${RESET}
2464
2523
  if (apiRefFiles.length > 5) log(` ${DIM}... 외 ${apiRefFiles.length - 5}개${RESET}`);
2465
2524
  log("");
2466
2525
  warn(`정적 호스팅에는 API 서버가 없어 404가 발생합니다.`);
2467
- info(`💡 백엔드가 필요하다면: ${CYAN}VITE_API_URL=https://{backend}.gencow.app npm run build${RESET} 후 배포`);
2526
+ info(`💡 백엔드가 필요하다면: ${CYAN}VITE_API_URL=https://{backend}.{도메인} npm run build${RESET} 후 배포`);
2468
2527
  info(`💡 별도 백엔드를 사용한다면: ${CYAN}VITE_API_URL${RESET} 환경변수로 빌드 대상을 지정하세요.`);
2469
2528
  log("");
2470
2529
 
@@ -2644,23 +2703,29 @@ ${BOLD}Examples:${RESET}
2644
2703
  warn("서버 시작 실패 원인:");
2645
2704
  for (const line of errData.crashLogs) log(` ${DIM}${line}${RESET}`);
2646
2705
  }
2647
- error("백엔드 배포 실패로 프론트엔드 배포를 건너뜁니다.");
2648
- process.exit(1);
2649
- }
2706
+ if (targetDir) {
2707
+ // --static 모드: 백엔드 실패해도 프론트엔드 배포는 계속 진행
2708
+ warn("백엔드 배포 실패 — 정적 파일 배포는 계속 진행합니다.");
2709
+ log("");
2710
+ } else {
2711
+ error("백엔드 배포 실패로 프론트엔드 배포를 건너뜁니다.");
2712
+ process.exit(1);
2713
+ }
2714
+ } else {
2715
+ const backendData = await backendDeployRes.json();
2716
+ const backendElapsed = ((Date.now() - backendStartTime) / 1000).toFixed(1);
2717
+ success(`백엔드 빌드 완료! (${backendElapsed}s)`);
2650
2718
 
2651
- const backendData = await backendDeployRes.json();
2652
- const backendElapsed = ((Date.now() - backendStartTime) / 1000).toFixed(1);
2653
- success(`백엔드 빌드 완료! (${backendElapsed}s)`);
2719
+ // Health check 백엔드 앱 URL로 실제 응답 확인
2720
+ if (backendData.url) {
2721
+ await this._verifyAppReady(backendData.url, appId);
2722
+ }
2654
2723
 
2655
- // Health check — 백엔드 앱 URL로 실제 응답 확인
2656
- if (backendData.url) {
2657
- await this._verifyAppReady(backendData.url, appId);
2724
+ info(`URL: ${backendData.url}`);
2725
+ info(`Hash: ${backendData.bundleHash}`);
2726
+ updateEnvLocalUrl(backendData.url);
2727
+ log("");
2658
2728
  }
2659
-
2660
- info(`URL: ${backendData.url}`);
2661
- info(`Hash: ${backendData.bundleHash}`);
2662
- updateEnvLocalUrl(backendData.url);
2663
- log("");
2664
2729
  log(` ${BOLD}── 프론트엔드 배포 ──────────────────${RESET}\n`);
2665
2730
  }
2666
2731
 
@@ -3937,7 +4002,7 @@ process.exit(0);
3937
4002
  info(`${DIM}gencow.json 생성됨 (appId: ${appName})${RESET}`);
3938
4003
 
3939
4004
  // .env에 VITE_API_URL 자동 설정
3940
- updateEnvLocalUrl(`https://${appName}.gencow.app`);
4005
+ updateEnvLocalUrl(getAppUrl(appName, creds.platformUrl));
3941
4006
 
3942
4007
  // 프로비저닝 대기
3943
4008
  info("프로비저닝 대기 중...");
@@ -3951,7 +4016,7 @@ process.exit(0);
3951
4016
  process.exit(1);
3952
4017
  }
3953
4018
 
3954
- const appUrl = `https://${appName}.gencow.app`;
4019
+ const appUrl = getAppUrl(appName, creds.platformUrl);
3955
4020
 
3956
4021
  // ── 배포 함수 (번들 크기 + 소요 시간 표시) ────────────
3957
4022
  async function deploy(reason = "initial") {
@@ -4073,7 +4138,7 @@ process.exit(0);
4073
4138
 
4074
4139
  async function connectLogStream() {
4075
4140
  const { WebSocket: WS } = await import("ws");
4076
- const wsUrl = `wss://${appName}.gencow.app/ws`;
4141
+ const wsUrl = getWsUrl(appName, creds.platformUrl);
4077
4142
 
4078
4143
  try {
4079
4144
  logWs = new WS(wsUrl);
@@ -4150,7 +4215,7 @@ ${BOLD}${CYAN}🚀 Gencow Cloud Dev${RESET}
4150
4215
 
4151
4216
  ${GREEN}▸${RESET} App: ${BOLD}${appName}${RESET}
4152
4217
  ${GREEN}▸${RESET} URL: ${DIM}${appUrl}${RESET}
4153
- ${GREEN}▸${RESET} Dashboard: ${DIM}https://gencow.app/_cloud/apps/${appName}${RESET}
4218
+ ${GREEN}▸${RESET} Dashboard: ${DIM}${getDashboardUrl(appName, creds.platformUrl)}${RESET}
4154
4219
  ${GREEN}▸${RESET} Watching: ${DIM}${functionsDir}/ (${watchedFiles.join(", ") || "*.ts"})${RESET}
4155
4220
  ${GREEN}▸${RESET} Mode: ${DIM}Cloud (PostgreSQL)${RESET}
4156
4221
 
@@ -11,6 +11,8 @@ import {
11
11
  isNodeBuiltin,
12
12
  formatAuditError,
13
13
  getPlatformPackageList,
14
+ auditBundleSize,
15
+ formatBundleSizeWarning,
14
16
  } from "../deploy-auditor.mjs";
15
17
 
16
18
  describe("isPlatformPackage", () => {
@@ -125,3 +127,92 @@ describe("getPlatformPackageList", () => {
125
127
  expect(list).toEqual(sorted);
126
128
  });
127
129
  });
130
+
131
+ describe("auditBundleSize", () => {
132
+ const MB = 1024 * 1024;
133
+
134
+ it("50MB 이하 → 경고 없음 (기본 임계치)", () => {
135
+ const result = auditBundleSize(30 * MB);
136
+ expect(result.warned).toBe(false);
137
+ expect(result.bundleSizeMB).toBe(30);
138
+ expect(result.thresholdMB).toBe(50);
139
+ });
140
+
141
+ it("50MB 초과 → 경고 발생 (기본 임계치)", () => {
142
+ const result = auditBundleSize(60 * MB);
143
+ expect(result.warned).toBe(true);
144
+ expect(result.bundleSizeMB).toBe(60);
145
+ expect(result.thresholdMB).toBe(50);
146
+ });
147
+
148
+ it("정확히 50MB → 경고 없음 (초과가 아닌 이상)", () => {
149
+ const result = auditBundleSize(50 * MB);
150
+ expect(result.warned).toBe(false);
151
+ });
152
+
153
+ it("50MB + 1byte → 경고 발생", () => {
154
+ const result = auditBundleSize(50 * MB + 1);
155
+ expect(result.warned).toBe(true);
156
+ });
157
+
158
+ it("커스텀 임계치 100MB → 60MB는 경고 없음", () => {
159
+ const result = auditBundleSize(60 * MB, 100);
160
+ expect(result.warned).toBe(false);
161
+ expect(result.thresholdMB).toBe(100);
162
+ });
163
+
164
+ it("커스텀 임계치 30MB → 35MB는 경고", () => {
165
+ const result = auditBundleSize(35 * MB, 30);
166
+ expect(result.warned).toBe(true);
167
+ expect(result.thresholdMB).toBe(30);
168
+ });
169
+
170
+ it("0 bytes → 경고 없음", () => {
171
+ const result = auditBundleSize(0);
172
+ expect(result.warned).toBe(false);
173
+ expect(result.bundleSizeMB).toBe(0);
174
+ });
175
+
176
+ it("소수점 첫째 자리까지 반올림", () => {
177
+ const result = auditBundleSize(52.35 * MB);
178
+ // 52.35 → 52.4 (반올림)
179
+ expect(result.bundleSizeMB).toBeCloseTo(52.4, 0);
180
+ });
181
+ });
182
+
183
+ describe("formatBundleSizeWarning", () => {
184
+ it("경고 없으면 빈 문자열 반환", () => {
185
+ const msg = formatBundleSizeWarning({ warned: false, bundleSizeMB: 20, thresholdMB: 50 });
186
+ expect(msg).toBe("");
187
+ });
188
+
189
+ it("경고 있으면 번들 크기 정보 포함", () => {
190
+ const msg = formatBundleSizeWarning({ warned: true, bundleSizeMB: 75, thresholdMB: 50 });
191
+ expect(msg).toContain("75");
192
+ expect(msg).toContain("50");
193
+ });
194
+
195
+ it("메모리 사용 경고 메시지 포함", () => {
196
+ const msg = formatBundleSizeWarning({ warned: true, bundleSizeMB: 60, thresholdMB: 50 });
197
+ expect(msg).toContain("메모리");
198
+ expect(msg).toContain("크레딧");
199
+ });
200
+
201
+ it("업그레이드 안내 포함", () => {
202
+ const msg = formatBundleSizeWarning({ warned: true, bundleSizeMB: 60, thresholdMB: 50 });
203
+ expect(msg).toContain("Pro");
204
+ expect(msg).toContain("Scale");
205
+ expect(msg).toContain("업그레이드");
206
+ });
207
+
208
+ it("차단하지 않는다는 안내 포함", () => {
209
+ const msg = formatBundleSizeWarning({ warned: true, bundleSizeMB: 60, thresholdMB: 50 });
210
+ expect(msg).toContain("차단하지 않습니다");
211
+ });
212
+
213
+ it("권장사항 포함 (devDependencies, 대안 패키지)", () => {
214
+ const msg = formatBundleSizeWarning({ warned: true, bundleSizeMB: 60, thresholdMB: 50 });
215
+ expect(msg).toContain("devDependencies");
216
+ expect(msg).toContain("dayjs");
217
+ });
218
+ });
@@ -172,7 +172,7 @@ describe("buildAuthSection — 인증 섹션", () => {
172
172
  const md = buildAuthSection();
173
173
  expect(md).toContain("VITE_API_URL");
174
174
  expect(md).toContain("프록시");
175
- expect(md).toContain("gencowAuth(import.meta.env.VITE_API_URL)");
175
+ expect(md).toContain("createAuthClient(import.meta.env.VITE_API_URL)");
176
176
  });
177
177
  });
178
178
 
@@ -370,9 +370,9 @@ describe("buildFrontendSetup — 프론트엔드 초기 설정", () => {
370
370
  expect(md).toContain("3단계");
371
371
  });
372
372
 
373
- it("gencowAuth 설정 예제 포함", () => {
373
+ it("createAuthClient 설정 예제 포함", () => {
374
374
  const md = buildFrontendSetup();
375
- expect(md).toContain("gencowAuth");
375
+ expect(md).toContain("createAuthClient");
376
376
  expect(md).toContain("signIn, signUp, signOut, useAuth");
377
377
  });
378
378
 
@@ -216,3 +216,72 @@ export function formatAuditError(result) {
216
216
  export function getPlatformPackageList() {
217
217
  return [...PLATFORM_PACKAGES].sort();
218
218
  }
219
+
220
+ // ── Bundle Size Audit ──────────────────────────────────────
221
+
222
+ /**
223
+ * 번들 크기 임계치 (MB) — 이 이상이면 메모리 경고.
224
+ * pricing_tiers.features.maxBundleSizeMB와 동기화.
225
+ * 기본값은 Hobby tier 기준 (50 MB).
226
+ */
227
+ const DEFAULT_BUNDLE_SIZE_WARNING_MB = 50;
228
+
229
+ /**
230
+ * @typedef {Object} BundleSizeAuditResult
231
+ * @property {boolean} warned - 경고가 발생했는지
232
+ * @property {number} bundleSizeMB - 번들 크기 (MB)
233
+ * @property {number} thresholdMB - 경고 임계치 (MB)
234
+ */
235
+
236
+ /**
237
+ * 번들(tar.gz) 크기를 검사하여 메모리 사용 경고 여부를 반환한다.
238
+ * 경고만 출력하며 배포를 차단하지 않는다.
239
+ *
240
+ * 큰 번들 = 많은 node_modules = 높은 RSS = 더 빠른 크레딧 소진.
241
+ * 사용자에게 리소스 소비를 인지시키고 업그레이드를 안내하는 목적.
242
+ *
243
+ * @param {number} bundleSizeBytes - 번들 파일 크기 (bytes)
244
+ * @param {number} [thresholdMB] - 경고 임계치 (MB). 기본값: 50
245
+ * @returns {BundleSizeAuditResult}
246
+ */
247
+ export function auditBundleSize(bundleSizeBytes, thresholdMB) {
248
+ const threshold = thresholdMB || DEFAULT_BUNDLE_SIZE_WARNING_MB;
249
+ const bundleSizeMB = bundleSizeBytes / (1024 * 1024);
250
+
251
+ return {
252
+ warned: bundleSizeMB > threshold,
253
+ bundleSizeMB: Math.round(bundleSizeMB * 10) / 10,
254
+ thresholdMB: threshold,
255
+ };
256
+ }
257
+
258
+ /**
259
+ * 번들 크기 경고 메시지를 포맷한다.
260
+ * 차단하지 않고 안내만 제공 — 메모리 사용 + 업그레이드 유도.
261
+ *
262
+ * @param {BundleSizeAuditResult} result
263
+ * @returns {string} 포맷된 경고 메시지 (경고 없으면 빈 문자열)
264
+ */
265
+ export function formatBundleSizeWarning(result) {
266
+ if (!result.warned) return "";
267
+
268
+ const lines = [
269
+ "",
270
+ ` ⚠️ 번들 크기 경고: ${result.bundleSizeMB} MB (임계치: ${result.thresholdMB} MB)`,
271
+ "",
272
+ " 이 앱은 런타임 메모리를 많이 사용할 수 있습니다.",
273
+ " 큰 번들 = 많은 패키지 로딩 = 높은 RSS = 더 빠른 크레딧 소진",
274
+ "",
275
+ " 권장사항:",
276
+ " • 불필요한 devDependencies를 dependencies에서 제거하세요",
277
+ " • 무거운 패키지를 가벼운 대안으로 교체해보세요",
278
+ " (예: moment → dayjs, lodash → lodash-es)",
279
+ " • 메모리 한도가 부족하면 Pro/Scale 플랜으로 업그레이드하세요",
280
+ " (Hobby: 256MB, Pro: 512MB, Scale: 1024MB)",
281
+ "",
282
+ " 💡 이 경고는 배포를 차단하지 않습니다.",
283
+ "",
284
+ ];
285
+
286
+ return lines.join("\n");
287
+ }
@@ -103,13 +103,13 @@ export function buildReactUsage(apiObj) {
103
103
  if (fns.queries.length > 0) {
104
104
  md += `// ${ns} 데이터 조회 (실시간 자동 갱신)\n`;
105
105
  md += `// crud() list는 { data: ${capitalize(ns)}[], total: number } 반환\n`;
106
- md += `const result = useQuery(api.${ns}.${fns.queries[0]});\n`;
106
+ md += `const { data: result } = useQuery(api.${ns}.${fns.queries[0]});\n`;
107
107
  md += `result?.data.map(item => ...) // 데이터 배열 접근\n`;
108
108
  md += `result?.total // 전체 건수 (페이지네이션용)\n`;
109
109
  }
110
110
  if (fns.mutations.length > 0) {
111
111
  md += `// ${ns} 데이터 변경 (로딩/에러 자동 관리)\n`;
112
- md += `const [${fns.mutations[0]}${capitalize(ns)}, isPending] = useMutation(api.${ns}.${fns.mutations[0]});\n`;
112
+ md += `const { mutate: ${fns.mutations[0]}${capitalize(ns)}, isPending } = useMutation(api.${ns}.${fns.mutations[0]});\n`;
113
113
  }
114
114
  md += `\n`;
115
115
  }
@@ -122,7 +122,7 @@ export function buildReactUsage(apiObj) {
122
122
  md += `fetch("/api/query", { body: JSON.stringify({ name: "tasks.create", args: {...} }) })\n`;
123
123
  md += `apiPost("tasks/create", { ... }) // ❌ 래퍼도 만들지 마세요\n\n`;
124
124
  md += `// ✅ useMutation을 사용하세요 — 실시간 동기화 + 로딩 상태 자동 관리\n`;
125
- md += `const [create] = useMutation(api.tasks.create);\n`;
125
+ md += `const { mutate: create } = useMutation(api.tasks.create);\n`;
126
126
  md += `await create({ title: "새 태스크" });\n`;
127
127
  md += `// → 서버가 WebSocket으로 useQuery를 자동 갱신 — fetchTasks() 같은 수동 리페치 불필요!\n`;
128
128
  md += `\`\`\`\n\n`;
@@ -132,15 +132,15 @@ export function buildReactUsage(apiObj) {
132
132
  md += `선택된 항목이 없을 때 등 조건부로 쿼리를 건너뛰어야 할 때:\n\n`;
133
133
  md += `\`\`\`typescript\n`;
134
134
  md += `// 방법 A: "skip" 토큰 — 추천\n`;
135
- md += `const messages = useQuery(api.chat.getMessages,\n`;
135
+ md += `const { data: messages } = useQuery(api.chat.getMessages,\n`;
136
136
  md += ` conversationId ? { conversationId } : "skip"\n`;
137
137
  md += `);\n\n`;
138
138
  md += `// 방법 B: enabled 옵션 (TanStack Query 스타일)\n`;
139
- md += `const messages = useQuery(api.chat.getMessages,\n`;
139
+ md += `const { data: messages } = useQuery(api.chat.getMessages,\n`;
140
140
  md += ` { conversationId },\n`;
141
141
  md += ` { enabled: !!conversationId }\n`;
142
142
  md += `);\n`;
143
- md += `// → skip 상태에서는 API 호출 없이 undefined 반환\n`;
143
+ md += `// → skip 상태에서는 API 호출 없이 data=undefined, isLoading=false\n`;
144
144
  md += `\`\`\`\n\n`;
145
145
 
146
146
  return md;
@@ -153,9 +153,9 @@ export function buildFrontendSetup() {
153
153
  md += `### 1단계: Auth 클라이언트 생성\n\n`;
154
154
  md += `\`\`\`typescript\n`;
155
155
  md += `// src/lib/auth.ts\n`;
156
- md += `import { gencowAuth } from "@gencow/react";\n\n`;
156
+ md += `import { createAuthClient } from "@gencow/react";\n\n`;
157
157
  md += `// VITE_API_URL은 .env에서 자동 읽음 (gencow init 시 자동 설정)\n`;
158
- md += `export const { signIn, signUp, signOut, useAuth } = gencowAuth();\n`;
158
+ md += `export const { signIn, signUp, signOut, useAuth } = createAuthClient();\n`;
159
159
  md += `\`\`\`\n\n`;
160
160
  md += `### 2단계: GencowProvider 설정\n\n`;
161
161
  md += `\`\`\`tsx\n`;
@@ -176,8 +176,8 @@ export function buildFrontendSetup() {
176
176
  md += `\`\`\`typescript\n`;
177
177
  md += `import { api } from "@/gencow/api"; // gencow dev가 자동 생성\n`;
178
178
  md += `import { useQuery, useMutation } from "@gencow/react";\n\n`;
179
- md += `const tasks = useQuery(api.tasks.list); // 실시간 구독\n`;
180
- md += `const [create] = useMutation(api.tasks.create); // 데이터 변경\n`;
179
+ md += `const { data: tasks } = useQuery(api.tasks.list); // 실시간 구독\n`;
180
+ md += `const { mutate: create } = useMutation(api.tasks.create); // 데이터 변경\n`;
181
181
  md += `\`\`\`\n\n`;
182
182
  md += `### ❌ 흔한 실수\n\n`;
183
183
  md += `\`\`\`typescript\n`;
@@ -197,9 +197,9 @@ export function buildFrontendSetup() {
197
197
  md += `// GencowProvider에 token={null} 전달 시:\n`;
198
198
  md += `<GencowProvider baseUrl={apiUrl} token={null}>\n\n`;
199
199
  md += `// ❌ 데이터가 표시되지 않음 (token=null → 자동 skip)\n`;
200
- md += `const data = useQuery(api.tasks.list);\n\n`;
200
+ md += `const { data } = useQuery(api.tasks.list);\n\n`;
201
201
  md += `// ✅ { public: true } 옵션 추가 → 인증 없이 호출\n`;
202
- md += `const data = useQuery(api.tasks.list, {}, { public: true });\n\n`;
202
+ md += `const { data } = useQuery(api.tasks.list, {}, { public: true });\n\n`;
203
203
  md += `// ✅ 서버 사이드에서도 public: true 필요\n`;
204
204
  md += `// gencow/tasks.ts\n`;
205
205
  md += `export const list = query("tasks.list", {\n`;
@@ -228,9 +228,9 @@ export function buildAuthSection() {
228
228
  md += `const BACKEND_URL = process.env.VITE_API_URL || "http://localhost:5456";\n`;
229
229
  md += `// proxy: { "/api": { target: BACKEND_URL }, "/ws": { target: BACKEND_URL, ws: true } }\n`;
230
230
  md += `\`\`\`\n\n`;
231
- md += `\`gencowAuth()\` 사용 시 반드시 \`VITE_API_URL\` 환경변수를 전달하세요:\n\n`;
231
+ md += `\`createAuthClient()\` 사용 시 반드시 \`VITE_API_URL\` 환경변수를 전달하세요:\n\n`;
232
232
  md += `\`\`\`typescript\n`;
233
- md += `const { signIn, useAuth } = gencowAuth(import.meta.env.VITE_API_URL);\n`;
233
+ md += `const { signIn, useAuth } = createAuthClient(import.meta.env.VITE_API_URL);\n`;
234
234
  md += `\`\`\`\n\n`;
235
235
  md += `> ⚠️ \`VITE_API_URL\`은 \`.env\`에 설정하세요. \`gencow init\` / \`gencow deploy\` 시 자동으로 설정됩니다.\n\n`;
236
236
  return md;
@@ -288,9 +288,12 @@ export function buildAiPrompt(apiObj, namespaces) {
288
288
  for (const m of fns.mutations) md += ` - api.${ns}.${m} (mutation)\n`;
289
289
  }
290
290
  md += `\n`;
291
- md += `React Hook 사용법:\n`;
292
- md += ` useQuery(api.namespace.fnName) // 데이터 구독 (실시간 자동 갱신)\n`;
293
- md += ` useMutation(api.namespace.fnName) // 데이터 변경 (로딩/에러 자동 관리)\n`;
291
+ md += `React Hook 사용법 (TanStack Query 호환):\n`;
292
+ md += ` const { data, isLoading, error } = useQuery(api.namespace.fnName) // 데이터 구독\n`;
293
+ md += ` const { mutate, isPending, error } = useMutation(api.namespace.fnName) // 데이터 변경\n`;
294
+ md += ` // ⚠️ useQuery는 { data, isLoading, error } 객체를 반환해. data가 undefined이면 로딩 중.\n`;
295
+ md += ` // ⚠️ useMutation은 { mutate, mutateAsync, isPending, error } 객체를 반환해.\n`;
296
+ md += ` // TanStack Query처럼 튜플이 아닌 객체야! const [fn] = useMutation(...) 은 에러!\n`;
294
297
  md += `\n`;
295
298
  md += `⚠️ 중요 규칙:\n`;
296
299
  md += `- 반드시 useQuery와 useMutation을 사용해서 데이터와 연결해줘.\n`;
@@ -332,7 +335,7 @@ export function buildAiPrompt(apiObj, namespaces) {
332
335
  md += `- import { ai } from "./ai"; + ai.chat()을 사용해서 AI를 호출하고, OpenAI SDK를 직접 설치하지 마.\n`;
333
336
  md += `배포 규칙:\n`;
334
337
  md += `- 백엔드: \`npx gencow deploy\` (gencow/ 폴더만 배포됨. 프론트엔드는 포함 안 됨)\n`;
335
- md += `- 풀스택: VITE_API_URL=https://{앱ID}.gencow.app npm run build 후 \`npx gencow deploy --static dist/\`\n`;
338
+ md += `- 풀스택: VITE_API_URL=https://{앱ID}.{도메인} npm run build 후 \`npx gencow deploy --static dist/\`\n`;
336
339
  md += ` → 백엔드가 감지되면 자동으로 백엔드 먼저 배포 후 프론트엔드 배포\n`;
337
340
  md += `- 프론트엔드만 배포: \`npx gencow deploy --static --no-backend dist/\`\n`;
338
341
  md += `- 환경변수는 \`npx gencow env set KEY=VALUE\`로 클라우드에 설정해. 즉시 반영 (재시작 불필요).\n`;
@@ -351,7 +354,7 @@ export function buildAiPrompt(apiObj, namespaces) {
351
354
  md += ` import { crud } from "@gencow/core";\n`;
352
355
  md += ` export const { get, list, create, update, remove } = crud(tasks);\n`;
353
356
  md += `- ⚠️ crud().list 반환값은 { data: T[], total: number } 객체야. 배열이 아님!\n`;
354
- md += ` 프론트: const result = useQuery(api.tasks.list); result?.data.map(t => ...) ← .data 필수\n`;
357
+ md += ` 프론트: const { data: result } = useQuery(api.tasks.list); result?.data.map(t => ...) ← .data 필수\n`;
355
358
  md += ` result?.total → 전체 건수 (페이지네이션용)\n`;
356
359
  md += `- crud() list는 페이지네이션, 검색, 정렬, 필터링을 내장 지원해:\n`;
357
360
  md += ` useQuery(api.tasks.list, { page: 2, limit: 20 }) // 페이지네이션\n`;
@@ -472,7 +475,7 @@ export function buildDeploySection() {
472
475
  md += `npx gencow env set DATABASE_URL=postgres://... # 클라우드에 설정\n`;
473
476
  md += `npx gencow env list # 클라우드 환경변수 목록\n`;
474
477
  md += `\`\`\`\n\n`;
475
- md += `> 배포 후 앱은 \`https://{앱이름}.gencow.app\`에서 접근 가능\n\n`;
478
+ md += `> 배포 후 앱은 \`https://{앱이름}.{도메인}\`에서 접근 가능\n\n`;
476
479
  md += `### 환경변수 관리\n\n`;
477
480
  md += `| 명령어 | 설명 |\n`;
478
481
  md += `| :--- | :--- |\n`;
@@ -504,7 +507,7 @@ export function buildDeploySection() {
504
507
  md += `\`\`\`bash\n`;
505
508
  md += `# 1. 백엔드 URL을 환경변수로 빌드\n`;
506
509
  md += `cd frontend\n`;
507
- md += `VITE_API_URL=https://{앱ID}.gencow.app npm run build\n\n`;
510
+ md += `VITE_API_URL=https://{앱ID}.{도메인} npm run build\n\n`;
508
511
  md += `# 2. --static 배포 — 백엔드가 감지되면 자동으로 백엔드 먼저 배포 후 프론트엔드 배포\n`;
509
512
  md += `gencow deploy --static dist/\n`;
510
513
  md += `\`\`\`\n\n`;
@@ -532,7 +535,7 @@ export function buildDeploySection() {
532
535
  md += `> 📦 langfuse, axios, cheerio 등 추가 패키지도 \`npm install\` 후 \`gencow deploy\`하면 자동 설치됩니다.\n`;
533
536
  md += `> ⛔ \`child_process\`, \`vm\`, \`os\`, \`cluster\`, \`worker_threads\` 모듈은 보안상 차단됩니다.\n\n`;
534
537
  md += `### CORS 설정\n\n`;
535
- md += `- \`*.gencow.app\` 서브도메인 간 요청은 **자동 허용**됩니다.\n`;
538
+ md += `- \`*.{BASE_DOMAIN}\` 서브도메인 간 요청은 **자동 허용**됩니다.\n`;
536
539
  md += `- 커스텀 도메인에서 API를 호출하려면 환경변수를 설정하세요:\n\n`;
537
540
  md += `\`\`\`bash\n`;
538
541
  md += `gencow env set CORS_ORIGINS=https://myapp.com,https://www.myapp.com # 클라우드에 설정\n`;
@@ -594,7 +597,7 @@ export function buildDevTips() {
594
597
  md += ` - ⚠️ drizzle.config.ts 없으면 generate 실패 경고 출력 (deploy는 계속 진행, 하지만 migrations/ 없으면 플랫폼이 스키마 스킵)\n`;
595
598
  md += `- MCP 서버를 사용하면 AI가 이 구조를 자동으로 인식합니다.\n`;
596
599
  md += `- 로컬 개발: \`gencow dev\` — 로컬 서버 시작\n`;
597
- md += `- 배포된 앱: \`https://{앱ID}.gencow.app\` — .env의 VITE_API_URL에 자동 설정됨\n`;
600
+ md += `- 배포된 앱: \`https://{앱ID}.{도메인}\` — .env의 VITE_API_URL에 자동 설정됨\n`;
598
601
  md += `- Self-fetch: \`process.env.GENCOW_INTERNAL_URL\` — cron/mutation에서 다른 함수 호출 시 사용 (자동 설정)\n`;
599
602
  md += `- .env 파일은 로컬 전용. 클라우드에는 \`gencow env push\`로 올리세요.\n`;
600
603
  return md;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gencow",
3
- "version": "0.1.112",
3
+ "version": "0.1.114",
4
4
  "description": "Gencow — AI Backend Engine",
5
5
  "type": "module",
6
6
  "bin": {
@@ -117,7 +117,7 @@ console.log(`✅ Core bundled for codegen: ${(await import("fs")).statSync(resol
117
117
  // ── Dashboard 정적 파일 복사 ──────────────────────────
118
118
  // apps/dashboard/ 의 Vite 빌드 결과 (dist/) → packages/cli/dashboard/
119
119
  // npm publish 시 함께 배포되어 로컬 개발 시 /_dashboard/ 대시보드 제공
120
- // (클라우드에서는 gencow.dev/_cloud/ 사용, /_dashboard/ 비활성화)
120
+ // (클라우드에서는 gencow.app/ 에서 Cloud Dashboard 사용, /_dashboard/ 비활성화)
121
121
  //
122
122
  const dashboardSrc = resolve(cliRoot, "../../apps/dashboard/dist");
123
123
  const dashboardDest = resolve(cliRoot, "dashboard");