byuckchon-frontend-cli 1.9.1 → 1.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -180,10 +180,10 @@ FE 전반의 규칙(폴더 구조, 네이밍, 스웨거 → 코드 변환 규칙
180
180
  | 프레임워크 | 위치 |
181
181
  | --- | --- |
182
182
  | React (Vite/CRA 등) | `src/api/api-codegen.md` |
183
- | Next.js | `lib/api/api-codegen.md` |
183
+ | Next.js | `src/lib/api/api-codegen.md` |
184
184
 
185
185
  - 이 파일은 `bc.config.json` 의 `docs` 에 자동 등록되어 **chat 시작 시 주입**됩니다.
186
- - 하나의 .md React/Next 모두 다루며(차이는 위치뿐), 규칙에 맞게 직접 다듬어 쓰면 됩니다.
186
+ - 하나의 문서에 React axios 규칙과 Next.js의 fetch Server/Client 경계 규칙이 함께 들어갑니다.
187
187
  - 이미 파일이 있으면 덮어쓰지 않습니다.
188
188
 
189
189
  ### OpenAPI / 코드 컨텍스트 — 자동 주입 (v1.4+)
@@ -193,6 +193,10 @@ FE 전반의 규칙(폴더 구조, 네이밍, 스웨거 → 코드 변환 규칙
193
193
 
194
194
  - **OpenAPI**: chat 시작 시 자동 fetch + 1시간 디스크 캐시 → 엔드포인트 요약을 시스템 프롬프트에 박음.
195
195
  - 헤더에 `openapi` 줄로 표시. 캐시 hit 면 `(cached)`, fresh fetch 면 `(live)`.
196
+ - **세션 중 서버가 스펙을 바꿔도 자동 대응 (v1.10+)**: `search_openapi` / `get_openapi_endpoint` 가
197
+ 캐시에서 엔드포인트를 못 찾으면 **딱 한 번 최신본을 다시 받아 재검색**합니다 (`🔄 OpenAPI 스펙 새로고침`).
198
+ 남용 방지를 위해 세션당 횟수·간격이 제한됩니다. "방금 스웨거 업데이트했어, 다시 읽어줘" 라고 하면
199
+ 즉시 강제 새로고침(`refresh_openapi`)합니다.
196
200
  - **코드 인덱스**: chat 시작 시 인덱스 파일이 없으면 **백그라운드에서 자동 빌드**.
197
201
  - 빌드 중에는 화면에 `📚 인덱싱 중 ...` 진행 표시. 끝나면 `✓` 메시지 한 줄.
198
202
  - OpenAI 키가 없으면 빌드를 건너뛰고 도움 메시지를 띄움 (Anthropic 은 임베딩 API 미제공).
package/bin/index.js CHANGED
@@ -28,7 +28,7 @@ const program = new Command();
28
28
  program
29
29
  .name('bc')
30
30
  .description('Byuckchon Frontend Workbench — 프로젝트 스타터 + AI 어시스턴트')
31
- .version('1.9.0');
31
+ .version('1.10.0');
32
32
 
33
33
  program
34
34
  .command('init')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "byuckchon-frontend-cli",
3
- "version": "1.9.1",
3
+ "version": "1.9.3",
4
4
  "description": "Byuckchon Frontend Workbench — project starter + AI chat + codebase RAG + OpenAPI codegen",
5
5
  "type": "module",
6
6
  "engines": {
@@ -10,12 +10,6 @@
10
10
  "byuckchon-frontend-cli": "./bin/index.js",
11
11
  "bc": "./bin/index.js"
12
12
  },
13
- "scripts": {
14
- "start": "node bin/index.js",
15
- "chat": "node bin/index.js chat",
16
- "init": "node bin/index.js init",
17
- "config": "node bin/index.js config show"
18
- },
19
13
  "dependencies": {
20
14
  "@ai-sdk/anthropic": "^3.0.85",
21
15
  "@ai-sdk/openai": "^3.0.73",
@@ -65,5 +59,11 @@
65
59
  "repository": {
66
60
  "type": "git",
67
61
  "url": "git+https://github.com/RevolutionaryWarrior/byuckchon-frontend-cli.git"
62
+ },
63
+ "scripts": {
64
+ "start": "node bin/index.js",
65
+ "chat": "node bin/index.js chat",
66
+ "init": "node bin/index.js init",
67
+ "config": "node bin/index.js config show"
68
68
  }
69
- }
69
+ }
package/src/ai/tools.js CHANGED
@@ -44,6 +44,39 @@ export function buildTools({ projectRoot, effective, onEvent = () => {}, openapi
44
44
  return _openapiDocPromise;
45
45
  }
46
46
 
47
+ // ── live refetch (서버가 세션 도중 스펙을 바꾼 경우 대비) ──
48
+ // 남용 방지: 세션당 최대 횟수 + 최소 간격 throttle.
49
+ const REFRESH_MAX = 6;
50
+ const REFRESH_MIN_INTERVAL_MS = 10_000;
51
+ let _refreshCount = 0;
52
+ let _lastRefreshAt = 0;
53
+
54
+ /**
55
+ * 캐시를 무시하고 OpenAPI 스펙을 다시 fetch 한다.
56
+ * @returns {Promise<{ doc: object|null, refreshed: boolean, reason?: string }>}
57
+ */
58
+ async function refreshOpenApiDoc() {
59
+ if (!openapiSource) return { doc: null, refreshed: false, reason: 'no-source' };
60
+
61
+ const now = Date.now();
62
+ if (_refreshCount >= REFRESH_MAX) {
63
+ return { doc: await loadOpenApiDoc(), refreshed: false, reason: 'limit' };
64
+ }
65
+ if (now - _lastRefreshAt < REFRESH_MIN_INTERVAL_MS) {
66
+ return { doc: await loadOpenApiDoc(), refreshed: false, reason: 'throttled' };
67
+ }
68
+
69
+ _refreshCount += 1;
70
+ _lastRefreshAt = now;
71
+ _openapiDocPromise = getCachedOpenApi(openapiSource, { force: true })
72
+ .then((res) => res.doc ?? null)
73
+ .catch(() => null);
74
+
75
+ const doc = await _openapiDocPromise;
76
+ onEvent({ kind: 'openapi_refreshed', ok: !!doc });
77
+ return { doc, refreshed: true };
78
+ }
79
+
47
80
  function safePath(p) {
48
81
  if (!p || typeof p !== 'string') {
49
82
  throw new Error('path 가 비어있습니다');
@@ -188,7 +221,11 @@ export function buildTools({ projectRoot, effective, onEvent = () => {}, openapi
188
221
  // ─────────── OpenAPI 툴 ───────────
189
222
 
190
223
  async function searchOpenApi({ query, limit = 40 }) {
191
- const doc = await loadOpenApiDoc();
224
+ let doc = await loadOpenApiDoc();
225
+ if (!doc) {
226
+ // 첫 로드 실패면 한 번 live refetch 시도.
227
+ ({ doc } = await refreshOpenApiDoc());
228
+ }
192
229
  if (!doc) {
193
230
  return {
194
231
  ok: false,
@@ -197,11 +234,25 @@ export function buildTools({ projectRoot, effective, onEvent = () => {}, openapi
197
234
  '(NestJS 는 보통 /api/docs 가 아니라 /api/docs-json).',
198
235
  };
199
236
  }
200
- const hits = searchEndpoints(doc, query, { limit });
237
+
238
+ let hits = searchEndpoints(doc, query, { limit });
239
+ let refreshed = false;
240
+
241
+ // 캐시된 스펙에서 못 찾으면 → 서버에서 방금 추가됐을 수 있으니 딱 한 번 live refetch 후 재검색.
242
+ if (hits.length === 0) {
243
+ const r = await refreshOpenApiDoc();
244
+ if (r.refreshed && r.doc) {
245
+ refreshed = true;
246
+ doc = r.doc;
247
+ hits = searchEndpoints(doc, query, { limit });
248
+ }
249
+ }
250
+
201
251
  return {
202
252
  ok: true,
203
253
  query,
204
254
  count: hits.length,
255
+ refreshed,
205
256
  endpoints: hits.map((e) => ({
206
257
  method: e.method,
207
258
  path: e.path,
@@ -210,17 +261,63 @@ export function buildTools({ projectRoot, effective, onEvent = () => {}, openapi
210
261
  })),
211
262
  hint:
212
263
  hits.length === 0
213
- ? '매치 없음. 다른 키워드로 재시도하거나, query 를 비워 전체 목록을 받아 path 를 직접 고르세요.'
214
- : '상세 스키마가 필요하면 get_openapi_endpoint(path, method)호출하세요.',
264
+ ? (refreshed
265
+ ? '최신 스펙을 다시 받아왔는데도 매치가 없습니다. 다른 키워드로 재시도하거나 query 비워 전체 목록을 확인하세요.'
266
+ : '매치 없음. 다른 키워드로 재시도하거나, query 를 비워 전체 목록을 받아 path 를 직접 고르세요.')
267
+ : (refreshed
268
+ ? '캐시엔 없던 항목을 최신 스펙에서 찾았습니다. 상세는 get_openapi_endpoint(path, method).'
269
+ : '상세 스키마가 필요하면 get_openapi_endpoint(path, method) 를 호출하세요.'),
215
270
  };
216
271
  }
217
272
 
218
273
  async function getOpenApiEndpoint({ path: epPath, method }) {
219
- const doc = await loadOpenApiDoc();
274
+ let doc = await loadOpenApiDoc();
275
+ if (!doc) {
276
+ ({ doc } = await refreshOpenApiDoc());
277
+ }
220
278
  if (!doc) {
221
279
  return { ok: false, error: 'OpenAPI 스펙을 불러올 수 없습니다.' };
222
280
  }
223
- return getEndpoint(doc, epPath, method);
281
+
282
+ let result = getEndpoint(doc, epPath, method);
283
+
284
+ // 못 찾으면 → 최신 스펙으로 한 번 더.
285
+ if (!result.ok) {
286
+ const r = await refreshOpenApiDoc();
287
+ if (r.refreshed && r.doc) {
288
+ const retry = getEndpoint(r.doc, epPath, method);
289
+ if (retry.ok) result = { ...retry, refreshed: true };
290
+ else result = { ...retry, refreshed: true };
291
+ }
292
+ }
293
+ return result;
294
+ }
295
+
296
+ async function refreshOpenApi() {
297
+ if (!openapiSource) {
298
+ return { ok: false, error: 'bc.config.json 에 api.openapi 가 설정되어 있지 않습니다.' };
299
+ }
300
+ const r = await refreshOpenApiDoc();
301
+ if (!r.doc) {
302
+ return {
303
+ ok: false,
304
+ refreshed: r.refreshed,
305
+ error:
306
+ r.reason === 'throttled'
307
+ ? '방금 새로고침했습니다. 잠시 후 다시 시도하세요.'
308
+ : '스펙을 다시 받아오지 못했습니다 (네트워크/URL 확인).',
309
+ };
310
+ }
311
+ const count = (r.doc.paths ? Object.keys(r.doc.paths).length : 0);
312
+ return {
313
+ ok: true,
314
+ refreshed: r.refreshed,
315
+ reason: r.refreshed ? undefined : r.reason,
316
+ paths: count,
317
+ message: r.refreshed
318
+ ? `최신 OpenAPI 스펙을 다시 받아왔습니다 (path ${count}개).`
319
+ : '최근에 이미 새로고침되어 캐시를 재사용했습니다.',
320
+ };
224
321
  }
225
322
 
226
323
  // ─────────── Figma 툴 ───────────
@@ -381,6 +478,20 @@ export function buildTools({ projectRoot, effective, onEvent = () => {}, openapi
381
478
  }),
382
479
  execute: getOpenApiEndpoint,
383
480
  }),
481
+ refresh_openapi: tool({
482
+ description:
483
+ '연결된 OpenAPI(Swagger) 스펙을 캐시 무시하고 서버에서 다시 받아온다. ' +
484
+ '사용자가 "방금 스웨거(백엔드 API) 를 업데이트했다 / 다시 읽어라" 라고 하거나, ' +
485
+ 'search_openapi 가 분명히 있어야 할 엔드포인트를 못 찾을 때 호출. ' +
486
+ '(search_openapi / get_openapi_endpoint 는 못 찾으면 자동으로 한 번 새로고침하므로, ' +
487
+ '명시적 요청이 있을 때만 직접 부르면 된다.)',
488
+ inputSchema: jsonSchema({
489
+ type: 'object',
490
+ properties: {},
491
+ additionalProperties: false,
492
+ }),
493
+ execute: refreshOpenApi,
494
+ }),
384
495
  write_file: tool({
385
496
  description:
386
497
  '새 파일을 만들거나 기존 파일을 통째로 덮어쓴다. 새 파일을 만들기 전에 반드시 1) 비슷한 기존 파일을 read_file 로 보고 2) 같은 폴더 컨벤션(barrel 파일, 네이밍, import 순서) 을 따른다.',
@@ -145,7 +145,7 @@ export async function adoptCommand(opts = {}) {
145
145
  console.log(chalk.green(`\n ✓ ${CONFIG_PATHS.projectFileName} 작성 완료.`));
146
146
  console.log(chalk.dim(` ${targetFile}`));
147
147
 
148
- // API 코드 컨벤션 .md 를 API 루트(src/api | lib/api)에 깐다 (이미 있으면 유지).
148
+ // API 코드 컨벤션 .md 를 API 루트(src/api | src/lib/api)에 깐다 (이미 있으면 유지).
149
149
  try {
150
150
  const { relPath, written } = await scaffoldApiConventionDoc({
151
151
  projectRoot: cwd,
@@ -121,6 +121,9 @@ export async function chatCommand(opts = {}) {
121
121
  '가져와서 zod/타입/요청 함수를 만든다. ' +
122
122
  '예: 사용자가 "inquiries" 라고 하면 search_openapi("inquiries") 로 ' +
123
123
  '`/api/admin/inquiries` 같은 실제 경로를 찾아낸다. ' +
124
+ '스펙은 캐시(최대 1시간)라 서버가 방금 바꿨으면 오래됐을 수 있다 — ' +
125
+ 'search_openapi/get_openapi_endpoint 는 못 찾으면 자동으로 한 번 최신본을 다시 받아온다. ' +
126
+ '사용자가 "방금 스웨거 업데이트했어/다시 읽어" 라고 하면 `refresh_openapi()` 를 먼저 호출한다. ' +
124
127
  '이미 `*.gen.ts` 가 있으면 그걸 import 해서 쓰는 것도 좋다.';
125
128
  }
126
129
  } catch {
@@ -264,6 +267,12 @@ async function runOnce({ cfg, resolved, system, prompt }) {
264
267
  effective: cfg.effective,
265
268
  openapiSource: cfg.effective.api?.openapi ?? null,
266
269
  onEvent: (ev) => {
270
+ if (ev.kind === 'openapi_refreshed') {
271
+ console.log(
272
+ chalk.dim(ev.ok ? ' 🔄 OpenAPI 스펙 새로고침' : ' ⚠️ OpenAPI 새로고침 실패'),
273
+ );
274
+ return;
275
+ }
267
276
  const label =
268
277
  ev.kind === 'write_created'
269
278
  ? '🆕'
@@ -489,6 +498,12 @@ async function runReadlineFallback({ cfg, resolved, system, session, openapiInfo
489
498
  effective: cfg.effective,
490
499
  openapiSource: cfg.effective.api?.openapi ?? null,
491
500
  onEvent: (ev) => {
501
+ if (ev.kind === 'openapi_refreshed') {
502
+ console.log(
503
+ chalk.dim(ev.ok ? '\n 🔄 OpenAPI 스펙 새로고침' : '\n ⚠️ OpenAPI 새로고침 실패'),
504
+ );
505
+ return;
506
+ }
492
507
  const label =
493
508
  ev.kind === 'write_created'
494
509
  ? '🆕 생성'
@@ -9,15 +9,15 @@ const TEMPLATE_PATH = path.resolve(
9
9
 
10
10
  /**
11
11
  * 프레임워크에 맞는 API 루트 폴더.
12
- * - Next.js → lib/api
12
+ * - Next.js → src/lib/api
13
13
  * - 그 외 React 계열 → src/api
14
14
  */
15
15
  export function apiRootForFramework(framework) {
16
- return framework === 'next' ? 'lib/api' : 'src/api';
16
+ return framework === 'next' ? 'src/lib/api' : 'src/api';
17
17
  }
18
18
 
19
19
  /**
20
- * API 코드 컨벤션 .md 를 프로젝트의 API 루트(`src/api` 또는 `lib/api`)에 깐다.
20
+ * API 코드 컨벤션 .md 를 프로젝트의 API 루트(`src/api` 또는 `src/lib/api`)에 깐다.
21
21
  *
22
22
  * @param {object} args
23
23
  * @param {string} args.projectRoot
@@ -26,7 +26,7 @@ export async function createProject(config) {
26
26
  await createPackageJson(rootDir, config);
27
27
  await createBaseFiles(rootDir, config);
28
28
  await createReadme(rootDir, config);
29
- // API 코드 컨벤션 .md 를 프레임워크에 맞는 API 루트(src/api | lib/api)에 깐다.
29
+ // API 코드 컨벤션 .md 를 프레임워크에 맞는 API 루트(src/api | src/lib/api)에 깐다.
30
30
  await scaffoldApiConventionDoc({ projectRoot: rootDir, framework: config.framework });
31
31
  await createBcConfig(rootDir, config);
32
32
 
@@ -23,8 +23,13 @@ function keyFor(input) {
23
23
  *
24
24
  * - 네트워크 실패 시: 만료된 캐시라도 있으면 그걸로 폴백 (offline-friendly).
25
25
  * - 캐시는 .bc/cache/openapi-<hash>.json 에 저장.
26
+ *
27
+ * @param {string} input OpenAPI URL 또는 파일 경로
28
+ * @param {object} [opts]
29
+ * @param {boolean} [opts.force] true 면 TTL 무시하고 무조건 live refetch (캐시 갱신).
30
+ * 서버가 세션 도중 스펙을 바꿨을 때 사용.
26
31
  */
27
- export async function getCachedOpenApi(input) {
32
+ export async function getCachedOpenApi(input, { force = false } = {}) {
28
33
  const dir = await getCacheDir();
29
34
  const file = path.join(dir, `openapi-${keyFor(input)}.json`);
30
35
 
@@ -39,7 +44,7 @@ export async function getCachedOpenApi(input) {
39
44
  /* miss */
40
45
  }
41
46
 
42
- if (cachedFresh && cached) {
47
+ if (!force && cachedFresh && cached) {
43
48
  return { doc: cached, cached: true, source: input };
44
49
  }
45
50
 
package/src/ui/ChatApp.js CHANGED
@@ -871,6 +871,18 @@ export function ChatApp({
871
871
 
872
872
  // 툴 실행 이벤트는 채팅에 시스템 메시지로 표시 (사용자가 무엇이 일어났는지 보게).
873
873
  const onToolEvent = (ev) => {
874
+ if (ev.kind === 'openapi_refreshed') {
875
+ setMessages((m) => [
876
+ ...m,
877
+ {
878
+ role: 'system-info',
879
+ text: ev.ok
880
+ ? '🔄 OpenAPI 스펙 새로고침 (서버에서 최신본 다시 받음)'
881
+ : '⚠️ OpenAPI 새로고침 실패 (네트워크/URL 확인)',
882
+ },
883
+ ]);
884
+ return;
885
+ }
874
886
  const labels = {
875
887
  write_created: '🆕 생성',
876
888
  write_overwritten: '✏️ 덮어씀',
@@ -4,7 +4,8 @@
4
4
  > 사용자가 Swagger JSON(또는 엔드포인트)을 제공하면, AI 는 이 문서의 규칙에 맞춰
5
5
  > `api / zod / type / service / index` 파일을 생성한다.
6
6
  >
7
- > 스택: `@tanstack/react-query` + `axios`. **React / Next.js 공용**이며, 차이는 §0 의 "위치"뿐이다.
7
+ > 대상: React(Vite/CRA 등), Next.js App Router
8
+ > 스택: `@tanstack/react-query` + React는 `axios`, Next.js는 native `fetch`.
8
9
 
9
10
  ---
10
11
 
@@ -16,30 +17,25 @@
16
17
  2. **기존 코드 우선.** 공용 유틸(`cacheConfig`, `queryKey`, `captureSentryError`, `metaSchema`,
17
18
  `PaginationParams` 등)은 새로 만들지 말고 그대로 가져다 쓴다. import 경로가 확실치 않으면
18
19
  `search_code` 로 실제 export 위치를 확인한다.
19
- 3. **5파일 세트는 항상 함께.** `api / zod / type / service / index` 한 번에 생성한다.
20
+ 3. **계약 파일은 항상 함께.** React는 `api / zod / type / service / index`를 한 번에 생성한다.
21
+ Next.js는 공용 `zod / type`, 서버 기본 호출 파일, 필요한 경우 Client 호출 파일과 service를 생성한다.
22
+ 이미 같은 리소스가 있으면 기존 파일 구성과 이름을 우선한다.
20
23
 
21
24
  ---
22
25
 
23
- ## 0. 위치 & 폴더 구조 ⚠️ (React vs Next 차이는 여기뿐)
26
+ ## 0. 위치 & 폴더 구조
24
27
 
25
- API 코드의 **루트 위치는 프레임워크마다 다르다.**
26
-
27
- | 프레임워크 | API 루트 |
28
- | --- | --- |
29
- | **React** (Vite/CRA 등) | `src/api` |
30
- | **Next.js** | `lib/api` |
31
-
32
- > 그 외 폴더/파일 구조와 규칙은 **완전히 동일**하다. 아래 예시는 `src/api` 기준이며,
33
- > Next 면 `src/api` 를 `lib/api` 로 바꿔 읽으면 된다.
28
+ API 루트는 React에서 `src/api`, Next.js에서 `src/lib/api`다.
34
29
 
35
30
  리소스(도메인) 하나당 폴더 하나. 폴더명은 **소문자**(여러 단어는 kebab-case: `favorite-stores`).
36
31
 
37
32
  ```
38
- src/api/ # (Next: lib/api/)
39
- ├── instance.ts # axios 인스턴스 (interceptor 포함)
33
+ src/api/ # Next.js: src/lib/api/
34
+ ├── instance.ts # React: axios 인스턴스
40
35
  ├── index.ts # 모든 API 모듈 export
41
36
  └── user/ # 리소스 폴더 (소문자)
42
- ├── user.api.ts # axios 호출 함수 (순수 함수)
37
+ ├── user.api.ts # React: axios / Next.js: Server fetch 호출
38
+ ├── user.api.client.ts # Next.js: Client 호출이 필요할 때만 생성
43
39
  ├── user.zod.ts # 응답/요청 zod 스키마
44
40
  ├── user.type.ts # zod 로부터 추론한 타입 + 입력 타입
45
41
  ├── user.service.ts # react-query 훅 (use~) — 비즈니스 로직
@@ -51,6 +47,67 @@ src/api/ # (Next: lib/api/)
51
47
 
52
48
  ---
53
49
 
50
+ ## Next.js 전용 규칙 — Server/Client 경계
51
+
52
+ > 이 섹션의 **Next.js**는 프레임워크다. 문서의 **next(무한스크롤)**와는 관련이 없다.
53
+
54
+ Next.js App Router는 API 코드를 생성하기 전에 실행 환경을 분류한다.
55
+
56
+ | 환경 | 사용처 | 허용 의존성 |
57
+ | --- | --- | --- |
58
+ | Server | Server Component, Route Handler, Server Action | `cookies`, `headers`, 서버 토큰, `serverAuthHttp` |
59
+ | Client | `'use client'`, React Query hook | 브라우저 API, `clientAuthHttp` |
60
+ | 공용 | type, 순수 zod schema, 직렬화 가능한 상수 | 서버/브라우저 전용 의존성 없음 |
61
+
62
+ ### 공용 barrel에서 Server 모듈을 내보내지 않는다
63
+
64
+ Client Component가 공용 `index.ts`를 가져올 때 그 barrel이 Server 모듈까지 export하면 다음 오류가 발생할 수 있다.
65
+
66
+ ```text
67
+ This module cannot be imported from a Client Component module.
68
+ It should only be used from a Server Component.
69
+ ```
70
+
71
+ ```ts
72
+ // src/lib/api/http/index.ts
73
+ export * from './httpBase';
74
+ export * from './publicHttp';
75
+ export * from './clientAuthHttp';
76
+ // 금지: export * from './serverAuthHttp';
77
+ ```
78
+
79
+ `serverAuthHttp`와 Server API는 barrel을 거치지 않고 절대경로로 직접 import한다.
80
+
81
+ ```ts
82
+ import { serverAuthHttp } from '@/lib/api/http/serverAuthHttp';
83
+ ```
84
+
85
+ - Server 파일은 가능하면 `import 'server-only';`로 경계를 표시한다.
86
+ - 인증 호출은 Server를 기본으로 하고 Client에서도 호출해야 할 때만 `*.client.ts`를 만든다.
87
+ - Client service는 Client 호출 파일만 import하며 Server 파일을 간접 참조하지 않는다.
88
+ - `useQuery`/`useMutation`을 export하는 service는 Client 전용이다.
89
+ - type과 zod schema만 Server/Client가 공유한다.
90
+
91
+ ### HTTP와 인증 컨텍스트를 분리한다
92
+
93
+ - `serverAuthHttp`: 서버 쿠키·헤더·비공개 토큰 사용. 공용 barrel export 금지.
94
+ - `clientAuthHttp`: 브라우저에 노출 가능한 인증 상태만 사용.
95
+ - `publicHttp`: 양쪽에서 안전할 때만 공용 사용.
96
+ - 기존 fetch wrapper를 먼저 찾는다. 없으면 자동 생성하지 말고 fetch/auth 방식을 사용자에게 묻는다.
97
+ - `cookies()`/`headers()`는 모듈 최상위가 아니라 요청 함수 내부에서 호출한다.
98
+ - 서버 비밀값에 `NEXT_PUBLIC_`을 붙이지 않는다.
99
+ - 기존 Route Handler/BFF, base URL, CORS, Edge Runtime 정책을 확인하고 재사용한다.
100
+
101
+ ### 캐시와 전달 값을 구분한다
102
+
103
+ - React Query 무효화는 Client 캐시만 갱신한다.
104
+ - Next.js의 `fetch` cache/`next`, `revalidatePath`/`revalidateTag`, Cache Components는 기존 정책을 따른다.
105
+ - 사용자별 인증 응답을 전역 캐시하지 않는다.
106
+ - Server에서 Client로 응답 wrapper, `Headers`, `Error`, 함수 등을 전달하지 않고 JSON 직렬화 가능한 data만 전달한다.
107
+ - Server Action은 요청받았거나 기존 프로젝트가 같은 패턴을 사용할 때만 만든다.
108
+
109
+ ---
110
+
54
111
  ## 1. 가장 먼저 — "next(무한스크롤)" 인지 판단하라
55
112
 
56
113
  > 여기서 말하는 "next" 는 **프레임워크 Next.js 가 아니라**, **커서 기반 무한스크롤 패턴**을 가리킨다.
@@ -76,13 +133,17 @@ src/api/ # (Next: lib/api/)
76
133
 
77
134
  ### 2-1. `user.api.ts` — 순수 호출 함수
78
135
 
79
- - `import baseInstance from '../instance';` 사용 (axios 인스턴스).
80
- - 함수는 `async`, 내부에서 `const { data } = await baseInstance.X(...)` 후 `return data;`.
81
- - 응답 본문이 없는 경우(`204 No Content`) `return data` 생략 가능.
82
- - 쿼리스트링은 `{ params }`, path 파라미터는 템플릿 리터럴.
136
+ - React는 `baseInstance`(axios)를 사용한다.
137
+ - Next.js는 Server 파일에서 기존 `serverAuthHttp`를 절대경로로 import한다. Client 호출이 필요할 때만
138
+ `*.client.ts`를 만들고 기존 `clientAuthHttp`를 사용한다. wrapper가 없으면 자동 생성하지 않는다.
139
+ - React는 `const { data } = await baseInstance.X(...)` 후 `data`를 반환한다.
140
+ - Next.js는 fetch wrapper의 generic에 응답 타입을 전달하고 JSON data를 반환받는다.
141
+ - 쿼리스트링은 기존 wrapper 규칙(axios의 `{ params }`, fetch wrapper의 `searchParams` 등)을 따른다.
142
+ path 파라미터는 템플릿 리터럴을 사용한다.
83
143
  - **여기서는 zod 파싱을 하지 않는다.** (파싱은 service 의 queryFn 책임)
84
144
 
85
145
  ```ts
146
+ // React
86
147
  import baseInstance from '../instance';
87
148
 
88
149
  const RESOURCE = '/api/user';
@@ -106,6 +167,18 @@ export const deleteUser = async (userId: string) => {
106
167
  };
107
168
  ```
108
169
 
170
+ ```ts
171
+ // Next.js Server
172
+ import { serverAuthHttp } from '@/lib/api/http/serverAuthHttp';
173
+ import type { UserItem } from './user.type';
174
+
175
+ const RESOURCE = '/api/user';
176
+
177
+ export const getUser = async (userId: string) => {
178
+ return serverAuthHttp<UserItem>(`${RESOURCE}/${userId}`);
179
+ };
180
+ ```
181
+
109
182
  ### 2-2. `user.zod.ts` — 스키마
110
183
 
111
184
  - `import { z } from 'zod';`
@@ -213,6 +286,7 @@ export const useDeleteUser = () => {
213
286
  ### 2-5. `index.ts` (리소스) — 노출
214
287
 
215
288
  - **`service` 와 `type` 만** 재노출 (`api`, `zod` 는 노출하지 않음).
289
+ - Next.js에서는 Server 호출 파일을 Client가 접근 가능한 barrel에서 재노출하지 않는다.
216
290
 
217
291
  ```ts
218
292
  export * from './user.service';
@@ -230,6 +304,7 @@ export * from './user';
230
304
 
231
305
  ### 2-7. `src/api/instance.ts` — axios 인스턴스
232
306
 
307
+ - React 전용 규칙이다.
233
308
  - 이미 있으면 **건드리지 않는다.** baseURL/interceptor 설정이 여기 모여 있다.
234
309
  - 새 리소스는 항상 이 `baseInstance` 를 import 해서 쓴다.
235
310
 
@@ -374,7 +449,7 @@ export const useGetUser = (options?: Record<string, any>) => {
374
449
 
375
450
  ## 7. 생성 시 체크리스트 ✅
376
451
 
377
- 1. [ ] 위치를 맞췄다 (React `src/api` / Next `lib/api`).
452
+ 1. [ ] 위치를 맞췄다 (React `src/api` / Next.js `src/lib/api`).
378
453
  2. [ ] 엔드포인트가 **next(무한스크롤)** 인지 판단했다. (§1)
379
454
  3. [ ] `api / zod / type / service / index` 5파일을 모두 만들었다.
380
455
  4. [ ] Swagger 응답을 zod 로 정확히 매핑(nullable/optional/enum/date/$ref).
@@ -385,6 +460,8 @@ export const useGetUser = (options?: Record<string, any>) => {
385
460
  9. [ ] 리소스 `index.ts` + 루트 `index.ts` 노출 추가.
386
461
  10. [ ] next 면 `useInfiniteQuery` + `metaSchema` + `select` 평탄화 적용.
387
462
  11. [ ] 공용 유틸을 재사용하고, Swagger 에 없는 필드를 추측으로 만들지 않았다.
463
+ 12. [ ] Next.js면 Server/Client 환경을 분류하고 Server 모듈을 공용 barrel에서 내보내지 않았다.
464
+ 13. [ ] Next.js면 기존 fetch/auth wrapper와 캐시·runtime 정책을 확인했다.
388
465
 
389
466
  ---
390
467
 
@@ -397,4 +474,5 @@ export const useGetUser = (options?: Record<string, any>) => {
397
474
  - ❌ Swagger 의 `nullable` 무시하고 필수로 선언.
398
475
  - ❌ 일반 목록인데 `useInfiniteQuery` 사용 (또는 그 반대).
399
476
  - ❌ 공용 타입/유틸(`metaSchema`, `PaginationParams`)을 중복 재정의.
400
- - ❌ React 인데 `lib/api`, Next 인데 `src/api` 만드는 위치 실수.
477
+ - ❌ React Next.js의 API 루트 또는 axios/fetch 규칙을 혼용.
478
+ - ❌ Next.js 공용 `index.ts`에서 Server 전용 API나 `serverAuthHttp`를 export.