PageSourceSearch

https://order-agent-ffr7.onrender.com/static/js/api.js?v=20260723f

js order-agent-ffr7.onrender.com collected 2026-10-03 11:41:56 UTC 18,283 bytes, 450 lines download raw bytes

1/* API 호출 헬퍼 (JWT 인증 지원) */
2const API_BASE = window.location.origin;
3
4const api = {
5  // ── JWT 토큰 관리 ──
6  getToken() {
7    return localStorage.getItem("jwt_token") || "";
8  },
9  setToken(token) {
10    localStorage.setItem("jwt_token", token);
11  },
12  clearToken() {
13    localStorage.removeItem("jwt_token");
14  },
15  isLoggedIn() {
16    return !!this.getToken();
17  },
18
19  // ── 공통 헤더 ──
20  _headers(contentType = "application/json") {
21    const h = {};
22    if (contentType) h["Content-Type"] = contentType;
23    const token = this.getToken();
24    if (token) h["Authorization"] = `Bearer ${token}`;
25    return h;
26  },
27
28  // ── 공통 요청 ──
29  async request(method, path, body = null) {
30    const opts = {
31      method,
32      headers: this._headers(),
33    };
34    if (body) opts.body = JSON.stringify(body);
35
36    const res = await fetch(API_BASE + path, opts);
37
38    // 401 → 로그인 필요
39    if (res.status === 401) {
40      this.clearToken();
41      if (typeof window.onAuthRequired === "function") {
42        window.onAuthRequired();
43      }
44      throw new Error("인증이 만료되었습니다. 다시 로그인해주세요.");
45    }
46
47    if (!res.ok) {
48      const err = await res.json().catch(() => ({ detail: res.statusText }));
49      const msg = err.detail || "API 오류";
50      const e = new Error(msg);
51      e.status = res.status;
52      throw e;
53    }
54    return res.json();
55  },
56
57  get:    (path) => api.request("GET", path),
58  post:   (path, body) => api.request("POST", path, body),
59  put:    (path, body) => api.request("PUT", path, body),
60  patch:  (path, body) => api.request("PATCH", path, body),
61  delete: (path) => api.request("DELETE", path),
62
63  // 폼 데이터 (파일 업로드) — JWT 헤더 포함
64  async postForm(path, formData) {
65    const headers = {};
66    const token = this.getToken();
67    if (token) headers["Authorization"] = `Bearer ${token}`;
68
69    const res = await fetch(API_BASE + path, {
70      method: "POST",
71      body: formData,
72      headers,
73    });
74
75    if (res.status === 401) {
76      this.clearToken();
77      if (typeof window.onAuthRequired === "function") window.onAuthRequired();
78      throw new Error("인증이 만료되었습니다. 다시 로그인해주세요.");
79    }
80    if (!res.ok) throw new Error(await res.text());
81    return res.json();
82  },
83
84  // ── 인증 API (토큰 불필요) ──
85  async login(empCd, password) {
86    const res = await fetch(API_BASE + "/api/auth/login", {
87      method: "POST",
88      headers: { "Content-Type": "application/json" },
89      body: JSON.stringify({ emp_cd: empCd, password }),
90    });
91    if (!res.ok) {
92      const err = await res.json().catch(() => ({ detail: "로그인 실패" }));
93      throw new Error(err.detail || "로그인 실패");
94    }
95    const data = await res.json();
96    if (data.token) this.setToken(data.token);
97    return data;
98  },
99
100  async getEmployees() {
101    const res = await fetch(API_BASE + "/api/auth/employees");
102    if (!res.ok) throw new Error("직원 목록 조회 실패");
103    return res.json();
104  },
105
106  async refreshToken() {
107    try {
108      const data = await this.post("/api/auth/refresh");
109      if (data.token) this.setToken(data.token);
110      return data;
111    } catch (e) {
112      this.clearToken();
113      throw e;
114    }
115  },
116
117  async getMe() {
118    return this.get("/api/auth/me");
119  },
120
121  logout() {
122    this.clearToken();
123    if (typeof window.onAuthRequired === "function") window.onAuthRequired();
124  },
125
126  // ── 거래처 ──
127  getCustomers:  () => api.get("/api/customers/"),
128  customerSyncERP() { return api.post("/api/customers/sync-erp"); },
129  customerCount() { return api.get("/api/customers/count"); },
130  // ── 발주서 처리 ──
131  processOrder:  (body) => api.post("/api/orders/process", body),
132  confirmOrder:  (body) => api.post("/api/orders/confirm", body),
133  submitERP:     (orderId, empCd = "") => api.post(`/api/orders/submit-erp/${orderId}?emp_cd=${encodeURIComponent(empCd)}`),
134  listOrders:    (limit = 20) => api.get(`/api/orders/list?limit=${limit}`),
135  getOrder:      (orderId) => api.get(`/api/orders/${orderId}`),
136  // 이미지 OCR 처리
137  async processImage(formData) {
138    const headers = {};
139    const token = this.getToken();
140    if (token) headers["Authorization"] = `Bearer ${token}`;
141
142    const res = await fetch(API_BASE + "/api/orders/process-image", {
143      method: "POST",
144      body: formData,
145      headers,
146    });
147    if (res.status === 401) {
148      this.clearToken();
149      if (typeof window.onAuthRequired === "function") window.onAuthRequired();
150      throw new Error("인증이 만료되었습니다.");
151    }
152    if (!res.ok) {
153      const err = await res.json().catch(() => ({ detail: res.statusText }));
154      throw new Error(err.detail || "OCR 처리 오류");
155    }
156    return res.json();
157  },
158
159  // ── 견적서입력 ──
160  processSaleOrder:  (body) => api.post("/api/sale-orders/process", body),
161  confirmSaleOrder:  (body) => api.post("/api/sale-orders/confirm", body),
162  submitSaleOrderERP: (orderId, empCd = "") => api.post(`/api/sale-orders/submit-erp/${orderId}?emp_cd=${encodeURIComponent(empCd)}`),
163  listSaleOrders:    (limit = 20) => api.get(`/api/sale-orders/list?limit=${limit}`),
164  getSaleOrder:      (orderId) => api.get(`/api/sale-orders/${orderId}`),
165  async processSaleOrderImage(formData) {
166    const headers = {};
167    const token = this.getToken();
168    if (token) headers["Authorization"] = `Bearer ${token}`;
169
170    const res = await fetch(API_BASE + "/api/sale-orders/process-image", {
171      method: "POST",
172      body: formData,
173      headers,
174    });
175    if (res.status === 401) {
176      this.clearToken();
177      if (typeof window.onAuthRequired === "function") window.onAuthRequired();
178      throw new Error("인증이 만료되었습니다.");
179    }
180    if (!res.ok) {
181      const err = await res.json().catch(() => ({ detail: res.statusText }));
182      throw new Error(err.detail || "OCR 처리 오류");
183    }
184    return res.json();
185  },
186
187  // ── 자료관리 ──
188  materialsSyncAll:     () => api.post("/api/materials/sync"),
189  materialsSyncOne:     (sourceId) => api.post(`/api/materials/sync/${sourceId}`),
190  materialsSearch:      (q, vendor = "", limit = 20) => {
191    const params = new URLSearchParams({ q, limit });
192    if (vendor) params.append("vendor", vendor);
193    return api.get(`/api/materials/search?${params}`);
194  },
195  materialsSearchDocs:  (q, category = "", limit = 20) => {
196    const params = new URLSearchParams({ q, limit });
197    if (category) params.append("category", category);
198    return api.get(`/api/materials/search-docs?${params}`);
199  },
200  materialsSources:     () => api.get("/api/materials/sources"),
201  materialsAddSource:   (body) => api.post("/api/materials/sources", body),
202  materialsDeleteSource:(id) => api.delete(`/api/materials/sources/${id}`),
203  materialsToggleSource:(id) => api.put(`/api/materials/sources/${id}/toggle`),
204
205  // ── 상담봇 (assistant) ──
206  // 주의: 자료검색(/api/materials/drive/*)과는 **다른 자료**다.
207  //       자료검색은 기존 Drive 폴더, 상담봇은 인증·기술자료 KB 번들을 본다. 섞지 말 것.
208  //
209  // 전용 요청 함수를 쓰는 이유: 상담봇 백엔드는 실패 사유를 detail 객체
210  // ({message, hint} / 422 검증 배열)로 돌려준다. 공용 request() 는 detail 을 그대로
211  // Error() 에 넣어 "[object Object]" 가 되므로, 화면에 사유가 안 보인다.
212  // 공용 request() 를 고치면 모든 페이지에 영향이 가므로 여기만 따로 둔다.
213  async assistantRequest(method, path, body = null) {
214    const opts = { method, headers: this._headers() };
215    if (body) opts.body = JSON.stringify(body);
216    const res = await fetch(API_BASE + path, opts);
217
218    if (res.status === 401) {
219      this.clearToken();
220      if (typeof window.onAuthRequired === "function") window.onAuthRequired();
221      throw new Error("인증이 만료되었습니다. 다시 로그인해주세요.");
222    }
223    const json = await res.json().catch(() => ({}));
224    if (!res.ok) {
225      const d = json && json.detail;
226      let msg;
227      if (typeof d === "string") msg = d;
228      else if (d && d.message) msg = d.message + (d.hint ? ` (${d.hint})` : "");
229      else if (Array.isArray(d) && d.length) {
230        msg = d.map(x => (x.loc ? x.loc.join(".") + ": " : "") + (x.msg || "")).join(" / ");
231      } else msg = "요청을 처리하지 못했습니다.";
232      const e = new Error(`HTTP ${res.status} — ${msg}`);
233      e.status = res.status;
234      e.detail = d;
235      throw e;
236    }
237    return json;
238  },
239  assistantHealth:  () => api.assistantRequest("GET", "/api/assistant/health"),
240  assistantChat:    (message, sessionId = "", mode = "internal") =>
241                      api.assistantRequest("POST", "/api/assistant/chat",
242                        { message, session_id: sessionId, mode }),
243  assistantClarify: (sessionId, choice, mode = "internal") =>
244                      api.assistantRequest("POST", "/api/assistant/clarify",
245                        { session_id: sessionId, choice, mode }),
246  /* PDF 원본 — fetch 로 받아야 JWT 를 실을 수 있다. 실패 응답은 JSON detail 을 그대로 올린다. */
247  assistantDownload: async (docId, mode = "internal") => {
248    const url = `/api/assistant/file/${encodeURIComponent(docId)}/download?mode=${mode}`;
249    const token = api.getToken();
250    const res = await fetch(url, token ? { headers: { Authorization: `Bearer ${token}` } } : {});
251    if (!res.ok) {
252      let detail = null;
253      try { detail = (await res.json()).detail; } catch (e) { /* 본문이 JSON 이 아닐 수 있다 */ }
254      const err = new Error((detail && detail.message) || `다운로드 실패 (${res.status})`);
255      err.status = res.status; err.detail = detail;
256      throw err;
257    }
258    const cd = res.headers.get("Content-Disposition") || "";
259    const m = /filename\*=UTF-8''([^;]+)/i.exec(cd);
260    return { blob: await res.blob(), filename: m ? decodeURIComponent(m[1]) : "" };
261  },
262  assistantReset:   (sessionId) => api.assistantRequest("POST", "/api/assistant/reset",
263                        { session_id: sessionId }),
264  // spec/history/file 래퍼는 두지 않는다. 상담봇 답변은 /chat 이 근거·주의문구·배지를
265  // 한 번에 묶어 주는데, 이 엔드포인트들을 UI 가 따로 부르면 그 안전장치를 우회한 원문이
266  // 화면에 붙는다(제품분석 원문, 삭제된 인증 문구, 상담이력 전문). 필요해지면 그때
267  // 화면 요구사항과 함께 추가할 것.
268
269  // ── 자료검색 (Drive 문서 브라우저) ──
270  driveCategories:     () => api.get("/api/materials/drive/categories"),
271  driveDocuments:      (category = "", q = "", limit = 200, offset = 0) => {
272    const params = new URLSearchParams({ limit, offset });
273    if (category) params.append("category", category);
274    if (q) params.append("q", q);
275    return api.get(`/api/materials/drive/documents?${params}`);
276  },
277
278  // ── 단가표 조회 (Sheets 뷰어) ──
279  priceSheetVendors:   () => api.get("/api/materials/price-sheets/vendors"),
280  priceSheetTabs:      (sourceId) => api.get(`/api/materials/price-sheets/${sourceId}/tabs`),
281  priceSheetData:      (sourceId, q = "", tab = "", limit = 500, offset = 0) => {
282    const params = new URLSearchParams({ limit, offset });
283    if (q) params.append("q", q);
284    if (tab) params.append("tab", tab);
285    return api.get(`/api/materials/price-sheets/${sourceId}?${params}`);
286  },
287
288  // ── 발주서 학습 (Training) ──
289  trainingPreviewExcel: (formData) => api.postForm("/api/training/preview-excel", formData),
290  trainingUpload: (formData) => api.postForm("/api/training/upload", formData),
291  trainingSaveJson: (body) => api.post("/api/training/save-json", body),
292  trainingPairs: (custCode = "", limit = 50) => {
293    const params = new URLSearchParams({ limit });
294    if (custCode) params.append("cust_code", custCode);
295    return api.get(`/api/training/pairs?${params}`);
296  },
297  trainingPairDetail: (pairId) => api.get(`/api/training/pairs/${pairId}`),
298  trainingDeletePair: (pairId) => api.delete(`/api/training/pairs/${pairId}`),
299  trainingStats: () => api.get("/api/training/stats"),
300
301  // ── 대량 학습 (Bulk Training) ──
302  bulkCreateSession: (formData) => api.postForm("/api/training/bulk/create-session", formData),
303  bulkExtractPo: (formData) => api.postForm("/api/training/bulk/extract-po", formData),
304  bulkSuggestMatches: (sessionId) => {
305    const fd = new FormData();
306    fd.append("session_id", sessionId);
307    return api.postForm("/api/training/bulk/suggest-matches", fd);
308  },
309  bulkConfirm: (body) => api.post("/api/training/bulk/confirm", body),
310  bulkSession: (sessionId) => api.get(`/api/training/bulk/session/${sessionId}`),
311
312  // ── 재고 조회 ──
313  inventoryAutocomplete(q, limit = 15) {
314    return api.get(`/api/inventory/autocomplete?q=${encodeURIComponent(q)}&limit=${limit}`);
315  },
316  inventorySearch(q, whCd = "", baseDate = "") {
317    const params = new URLSearchParams({ q });
318    if (whCd) params.append("wh_cd", whCd);
319    if (baseDate) params.append("base_date", baseDate);
320    return api.get(`/api/inventory/search?${params}`);
321  },
322  inventoryCheck(prodCd, whCd = "", baseDate = "") {
323    return api.post("/api/inventory/check", { prod_cd: prodCd, wh_cd: whCd, base_date: baseDate });
324  },
325
326  // ── 오더리스트 ──
327  orderlistSync(tab = "") {
328    return api.post(`/api/orderlist/sync${tab ? '?tab=' + encodeURIComponent(tab) : ''}`);
329  },
330  orderlistData(query = "", tab = "", page = 1, pageSize = 50) {
331    const params = new URLSearchParams({ page, page_size: pageSize });
332    if (tab) params.set("tab", tab);
333    if (query) params.set("query", query);
334    return api.get(`/api/orderlist/data?${params}`);
335  },
336  orderlistTabs() {
337    return api.get("/api/orderlist/tabs");
338  },
339  orderlistAutocomplete(q, limit = 15) {
340    return api.get(`/api/orderlist/autocomplete?q=${encodeURIComponent(q)}&limit=${limit}`);
341  },
342  orderlistSummary() {
343    return api.get("/api/orderlist/summary");
344  },
345
346  // ── AI 대시보드 ──
347  dashboardStats(days = 30) {
348    return api.get(`/api/dashboard/stats?days=${days}`);
349  },
350  dashboardThreshold(custCode) {
351    return api.get(`/api/dashboard/threshold/${encodeURIComponent(custCode)}`);
352  },
353
354  // ── 설정 ──
355  getModels:    () => api.get("/api/settings/models"),
356  setModel:     (modelId) => api.post("/api/settings/models", { model_id: modelId }),
357  getSettings:  () => api.get("/api/settings/"),
358
359  // ── 자료/오더리스트 자동 동기화 스케줄 ──
360  syncScheduleGet: () => api.get("/api/settings/sync-schedule"),
361  syncScheduleSet: (hour, minute, enabled) => api.request("PUT", "/api/settings/sync-schedule", { hour, minute, enabled }),
362  syncScheduleRunNow: () => api.post("/api/settings/sync-schedule/run-now"),
363
364  // ── 활동 로그 (관리자) ──
365  activityLogs(page = 1, pageSize = 50, empCd = "", action = "", dateFrom = "", dateTo = "") {
366    const params = new URLSearchParams({ page, page_size: pageSize });
367    if (empCd) params.set("emp_cd", empCd);
368    if (action) params.set("action", action);
369    if (dateFrom) params.set("date_from", dateFrom);
370    if (dateTo) params.set("date_to", dateTo);
371    return api.get(`/api/activity/logs?${params}`);
372  },
373  activitySummary() {
374    return api.get("/api/activity/summary");
375  },
376  activityEmployees() {
377    return api.get("/api/activity/employees");
378  },
379
380  // ── 택배 발송 조회 ──
381  shippingSearch(q = "", date = "", warehouse = "", page = 1, pageSize = 50) {
382    const params = new URLSearchParams({ page, page_size: pageSize });
383    if (q) params.set("q", q);
384    if (date) params.set("date", date);
385    if (warehouse) params.set("warehouse", warehouse);
386    return api.get(`/api/shipping/search?${params}`);
387  },
388  shippingDaily(date, warehouse = "") {
389    const params = new URLSearchParams({ date });
390    if (warehouse) params.set("warehouse", warehouse);
391    return api.get(`/api/shipping/daily?${params}`);
392  },
393  shippingTrack(slipNos) {
394    return api.post("/api/shipping/track", { slip_nos: slipNos });
395  },
396  shippingRegister(data) {
397    return api.post("/api/shipping/register", data);
398  },
399  shippingRegisterBulk(items) {
400    return api.post("/api/shipping/register-bulk", { items });
401  },
402  shippingUploadExcel(formData) {
403    return api.postForm("/api/shipping/upload-excel", formData);
404  },
405  shippingStats() {
406    return api.get("/api/shipping/stats");
407  },
408  shippingSync(slipNos) {
409    return api.post("/api/shipping/sync", { slip_nos: slipNos });
410  },
411  shippingAutoFetch(warehouse = "", fromDate = "", toDate = "", days = 7) {
412    const params = new URLSearchParams({ days });
413    if (warehouse) params.set("warehouse", warehouse);
414    if (fromDate) params.set("from_date", fromDate);
415    if (toDate) params.set("to_date", toDate);
416    return api.post(`/api/shipping/auto-fetch?${params}`);
417  },
418  shippingDelete(id) {
419    return api.request("DELETE", `/api/shipping/${id}`);
420  },
421
422
423  // ── 구매입력 ──
424  processPurchase:    (body) => api.post("/api/purchases/process", body),
425  confirmPurchase:    (body) => api.post("/api/purchases/confirm", body),
426  submitPurchaseERP:  (orderId, empCd = "") => api.post(`/api/purchases/submit-erp/${orderId}?emp_cd=${encodeURIComponent(empCd)}`),
427  listPurchases:      (limit = 20) => api.get(`/api/purchases/list?limit=${limit}`),
428  getPurchase:        (orderId) => api.get(`/api/purchases/${orderId}`),
429  async processPurchaseImage(formData) {
430    const headers = {};
431    const token = this.getToken();
432    if (token) headers["Authorization"] = `Bearer ${token}`;
433
434    const res = await fetch(API_BASE + "/api/purchases/process-image", {
435      method: "POST",
436      body: formData,
437      headers,
438    });
439    if (res.status === 401) {
440      this.clearToken();
441      if (typeof window.onAuthRequired === "function") window.onAuthRequired();
442      throw new Error("인증이 만료되었습니다.");
443    }
444    if (!res.ok) {
445      const err = await res.json().catch(() => ({ detail: res.statusText }));
446      throw new Error(err.detail || "OCR 처리 오류");
447    }
448    return res.json();
449  },
450};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.