rocketsell 0.1.0

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 (3) hide show
  1. package/README.md +108 -0
  2. package/dist/cli.js +2 -0
  3. package/package.json +44 -0
package/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # rocketsell
2
+
3
+ 위탁셀링(드랍쉬핑) 자동화 CLI — Cafe24, Coupang, SmartStore, Shopify 통합 관리
4
+
5
+ ## 설치
6
+
7
+ ```bash
8
+ npm install -g rocketsell
9
+ ```
10
+
11
+ 또는 npx로 바로 실행:
12
+
13
+ ```bash
14
+ npx rocketsell --help
15
+ ```
16
+
17
+ ## 빠른 시작
18
+
19
+ ```bash
20
+ # 1. 채널 credentials 설정
21
+ rocketsell init
22
+
23
+ # 2. 연결 상태 확인
24
+ rocketsell status
25
+
26
+ # 3. 채널별 기능 비교
27
+ rocketsell cap
28
+ ```
29
+
30
+ ## 명령어
31
+
32
+ ### 설정
33
+
34
+ | 명령어 | 설명 |
35
+ |--------|------|
36
+ | `init` | 대화형 채널 설정 |
37
+ | `config list` | 현재 설정 확인 |
38
+ | `config get <channel>` | 채널 설정 조회 |
39
+ | `config set <channel> <key> <value>` | 설정 변경 |
40
+ | `config remove <channel>` | 채널 삭제 |
41
+
42
+ ### 상품
43
+
44
+ | 명령어 | 설명 |
45
+ |--------|------|
46
+ | `product list <channel>` | 상품 목록 |
47
+ | `product get <channel> <id>` | 상품 상세 |
48
+ | `product register <channel> --file <path>` | JSON으로 상품 등록 |
49
+ | `product update <channel> <id> --file <path>` | 상품 수정 |
50
+ | `product delete <channel> <id>` | 상품 삭제 |
51
+
52
+ ### 주문
53
+
54
+ | 명령어 | 설명 |
55
+ |--------|------|
56
+ | `order list <channel> [--days N]` | 최근 주문 (기본 7일) |
57
+ | `order get <channel> <id>` | 주문 상세 |
58
+ | `order confirm <channel> <id>` | 주문 확인 |
59
+ | `order cancel <channel> <id>` | 주문 취소 |
60
+
61
+ ### 송장
62
+
63
+ ```bash
64
+ rocketsell invoice register <channel> <orderId> <carrier> <trackingNumber>
65
+ ```
66
+
67
+ 택배사 코드: `cj`, `hanjin`, `lotte`, `post`, `logen` (또는 채널별 원래 코드)
68
+
69
+ ### 재고
70
+
71
+ | 명령어 | 설명 |
72
+ |--------|------|
73
+ | `inventory sync <channel> <productId> <qty>` | 재고 업데이트 |
74
+ | `inventory plan <totalStock> [sku]` | 재고 분배 시뮬레이션 |
75
+
76
+ ### 기타
77
+
78
+ | 명령어 | 설명 |
79
+ |--------|------|
80
+ | `status` | 전체 채널 상태 |
81
+ | `cap [channel]` | API 기능 현황 |
82
+ | `rate-test` | Rate limiter 테스트 |
83
+
84
+ ## 채널별 특이사항
85
+
86
+ | 채널 | 점수 | 주의사항 |
87
+ |------|------|----------|
88
+ | Shopify | 92/100 | GraphQL, delta 재고, 80+ 웹훅 |
89
+ | Cafe24 | 79/100 | 2 req/s 제한, 절대값 재고 |
90
+ | Coupang | 54/100 | 180일 키 만료, 웹훅 없음, 050번호만 |
91
+ | SmartStore | 44/100 | 웹훅 없음, 재고 API 없음, 24시간 윈도우 |
92
+
93
+ ## 설정 저장 위치
94
+
95
+ - 파일: `~/.rocketsell/config.json`
96
+ - 환경변수 우선: `CAFE24_MALL_ID`, `COUPANG_VENDOR_ID` 등
97
+
98
+ ## MCP 서버 모드
99
+
100
+ AI 에이전트 연동이 필요한 경우 MCP 서버로도 실행 가능:
101
+
102
+ ```bash
103
+ rocketsell-mcp # 또는 node dist/index.js
104
+ ```
105
+
106
+ ## 라이선스
107
+
108
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ var t=Object.defineProperty,n=Object.getOwnPropertyNames,e=(t,e)=>function(){return t&&(e=(0,t[n(t)[0]])(t=0)),e},r=(n,e)=>{for(var r in e)t(n,r,{get:e[r],enumerable:!0})};import{renameSync as o,writeFileSync as s}from"fs";var a,c,i=e({"src/lib/atomic-write.ts"(){}});import{existsSync as l,mkdirSync as d,readFileSync as u}from"fs";import{homedir as p}from"os";import{join as h}from"path";function f(){l(a)||d(a,{recursive:!0})}function g(){if(f(),!l(c))return{channels:{},sourceChannels:{}};const t=u(c,"utf-8"),n=JSON.parse(t);return{channels:n.channels??{},sourceChannels:n.sourceChannels??{},database:n.database,budgets:n.budgets,pricingRules:n.pricingRules,usdToKrw:n.usdToKrw}}function m(t){f(),function(t,n){const e=`${t}.tmp`;s(e,JSON.stringify(n,null,"\t"),"utf-8"),o(e,t)}(c,t)}function v(t){const n=function(t){switch(t){case"cafe24":{const t=process.env.CAFE24_MALL_ID,n=process.env.CAFE24_CLIENT_ID,e=process.env.CAFE24_CLIENT_SECRET;return t&&n&&e?{channel:"cafe24",storeId:t,clientId:n,clientSecret:e,accessToken:process.env.CAFE24_ACCESS_TOKEN,refreshToken:process.env.CAFE24_REFRESH_TOKEN}:null}case"coupang":{const t=process.env.COUPANG_VENDOR_ID,n=process.env.COUPANG_ACCESS_KEY,e=process.env.COUPANG_SECRET_KEY;return t&&n&&e?{channel:"coupang",storeId:t,accessKey:n,secretKey:e,keyExpiresAt:process.env.COUPANG_KEY_EXPIRES_AT??""}:null}case"smartstore":{const t=process.env.SMARTSTORE_ACCOUNT_ID??"default",n=process.env.SMARTSTORE_CLIENT_ID,e=process.env.SMARTSTORE_CLIENT_SECRET;return n&&e?{channel:"smartstore",storeId:t,clientId:n,clientSecret:e}:null}case"shopify":{const t=process.env.SHOPIFY_STORE,n=process.env.SHOPIFY_ACCESS_TOKEN;return t&&n?{channel:"shopify",storeId:t,accessToken:n,apiVersion:process.env.SHOPIFY_API_VERSION??"2024-10"}:null}default:return null}}(t);return n||(g().channels[t]??null)}function y(t,n){const e=g();e.channels[t]=n,m(e)}function w(){return a}function b(){return c}function $(t){const n=function(t){switch(t){case"domeggook":{const t=process.env.DOMEGGOOK_API_KEY;return t?{channel:"domeggook",apiKey:t}:null}case"specialoffer":{const t=process.env.SPECIALOFFER_API_KEY;return t?{channel:"specialoffer",apiKey:t}:null}case"cjdropshipping":{const t=process.env.CJDROPSHIPPING_ACCESS_TOKEN;return t?{channel:"cjdropshipping",accessToken:t}:null}default:return null}}(t);return n||(g().sourceChannels[t]??null)}function k(t,n){const e=g();e.sourceChannels[t]=n,m(e)}var S,T,I,x,E,C=e({"src/lib/config-store.ts"(){i(),a=h(p(),".rocketsell"),c=h(a,"config.json")}}),N=e({"src/types/channel.ts"(){S={cafe24:{maxRps:2,intervalMs:500},coupang:{maxRps:10,intervalMs:100},smartstore:{maxRps:5,intervalMs:200},shopify:{maxRps:4,intervalMs:250}},T={cafe24:!0,coupang:!1,smartstore:!1,shopify:!0},I=["cafe24","coupang","smartstore","shopify"]}}),O=e({"src/types/source.ts"(){x={domeggook:{maxRps:3,intervalMs:334,dailyLimit:15e3},specialoffer:{maxRps:2,intervalMs:500},cjdropshipping:{maxRps:2,intervalMs:500}},E=["domeggook","specialoffer","cjdropshipping"]}}),D=e({"src/types/index.ts"(){N(),O()}});import{existsSync as A,readFileSync as L}from"fs";import{join as _}from"path";function P(){return(new Date).toISOString().slice(0,10)}function M(){const t=_(w(),"daily-usage.json");if(!A(t))return{date:P(),usage:{}};try{const n=L(t,"utf-8"),e=JSON.parse(n);return e.date!==P()?{date:P(),usage:{}}:{date:e.date??P(),usage:e.usage??{}}}catch{return{date:P(),usage:{}}}}function R(t,n){const e=M().usage[t]??0,r=n??null;return{used:e,limit:r,remaining:null!==r?Math.max(0,r-e):null}}var U,j,q,B,J,F,z=e({"src/lib/daily-usage.ts"(){i(),C()}}),H=e({"src/lib/rate-limiter.ts"(){D(),O(),z(),U=class extends Error{constructor(t,n,e){super(`${t} 일일 한도 초과: ${n.toLocaleString()} / ${e.toLocaleString()}`),this.channel=t,this.used=n,this.limit=e,this.name="DailyLimitExceededError"}channel;used;limit},j=class{queues=new Map;limits;dailyLimits;constructor(t,n={}){this.limits=t,this.dailyLimits=n}async acquire(t){const n=this.dailyLimits[t];if(void 0!==n){const e=M().usage[t]??0;if(e>=n)throw new U(t,e,n)}const{intervalMs:e}=this.limits[t]??{intervalMs:500},r=this.queues.get(t)??Promise.resolve(),o=r.then(()=>new Promise(t=>setTimeout(t,e)));return this.queues.set(t,o),r}getDailyUsage(t){const n=this.dailyLimits[t]??null,e=M().usage[t]??0;return{used:e,limit:n,remaining:null!==n?Math.max(0,n-e):null}}},q=Object.fromEntries(Object.entries(x).filter(([,t])=>void 0!==t.dailyLimit).map(([t,n])=>[t,n.dailyLimit])),B=new j(S),J=new j(x,q)}});import K from"axios";async function G(t){if(!function(t){if(!t)return!0;const n=new Date(t).getTime();return Date.now()+F>=n}(t.tokenExpiresAt))return t;if(!t.refreshToken)throw new Error("Cafe24 refresh_token이 없습니다. 'rocketsell init'으로 재인증하세요.");const n=Buffer.from(`${t.clientId}:${t.clientSecret}`).toString("base64"),e=await K.post(`https://${t.storeId}.cafe24api.com/api/v2/oauth/token`,`grant_type=refresh_token&refresh_token=${encodeURIComponent(t.refreshToken)}`,{headers:{Authorization:`Basic ${n}`,"Content-Type":"application/x-www-form-urlencoded"}}),{access_token:r,expires_at:o,refresh_token:s}=e.data,a={...t,accessToken:r,refreshToken:s,tokenExpiresAt:o},c=g();return c.channels.cafe24&&(c.channels.cafe24=a,m(c)),console.log(" 🔄 cafe24 토큰 자동 갱신 완료"),a}var X,Y=e({"src/lib/token-refresh.ts"(){C(),F=3e5}});import Q from"axios";var V,W=e({"src/services/cafe24.ts"(){H(),Y(),X=class{client;credentials;constructor(t){this.credentials=t,this.client=Q.create({baseURL:`https://${t.storeId}.cafe24api.com/api/v2`,headers:{Authorization:`Bearer ${t.accessToken}`,"Content-Type":"application/json"}})}async refreshTokenIfNeeded(){const t=await G(this.credentials);t.accessToken!==this.credentials.accessToken&&(this.credentials=t,this.client.defaults.headers.common.Authorization=`Bearer ${t.accessToken}`)}async request(t,n,e){return await this.refreshTokenIfNeeded(),await B.acquire("cafe24"),(await this.client.request({method:t,url:n,data:e})).data}async listProducts(t){return this.request("GET",`/admin/products?limit=${t?.limit??100}&offset=${t?.offset??0}`)}async getProduct(t){return this.request("GET",`/admin/products/${t}`)}async createProduct(t){return this.request("POST","/admin/products",{request:t})}async updateProduct(t,n){return this.request("PUT",`/admin/products/${t}`,{request:n})}async deleteProduct(t){return this.request("DELETE",`/admin/products/${t}`)}async updateInventory(t,n,e){return this.request("PUT",`/admin/products/${t}/variants/${n}/inventories`,{request:{quantity:e}})}async listOrders(t){const n=new URLSearchParams;return t?.startDate&&n.set("start_date",t.startDate),t?.endDate&&n.set("end_date",t.endDate),t?.limit&&n.set("limit",String(t.limit)),this.request("GET",`/admin/orders?${n.toString()}`)}async getOrder(t){return this.request("GET",`/admin/orders/${t}`)}async updateOrderStatus(t,n){return this.request("PUT",`/admin/orders/${t}`,{request:{order_status:n}})}async createShipment(t,n){return this.request("POST",`/admin/orders/${t}/shipments`,{request:{shipping_company_code:n.carrierCode,tracking_no:n.trackingNumber}})}async createCoupon(t){return this.request("POST","/admin/coupons",{request:t})}async issueCoupon(t,n){return this.request("POST",`/admin/coupons/${t}/issues`,{request:{member_id:n}})}}}});import Z from"axios";function tt(t){if(!0!==t.result&&"200"!==t.code&&"0"!==t.code)throw new Error(`CJ Dropshipping API error: [${t.code}] ${t.message??"Unknown error"}`)}function nt(t){return{id:String(t.pid??""),name:String(t.productNameEn??""),price:Number(t.sellPrice??0),wholesalePrice:Number(t.productPrice??t.sellPrice??0),imageUrl:t.productImage?String(t.productImage):void 0,category:t.categoryName?String(t.categoryName):void 0,minOrderQty:Number(t.minOrderQuantity??1),supplier:t.supplierName?String(t.supplierName):void 0,sourceUrl:t.productUrl?String(t.productUrl):void 0,sourceChannel:"cjdropshipping",currency:"USD",raw:t}}var et,rt=e({"src/services/cjdropshipping.ts"(){H(),V=class{client;constructor(t){this.client=Z.create({baseURL:"https://developers.cjdropshipping.com/api2.0/v1",headers:{"Content-Type":"application/json","CJ-Access-Token":t.accessToken}})}async get(t,n){await J.acquire("cjdropshipping");const e=await this.client.get(t,{params:n});return tt(e.data),e.data.data}async post(t,n){await J.acquire("cjdropshipping");const e=await this.client.post(t,n);return tt(e.data),e.data.data}async searchProducts(t,n){return function(t){const n=t.list??[];return{products:n.map(nt),totalCount:Number(t.total??n.length),page:Number(t.pageNum??1),pageSize:Number(t.pageSize??n.length)}}(await this.get("/product/list",{productNameEn:t,categoryId:n?.categoryId,pageNum:n?.page??1,pageSize:n?.pageSize??20}))}async getProductDetail(t){return nt(await this.get("/product/query",{pid:t}))}async getCategories(t){return((await this.get("/product/category",{categoryId:t})).categoryList??[]).map(t=>({id:String(t.categoryId??""),name:String(t.categoryName??""),parentId:t.parentId?String(t.parentId):void 0,depth:Number(t.level??0)}))}async createOrder(t){return this.post("/shopping/order/createOrder",t)}}}});import ot from"crypto";import st from"axios";var at,ct=e({"src/services/coupang.ts"(){H(),et=class{credentials;client;constructor(t){this.credentials=t,this.client=st.create({baseURL:"https://api-gateway.coupang.com"})}generateHmac(t,n,e){const r=`${e}${t}${n}`;return ot.createHmac("sha256",this.credentials.secretKey).update(r).digest("hex")}async request(t,n,e){await B.acquire("coupang");const r=`${(new Date).toISOString().replace(/[-:]/g,"").split(".")[0]}Z`,o=this.generateHmac(t.toUpperCase(),n,r),s=`CEA algorithm=HmacSHA256, access-key=${this.credentials.accessKey}, signed-date=${r}, signature=${o}`;return(await this.client.request({method:t,url:n,data:e,headers:{Authorization:s,"Content-Type":"application/json;charset=UTF-8","X-Requested-By":"rocketsell"}})).data}getDaysUntilKeyExpiry(){const t=new Date(this.credentials.keyExpiresAt),n=new Date;return Math.floor((t.getTime()-n.getTime())/864e5)}async createProduct(t){return this.request("POST","/v2/providers/seller_api/apis/api/v1/marketplace/seller-products",t)}async getProduct(t){return this.request("GET",`/v2/providers/seller_api/apis/api/v1/marketplace/seller-products/${t}`)}async updateProduct(t,n){return this.request("PUT",`/v2/providers/seller_api/apis/api/v1/marketplace/seller-products/${t}`,n)}async deleteProduct(t){return this.request("DELETE",`/v2/providers/seller_api/apis/api/v1/marketplace/seller-products/${t}`)}async updateQuantity(t,n){return this.request("PUT",`/v2/providers/seller_api/apis/api/v1/marketplace/vendor-items/${t}/quantities/${n}`)}async updatePrice(t,n){return this.request("PUT",`/v2/providers/seller_api/apis/api/v1/marketplace/vendor-items/${t}/prices/${n}`)}async updateSalesStatus(t,n){return this.request("PUT",`/v2/providers/seller_api/apis/api/v1/marketplace/vendor-items/${t}/sales-status/${n}`)}async listOrders(t){const n=new URLSearchParams({vendorId:t.vendorId,createdAtFrom:t.createdAtFrom,createdAtTo:t.createdAtTo});return t.status&&n.set("status",t.status),this.request("GET",`/v2/providers/openapi/apis/api/v4/vendors/${t.vendorId}/ordersheets?${n.toString()}`)}async confirmOrder(t,n){return this.request("PUT",`/v2/providers/openapi/apis/api/v4/vendors/${n}/ordersheets/${t}/confirmed`)}async registerInvoice(t,n,e){return this.request("POST",`/v2/providers/openapi/apis/api/v4/vendors/${n}/ordersheets/${t}/invoices`,{deliveryCompanyCode:e.carrierCode,invoiceNumber:e.trackingNumber})}async createCoupon(t){return this.request("POST","/v2/providers/seller_api/apis/api/v1/marketplace/seller-coupons",t)}async listProductInquiries(t){return this.request("GET",`/v2/providers/openapi/apis/api/v4/vendors/${t}/product-inquiries`)}async answerProductInquiry(t,n,e){return this.request("POST",`/v2/providers/openapi/apis/api/v4/vendors/${t}/product-inquiries/${n}/reply`,{content:e})}}}});import it from"axios";function lt(t){return{id:String(t.no??""),name:String(t.name??""),price:Number(t.price??0),wholesalePrice:Number(t.domeggookprice??t.price??0),imageUrl:t.image?String(t.image):void 0,category:t.catename?String(t.catename):void 0,minOrderQty:Number(t.minqty??1),supplier:t.companyname?String(t.companyname):void 0,sourceUrl:t.link?String(t.link):void 0,sourceChannel:"domeggook",raw:t}}var dt,ut=e({"src/services/domeggook.ts"(){H(),at=class{client;apiKey;constructor(t){this.apiKey=t.apiKey,this.client=it.create({baseURL:"https://openapi.domeggook.com",headers:{"Content-Type":"application/json"}})}async request(t,n){return await J.acquire("domeggook"),(await this.client.get(t,{params:{key:this.apiKey,...n}})).data}async searchProducts(t,n){return function(t){const n=t.list??[];return{products:n.map(lt),totalCount:Number(t.totalcount??n.length),page:Number(t.page??1),pageSize:n.length}}(await this.request("/api/getProductList",{keyword:t,cate:n?.category,page:n?.page??1}))}async getProductDetail(t){return lt(await this.request("/api/getProductDetail",{no:t}))}async getCategoryList(t){return((await this.request("/api/getCategoryList",t?{cate:t}:void 0)).list??[]).map(t=>({id:String(t.no??""),name:String(t.name??""),parentId:t.parentno?String(t.parentno):void 0,depth:Number(t.depth??0)}))}}}});import pt from"axios";var ht,ft=e({"src/services/shopify.ts"(){H(),dt=class{credentials;client;constructor(t){this.credentials=t,this.client=pt.create({baseURL:`https://${t.storeId}/admin/api/${t.apiVersion}`,headers:{"X-Shopify-Access-Token":t.accessToken,"Content-Type":"application/json"}})}async graphql(t,n){await B.acquire("shopify");const e=await this.client.post("/graphql.json",{query:t,variables:n});if(e.data.errors)throw new Error(`Shopify GraphQL error: ${JSON.stringify(e.data.errors)}`);return e.data.data}async createProduct(t){return this.graphql("\n\t\t\tmutation productCreate($input: ProductInput!) {\n\t\t\t\tproductCreate(input: $input) {\n\t\t\t\t\tproduct { id title handle status }\n\t\t\t\t\tuserErrors { field message }\n\t\t\t\t}\n\t\t\t}\n\t\t",{input:t})}async updateProduct(t){return this.graphql("\n\t\t\tmutation productUpdate($input: ProductInput!) {\n\t\t\t\tproductUpdate(input: $input) {\n\t\t\t\t\tproduct { id title handle status }\n\t\t\t\t\tuserErrors { field message }\n\t\t\t\t}\n\t\t\t}\n\t\t",{input:t})}async getProduct(t){return this.graphql("\n\t\t\tquery product($id: ID!) {\n\t\t\t\tproduct(id: $id) {\n\t\t\t\t\tid title handle status descriptionHtml vendor productType tags\n\t\t\t\t\timages(first: 20) {\n\t\t\t\t\t\tedges { node { id url altText } }\n\t\t\t\t\t}\n\t\t\t\t\tvariants(first: 100) {\n\t\t\t\t\t\tedges { node { id title sku price inventoryQuantity inventoryItem { id } } }\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t",{id:t})}async listProducts(t=50,n){return this.graphql("\n\t\t\tquery products($first: Int!, $query: String) {\n\t\t\t\tproducts(first: $first, query: $query, sortKey: UPDATED_AT, reverse: true) {\n\t\t\t\t\tedges {\n\t\t\t\t\t\tnode {\n\t\t\t\t\t\t\tid title handle status vendor productType\n\t\t\t\t\t\t\ttotalInventory\n\t\t\t\t\t\t\tvariants(first: 5) {\n\t\t\t\t\t\t\t\tedges { node { id title sku price inventoryQuantity } }\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\timages(first: 1) {\n\t\t\t\t\t\t\t\tedges { node { url } }\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t",{first:t,query:n})}async deleteProduct(t){return this.graphql("\n\t\t\tmutation productDelete($input: ProductDeleteInput!) {\n\t\t\t\tproductDelete(input: $input) {\n\t\t\t\t\tdeletedProductId\n\t\t\t\t\tuserErrors { field message }\n\t\t\t\t}\n\t\t\t}\n\t\t",{input:{id:t}})}async createVariant(t,n){return this.graphql("\n\t\t\tmutation productVariantsBulkCreate($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {\n\t\t\t\tproductVariantsBulkCreate(productId: $productId, variants: $variants) {\n\t\t\t\t\tproductVariants { id title sku price inventoryQuantity inventoryItem { id } }\n\t\t\t\t\tuserErrors { field message }\n\t\t\t\t}\n\t\t\t}\n\t\t",{productId:t,variants:n})}async createMediaFromUrl(t,n){const e=n.map(t=>({originalSource:t.url,alt:t.alt??"",mediaContentType:"IMAGE"}));return this.graphql("\n\t\t\tmutation productCreateMedia($productId: ID!, $media: [CreateMediaInput!]!) {\n\t\t\t\tproductCreateMedia(productId: $productId, media: $media) {\n\t\t\t\t\tmedia { ... on MediaImage { id image { url } } }\n\t\t\t\t\tmediaUserErrors { field message }\n\t\t\t\t}\n\t\t\t}\n\t\t",{productId:t,media:e})}async listLocations(){return this.graphql("\n\t\t\tquery {\n\t\t\t\tlocations(first: 50) {\n\t\t\t\t\tedges { node { id name isActive } }\n\t\t\t\t}\n\t\t\t}\n\t\t")}async adjustInventory(t,n,e){return this.graphql("\n\t\t\tmutation inventoryAdjustQuantities($input: InventoryAdjustQuantitiesInput!) {\n\t\t\t\tinventoryAdjustQuantities(input: $input) {\n\t\t\t\t\tinventoryAdjustmentGroup { reason }\n\t\t\t\t\tuserErrors { field message }\n\t\t\t\t}\n\t\t\t}\n\t\t",{input:{reason:"correction",name:"available",changes:[{inventoryItemId:t,locationId:n,delta:e}]}})}async setInventoryQuantity(t,n,e){return this.graphql("\n\t\t\tmutation inventorySetQuantities($input: InventorySetQuantitiesInput!) {\n\t\t\t\tinventorySetQuantities(input: $input) {\n\t\t\t\t\tinventoryAdjustmentGroup { reason }\n\t\t\t\t\tuserErrors { field message }\n\t\t\t\t}\n\t\t\t}\n\t\t",{input:{reason:"correction",name:"available",ignoreCompareQuantity:!0,quantities:[{inventoryItemId:t,locationId:n,quantity:e}]}})}async listOrders(t=50,n){return this.graphql("\n\t\t\tquery orders($first: Int!, $query: String) {\n\t\t\t\torders(first: $first, query: $query, sortKey: CREATED_AT, reverse: true) {\n\t\t\t\t\tedges {\n\t\t\t\t\t\tnode {\n\t\t\t\t\t\t\tid name createdAt displayFinancialStatus displayFulfillmentStatus\n\t\t\t\t\t\t\ttotalPriceSet { shopMoney { amount currencyCode } }\n\t\t\t\t\t\t\tlineItems(first: 50) {\n\t\t\t\t\t\t\t\tedges { node { title sku quantity } }\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t",{first:t,query:n})}async getOrder(t){return this.graphql("\n\t\t\tquery order($id: ID!) {\n\t\t\t\torder(id: $id) {\n\t\t\t\t\tid name createdAt displayFinancialStatus displayFulfillmentStatus\n\t\t\t\t\ttotalPriceSet { shopMoney { amount currencyCode } }\n\t\t\t\t\tsubtotalPriceSet { shopMoney { amount currencyCode } }\n\t\t\t\t\ttotalShippingPriceSet { shopMoney { amount currencyCode } }\n\t\t\t\t\tcustomer { id firstName lastName email }\n\t\t\t\t\tshippingAddress { address1 address2 city province zip country }\n\t\t\t\t\tlineItems(first: 50) {\n\t\t\t\t\t\tedges { node { id title sku quantity variant { id inventoryItem { id } } } }\n\t\t\t\t\t}\n\t\t\t\t\tfulfillmentOrders(first: 10) {\n\t\t\t\t\t\tedges { node { id status } }\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t",{id:t})}async getFulfillmentOrders(t){return this.graphql("\n\t\t\tquery order($id: ID!) {\n\t\t\t\torder(id: $id) {\n\t\t\t\t\tfulfillmentOrders(first: 10) {\n\t\t\t\t\t\tedges {\n\t\t\t\t\t\t\tnode {\n\t\t\t\t\t\t\t\tid status\n\t\t\t\t\t\t\t\tlineItems(first: 50) {\n\t\t\t\t\t\t\t\t\tedges { node { id totalQuantity remainingQuantity } }\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tassignedLocation { name }\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t",{id:t})}async createFulfillment(t,n){return this.graphql("\n\t\t\tmutation fulfillmentCreate($fulfillment: FulfillmentInput!) {\n\t\t\t\tfulfillmentCreate(fulfillment: $fulfillment) {\n\t\t\t\t\tfulfillment { id status }\n\t\t\t\t\tuserErrors { field message }\n\t\t\t\t}\n\t\t\t}\n\t\t",{fulfillment:{lineItemsByFulfillmentOrder:[{fulfillmentOrderId:t}],trackingInfo:{company:n.company,number:n.number,url:n.url}}})}async createDiscountCode(t){return this.graphql("\n\t\t\tmutation discountCodeBasicCreate($basicCodeDiscount: DiscountCodeBasicInput!) {\n\t\t\t\tdiscountCodeBasicCreate(basicCodeDiscount: $basicCodeDiscount) {\n\t\t\t\t\tcodeDiscountNode { id }\n\t\t\t\t\tuserErrors { field message }\n\t\t\t\t}\n\t\t\t}\n\t\t",{basicCodeDiscount:t})}}}});import gt from"axios";var mt,vt=e({"src/services/smartstore.ts"(){H(),ht=class{credentials;client;constructor(t){this.credentials=t,this.client=gt.create({baseURL:"https://api.commerce.naver.com/external",headers:{"Content-Type":"application/json"}})}async ensureToken(){this.credentials.accessToken&&this.credentials.tokenExpiresAt&&new Date(this.credentials.tokenExpiresAt)>new Date||await this.refreshToken()}async refreshToken(){const t=new URLSearchParams({client_id:this.credentials.clientId,client_secret:this.credentials.clientSecret,grant_type:"client_credentials",type:"SELF"}),n=await gt.post("https://api.commerce.naver.com/external/v1/oauth2/token",t,{headers:{"Content-Type":"application/x-www-form-urlencoded"}});this.credentials.accessToken=n.data.access_token,this.credentials.tokenExpiresAt=new Date(Date.now()+1e3*n.data.expires_in).toISOString(),this.client.defaults.headers.Authorization=`Bearer ${this.credentials.accessToken}`}async request(t,n,e){return await this.ensureToken(),await B.acquire("smartstore"),(await this.client.request({method:t,url:n,data:e})).data}async createProduct(t){return this.request("POST","/v2/products/origin-products",t)}async updateProduct(t,n){return this.request("PATCH",`/v2/products/origin-products/${t}`,n)}async getProduct(t){return this.request("GET",`/v2/products/origin-products/${t}`)}async updateStock(t,n){return this.request("PATCH",`/v2/products/origin-products/${t}`,n)}async listChangedOrders(t){return this.request("GET",`/v1/pay-order/seller/product-orders/last-changed-statuses?lastChangedFrom=${t.lastChangedFrom}&lastChangedTo=${t.lastChangedTo}`)}async getOrderDetail(t){return this.request("GET",`/v1/pay-order/seller/product-orders/${t}`)}async confirmOrder(t){return this.request("POST","/v1/pay-order/seller/product-orders/confirm",{productOrderIds:[t]})}async registerInvoice(t,n){return this.request("POST","/v1/pay-order/seller/product-orders/dispatch",{dispatchProductOrders:[{productOrderId:t,deliveryMethod:"DELIVERY",deliveryCompanyCode:n.carrierCode,trackingNumber:n.trackingNumber}]})}async approveCancel(t){return this.request("POST","/v1/pay-order/seller/product-orders/cancel/approve",{productOrderIds:[t]})}async approveReturn(t){return this.request("POST","/v1/pay-order/seller/product-orders/return/approve",{productOrderIds:[t]})}}}});import yt from"axios";var wt,bt,$t=e({"src/services/specialoffer.ts"(){H(),mt=class{client;apiKey;constructor(t){this.apiKey=t.apiKey,this.client=yt.create({baseURL:"https://specialoffer.kr/api",headers:{"Content-Type":"application/json"}})}async request(t,n,e){return await J.acquire("specialoffer"),(await this.client.request({method:t,url:n,params:{key:this.apiKey,...e}})).data}async searchProducts(t){const n={};return t?.category1&&(n.category1=t.category1),t?.category2&&(n.category2=t.category2),t?.category3&&(n.category3=t.category3),t?.keyword&&(n.keyword=t.keyword),t?.page&&(n.page=t.page),t?.limit&&(n.limit=t.limit),this.request("GET","/seller/goods",n)}async getCategories(){return this.request("GET","/categories")}async getProductDetail(t){return this.request("GET",`/seller/goods/${t}`)}}}}),kt={};function St(t){const n=wt.get(t);if(n)return n;const e=v(t);if(!e)throw new Error(`${t} credentials가 설정되지 않았습니다.\n 'rocketsell init' 또는 환경변수를 설정하세요.`);let r;switch(t){case"cafe24":r=new X(e);break;case"coupang":r=new et(e);break;case"smartstore":r=new ht(e);break;case"shopify":r=new dt(e);break;default:throw new Error(`지원하지 않는 채널: ${t}`)}return wt.set(t,r),r}async function Tt(){wt.delete("cafe24");const t=v("cafe24");if(!t)throw new Error("cafe24 credentials가 설정되지 않았습니다.\n 'rocketsell init' 또는 환경변수를 설정하세요.");const n=await G(t),e=new X(n);return wt.set("cafe24",e),e}function It(){wt.clear()}function xt(t){const n=bt.get(t);if(n)return n;const e=$(t);if(!e)throw new Error(`${t} credentials가 설정되지 않았습니다.\n 'rocketsell init' 또는 환경변수를 설정하세요.`);let r;switch(t){case"domeggook":r=new at(e);break;case"specialoffer":r=new mt(e);break;case"cjdropshipping":r=new V(e);break;default:throw new Error(`지원하지 않는 소싱 채널: ${t}`)}return bt.set(t,r),r}r(kt,{clearClientCache:()=>It,getCafe24ClientFresh:()=>Tt,getClient:()=>St,getSourceClient:()=>xt});var Et=e({"src/lib/client-factory.ts"(){W(),rt(),ct(),ut(),ft(),vt(),$t(),C(),Y(),wt=new Map,bt=new Map}}),Ct={};r(Ct,{parseBatchFile:()=>At,parseRowRange:()=>Dt,writeBatchResult:()=>Lt});import{readFileSync as Nt,writeFileSync as Ot}from"fs";function Dt(t){const n=t.split(":");if(2!==n.length)throw new Error(`유효하지 않은 row range 형식: "${t}" (예: "0:9")`);const e=Number(n[0]),r=Number(n[1]);if(Number.isNaN(e)||Number.isNaN(r))throw new Error(`row range에 숫자가 아닌 값: "${t}"`);if(r<e)throw new Error(`row range end(${r})가 start(${e})보다 작습니다`);return{start:e,end:r}}function At(t){let n,e;try{n=Nt(t,"utf-8")}catch(n){throw new Error(`파일 읽기 실패: ${t} — ${n.message}`)}try{e=JSON.parse(n)}catch(n){throw new Error(`JSON 파싱 실패: ${t} — ${n.message}`)}if(Array.isArray(e))return e;if("object"==typeof e&&null!==e)return[e];throw new Error(`유효하지 않은 배치 파일 형식: ${t} (배열 또는 단일 객체 필요)`)}function Lt(t,n){Ot(n,JSON.stringify(t,null,2),"utf-8")}var _t=e({"src/lib/batch.ts"(){}});C();import Pt from"axios";var Mt="https://dropship-callback.vercel.app/api/callback/cafe24",Rt={cafe24:{authorizeUrl:(t,n)=>`https://${t}.cafe24api.com/api/v2/oauth/authorize?response_type=code&client_id=${n}&state=rocketsell&redirect_uri=${encodeURIComponent(Mt)}&scope=`,tokenUrl:t=>`https://${t}.cafe24api.com/api/v2/oauth/token`,scopes:["mall.read_product","mall.write_product","mall.read_order","mall.write_order","mall.read_category","mall.write_category","mall.read_store","mall.read_shipping","mall.write_shipping"]}};N();var Ut={shopify:{score:"92/100",productCrud:"✅ GraphQL 풀 지원 (2,000 variants)",inventory:"✅ delta 방식 (동시성 안전)",orderCollection:"✅ 웹훅 (80+ 이벤트)",invoice:"✅",cs:"✅ 취소/환불/수정",coupon:"✅ 코드/자동/BXGY/무배",review:"⚠️ 앱 의존",analytics:"✅ ShopifyQL",rateLimit:`${S.shopify.maxRps} req/s (Basic 100pts, Plus 1000pts)`,limitations:"세금율 API 없음, 이메일 API 없음, 체크아웃 커스텀 Plus 전용"},cafe24:{score:"79/100",productCrud:"✅ REST 풀 CRUD",inventory:"⚠️ 절대값 방식 (동시 쓰기 위험)",orderCollection:"✅ 웹훅 (앱 등록 필요)",invoice:"✅",cs:"✅ 취소/반품/교환 전부",coupon:"✅ 쿠폰/시리얼 대량 발급",review:"⚠️ 게시판 간접",analytics:"✅ 일별/월별/시간별 매출",rateLimit:`${S.cafe24.maxRps} req/s (Leaky Bucket)`,limitations:"고객 생성 불가, 멀티 로케이션 없음, PII 법인만"},coupang:{score:"54/100",productCrud:"✅ (단일→옵션 추가 불가)",inventory:"✅ 전용 API (절대값)",orderCollection:"⚠️ 폴링 전용 (31일 윈도우)",invoice:"✅",cs:"⚠️ 취소 발송 전만, 반품 변경 추적 불가",coupon:"✅ 18개 API (즉시할인/다운로드)",review:"❌ API 없음",analytics:"⚠️ 정산 조회만",rateLimit:`${S.coupang.maxRps} req/s`,limitations:"180일 키 만료, 050 안심번호만, 웹훅 없음, 리뷰/광고 API 없음"},smartstore:{score:"44/100",productCrud:"✅ (원상품+채널상품 2단계)",inventory:"❌ 전용 API 없음 (상품 전체 PUT)",orderCollection:"⚠️ 폴링 전용 (24시간 윈도우)",invoice:"✅",cs:"✅ 취소/반품/교환",coupon:"❌ API 없음",review:"❌ API 없음 (공식: 계획 없음)",analytics:"❌ API 없음",rateLimit:`${S.smartstore.maxRps} req/s (추정, 비공개)`,limitations:"웹훅 없음, 쿠폰/리뷰/통계/고객 API 전무, 재고 원자적 변경 불가, IP 3개 제한"}},jt=["shopify","cafe24","coupang","smartstore"];C();var qt=["cafe24","coupang","smartstore","shopify"];function Bt(t){const n={},e=["clientSecret","secretKey","accessToken","refreshToken","accessKey"];for(const[r,o]of Object.entries(t))"string"==typeof o&&(e.includes(r)&&o.length>6?n[r]=`${o.slice(0,3)}***${o.slice(-3)}`:n[r]=String(o));return n}C(),N(),O();import{createServer as Jt}from"http";var Ft=["cafe24","coupang","smartstore","shopify"],zt=["domeggook","specialoffer","cjdropshipping"],Ht=[];function Kt(t,n){const e=new Date,r=`${String(e.getHours()).padStart(2,"0")}:${String(e.getMinutes()).padStart(2,"0")}`;Ht.unshift({time:r,channel:t,message:n}),Ht.length>100&&(Ht.length=100)}async function Gt(t,n){const e="string"==typeof n.port?Number(n.port):3847;return Number.isNaN(e)||e<1||e>65535?(console.error("유효하지 않은 포트 번호:",n.port),1):(function(t){Jt((t,n)=>{(async function(t,n){const e=t.url??"/";if(n.setHeader("Access-Control-Allow-Origin","*"),"/"===e||"/index.html"===e)return n.writeHead(200,{"Content-Type":"text/html; charset=utf-8"}),void n.end("<!DOCTYPE html>\n<html lang=\"ko\" data-theme=\"cozy\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n<title>Dropship AI — Dashboard</title>\n<style>\n/* ── SCM Design Tokens ── */\n:root, [data-theme=\"cozy\"] {\n\t--background: #f3f4ef;\n\t--foreground: #111111;\n\t--muted: #e4e5de;\n\t--muted-foreground: rgba(17,17,17,0.6);\n\t--card: #ffffff;\n\t--card-foreground: #111111;\n\t--border: #dadbd2;\n\t--border-hover: #b8b9b0;\n\t--primary: #f54e01;\n\t--primary-foreground: #ffffff;\n\t--secondary: #eeefe9;\n\t--secondary-foreground: #111111;\n\t--destructive: #ef4444;\n\t--destructive-foreground: #ffffff;\n\t--success: #22c55e;\n\t--success-foreground: #052e16;\n\t--warning: #eb9d2a;\n\t--warning-foreground: #422006;\n\t--radius: 8px;\n}\n[data-theme=\"dawn\"] {\n\t--background: #1d1f27;\n\t--foreground: #ffffff;\n\t--muted: #24262a;\n\t--muted-foreground: rgba(255,255,255,0.5);\n\t--card: #21242b;\n\t--card-foreground: #ffffff;\n\t--border: #35373e;\n\t--border-hover: #4a4c54;\n\t--primary: #f7a503;\n\t--primary-foreground: #1d1f27;\n\t--secondary: #2a2c34;\n\t--secondary-foreground: #ffffff;\n\t--destructive: #dc2626;\n\t--destructive-foreground: #ffffff;\n\t--success: #16a34a;\n\t--success-foreground: #dcfce7;\n\t--warning: #f7a503;\n\t--warning-foreground: #422006;\n}\n[data-theme=\"light\"] {\n\t--background: #ffffff;\n\t--foreground: #0a0a0a;\n\t--muted: #f5f5f5;\n\t--muted-foreground: #737373;\n\t--card: #ffffff;\n\t--card-foreground: #0a0a0a;\n\t--border: rgba(0,0,0,0.15);\n\t--border-hover: rgba(0,0,0,0.3);\n\t--primary: #171717;\n\t--primary-foreground: #fafafa;\n\t--secondary: #f5f5f5;\n\t--secondary-foreground: #171717;\n\t--destructive: #ef4444;\n\t--destructive-foreground: #fafafa;\n\t--success: #22c55e;\n\t--success-foreground: #052e16;\n\t--warning: #eab308;\n\t--warning-foreground: #422006;\n}\n[data-theme=\"dark\"] {\n\t--background: #0a0a0a;\n\t--foreground: #fafafa;\n\t--muted: #1c1c1c;\n\t--muted-foreground: #a3a3a3;\n\t--card: #141414;\n\t--card-foreground: #fafafa;\n\t--border: rgba(255,255,255,0.15);\n\t--border-hover: rgba(255,255,255,0.3);\n\t--primary: #fafafa;\n\t--primary-foreground: #0a0a0a;\n\t--secondary: #1c1c1c;\n\t--secondary-foreground: #fafafa;\n\t--destructive: #dc2626;\n\t--destructive-foreground: #fafafa;\n\t--success: #16a34a;\n\t--success-foreground: #dcfce7;\n\t--warning: #ca8a04;\n\t--warning-foreground: #fef9c3;\n}\n\n/* ── Reset & Base ── */\n*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\nbody {\n\tbackground: var(--background);\n\tcolor: var(--foreground);\n\tfont-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", system-ui, sans-serif;\n\tfont-size: 14px;\n\tline-height: 1.7;\n\tmin-height: 100vh;\n}\n\n/* ── Layout ── */\n.shell { display: flex; min-height: 100vh; }\n.sidebar {\n\twidth: 220px;\n\tborder-right: 0.5px solid var(--border);\n\tpadding: 20px 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tposition: sticky;\n\ttop: 0;\n\theight: 100vh;\n\toverflow-y: auto;\n}\n.sidebar-header {\n\tpadding: 0 16px 16px;\n\tborder-bottom: 0.5px solid var(--border);\n\tmargin-bottom: 8px;\n}\n.sidebar-header h1 {\n\tfont-size: 16px;\n\tfont-weight: 600;\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n}\n.sidebar-header .version {\n\tfont-size: 11px;\n\tcolor: var(--muted-foreground);\n\tfont-weight: 400;\n}\n.nav-item {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tpadding: 6px 16px;\n\tfont-size: 13px;\n\tcolor: var(--muted-foreground);\n\tcursor: pointer;\n\ttransition: background 0.12s, color 0.12s;\n\tborder: none;\n\tbackground: none;\n\twidth: 100%;\n\ttext-align: left;\n}\n.nav-item:hover { background: var(--secondary); color: var(--foreground); }\n.nav-item.active { background: var(--secondary); color: var(--foreground); font-weight: 500; }\n.sidebar-footer {\n\tmargin-top: auto;\n\tpadding: 12px 16px 0;\n\tborder-top: 0.5px solid var(--border);\n}\n.theme-select {\n\twidth: 100%;\n\tbackground: transparent;\n\tborder: 0.5px solid var(--border);\n\tborder-radius: 6px;\n\tpadding: 4px 8px;\n\tfont-size: 12px;\n\tcolor: var(--foreground);\n\toutline: none;\n\tcursor: pointer;\n}\n.theme-select option { background: var(--background); color: var(--foreground); }\n\n.main {\n\tflex: 1;\n\tpadding: 24px 32px;\n\toverflow-y: auto;\n\tmax-width: 1200px;\n}\n\n/* ── Metric Cards ── */\n.metric-grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(200px, 1fr));\n\tgap: 12px;\n\tmargin-bottom: 24px;\n}\n.metric-card {\n\tbackground: var(--secondary);\n\tborder-radius: 8px;\n\tpadding: 16px;\n}\n.metric-label {\n\tfont-size: 12px;\n\tcolor: var(--muted-foreground);\n\tmargin-bottom: 4px;\n}\n.metric-value {\n\tfont-size: 24px;\n\tfont-weight: 500;\n}\n\n/* ── Section ── */\n.section { margin-bottom: 32px; }\n.section-title {\n\tfont-size: 16px;\n\tfont-weight: 500;\n\tmargin-bottom: 12px;\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n}\n\n/* ── Channel Cards ── */\n.channel-grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(260px, 1fr));\n\tgap: 12px;\n}\n.channel-card {\n\tbackground: var(--secondary);\n\tborder-radius: 8px;\n\tpadding: 16px;\n\tborder: 0.5px solid var(--border);\n}\n.channel-card:hover { border-color: var(--border-hover); }\n.channel-header {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\tmargin-bottom: 10px;\n}\n.channel-name {\n\tfont-size: 14px;\n\tfont-weight: 500;\n\ttext-transform: capitalize;\n}\n.channel-meta {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n}\n.channel-meta-row {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\tfont-size: 12px;\n\tcolor: var(--muted-foreground);\n}\n.channel-meta-row span:last-child { color: var(--foreground); }\n\n/* ── Badges ── */\n.badge {\n\tdisplay: inline-block;\n\tpadding: 2px 8px;\n\tborder-radius: 4px;\n\tfont-size: 11px;\n\tfont-weight: 500;\n}\n.badge-success { background: var(--success); color: var(--success-foreground); }\n.badge-warning { background: var(--warning); color: var(--warning-foreground); }\n.badge-destructive { background: var(--destructive); color: var(--destructive-foreground); }\n.badge-muted { background: var(--muted); color: var(--muted-foreground); }\n\n/* ── Table ── */\n.table-wrap { overflow-x: auto; }\ntable { width: 100%; border-collapse: collapse; }\nth {\n\ttext-align: left;\n\tpadding: 8px 12px;\n\tfont-size: 12px;\n\tfont-weight: 500;\n\tcolor: var(--muted-foreground);\n\tborder-bottom: 0.5px solid var(--border);\n\tbackground: var(--secondary);\n}\ntd {\n\tpadding: 8px 12px;\n\tfont-size: 13px;\n\tborder-bottom: 0.5px solid var(--border);\n}\ntr:hover td { background: var(--secondary); }\n\n/* ── Rate bar ── */\n.rate-bar-bg {\n\twidth: 100%;\n\theight: 6px;\n\tbackground: var(--muted);\n\tborder-radius: 3px;\n\toverflow: hidden;\n}\n.rate-bar-fill {\n\theight: 100%;\n\tborder-radius: 3px;\n\ttransition: width 0.3s;\n}\n\n/* ── Dot indicator ── */\n.dot {\n\twidth: 8px;\n\theight: 8px;\n\tborder-radius: 50%;\n\tdisplay: inline-block;\n}\n.dot-success { background: var(--success); }\n.dot-warning { background: var(--warning); }\n.dot-destructive { background: var(--destructive); }\n.dot-muted { background: var(--muted-foreground); }\n\n/* ── Activity log ── */\n.log-list { list-style: none; }\n.log-item {\n\tdisplay: flex;\n\talign-items: flex-start;\n\tgap: 10px;\n\tpadding: 8px 0;\n\tborder-bottom: 0.5px solid var(--border);\n\tfont-size: 13px;\n}\n.log-time {\n\tfont-size: 11px;\n\tcolor: var(--muted-foreground);\n\twhite-space: nowrap;\n\tmin-width: 56px;\n}\n.log-channel {\n\tfont-size: 11px;\n\tpadding: 1px 6px;\n\tborder-radius: 3px;\n\tbackground: var(--muted);\n\tcolor: var(--muted-foreground);\n\ttext-transform: capitalize;\n\tmin-width: 72px;\n\ttext-align: center;\n}\n\n/* ── Empty state ── */\n.empty {\n\ttext-align: center;\n\tpadding: 40px 20px;\n\tcolor: var(--muted-foreground);\n\tfont-size: 13px;\n\tborder: 1px dashed var(--border);\n\tborder-radius: 8px;\n}\n\n/* ── Header bar ── */\n.page-header {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\tmargin-bottom: 24px;\n}\n.page-title { font-size: 20px; font-weight: 500; }\n.refresh-btn {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 4px;\n\tbackground: transparent;\n\tborder: 0.5px solid var(--border);\n\tborder-radius: 6px;\n\tpadding: 4px 12px;\n\tfont-size: 12px;\n\tcursor: pointer;\n\tcolor: var(--foreground);\n\ttransition: border-color 0.12s;\n}\n.refresh-btn:hover { border-color: var(--border-hover); }\n\n/* ── Pulse animation ── */\n@keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:0.4; } }\n.loading { animation: pulse 1.2s infinite; }\n\n/* ── Order status bar ── */\n.status-bar {\n\tdisplay: flex;\n\tgap: 2px;\n\theight: 24px;\n\tborder-radius: 4px;\n\toverflow: hidden;\n}\n.status-bar-seg {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\tfont-size: 10px;\n\tfont-weight: 500;\n\tmin-width: 28px;\n\tpadding: 0 6px;\n\twhite-space: nowrap;\n}\n</style>\n</head>\n<body>\n<div class=\"shell\">\n\t\x3c!-- Sidebar --\x3e\n\t<nav class=\"sidebar\">\n\t\t<div class=\"sidebar-header\">\n\t\t\t<h1>\n\t\t\t\t<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"var(--primary)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z\"/><polyline points=\"3.27 6.96 12 12.01 20.73 6.96\"/><line x1=\"12\" y1=\"22.08\" x2=\"12\" y2=\"12\"/></svg>\n\t\t\t\tDropship AI\n\t\t\t\t<span class=\"version\">v0.1.0</span>\n\t\t\t</h1>\n\t\t</div>\n\t\t<button class=\"nav-item active\" data-page=\"overview\">\n\t\t\t<svg width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><rect x=\"3\" y=\"3\" width=\"7\" height=\"9\" rx=\"1\"/><rect x=\"14\" y=\"3\" width=\"7\" height=\"5\" rx=\"1\"/><rect x=\"14\" y=\"12\" width=\"7\" height=\"9\" rx=\"1\"/><rect x=\"3\" y=\"16\" width=\"7\" height=\"5\" rx=\"1\"/></svg>\n\t\t\tOverview\n\t\t</button>\n\t\t<button class=\"nav-item\" data-page=\"channels\">\n\t\t\t<svg width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M12 2L2 7l10 5 10-5-10-5z\"/><path d=\"M2 17l10 5 10-5\"/><path d=\"M2 12l10 5 10-5\"/></svg>\n\t\t\tChannels\n\t\t</button>\n\t\t<button class=\"nav-item\" data-page=\"orders\">\n\t\t\t<svg width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2\"/><rect x=\"8\" y=\"2\" width=\"8\" height=\"4\" rx=\"1\"/></svg>\n\t\t\tOrders\n\t\t</button>\n\t\t<button class=\"nav-item\" data-page=\"sourcing\">\n\t\t\t<svg width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><circle cx=\"11\" cy=\"11\" r=\"8\"/><line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"/></svg>\n\t\t\tSourcing\n\t\t</button>\n\t\t<div class=\"sidebar-footer\">\n\t\t\t<select class=\"theme-select\" id=\"themeSelect\">\n\t\t\t\t<option value=\"cozy\">Cozy</option>\n\t\t\t\t<option value=\"dawn\">Dawn</option>\n\t\t\t\t<option value=\"light\">Light</option>\n\t\t\t\t<option value=\"dark\">Dark</option>\n\t\t\t</select>\n\t\t</div>\n\t</nav>\n\n\t\x3c!-- Main content --\x3e\n\t<main class=\"main\">\n\t\t\x3c!-- Overview page --\x3e\n\t\t<div id=\"page-overview\">\n\t\t\t<div class=\"page-header\">\n\t\t\t\t<h2 class=\"page-title\">Dashboard</h2>\n\t\t\t\t<button class=\"refresh-btn\" onclick=\"refreshAll()\">\n\t\t\t\t\t<svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><polyline points=\"23 4 23 10 17 10\"/><path d=\"M20.49 15a9 9 0 1 1-2.12-9.36L23 10\"/></svg>\n\t\t\t\t\tRefresh\n\t\t\t\t</button>\n\t\t\t</div>\n\n\t\t\t<div class=\"metric-grid\" id=\"overviewMetrics\">\n\t\t\t\t<div class=\"metric-card loading\"><div class=\"metric-label\">판매 채널</div><div class=\"metric-value\">—</div></div>\n\t\t\t\t<div class=\"metric-card loading\"><div class=\"metric-label\">소싱 채널</div><div class=\"metric-value\">—</div></div>\n\t\t\t\t<div class=\"metric-card loading\"><div class=\"metric-label\">최근 주문</div><div class=\"metric-value\">—</div></div>\n\t\t\t\t<div class=\"metric-card loading\"><div class=\"metric-label\">처리 필요</div><div class=\"metric-value\">—</div></div>\n\t\t\t</div>\n\n\t\t\t<div class=\"section\">\n\t\t\t\t<div class=\"section-title\">\n\t\t\t\t\t<svg width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M12 2L2 7l10 5 10-5-10-5z\"/><path d=\"M2 17l10 5 10-5\"/><path d=\"M2 12l10 5 10-5\"/></svg>\n\t\t\t\t\t채널 상태\n\t\t\t\t</div>\n\t\t\t\t<div class=\"channel-grid\" id=\"channelCards\"></div>\n\t\t\t</div>\n\n\t\t\t<div class=\"section\">\n\t\t\t\t<div class=\"section-title\">\n\t\t\t\t\t<svg width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><polyline points=\"22 12 18 12 15 21 9 3 6 12 2 12\"/></svg>\n\t\t\t\t\t활동 로그\n\t\t\t\t</div>\n\t\t\t\t<ul class=\"log-list\" id=\"activityLog\"></ul>\n\t\t\t</div>\n\t\t</div>\n\n\t\t\x3c!-- Channels page --\x3e\n\t\t<div id=\"page-channels\" style=\"display:none\">\n\t\t\t<div class=\"page-header\">\n\t\t\t\t<h2 class=\"page-title\">채널 상세</h2>\n\t\t\t\t<button class=\"refresh-btn\" onclick=\"refreshAll()\">\n\t\t\t\t\t<svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><polyline points=\"23 4 23 10 17 10\"/><path d=\"M20.49 15a9 9 0 1 1-2.12-9.36L23 10\"/></svg>\n\t\t\t\t\tRefresh\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t\t<div class=\"section\">\n\t\t\t\t<div class=\"section-title\">판매 채널</div>\n\t\t\t\t<div class=\"channel-grid\" id=\"sellingChannelDetail\"></div>\n\t\t\t</div>\n\t\t\t<div class=\"section\">\n\t\t\t\t<div class=\"section-title\">소싱 채널</div>\n\t\t\t\t<div class=\"channel-grid\" id=\"sourcingChannelDetail\"></div>\n\t\t\t</div>\n\t\t\t<div class=\"section\">\n\t\t\t\t<div class=\"section-title\">Rate Limit 현황</div>\n\t\t\t\t<div class=\"table-wrap\">\n\t\t\t\t\t<table id=\"rateLimitTable\">\n\t\t\t\t\t\t<thead><tr><th>채널</th><th>최대 RPS</th><th>간격(ms)</th><th>일일 제한</th><th>사용률</th></tr></thead>\n\t\t\t\t\t\t<tbody></tbody>\n\t\t\t\t\t</table>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t\x3c!-- Orders page --\x3e\n\t\t<div id=\"page-orders\" style=\"display:none\">\n\t\t\t<div class=\"page-header\">\n\t\t\t\t<h2 class=\"page-title\">주문 현황</h2>\n\t\t\t\t<button class=\"refresh-btn\" onclick=\"refreshAll()\">\n\t\t\t\t\t<svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><polyline points=\"23 4 23 10 17 10\"/><path d=\"M20.49 15a9 9 0 1 1-2.12-9.36L23 10\"/></svg>\n\t\t\t\t\tRefresh\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t\t<div class=\"metric-grid\" id=\"orderMetrics\"></div>\n\t\t\t<div class=\"section\">\n\t\t\t\t<div class=\"section-title\">주문 상태 분포</div>\n\t\t\t\t<div id=\"orderStatusBar\" style=\"margin-bottom:20px\"></div>\n\t\t\t</div>\n\t\t\t<div class=\"section\">\n\t\t\t\t<div class=\"section-title\">최근 주문</div>\n\t\t\t\t<div class=\"table-wrap\">\n\t\t\t\t\t<table id=\"orderTable\">\n\t\t\t\t\t\t<thead><tr><th>채널</th><th>주문ID</th><th>상태</th><th>금액</th><th>주문일</th><th>아이템</th></tr></thead>\n\t\t\t\t\t\t<tbody></tbody>\n\t\t\t\t\t</table>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t\x3c!-- Sourcing page --\x3e\n\t\t<div id=\"page-sourcing\" style=\"display:none\">\n\t\t\t<div class=\"page-header\">\n\t\t\t\t<h2 class=\"page-title\">소싱 채널</h2>\n\t\t\t</div>\n\t\t\t<div class=\"channel-grid\" id=\"sourcingCards\"></div>\n\t\t\t<div class=\"section\" style=\"margin-top:24px\">\n\t\t\t\t<div class=\"section-title\">채널 기능 비교</div>\n\t\t\t\t<div class=\"table-wrap\">\n\t\t\t\t\t<table id=\"sourcingCapTable\">\n\t\t\t\t\t\t<thead><tr><th>기능</th><th>도매꾹</th><th>특가몰</th><th>CJ Dropshipping</th></tr></thead>\n\t\t\t\t\t\t<tbody></tbody>\n\t\t\t\t\t</table>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</main>\n</div>\n\n<script>\n/* ── State ── */\nlet state = { channels: [], sourceChannels: [], orders: [], logs: [] };\n\n/* ── Theme ── */\nconst themeSelect = document.getElementById('themeSelect');\nconst saved = localStorage.getItem('ds-theme');\nif (saved) { document.documentElement.dataset.theme = saved; themeSelect.value = saved; }\nthemeSelect.addEventListener('change', (e) => {\n\tdocument.documentElement.dataset.theme = e.target.value;\n\tlocalStorage.setItem('ds-theme', e.target.value);\n});\n\n/* ── Navigation ── */\ndocument.querySelectorAll('.nav-item').forEach(btn => {\n\tbtn.addEventListener('click', () => {\n\t\tdocument.querySelectorAll('.nav-item').forEach(b => b.classList.remove('active'));\n\t\tbtn.classList.add('active');\n\t\tconst page = btn.dataset.page;\n\t\tdocument.querySelectorAll('[id^=\"page-\"]').forEach(p => p.style.display = 'none');\n\t\tdocument.getElementById('page-' + page).style.display = '';\n\t});\n});\n\n/* ── API helpers ── */\nasync function api(path) {\n\ttry {\n\t\tconst r = await fetch('/api' + path);\n\t\tif (!r.ok) return null;\n\t\treturn await r.json();\n\t} catch { return null; }\n}\n\n/* ── Badge helper ── */\nfunction badge(text, variant) {\n\treturn '<span class=\"badge badge-' + variant + '\">' + text + '</span>';\n}\nfunction dot(variant) {\n\treturn '<span class=\"dot dot-' + variant + '\"></span>';\n}\n\n/* ── Status helpers ── */\nconst STATUS_LABELS = {\n\tpending: '결제대기', paid: '결제완료', confirmed: '발주확인',\n\tpreparing: '준비중', shipped: '배송중', delivered: '배송완료',\n\tcancelled: '취소', return_requested: '반품요청', returned: '반품완료', exchanged: '교환완료'\n};\nconst STATUS_COLORS = {\n\tpending: 'var(--muted-foreground)', paid: 'var(--warning)',\n\tconfirmed: 'var(--primary)', preparing: 'var(--primary)',\n\tshipped: 'var(--success)', delivered: 'var(--success)',\n\tcancelled: 'var(--destructive)', return_requested: 'var(--destructive)',\n\treturned: 'var(--muted-foreground)', exchanged: 'var(--muted-foreground)'\n};\nfunction statusBadge(status) {\n\tconst map = { pending:'muted', paid:'warning', confirmed:'success', preparing:'warning',\n\t\tshipped:'success', delivered:'success', cancelled:'destructive',\n\t\treturn_requested:'destructive', returned:'muted', exchanged:'muted' };\n\treturn badge(STATUS_LABELS[status] || status, map[status] || 'muted');\n}\n\n/* ── Render functions ── */\nfunction renderOverviewMetrics() {\n\tconst selling = state.channels;\n\tconst sourcing = state.sourceChannels;\n\tconst connected = selling.filter(c => c.connected).length;\n\tconst srcConnected = sourcing.filter(c => c.connected).length;\n\tconst totalOrders = state.orders.length;\n\tconst actionNeeded = state.orders.filter(o => o.status === 'paid' || o.status === 'pending').length;\n\n\tdocument.getElementById('overviewMetrics').innerHTML =\n\t\tmc('판매 채널', connected + ' / ' + selling.length + ' 연결') +\n\t\tmc('소싱 채널', srcConnected + ' / ' + sourcing.length + ' 연결') +\n\t\tmc('최근 주문', totalOrders + '건') +\n\t\tmc('처리 필요', actionNeeded + '건', actionNeeded > 0 ? 'var(--destructive)' : '');\n}\nfunction mc(label, value, color) {\n\treturn '<div class=\"metric-card\"><div class=\"metric-label\">' + label +\n\t\t'</div><div class=\"metric-value\"' + (color ? ' style=\"color:' + color + '\"' : '') + '>' + value + '</div></div>';\n}\n\nfunction renderChannelCards() {\n\tconst html = state.channels.map(ch => {\n\t\tconst statusDot = ch.connected ? dot('success') : dot('destructive');\n\t\tconst statusText = ch.connected ? badge('연결됨','success') : badge('미연결','destructive');\n\t\tlet warning = '';\n\t\tif (ch.keyExpiry) {\n\t\t\tconst days = ch.keyExpiry;\n\t\t\tif (days <= 30) warning = '<div style=\"margin-top:8px;font-size:12px;color:var(--destructive)\">키 만료 ' + days + '일 전</div>';\n\t\t\telse warning = '<div style=\"margin-top:8px;font-size:12px;color:var(--muted-foreground)\">키 만료 ' + days + '일 후</div>';\n\t\t}\n\t\treturn '<div class=\"channel-card\">' +\n\t\t\t'<div class=\"channel-header\"><span class=\"channel-name\">' + statusDot + ' ' + ch.channel + '</span>' + statusText + '</div>' +\n\t\t\t'<div class=\"channel-meta\">' +\n\t\t\t\tmetaRow('Store ID', ch.storeId || '—') +\n\t\t\t\tmetaRow('Rate Limit', ch.rateLimit + ' req/s') +\n\t\t\t\tmetaRow('알림 방식', ch.webhook ? '웹훅' : '폴링') +\n\t\t\t'</div>' + warning + '</div>';\n\t}).join('');\n\tdocument.getElementById('channelCards').innerHTML = html || '<div class=\"empty\">채널 데이터를 불러오는 중...</div>';\n}\nfunction metaRow(label, value) {\n\treturn '<div class=\"channel-meta-row\"><span>' + label + '</span><span>' + value + '</span></div>';\n}\n\nfunction renderChannelDetail() {\n\tdocument.getElementById('sellingChannelDetail').innerHTML = state.channels.map(ch => {\n\t\tconst statusDot = ch.connected ? dot('success') : dot('destructive');\n\t\tconst statusText = ch.connected ? badge('연결됨','success') : badge('미연결','destructive');\n\t\tlet extra = '';\n\t\tif (!ch.connected) extra = '<div style=\"margin-top:8px;font-size:12px;color:var(--muted-foreground)\">' + (ch.hint || '') + '</div>';\n\t\tif (ch.keyExpiry) {\n\t\t\tconst d = ch.keyExpiry;\n\t\t\textra += '<div style=\"margin-top:6px;font-size:12px;color:' + (d <= 30 ? 'var(--destructive)' : 'var(--muted-foreground)') + '\">HMAC 키 만료: ' + d + '일 ' + (d<=30?'(갱신 필요!)':'후') + '</div>';\n\t\t}\n\t\treturn '<div class=\"channel-card\">' +\n\t\t\t'<div class=\"channel-header\"><span class=\"channel-name\">' + statusDot + ' ' + ch.channel + '</span>' + statusText + '</div>' +\n\t\t\t'<div class=\"channel-meta\">' +\n\t\t\t\tmetaRow('Store ID', ch.storeId || '—') +\n\t\t\t\tmetaRow('Max RPS', ch.rateLimit + ' req/s') +\n\t\t\t\tmetaRow('Interval', ch.intervalMs + 'ms') +\n\t\t\t\tmetaRow('Webhook', ch.webhook ? 'Yes' : 'No') +\n\t\t\t'</div>' + extra + '</div>';\n\t}).join('');\n\n\tdocument.getElementById('sourcingChannelDetail').innerHTML = state.sourceChannels.map(ch => {\n\t\tconst statusDot = ch.connected ? dot('success') : dot('destructive');\n\t\tconst statusText = ch.connected ? badge('연결됨','success') : badge('미연결','destructive');\n\t\treturn '<div class=\"channel-card\">' +\n\t\t\t'<div class=\"channel-header\"><span class=\"channel-name\">' + statusDot + ' ' + ch.channel + '</span>' + statusText + '</div>' +\n\t\t\t'<div class=\"channel-meta\">' +\n\t\t\t\tmetaRow('Max RPS', ch.rateLimit + ' req/s') +\n\t\t\t\tmetaRow('Interval', ch.intervalMs + 'ms') +\n\t\t\t\tmetaRow('일일 제한', ch.dailyLimit ? ch.dailyLimit.toLocaleString() + '회' : '없음') +\n\t\t\t'</div></div>';\n\t}).join('');\n\n\t// Rate limit table\n\tconst allCh = [...state.channels.map(c=>({...c,type:'판매'})), ...state.sourceChannels.map(c=>({...c,type:'소싱'}))];\n\tdocument.querySelector('#rateLimitTable tbody').innerHTML = allCh.map(ch => {\n\t\tconst usage = Math.random() * 0.4; // simulated\n\t\tconst color = usage > 0.8 ? 'var(--destructive)' : usage > 0.5 ? 'var(--warning)' : 'var(--success)';\n\t\treturn '<tr><td>' + ch.channel + ' <span style=\"font-size:11px;color:var(--muted-foreground)\">(' + ch.type + ')</span></td>' +\n\t\t\t'<td>' + ch.rateLimit + '</td><td>' + ch.intervalMs + '</td>' +\n\t\t\t'<td>' + (ch.dailyLimit ? ch.dailyLimit.toLocaleString() : '—') + '</td>' +\n\t\t\t'<td><div class=\"rate-bar-bg\"><div class=\"rate-bar-fill\" style=\"width:' + (usage*100) + '%;background:' + color + '\"></div></div></td></tr>';\n\t}).join('');\n}\n\nfunction renderOrders() {\n\tconst orders = state.orders;\n\tconst byStatus = {};\n\torders.forEach(o => { byStatus[o.status] = (byStatus[o.status]||0) + 1; });\n\n\tconst total = orders.length || 1;\n\tconst paid = byStatus.paid || 0;\n\tconst confirmed = byStatus.confirmed || 0;\n\tconst shipped = byStatus.shipped || 0;\n\tconst delivered = byStatus.delivered || 0;\n\tconst cancelled = byStatus.cancelled || 0;\n\tconst pending = byStatus.pending || 0;\n\n\tdocument.getElementById('orderMetrics').innerHTML =\n\t\tmc('전체 주문', orders.length + '건') +\n\t\tmc('결제 완료', paid + '건', paid > 0 ? 'var(--warning)' : '') +\n\t\tmc('배송 중', shipped + '건') +\n\t\tmc('배송 완료', delivered + '건', delivered > 0 ? 'var(--success)' : '') +\n\t\tmc('취소/반품', (cancelled + (byStatus.returned||0)) + '건', cancelled > 0 ? 'var(--destructive)' : '');\n\n\t// Status bar\n\tif (orders.length > 0) {\n\t\tconst segs = [\n\t\t\t{s:'pending',l:'대기',n:pending}, {s:'paid',l:'결제',n:paid},\n\t\t\t{s:'confirmed',l:'확인',n:confirmed}, {s:'shipped',l:'배송',n:shipped},\n\t\t\t{s:'delivered',l:'완료',n:delivered}, {s:'cancelled',l:'취소',n:cancelled}\n\t\t].filter(s => s.n > 0);\n\t\tdocument.getElementById('orderStatusBar').innerHTML = '<div class=\"status-bar\">' +\n\t\t\tsegs.map(s => '<div class=\"status-bar-seg\" style=\"flex:' + s.n + ';background:' + STATUS_COLORS[s.s] + ';color:#fff\">' + s.l + ' ' + s.n + '</div>').join('') + '</div>';\n\t} else {\n\t\tdocument.getElementById('orderStatusBar').innerHTML = '<div class=\"empty\">주문 데이터 없음</div>';\n\t}\n\n\t// Table\n\tdocument.querySelector('#orderTable tbody').innerHTML = orders.length === 0\n\t\t? '<tr><td colspan=\"6\" style=\"text-align:center;color:var(--muted-foreground);padding:24px\">주문 데이터를 불러올 수 없거나 비어있습니다</td></tr>'\n\t\t: orders.slice(0, 50).map(o => {\n\t\t\tconst items = (o.items || []).map(i => i.productName).join(', ') || '—';\n\t\t\tconst amt = typeof o.totalAmount === 'number' ? o.totalAmount.toLocaleString() + '원' : '—';\n\t\t\tconst date = o.orderedAt ? new Date(o.orderedAt).toLocaleDateString('ko-KR') : '—';\n\t\t\treturn '<tr><td>' + o.channel + '</td><td style=\"font-family:monospace;font-size:12px\">' +\n\t\t\t\t(o.channelOrderId||o.id||'—') + '</td><td>' + statusBadge(o.status) + '</td><td style=\"text-align:right\">' +\n\t\t\t\tamt + '</td><td>' + date + '</td><td style=\"max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap\">' + items + '</td></tr>';\n\t\t}).join('');\n}\n\nfunction renderSourcing() {\n\tdocument.getElementById('sourcingCards').innerHTML = state.sourceChannels.map(ch => {\n\t\tconst statusDot = ch.connected ? dot('success') : dot('destructive');\n\t\tconst statusText = ch.connected ? badge('연결됨','success') : badge('미연결','destructive');\n\t\treturn '<div class=\"channel-card\">' +\n\t\t\t'<div class=\"channel-header\"><span class=\"channel-name\">' + statusDot + ' ' + ch.channel + '</span>' + statusText + '</div>' +\n\t\t\t'<div class=\"channel-meta\">' +\n\t\t\t\tmetaRow('Max RPS', ch.rateLimit + ' req/s') +\n\t\t\t\tmetaRow('일일 제한', ch.dailyLimit ? ch.dailyLimit.toLocaleString() + '회' : '—') +\n\t\t\t'</div></div>';\n\t}).join('');\n\n\tconst features = [\n\t\t['상품 검색', true, true, true],\n\t\t['상품 상세', true, true, true],\n\t\t['카테고리 조회', true, true, true],\n\t\t['자동 발주', false, false, true],\n\t\t['일일 제한', '15,000회', '없음', '없음'],\n\t];\n\tdocument.querySelector('#sourcingCapTable tbody').innerHTML = features.map(f =>\n\t\t'<tr><td>' + f[0] + '</td>' +\n\t\t'<td>' + capCell(f[1]) + '</td>' +\n\t\t'<td>' + capCell(f[2]) + '</td>' +\n\t\t'<td>' + capCell(f[3]) + '</td></tr>'\n\t).join('');\n}\nfunction capCell(v) {\n\tif (v === true) return '<span style=\"color:var(--success)\">O</span>';\n\tif (v === false) return '<span style=\"color:var(--muted-foreground)\">—</span>';\n\treturn v;\n}\n\nfunction renderActivityLog() {\n\tconst logs = state.logs;\n\tif (logs.length === 0) {\n\t\tdocument.getElementById('activityLog').innerHTML = '<div class=\"empty\">활동 로그가 없습니다</div>';\n\t\treturn;\n\t}\n\tdocument.getElementById('activityLog').innerHTML = logs.slice(0, 30).map(l =>\n\t\t'<li class=\"log-item\"><span class=\"log-time\">' + l.time + '</span>' +\n\t\t'<span class=\"log-channel\">' + l.channel + '</span>' +\n\t\t'<span>' + l.message + '</span></li>'\n\t).join('');\n}\n\n/* ── Data fetch ── */\nasync function refreshAll() {\n\tconst [statusData, orderData] = await Promise.all([\n\t\tapi('/status'),\n\t\tapi('/orders'),\n\t]);\n\n\tif (statusData) {\n\t\tstate.channels = statusData.channels || [];\n\t\tstate.sourceChannels = statusData.sourceChannels || [];\n\t\tstate.logs = statusData.logs || [];\n\t}\n\tif (orderData) {\n\t\tstate.orders = orderData.orders || [];\n\t}\n\n\trenderOverviewMetrics();\n\trenderChannelCards();\n\trenderChannelDetail();\n\trenderOrders();\n\trenderSourcing();\n\trenderActivityLog();\n\n\tdocument.querySelectorAll('.loading').forEach(el => el.classList.remove('loading'));\n}\n\n/* ── Init ── */\nrefreshAll();\nsetInterval(refreshAll, 30000);\n<\/script>\n</body>\n</html>");if("/api/status"===e){const t={channels:Ft.map(t=>{const n=v(t),e=S[t],r={channel:t,connected:!!n,storeId:n?.storeId??null,rateLimit:e.maxRps,intervalMs:e.intervalMs,webhook:T[t],hint:(o=t,{cafe24:"CAFE24_MALL_ID, CAFE24_CLIENT_ID, CAFE24_CLIENT_SECRET",coupang:"COUPANG_VENDOR_ID, COUPANG_ACCESS_KEY, COUPANG_SECRET_KEY",smartstore:"SMARTSTORE_CLIENT_ID, SMARTSTORE_CLIENT_SECRET",shopify:"SHOPIFY_STORE, SHOPIFY_ACCESS_TOKEN"}[o])};var o;if("coupang"===t&&n){const t=n;if(t.keyExpiresAt){const n=Math.floor((new Date(t.keyExpiresAt).getTime()-Date.now())/864e5);r.keyExpiry=n}}return r}),sourceChannels:zt.map(t=>{const n=$(t),e=x[t];return{channel:t,connected:!!n,rateLimit:e.maxRps,intervalMs:e.intervalMs,dailyLimit:e.dailyLimit??null}}),logs:Ht};return n.writeHead(200,{"Content-Type":"application/json"}),void n.end(JSON.stringify(t))}if("/api/orders"!==e)n.writeHead(404,{"Content-Type":"application/json"}),n.end(JSON.stringify({error:"Not found"}));else try{const t=await async function(){const t=[];for(const n of Ft)if(v(n))try{const{getClient:e}=await Promise.resolve().then(()=>(Et(),kt)),r=e(n);if("getOrders"in r&&"function"==typeof r.getOrders){const e=await r.getOrders({days:7});Array.isArray(e)&&(t.push(...e),Kt(n,`주문 ${e.length}건 조회`))}}catch(t){Kt(n,`주문 조회 실패: ${t instanceof Error?t.message:String(t)}`)}return t.sort((t,n)=>{const e=new Date(t.orderedAt||0).getTime();return new Date(n.orderedAt||0).getTime()-e}),{orders:t}}();n.writeHead(200,{"Content-Type":"application/json"}),n.end(JSON.stringify(t))}catch{n.writeHead(200,{"Content-Type":"application/json"}),n.end(JSON.stringify({orders:[]}))}})(t,n).catch(t=>{console.error("Dashboard error:",t),n.writeHead(500),n.end("Internal Server Error")})}).listen(t,()=>{Kt("system","Dashboard 서버 시작"),console.log("\n Dropship AI Dashboard"),console.log(" ─────────────────────"),console.log(` Local: http://localhost:${t}`),console.log("\n Ctrl+C to stop\n")})}(e),new Promise(()=>{}))}var Xt="0.1.0",Yt=`\nrocketsell v${Xt} — 위탁셀링 자동화 CLI\n\n사용법:\n rocketsell <command> [options]\n\n인증:\n auth <channel> OAuth 자동 인증 (브라우저)\n --mall <id> --client-id <id> --client-secret <secret>\n\n설정:\n init 채널 credentials 대화형 설정\n config list 현재 설정 확인\n config get <channel> 채널 설정 확인\n config set <channel> <key> <value> 채널 설정 변경\n config remove <channel> 채널 설정 삭제\n\n상태:\n status 전체 채널 연동 상태\n cap [channel] 채널별 API 기능 현황\n\n상품:\n product list <channel> 상품 목록 조회\n product register <channel> --file <path> JSON 파일로 상품 등록\n product get <channel> <productId> 상품 상세 조회\n\n주문:\n order list <channel> [--days N] 최근 주문 조회 (기본 7일)\n order confirm <channel> <orderId> 주문 확인/발주\n order cancel <channel> <orderId> 주문 취소\n\n송장:\n invoice register <channel> <orderId> <carrier> <tracking>\n\n재고:\n inventory sync <channel> <productId> <qty> 재고 수량 업데이트\n inventory plan <totalStock> [sku] 재고 분배 시뮬레이션\n\n소싱:\n source search <channel> <keyword> [--page N] [--category ID]\n 소싱 채널에서 상품 검색\n source detail <channel> <productId> 소싱 상품 상세 조회\n source categories <channel> [--parent ID]\n 소싱 채널 카테고리 목록\n\n데이터 (PostgreSQL 필요):\n mapping add <src> <srcId> <sell> <sellId> 상품 매핑 추���\n mapping list [--source <ch>] [--selling <ch>] 매핑 목록\n log [list] [--channel <ch>] [--days N] 작업 이력 조회\n\n모니터링:\n dashboard [--port N] 웹 대시보드 (기본 포트: 3847)\n\n기타:\n rate-test Rate limiter 성능 테스트\n\n옵션:\n --help, -h 이 도움말\n --version, -v 버전 표시\n --json JSON 출력\n\n판매 채널: cafe24, coupang, smartstore, shopify\n소싱 채널: domeggook, specialoffer, cjdropshipping\n`.trim();C();import*as Qt from"readline";var Vt=["cafe24","coupang","smartstore","shopify"],Wt=["domeggook","specialoffer","cjdropshipping"];function Zt(t,n){return new Promise(e=>{t.question(n,t=>e(t.trim()))})}async function tn(t,n){switch(n){case"cafe24":{const n=await Zt(t," Mall ID: "),e=await Zt(t," Client ID: "),r=await Zt(t," Client Secret: ");return n&&e&&r?{channel:"cafe24",storeId:n,clientId:e,clientSecret:r}:null}case"coupang":{const n=await Zt(t," Vendor ID: "),e=await Zt(t," Access Key: "),r=await Zt(t," Secret Key: ");if(!n||!e||!r)return null;const o=new Date((new Date).getTime()+15552e6);return console.log(` ⚠️ 키 만료일: ${o.toISOString().split("T")[0]} (180일 후)`),{channel:"coupang",storeId:n,accessKey:e,secretKey:r,keyExpiresAt:o.toISOString()}}case"smartstore":{const n=await Zt(t," Client ID (애플리케이션 ID): "),e=await Zt(t," Client Secret: ");return n&&e?{channel:"smartstore",storeId:"default",clientId:n,clientSecret:e}:null}case"shopify":{const n=await Zt(t," Store domain (예: mystore.myshopify.com): "),e=await Zt(t," Access Token: ");return n&&e?{channel:"shopify",storeId:n,accessToken:e,apiVersion:await Zt(t," API Version (기본 2024-10): ")||"2024-10"}:null}default:return null}}async function nn(t,n){switch(n){case"domeggook":{const n=await Zt(t," API Key: ");return n?{channel:"domeggook",apiKey:n}:null}case"specialoffer":{const n=await Zt(t," API Key: ");return n?{channel:"specialoffer",apiKey:n}:null}case"cjdropshipping":{const n=await Zt(t," Access Token (CJ-Access-Token): ");return n?{channel:"cjdropshipping",accessToken:n}:null}default:return null}}Et(),N(),Et(),C();var en={cj:{cafe24:"0004",coupang:"CJGLS",smartstore:"CJGLS",shopify:"CJ대한통운"},hanjin:{cafe24:"0005",coupang:"HANJIN",smartstore:"HANJIN",shopify:"한진택배"},lotte:{cafe24:"0008",coupang:"LOTTE",smartstore:"LOTTE",shopify:"롯데택배"},post:{cafe24:"0001",coupang:"EPOST",smartstore:"EPOST",shopify:"우체국택배"},logen:{cafe24:"0010",coupang:"LOGEN",smartstore:"LOGEN",shopify:"로젠택배"}};C();import{appendFileSync as rn,existsSync as on,readFileSync as sn}from"fs";import{join as an}from"path";var cn=null,ln=!1,dn=null;async function un(){if(!1===dn)return null;const t=function(){const t=g();return t.database?.url??process.env.ROCKETSELL_DATABASE_URL??null}();if(!t)return dn=!1,null;if(cn)return cn;try{const n=await import("pg"),e=n.default?.Pool??n.Pool;return cn=new e({connectionString:t,max:5}),dn=!0,cn}catch{return dn=!1,null}}async function pn(t){ln||(await t.query("\n\t\tCREATE TABLE IF NOT EXISTS operations (\n\t\t\tid SERIAL PRIMARY KEY,\n\t\t\tcommand TEXT NOT NULL,\n\t\t\tchannel TEXT,\n\t\t\targs JSONB,\n\t\t\tresult TEXT,\n\t\t\terror_msg TEXT,\n\t\t\tduration_ms INTEGER,\n\t\t\tcreated_at TIMESTAMPTZ DEFAULT NOW()\n\t\t);\n\n\t\tCREATE TABLE IF NOT EXISTS product_mappings (\n\t\t\tid SERIAL PRIMARY KEY,\n\t\t\tsource_channel TEXT NOT NULL,\n\t\t\tsource_id TEXT NOT NULL,\n\t\t\tselling_channel TEXT NOT NULL,\n\t\t\tselling_id TEXT NOT NULL,\n\t\t\twholesale_price NUMERIC,\n\t\t\tselling_price NUMERIC,\n\t\t\tmargin_pct NUMERIC,\n\t\t\tcreated_at TIMESTAMPTZ DEFAULT NOW(),\n\t\t\tupdated_at TIMESTAMPTZ DEFAULT NOW(),\n\t\t\tUNIQUE(source_channel, source_id, selling_channel)\n\t\t);\n\n\t\tCREATE TABLE IF NOT EXISTS order_history (\n\t\t\tid SERIAL PRIMARY KEY,\n\t\t\tchannel TEXT NOT NULL,\n\t\t\torder_id TEXT NOT NULL,\n\t\t\tstatus TEXT NOT NULL,\n\t\t\tbuyer_name TEXT,\n\t\t\ttotal_amount NUMERIC,\n\t\t\titems JSONB,\n\t\t\ttracking_no TEXT,\n\t\t\tcarrier TEXT,\n\t\t\tcreated_at TIMESTAMPTZ DEFAULT NOW(),\n\t\t\tupdated_at TIMESTAMPTZ DEFAULT NOW(),\n\t\t\tUNIQUE(channel, order_id)\n\t\t);\n\n\t\tCREATE TABLE IF NOT EXISTS inventory_snapshots (\n\t\t\tid SERIAL PRIMARY KEY,\n\t\t\tchannel TEXT NOT NULL,\n\t\t\tproduct_id TEXT NOT NULL,\n\t\t\tsku TEXT,\n\t\t\tquantity INTEGER NOT NULL,\n\t\t\tsnapshot_at TIMESTAMPTZ DEFAULT NOW()\n\t\t);\n\n\t\tCREATE INDEX IF NOT EXISTS idx_inv_channel_product\n\t\t\tON inventory_snapshots(channel, product_id, snapshot_at DESC);\n\t"),ln=!0)}function hn(){return an(w(),"operations.jsonl")}async function fn(t){const n=await un();if(n)try{await pn(n),await n.query("INSERT INTO operations (command, channel, args, result, error_msg, duration_ms)\n\t\t\t VALUES ($1, $2, $3, $4, $5, $6)",[t.command,t.channel??null,t.args?JSON.stringify(t.args):null,t.result,t.errorMsg??null,t.durationMs])}catch{}else try{!function(t){const n=JSON.stringify({...t,created_at:(new Date).toISOString()});rn(hn(),`${n}\n`,"utf-8")}({command:t.command,channel:t.channel??null,args:t.args??null,result:t.result,error_msg:t.errorMsg??null,duration_ms:t.durationMs})}catch{}}async function gn(t,n){const[e]=t;switch(e){case"list":case"ls":case void 0:return await async function(t){const n="string"==typeof t.channel?t.channel:void 0,e="string"==typeof t.command?t.command:void 0,r="string"==typeof t.days?Number(t.days):7,o=await async function(t){const n=await un();if(!n)return function(t){const n=hn();if(!on(n))return[];const e=sn(n,"utf-8").trim().split("\n").filter(Boolean),r=Date.now()-24*(t?.days??7)*60*60*1e3;return e.map(t=>{try{return JSON.parse(t)}catch{return null}}).filter(n=>!(!n||new Date(n.created_at).getTime()<r||t?.channel&&n.channel!==t.channel||t?.command&&!String(n.command).includes(t.command))).reverse().slice(0,100)}(t);try{await pn(n);const e=[],r=[];t?.channel&&(r.push(t.channel),e.push(`channel = $${r.length}`)),t?.command&&(r.push(t.command),e.push(`command = $${r.length}`));const o=t?.days??7;r.push(o),e.push(`created_at > NOW() - INTERVAL '1 day' * $${r.length}`);const s=e.length>0?`WHERE ${e.join(" AND ")}`:"";return(await n.query(`SELECT * FROM operations ${s} ORDER BY created_at DESC LIMIT 100`,r)).rows}catch{return[]}}({channel:n,command:e,days:r});if(0===o.length)return console.log(`최근 ${r}일간 작업 이력이 없습니다.`),0;console.log(`\n작업 이력 (최근 ${r}일, ${o.length}건)\n${"─".repeat(80)}`);for(const t of o){const n=new Date(t.created_at).toLocaleString("ko-KR"),e="success"===t.result?"✓":"✗",r=t.duration_ms?`${t.duration_ms}ms`:"";console.log(` ${e} [${n}] ${t.command} ${t.channel??""} ${r}`),"error"===t.result&&t.error_msg&&console.log(` └ ${t.error_msg}`)}return console.log(""),0}(n);default:return console.log("사용법: rocketsell log [list] [--channel <ch>] [--days N]"),1}}Et(),C(),N();import{format as mn,subDays as vn}from"date-fns";Et(),N();import{readFileSync as yn}from"fs";function wn(t,n,e){switch(n){case"cafe24":return function(t,n){const e=t.variants[0]?.salePrice??t.variants[0]?.price??0,r=t.variants[0]?.price??e;return{product_name:t.name,description:t.description,price:e,retail_price:r!==e?r:void 0,detail_image:t.imageUrl||void 0,list_image:t.imageUrl||void 0,options:t.variants.map(t=>({value:t.optionLabel,qty:t.stockQuantity,price:t.price})),use_sale:"active"===t.status?"T":"F",...n}}(t,e);case"coupang":return function(t,n){return{displayProductName:t.name,generalProductName:t.name,productGroup:t.category??"기타",detailDescription:t.description,representativeImageUrl:t.imageUrl||void 0,items:t.variants.map(t=>({itemName:t.optionLabel,originalPrice:t.price,salePrice:t.salePrice??t.price,maximumBuyCount:99,maximumBuyForPerson:99,outboundShippingTimeDay:2,maximumBuyForPersonPeriod:30})),...n}}(t,e);case"smartstore":return function(t,n){const e=t.variants[0];return{channelProductDisplayStatusType:"active"===t.status?"ON_SALE":"WAIT",name:t.name,detailContent:t.description,representativeImage:t.imageUrl?{url:t.imageUrl}:void 0,salePrice:e?.salePrice??e?.price??0,stockQuantity:t.variants.reduce((t,n)=>t+n.stockQuantity,0),...n}}(t,e);case"shopify":return function(t,n){return{title:t.name,body_html:t.description,images:t.imageUrl?[{src:t.imageUrl}]:[],variants:t.variants.map(t=>({title:t.optionLabel,price:String(t.salePrice??t.price),compare_at_price:t.salePrice?String(t.price):void 0,sku:t.sku,inventory_quantity:t.stockQuantity})),status:"active"===t.status?"active":"draft",...n}}(t,e)}}function bn(t,n){if(!t||"object"!=typeof t)return null;const e=t;switch(n){case"cafe24":return String(e.product_no??e.productNo??"")||null;case"coupang":return String(e.sellerProductId??e.productId??"")||null;case"smartstore":return String(e.originProductNo??e.channelProductNo??"")||null;case"shopify":return String(e.id??"")||null}}function $n(t,n){n.json,console.log(JSON.stringify(t,null,2))}function kn(){console.log("사용법:\n rocketsell source search <channel> <keyword> [--page N] [--category ID]\n rocketsell source detail <channel> <productId>\n rocketsell source categories <channel>\n\n소싱 채널: domeggook, specialoffer, cjdropshipping")}function Sn(){return xt("domeggook")}function Tn(){return xt("specialoffer")}function In(){return xt("cjdropshipping")}function xn(t){switch(t){case"cafe24":return"설정 필요: CAFE24_MALL_ID, CAFE24_CLIENT_ID, CAFE24_CLIENT_SECRET";case"coupang":return"설정 필요: COUPANG_VENDOR_ID, COUPANG_ACCESS_KEY, COUPANG_SECRET_KEY";case"smartstore":return"설정 필요: SMARTSTORE_CLIENT_ID, SMARTSTORE_CLIENT_SECRET";case"shopify":return"설정 필요: SHOPIFY_STORE, SHOPIFY_ACCESS_TOKEN"}}_t(),H(),N(),Et(),O(),C(),N(),z(),C(),O();var En=["domeggook","specialoffer","cjdropshipping"];var Cn=await async function(t){const{positional:n,flags:e}=function(t){const n=[],e={};for(let r=0;r<t.length;r++){const o=t[r];if("--"===o){n.push(...t.slice(r+1));break}if(o.startsWith("--")){const n=o.indexOf("=");if(-1!==n)e[o.slice(2,n)]=o.slice(n+1);else{const n=t[r+1];n&&!n.startsWith("--")?(e[o.slice(2)]=n,r++):e[o.slice(2)]=!0}}else n.push(o)}return{positional:n,flags:e}}(t);if(e.version||e.v)return console.log(`rocketsell v${Xt}`),0;if(e.help||e.h||0===n.length)return console.log(Yt),0;const[r,...o]=n,s=Date.now();try{let t;switch(r){case"auth":case"login":t=await async function(t,n){const[e]=t;if(!e)return console.log("사용법:\n rocketsell auth <channel>\n\n 지원 채널: cafe24\n\n 예시:\n rocketsell auth cafe24 --mall onlymytho001 --client-id xxx --client-secret xxx"),1;const r=e;return Rt[r]?"cafe24"===r?async function(t){const n=(await import("readline")).createInterface({input:process.stdin,output:process.stdout}),e=t=>new Promise(e=>n.question(t,t=>e(t.trim())));let r=t.mall,o=t["client-id"],s=t["client-secret"];if(!(r&&o&&s||(console.log("\n🔐 cafe24 OAuth 인증\n"),r=r||await e(" Mall ID: "),o=o||await e(" Client ID: "),s=s||await e(" Client Secret: "),r&&o&&s)))return console.error("모든 값을 입력해야 합니다."),n.close(),1;const a=Rt.cafe24,c=a.scopes.join(","),i=a.authorizeUrl(r,o)+c;console.log("\n🔐 cafe24 OAuth 인증 시작\n"),console.log(" 1. 브라우저에서 권한을 승인하세요"),console.log(" 2. 승인 후 표시되는 인증 코드를 복사하세요\n");try{const{execFile:t}=await import("child_process");t("darwin"===process.platform?"open":"win32"===process.platform?"start":"xdg-open",[i]),console.log(" ⏳ 브라우저를 열었습니다...\n")}catch{console.log(` 브라우저를 직접 열어주세요:\n ${i}\n`)}const l=await e(" 인증 코드 붙여넣기: ");if(n.close(),!l)return console.error("\n❌ 인증 코드가 입력되지 않았습니다."),1;try{console.log("\n 📥 토큰 교환 중...");const t=a.tokenUrl(r),n=(await Pt.post(t,new URLSearchParams({grant_type:"authorization_code",code:l,redirect_uri:Mt}).toString(),{headers:{"Content-Type":"application/x-www-form-urlencoded"},auth:{username:o,password:s}})).data;return y("cafe24",{channel:"cafe24",storeId:r,clientId:o,clientSecret:s,accessToken:n.access_token,refreshToken:n.refresh_token,tokenExpiresAt:n.expires_at}),console.log("\n ✅ cafe24 인증 완료!"),console.log(` Mall ID: ${r}`),console.log(` 스코프: ${n.scopes?.join(", ")??c}`),console.log(` 만료: ${n.expires_at}`),console.log("\n 'rocketsell status' 로 연결을 확인하세요.\n"),0}catch(t){const n=t instanceof Error?t.message:String(t);return Pt.isAxiosError(t)&&t.response?.data?console.error("\n❌ 토큰 교환 실패:",t.response.data):console.error(`\n❌ 토큰 교환 실패: ${n}`),1}}(n):(console.error(`${r} OAuth는 아직 지원되지 않습니다.`),1):(console.error(`${r}은 OAuth 인증을 지원하지 않습니다.`),console.error("지원 채널: cafe24"),1)}(o,e);break;case"init":t=await async function(){const t=Qt.createInterface({input:process.stdin,output:process.stdout});console.log("\n🔧 rocketsell 채널 설정\n"),console.log(`설정 파일: ${b()}\n`),console.log("설정할 채널을 선택하세요 (쉼표로 복수 선택 가능):"),Vt.forEach((t,n)=>console.log(` ${n+1}. ${t}`));const n=(await Zt(t,"\n번호 입력 (예: 1,2): ")).split(",").map(t=>Number.parseInt(t.trim(),10)-1).filter(t=>t>=0&&t<Vt.length).map(t=>Vt[t]);if(0===n.length)return console.log("선택된 채널이 없습니다."),t.close(),1;for(const e of n){console.log(`\n--- ${e.toUpperCase()} 설정 ---`);const n=await tn(t,e);n&&(y(e,n),console.log(` ✅ ${e} 저장 완료`))}console.log("\n--- 소싱 채널 설정 ---\n"),console.log("설정할 소싱 채널을 선택하세요 (쉼표로 복수 선택 가능, Enter로 건너뛰기):"),Wt.forEach((t,n)=>console.log(` ${n+1}. ${t}`));const e=await Zt(t,"\n번호 입력 (예: 1,2): ");if(""!==e.trim()){const n=e.split(",").map(t=>Number.parseInt(t.trim(),10)-1).filter(t=>t>=0&&t<Wt.length).map(t=>Wt[t]);for(const e of n){console.log(`\n--- ${e.toUpperCase()} 설정 ---`);const n=await nn(t,e);n&&(k(e,n),console.log(` ✅ ${e} 저장 완료`))}}return t.close(),console.log(`\n설정이 ${b()} 에 저장되었습니다.`),console.log("'rocketsell status' 로 연결 상태를 확인하세요.\n"),0}();break;case"config":t=await async function(t,n){const[e,...r]=t;switch(e){case"list":return function(t){const n=g();if(!0===t.json)return console.log(JSON.stringify(n,null,2)),0;if(console.log(`\n📋 rocketsell 설정 (${b()})\n`),0===Object.keys(n.channels).length)return console.log(" 설정된 채널이 없습니다. 'rocketsell init' 으로 설정하세요.\n"),0;for(const t of qt){const e=n.channels[t];if(!e){console.log(` ${t.padEnd(14)} ❌ 미설정`);continue}const r=Bt(e);console.log(` ${t.padEnd(14)} ✅ 설정됨`);for(const[t,n]of Object.entries(r))"channel"!==t&&console.log(` ${t.padEnd(18)} ${n}`)}return console.log(),0}(n);case"get":return function(t){if(!t||!qt.includes(t))return console.error(`유효한 채널을 지정하세요: ${qt.join(", ")}`),1;const n=v(t);return n?(console.log(JSON.stringify(Bt(n),null,2)),0):(console.log(`${t}: 설정되지 않음`),1)}(r[0]);case"set":return function(t,n,e){if(!t||!n||void 0===e)return console.error("사용법: rocketsell config set <channel> <key> <value>"),1;const r=v(t)??{channel:t,storeId:""};return r[n]=e,y(t,r),console.log(`✅ ${t}.${n} = ${e}`),0}(r[0],r[1],r[2]);case"remove":case"rm":return(o=r[0])&&qt.includes(o)?(function(t){const n=g();delete n.channels[t],m(n)}(o),console.log(`✅ ${o} 설정이 삭제되었습니다.`),0):(console.error(`유효한 채널을 지정하세요: ${qt.join(", ")}`),1);default:return console.log("사용법: rocketsell config <list|get|set|remove> [args]"),1}var o}(o,e);break;case"status":case"health":t=await async function(t,n){console.log("\n=== 채널 연동 상태 ===\n");const e=[];for(const t of I){const n=v(t),r=!!n,o=T[t]?"웹훅":"폴링",s=S[t];let a,c;if(r){if(a="✅ 연결됨",c=`storeId: ${n?.storeId}`,"coupang"===t){const t=n;if(t.keyExpiresAt){const n=Math.floor((new Date(t.keyExpiresAt).getTime()-Date.now())/864e5);n<=30?(a="⚠️ 키 만료 임박",c+=` (${n}일 남음!)`):c+=` (키 만료: ${n}일 후)`}}}else a="❌ 미연결",c=xn(t);console.log(` ${t.padEnd(14)} ${a}`),console.log(` ${"".padEnd(14)} ${c}`),console.log(` ${"".padEnd(14)} [${s.maxRps} req/s] [${o}]\n`),e.push({channel:t,connected:r,status:a,note:c})}if(n.check){console.log("\n=== API 레이턴시 체크 ===\n");for(const t of I){if(!v(t)){console.log(` ${t.padEnd(14)} ⚠ credentials 없음 (건너뜀)`);continue}const n=Date.now();try{const{getClient:e}=await Promise.resolve().then(()=>(Et(),kt)),r=e(t);"listProducts"in r?await r.listProducts({limit:1}):"listOrders"in r&&await r.listOrders({limit:1});const o=Date.now()-n,s=o<1e3?"✅":o<3e3?"⚠️":"🐢";console.log(` ${t.padEnd(14)} ${s} ${o}ms`)}catch(e){const r=Date.now()-n,o=e instanceof Error?e.message.split("\n")[0]:String(e);console.log(` ${t.padEnd(14)} ❌ ${r}ms ${o}`)}}console.log()}return n.json&&console.log(JSON.stringify(e,null,2)),0}(0,e);break;case"cap":case"capabilities":t=function(t,n){const e=t[0];if(n.json)return console.log(JSON.stringify(e?Ut[e]:Ut,null,2)),0;const r=e?[e]:jt;for(const t of r){if(!Ut[t])return console.error(`알 수 없는 채널: ${t}`),1;console.log(`\n=== ${t.toUpperCase()} (${Ut[t].score}) ===\n`);for(const[n,e]of Object.entries(Ut[t]))"score"!==n&&console.log(` ${n.padEnd(18)} ${e}`)}return console.log(),0}(o,e);break;case"product":t=await async function(t,n){const[e,r,...o]=t;if(("register"===e||"create"===e)&&n["all-channels"])return async function(t){const n=t.file;if(!n)return console.error("--file <path> 옵션으로 상품 데이터 JSON 파일을 지정하세요."),1;const e=!0===t["dry-run"];let r;try{r=At(n)}catch(t){return console.error(`파일 읽기 실패: ${t.message}`),1}const o="string"==typeof t.rows?t.rows:null;if(o){const{parseRowRange:t}=await Promise.resolve().then(()=>(_t(),Ct)),{start:n,end:e}=t(o);r=r.slice(n,e+1)}console.log(`\n📦 전체 채널 상품 등록 (${r.length}건)${e?" [DRY-RUN]":""}\n`);const s={},a={};for(const t of I)if(e)console.log(` [DRY-RUN] ${t}: ${r.length}건 등록 건너뜀`);else try{const n=St(t),e=[];for(const o of r){const r=wn(o,t,o.channelOverrides?.[t]??{});let s;if(!("createProduct"in n))throw new Error(`${t} createProduct API 미지원`);s=await n.createProduct(r);const a=bn(s,t);a&&e.push(a)}s[t]=e,console.log(` ✅ ${t}: ${e.length}건 등록 완료`)}catch(n){const e=n instanceof Error?n.message.split("\n")[0]:String(n);a[t]=e,console.log(` ❌ ${t}: ${e}`)}const c=Object.keys(s),i=Object.keys(a);return console.log("\n결과:"),console.log(` 성공: ${c.join(", ")||"없음"}`),i.length>0&&(console.log(` 실패: ${i.join(", ")}`),console.log(` 재시도: rocketsell product register --all-channels --file ${n} (실패 채널만)`)),i.length>0&&0===c.length?1:0}(n);if(!e||!r)return console.log("사용법:\n rocketsell product list <channel>\n rocketsell product get <channel> <productId>\n rocketsell product register <channel> --file <path>\n rocketsell product register --all-channels --file <path>\n rocketsell product update <channel> <productId> --file <path>\n rocketsell product delete <channel> <productId>"),1;const s=r;if(!I.includes(s))return console.error(`유효한 채널: ${I.join(", ")}`),1;switch(e){case"list":return async function(t,n){const e=Number(n.limit)||20;switch(console.log(`\n📦 ${t} 상품 목록 (최대 ${e}건)\n`),t){case"cafe24":{const r=St(t);$n(await r.listProducts({limit:e}),n);break}case"coupang":St(t),console.log(" ⚠️ 쿠팡은 상품 목록 API가 없습니다. 상품 ID로 개별 조회하세요."),console.log(" 사용법: rocketsell product get coupang <sellerProductId>");break;case"smartstore":console.log(" ⚠️ 스마트스토어는 상품 목록 API가 없습니다. 상품 ID로 개별 조회하세요."),console.log(" 사용법: rocketsell product get smartstore <originProductNo>");break;case"shopify":{const r=St(t);$n(await r.listProducts(e),n);break}}return 0}(s,n);case"get":return async function(t,n,e){if(!n)return console.error("상품 ID를 지정하세요."),1;switch(console.log(`\n📦 ${t} 상품 조회: ${n}\n`),t){case"cafe24":{const r=St(t);$n(await r.getProduct(Number(n)),e);break}case"coupang":{const r=St(t);$n(await r.getProduct(n),e);break}case"smartstore":{const r=St(t);$n(await r.getProduct(n),e);break}case"shopify":{const r=St(t);$n(await r.getProduct(n),e);break}}return 0}(s,o[0],n);case"register":case"create":return async function(t,n){const e=n.file;if(!e)return console.error("--file <path> 옵션으로 상품 데이터 JSON 파일을 지정하세요."),1;const r=JSON.parse(yn(e,"utf-8"));switch(console.log(`\n📦 ${t} 상품 등록 중...\n`),t){case"cafe24":{const e=St(t);$n(await e.createProduct(r),n);break}case"coupang":{const e=St(t);$n(await e.createProduct(r),n);break}case"smartstore":{const e=St(t);$n(await e.createProduct(r),n);break}case"shopify":{const e=St(t);$n(await e.createProduct(r),n);break}}return console.log(" ✅ 등록 완료"),0}(s,n);case"update":return async function(t,n,e){if(!n)return console.error("상품 ID를 지정하세요."),1;const r=e.file;if(!r)return console.error("--file <path> 옵션으로 업데이트 데이터 JSON 파일을 지정하세요."),1;const o=JSON.parse(yn(r,"utf-8"));switch(console.log(`\n📦 ${t} 상품 수정: ${n}\n`),t){case"cafe24":{const r=St(t);$n(await r.updateProduct(Number(n),o),e);break}case"coupang":{const r=St(t);$n(await r.updateProduct(n,o),e);break}case"smartstore":{const r=St(t);$n(await r.updateProduct(n,o),e);break}case"shopify":{const r=St(t);$n(await r.updateProduct({id:n,...o}),e);break}}return console.log(" ✅ 수정 완료"),0}(s,o[0],n);case"delete":return async function(t,n){if(!n)return console.error("상품 ID를 지정하세요."),1;switch(console.log(`\n📦 ${t} 상품 삭제: ${n}\n`),t){case"cafe24":{const e=St(t);await e.deleteProduct(Number(n));break}case"coupang":{const e=St(t);await e.deleteProduct(n);break}case"smartstore":return console.log(" ⚠️ 스마트스토어는 상품 삭제 API를 지원하지 않습니다."),1;case"shopify":{const e=St(t);await e.deleteProduct(n);break}}return console.log(" ✅ 삭제 완료"),0}(s,o[0]);default:return console.error(`알 수 없는 하위 명령어: ${e}`),1}}(o,e);break;case"order":t=await async function(t,n){const[e,r,...o]=t;if("route"===e)return async function(t,n,e){return t?(console.log(`\n🔀 주문 라우팅: ${t}\n`),console.log(" 공급사 후보를 평가 중입니다..."),console.log(" (supplier-score 데이터 기반 자동 라우팅 — Phase 4에서 완전 구현)\n"),e.auto&&(console.log(" --auto 플래그 감지 → 스코어 기반 자동 선택"),console.log(" 현재: 기본값 domeggook 선택 (데이터 부족 시 fallback)")),0):(console.error("주문 ID를 지정하세요."),1)}(r,0,n);if(!e||!r)return console.log("사용법:\n rocketsell order list <channel> [--days N]\n rocketsell order get <channel> <orderId>\n rocketsell order confirm <channel> <orderId>\n rocketsell order cancel <channel> <orderId>\n rocketsell order route <orderId> --auto [--channel <ch>]"),1;const s=r;if(!I.includes(s))return console.error(`유효한 채널: ${I.join(", ")}`),1;switch(e){case"list":return async function(t,n){const e=Number(n.days)||7,r=new Date,o=vn(r,e),s=mn(o,"yyyy-MM-dd'T'00:00:00"),a=mn(r,"yyyy-MM-dd'T'23:59:59");switch(console.log(`\n📋 ${t} 주문 목록 (최근 ${e}일)\n`),t){case"cafe24":{const e=St(t),s=await e.listOrders({startDate:mn(o,"yyyy-MM-dd"),endDate:mn(r,"yyyy-MM-dd"),limit:Number(n.limit)||50});console.log(JSON.stringify(s,null,2));break}case"coupang":{const r=St(t),o=v("coupang");e>31&&console.log(" ⚠️ 쿠팡은 최대 31일 조회만 가능합니다.");const c=await r.listOrders({vendorId:o.storeId,createdAtFrom:s,createdAtTo:a,status:n.status});console.log(JSON.stringify(c,null,2));break}case"smartstore":{const n=St(t);e>1&&console.log(" ⚠️ 스마트스토어는 최대 24시간 윈도우만 가능합니다. 최근 24시간만 조회합니다.\n");const o=mn(vn(r,Math.min(e,1)),"yyyy-MM-dd'T'HH:mm:ss.SSS'+09:00'"),s=mn(r,"yyyy-MM-dd'T'HH:mm:ss.SSS'+09:00'"),a=await n.listChangedOrders({lastChangedFrom:o,lastChangedTo:s});console.log(JSON.stringify(a,null,2));break}case"shopify":{const e=St(t),r=await e.listOrders(Number(n.limit)||50,`created_at:>=${mn(o,"yyyy-MM-dd")}`);console.log(JSON.stringify(r,null,2));break}}return 0}(s,n);case"get":return async function(t,n){if(!n)return console.error("주문 ID를 지정하세요."),1;switch(console.log(`\n📋 ${t} 주문 상세: ${n}\n`),t){case"cafe24":{const e=St(t),r=await e.getOrder(n);console.log(JSON.stringify(r,null,2));break}case"smartstore":{const e=St(t),r=await e.getOrderDetail(n);console.log(JSON.stringify(r,null,2));break}default:console.log(` ${t}은 개별 주문 조회 API를 사용하세요.`)}return 0}(s,o[0]);case"confirm":return async function(t,n){if(!n)return console.error("주문 ID를 지정하세요."),1;switch(console.log(`\n✅ ${t} 주문 확인: ${n}\n`),t){case"cafe24":{const e=St(t);await e.updateOrderStatus(n,"accept");break}case"coupang":{const e=St(t),r=v("coupang");await e.confirmOrder(n,r.storeId);break}case"smartstore":{const e=St(t);await e.confirmOrder(n);break}case"shopify":return console.log(" Shopify는 자동 주문 확인 — 별도 확인 불필요"),0}return console.log(" ✅ 주문 확인 완료"),0}(s,o[0]);case"cancel":return async function(t,n){if(!n)return console.error("주문 ID를 지정하세요."),1;switch(console.log(`\n🚫 ${t} 주문 취소: ${n}\n`),t){case"cafe24":{const e=St(t);await e.updateOrderStatus(n,"cancel");break}case"coupang":return console.log(" ⚠️ 쿠팡은 발송 전 주문만 취소 가능합니다."),console.log(" 쿠팡 셀러 포탈에서 직접 취소하세요."),1;case"smartstore":{const e=St(t);await e.approveCancel(n);break}case"shopify":return console.log(" Shopify 주문 취소는 REST Admin API 필요 — 추후 지원"),1}return console.log(" ✅ 취소 완료"),0}(s,o[0]);default:return console.error(`알 수 없는 하위 명령어: ${e}`),1}}(o,e);break;case"invoice":t=await async function(t){const[n,e,r,o,s]=t;if(!("register"===n&&e&&r&&o&&s))return console.log("사용법:\n rocketsell invoice register <channel> <orderId> <carrier> <trackingNumber>\n\n택배사 코드 (alias):\n cj CJ대한통운\n hanjin 한진택배\n lotte 롯데택배\n post 우체국택배\n logen 로젠택배\n\n 또는 채널별 원래 코드를 직접 입력할 수 있습니다."),1;const a=e,c=en[o]?.[a]??o;switch(console.log(`\n🚚 ${a} 송장 등록`),console.log(` 주문: ${r}`),console.log(` 택배사: ${o} (${c})`),console.log(` 송장번호: ${s}\n`),a){case"cafe24":{const t=St(a);await t.createShipment(r,{carrierCode:c,trackingNumber:s});break}case"coupang":{const t=St(a),n=v("coupang");await t.registerInvoice(r,n.storeId,{carrierCode:c,trackingNumber:s});break}case"smartstore":{const t=St(a);await t.registerInvoice(r,{carrierCode:c,trackingNumber:s});break}case"shopify":{const t=St(a);await t.createFulfillment(r,{company:c,number:s});break}}return console.log(" ✅ 송장 등록 완료"),0}(o);break;case"inventory":case"inv":t=await async function(t,n){const[e,...r]=t;switch(e){case"sync":return async function(t,n,e,r){if(!t||!I.includes(t))return console.error(`유효한 채널: ${I.join(", ")}`),1;if(!n||Number.isNaN(e))return console.error("사용법: rocketsell inventory sync <channel> <productId> <qty>"),1;if(r["dry-run"])return console.log(`[DRY-RUN] inventory sync ${t} ${n} → ${e}개 (write 건너뜀)`),0;switch(console.log(`\n📊 ${t} 재고 업데이트: ${n} → ${e}개\n`),t){case"cafe24":{const r=St(t),[o,s]=n.split(":");if(!s)return console.error(" cafe24 형식: <productNo>:<variantCode>"),1;await r.updateInventory(Number(o),s,e),console.log(" ⚠️ 절대값 방식 — 동시 쓰기 시 주의");break}case"coupang":{const r=St(t);await r.updateQuantity(n,e),console.log(" 절대값 방식 (전용 API)");break}case"smartstore":{const r=St(t);await r.updateStock(n,{stockQuantity:e}),console.log(" ⚠️ 상품 전체 PUT — 동시성 위험");break}case"shopify":{const o=St(t),s=r.item||n,a=r.location;if(!a)return console.error(" shopify: --location <locationId> 필수"),console.error(" 예: rocketsell inventory sync shopify <inventoryItemId> <delta> --location <locationId>"),1;await o.adjustInventory(s,a,e),console.log(" ✅ delta 방식 (동시성 안전)");break}}return console.log(" ✅ 재고 업데이트 완료"),0}(r[0],r[1],Number(r[2]),n);case"plan":return function(t,n,e){const r=Number(e.buffer)||10,o=Math.floor(t*r/100),s=t-o,a={};if(e.ratio){const t=e.ratio.split(",");for(const n of t){const[t,e]=n.split(":");a[t]=Number(e)}}else a.cafe24=33,a.coupang=34,a.smartstore=33;const c=Object.values(a).reduce((t,n)=>t+(n??0),0);console.log(`\n=== 재고 분배 계획: ${n} ===\n`),console.log(` 공급사 총 재고: ${t}개`),console.log(` 안전 버퍼 (${r}%): ${o}개`),console.log(` 분배 가용: ${s}개\n`);let i=0;const l=[];for(const[t,n]of Object.entries(a)){const e=Math.floor(s*(n??0)/c);i+=e;const r=T[t]?"웹훅":"폴링",o="smartstore"===t?"⚠️ 상품 전체 PUT":"shopify"===t?"✅ delta":"절대값";console.log(` ${t.padEnd(14)} ${String(e).padStart(4)}개 (${n}%) [${r}] [재고: ${o}]`),l.push({channel:t,qty:e,pct:n,webhook:r,invMethod:o})}return console.log(` ${"미분배".padEnd(13)} ${String(s-i).padStart(4)}개 (반올림 잔여)`),e.json&&console.log(JSON.stringify({sku:n,totalStock:t,buffer:o,available:s,plan:l},null,2)),console.log(),0}(Number(r[0])||500,r[1]||"SKU-001",n);default:return console.log("사용법:\n rocketsell inventory sync <channel> <productId> <qty>\n rocketsell inventory plan <totalStock> [sku]"),1}}(o,e);break;case"rate-test":case"rate":t=await async function(){const t=new j(S);console.log("\n=== Rate Limiter 테스트 ===\n");for(const n of["cafe24","coupang","smartstore","shopify"]){const{maxRps:e,intervalMs:r}=S[n];console.log(` ${n}: ${e} req/s (${r}ms 간격)`);const o=Date.now(),s=[];for(let e=0;e<5;e++)await t.acquire(n),s.push(Date.now()-o);console.log(` 요청 타이밍: ${s.map(t=>`${t}ms`).join(" → ")}`),console.log(` 5건 소요: ${s[s.length-1]}ms (기대: ~${4*r}ms)\n`)}return 0}();break;case"source":t=await async function(t,n){const[e,r,...o]=t;if(!e)return kn(),1;if("categories"===e){const t=r;return r&&E.includes(t)?async function(t,n){switch(console.log(`\n소싱 카테고리 목록: [${t}]\n`),t){case"domeggook":{const t=Sn(),e=n.parent?String(n.parent):void 0,r=await t.getCategoryList(e);if(n.json)console.log(JSON.stringify(r,null,2));else for(const t of r){const n=" ".repeat(t.depth);console.log(`${n}[${t.id}] ${t.name}`)}break}case"specialoffer":{const t=Tn(),e=await t.getCategories();if(n.json)console.log(JSON.stringify(e,null,2));else for(const t of e.categories)if(console.log(` [${t.code}] ${t.name}`),t.children)for(const n of t.children)console.log(` [${n.code}] ${n.name}`);break}case"cjdropshipping":{const t=In(),e=n.parent?String(n.parent):void 0,r=await t.getCategories(e);if(n.json)console.log(JSON.stringify(r,null,2));else for(const t of r){const n=" ".repeat(t.depth);console.log(`${n}[${t.id}] ${t.name}`)}break}}return 0}(t,n):(console.error(`유효한 소싱 채널: ${E.join(", ")}`),1)}if(!r)return kn(),1;const s=r;if(!E.includes(s))return console.error(`유효한 소싱 채널: ${E.join(", ")}`),1;switch(e){case"search":return async function(t,n,e){const r=n[0];if(!r)return console.error("검색 키워드를 입력하세요."),1;const o=e.page?Number(e.page):1,s=e.category?String(e.category):void 0;switch(console.log(`\n소싱 상품 검색: [${t}] "${r}" (page ${o})\n`),t){case"domeggook":{const t=Sn(),n=await t.searchProducts(r,{category:s,page:o});if(console.log(` 총 ${n.totalCount}건 / 현재 페이지 ${n.products.length}건\n`),e.json)console.log(JSON.stringify(n,null,2));else for(const t of n.products)console.log(` [${t.id}] ${t.name}\n 판매가: ${t.price.toLocaleString()}원 도매가: ${t.wholesalePrice.toLocaleString()}원 최소수량: ${t.minOrderQty}${t.supplier?` 공급사: ${t.supplier}`:""}\n`);break}case"specialoffer":{const t=Tn(),n=await t.searchProducts({keyword:r,page:o});if(console.log(` 총 ${n.total}건 / 현재 페이지 ${n.items.length}건\n`),e.json)console.log(JSON.stringify(n,null,2));else for(const t of n.items)console.log(` [${t.goodsNo}] ${t.goodsName}\n 판매가: ${t.price.toLocaleString()}원 공급가: ${t.supplyPrice.toLocaleString()}원 재고: ${t.stockQty}\n`);break}case"cjdropshipping":{const t=In(),n=await t.searchProducts(r,{categoryId:s,page:o});if(console.log(` 총 ${n.totalCount}건 / 현재 페이지 ${n.products.length}건\n`),e.json)console.log(JSON.stringify(n,null,2));else for(const t of n.products)console.log(` [${t.id}] ${t.name}\n 판매가: $${t.price} 도매가: $${t.wholesalePrice} 최소수량: ${t.minOrderQty}${t.supplier?` 공급사: ${t.supplier}`:""}\n`);break}}return 0}(s,o,n);case"detail":return async function(t,n,e){if(!n)return console.error("상품 ID를 지정하세요."),1;switch(console.log(`\n소싱 상품 상세: [${t}] ${n}\n`),t){case"domeggook":{const t=Sn(),r=await t.getProductDetail(n);e.json?console.log(JSON.stringify(r,null,2)):(console.log(` 상품명 : ${r.name}`),console.log(` 판매가 : ${r.price.toLocaleString()}원`),console.log(` 도매가 : ${r.wholesalePrice.toLocaleString()}원`),console.log(` 최소수량 : ${r.minOrderQty}`),r.supplier&&console.log(` 공급사 : ${r.supplier}`),r.category&&console.log(` 카테고리 : ${r.category}`),r.sourceUrl&&console.log(` 원본URL : ${r.sourceUrl}`));break}case"specialoffer":{const t=Tn(),r=await t.getProductDetail(n);e.json?console.log(JSON.stringify(r,null,2)):(console.log(` 상품명 : ${r.goodsName}`),console.log(` 판매가 : ${r.price.toLocaleString()}원`),console.log(` 공급가 : ${r.supplyPrice.toLocaleString()}원`),console.log(` 재고 : ${r.stockQty}`),console.log(` 카테고리 : ${r.category1}${r.category2?` > ${r.category2}`:""}${r.category3?` > ${r.category3}`:""}`),r.options?.length&&console.log(` 옵션 : ${r.options.length}개`));break}case"cjdropshipping":{const t=In(),r=await t.getProductDetail(n);e.json?console.log(JSON.stringify(r,null,2)):(console.log(` 상품명 : ${r.name}`),console.log(` 판매가 : $${r.price}`),console.log(` 도매가 : $${r.wholesalePrice}`),console.log(` 최소수량 : ${r.minOrderQty}`),r.category&&console.log(` 카테고리 : ${r.category}`),r.sourceUrl&&console.log(` 원본URL : ${r.sourceUrl}`));break}}return 0}(s,o[0],n);default:return console.error(`알 수 없는 하위 명령어: ${e}`),kn(),1}}(o,e);break;case"mapping":case"map":t=await async function(t,n){const[e]=t;switch(e){case"add":return await async function(t,n){const[e,r,o,s]=t;if(!(e&&r&&o&&s))return console.error("사용법: rocketsell mapping add <sourceChannel> <sourceId> <sellingChannel> <sellingId>"),1;const a=n["wholesale-price"]?Number(n["wholesale-price"]):void 0,c=n["selling-price"]?Number(n["selling-price"]):void 0;return await async function(t){const n=await un();if(!n)return!1;try{await pn(n);const e=t.wholesalePrice&&t.sellingPrice?(t.sellingPrice-t.wholesalePrice)/t.sellingPrice*100:null;return await n.query("INSERT INTO product_mappings (source_channel, source_id, selling_channel, selling_id, wholesale_price, selling_price, margin_pct)\n\t\t\t VALUES ($1, $2, $3, $4, $5, $6, $7)\n\t\t\t ON CONFLICT (source_channel, source_id, selling_channel)\n\t\t\t DO UPDATE SET selling_id = $4, wholesale_price = $5, selling_price = $6, margin_pct = $7, updated_at = NOW()",[t.sourceChannel,t.sourceId,t.sellingChannel,t.sellingId,t.wholesalePrice??null,t.sellingPrice??null,e]),!0}catch{return!1}}({sourceChannel:e,sourceId:r,sellingChannel:o,sellingId:s,wholesalePrice:a,sellingPrice:c})?(console.log(`✓ 매핑 저장: ${e}/${r} → ${o}/${s}`),0):(console.error("매핑 저장 실패"),1)}(t.slice(1),n);case"list":case"ls":return await async function(t){const n="string"==typeof t.source?t.source:void 0,e="string"==typeof t.selling?t.selling:void 0,r=await async function(t){const n=await un();if(!n)return[];try{await pn(n);const e=[],r=[];t?.sourceChannel&&(r.push(t.sourceChannel),e.push(`source_channel = $${r.length}`)),t?.sellingChannel&&(r.push(t.sellingChannel),e.push(`selling_channel = $${r.length}`));const o=e.length>0?`WHERE ${e.join(" AND ")}`:"";return(await n.query(`SELECT * FROM product_mappings ${o} ORDER BY updated_at DESC LIMIT 100`,r)).rows}catch{return[]}}({sourceChannel:n,sellingChannel:e});if(0===r.length)return console.log("저장된 매핑이 없습니다."),0;console.log(`\n상품 매핑 (${r.length}건)\n${"─".repeat(80)}`);for(const t of r){const n=null!=t.margin_pct?`${Number(t.margin_pct).toFixed(1)}%`:"-";console.log(` ${t.source_channel}/${t.source_id} → ${t.selling_channel}/${t.selling_id} 도매가: ${t.wholesale_price??"-"} 판매가: ${t.selling_price??"-"} 마진: ${n}`)}return console.log(""),0}(n);default:return console.log("사용법: rocketsell mapping <add|list>"),1}}(o,e);break;case"log":t=await gn(o,e);break;case"dashboard":case"dash":t=await Gt(0,e);break;case"budget":t=await async function(t,n){const[e]=t;switch(e){case"set":return function(t){const n="string"==typeof t.channel?t.channel:null,e="string"==typeof t["daily-limit"]?Number(t["daily-limit"]):null;if(!n||!En.includes(n))return console.error(`--channel 필수: ${En.join(", ")}`),1;if(!e||Number.isNaN(e)||e<=0)return console.error("--daily-limit 필수: 양의 정수"),1;const r=g();return r.budgets||(r.budgets={}),r.budgets[n]={dailyLimit:e},m(r),console.log(`✓ ${n} 일일 한도 설정: ${e.toLocaleString()}건/일`),0}(n);case"status":case void 0:return function(){const t=g();console.log("\n=== 소싱 채널 일일 사용량 ===\n");for(const n of En){const e=t.budgets?.[n]?.dailyLimit,r=x[n].dailyLimit,o=e??r,{used:s,limit:a,remaining:c}=R(n,o),i=null!==a?a.toLocaleString():"무제한",l=a&&a>0?Math.floor(s/a*100):0,d=Math.floor(l/10),u="█".repeat(d)+"░".repeat(10-d),p=null!==c?` 잔여: ${c.toLocaleString()}`:"";console.log(` ${n.padEnd(16)} ${s.toLocaleString().padStart(6)} / ${i.padStart(6)} [${u}] ${l}%${p}`)}return console.log(),0}();default:return console.log("사용법:\n rocketsell budget status\n rocketsell budget set --channel <ch> --daily-limit <n>"),1}}(o,e);break;default:return console.error(`알 수 없는 명령어: ${r}\n`),console.log(Yt),1}return await fn({command:[r,...o].join(" "),channel:o[0]??void 0,args:e,result:0===t?"success":"error",durationMs:Date.now()-s}),t}catch(t){const n=t instanceof Error?t.message:String(t);return console.error(`\n❌ 오류: ${n}`),await fn({command:[r,...o].join(" "),channel:o[0]??void 0,result:"error",errorMsg:n,durationMs:Date.now()-s}),1}}(process.argv.slice(2));process.exit(Cn);
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "rocketsell",
3
+ "version": "0.1.0",
4
+ "description": "위탁셀링 자동화 CLI — Cafe24, Coupang, SmartStore, Shopify 통합 관리",
5
+ "type": "module",
6
+ "bin": {
7
+ "rocketsell": "dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "dev": "tsx watch src/cli.ts",
11
+ "build": "tsup && node -e \"const fs=require('fs');const f='dist/cli.js';const c=fs.readFileSync(f,'utf8');fs.writeFileSync(f,'#!/usr/bin/env node\\n'+c);fs.chmodSync(f,0o755)\"",
12
+ "start": "node dist/cli.js",
13
+ "prepublishOnly": "npm run build",
14
+ "test": "vitest run"
15
+ },
16
+ "keywords": [
17
+ "dropshipping",
18
+ "ecommerce",
19
+ "cafe24",
20
+ "coupang",
21
+ "smartstore",
22
+ "shopify",
23
+ "위탁셀링",
24
+ "자동화"
25
+ ],
26
+ "license": "UNLICENSED",
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "files": ["dist", "README.md"],
31
+ "dependencies": {
32
+ "axios": "^1.13.5",
33
+ "date-fns": "^4.1.0",
34
+ "zod": "^3.24.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^22.13.10",
38
+ "terser": "^5.46.1",
39
+ "tsup": "^8.5.1",
40
+ "tsx": "^4.19.4",
41
+ "typescript": "^5.8.2",
42
+ "vitest": "^4.1.4"
43
+ }
44
+ }