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.