// === ISVM 검색 → 네이버 등록 (real API) === const { useState: useStateIS, useMemo: useMemoIS, useEffect: useEffectIS } = React; // 네이버 검색/등록에 방해되는 특수문자 정제. // 네이버가 명시한 상품명 등록불가 문자: \ * ? " < > // - * → x (사용자 요청 — 곱하기 기호 의도) // - 나머지 → 제거 function cleanProductName(name) { if (!name) return name; return name .replace(/\*/g, "x") // 별표 → 곱하기 x .replace(/[\\?"<>]/g, "") // 네이버 차단 문자 제거 .replace(/[★☆♥♡◆◇▶◀▲▼●○■□◈◉◐◑▣▤▥▦▧▨▩※♠♣♣♤♧]/g, "") // 장식 기호 제거 .replace(/[【】〔〕〈〉《》「」『』]/g, "") // 장식 괄호 제거 .replace(/\s{2,}/g, " ") // 다중 공백 정리 .trim(); } function ISVMScreen({ products, setProducts, goto }) { const [keyword, setKeyword] = useStateIS(""); const [sort, setSort] = useStateIS("sales"); const [limit, setLimit] = useStateIS(500); // 한 번에 받을 최대 (ISVM은 limit만 받음, 페이지 미지원) const [results, setResults] = useStateIS([]); const [loading, setLoading] = useStateIS(false); const [error, setError] = useStateIS(""); const [health, setHealth] = useStateIS(null); const [selected, setSelected] = useStateIS(new Set()); const [registerForm, setRegisterForm] = useStateIS(null); const [naverPreview, setNaverPreview] = useStateIS(null); const [registered, setRegistered] = useStateIS(new Set()); const [creds, setCreds] = useStateIS(window.API.loadCreds()); // 클라이언트 측 페이지네이션 — 4 컬럼 × 8 행 = 32개/페이지 const PAGE_SIZE = 32; const [page, setPage] = useStateIS(1); const totalPages = Math.max(1, Math.ceil(results.length / PAGE_SIZE)); const currentPage = Math.min(page, totalPages); const pageStart = (currentPage - 1) * PAGE_SIZE; const pagedResults = results.slice(pageStart, pageStart + PAGE_SIZE); useEffectIS(() => { window.API.isvmHealth().then(setHealth); }, []); const doSearch = async () => { if (!keyword.trim()) { window.toast("검색어를 입력하세요"); return; } setLoading(true); setError(""); setResults([]); setSelected(new Set()); setPage(1); try { const data = await window.API.isvmSearch({ keyword: keyword.trim(), limit: parseInt(limit) || 500, sort }); // 네이버 등록/검색 방해 특수문자 정제 const cleaned = data.map(item => ({ ...item, name: cleanProductName(item.name) })); setResults(cleaned); window.toast(`${cleaned.length}개 상품 검색 완료`); } catch (e) { const isCors = String(e.message || e).includes("Failed to fetch") || String(e.message || e).includes("NetworkError"); setError(isCors ? "ISVM 서버에 직접 연결할 수 없습니다 (CORS). 데모용 샘플 데이터를 표시합니다." : `검색 실패: ${e.message}`); // Fallback demo data so UI is testable from any origin setResults(generateDemoResults(keyword.trim())); } finally { setLoading(false); } }; const toggleOne = (code) => { const s = new Set(selected); s.has(code) ? s.delete(code) : s.add(code); setSelected(s); }; const isLive = window.API.naverConfigured(); // 라이브 모드 등록 상태 const [templateProductId, setTemplateProductId] = useStateIS(""); const [registering, setRegistering] = useStateIS(false); const [registerResult, setRegisterResult] = useStateIS(null); // 상세설명 모드 const [detailMode, setDetailMode] = useStateIS("auto"); // "auto" | "manual" // 수동 모드용: 이미지 목록 (ISVM + PC 업로드 합쳐서 관리) // 각 항목: { id, source:'isvm'|'pc', url?:string, file?:File, kind?:string, selected:boolean } const [detailImages, setDetailImages] = useStateIS([]); const [loadingIsvmImages, setLoadingIsvmImages] = useStateIS(false); const [uploadProgress, setUploadProgress] = useStateIS(null); // { done, total, label } const openRegister = (item) => { setRegisterResult(null); setDetailMode("auto"); setDetailImages([]); setUploadProgress(null); // template 자동 선택 — 가장 최근 활성 상품 (있으면) const firstLiveProduct = products.find(p => p.originProductNo); setTemplateProductId(firstLiveProduct?.originProductNo ? String(firstLiveProduct.originProductNo) : ""); setRegisterForm({ product_code: item.product_code, name: item.name, brand: item.brand || "", purchase_price: item.purchase_price, // 권장 마진 60% sale_price: Math.round((item.purchase_price * 1.6) / 10) * 10, image_url: item.image_url, stock: 50, desc: `${item.name}\n\n공급사: ${item.brand || "-"} · ISVM 코드: ${item.product_code}`, leafCategoryId: firstLiveProduct?.naverCategoryId || "50000000", baseFee: 3000, }); // 라이브 모드면 ISVM 다중 이미지 사전 로드 (수동 모드 진입 대비) if (isLive && item.product_code) { setLoadingIsvmImages(true); window.API.isvmFetchProductImages(item.product_code) .then(imgs => { const list = imgs.map((img, i) => ({ id: 'isvm-' + i, source: 'isvm', url: img.url, kind: img.kind, selected: true, })); setDetailImages(list); }) .catch(err => console.warn("[ISVM 이미지 로드 실패]", err.message)) .finally(() => setLoadingIsvmImages(false)); } }; // 이미지 그리드 조작 const toggleDetailImage = (id) => { setDetailImages(prev => prev.map(it => it.id === id ? { ...it, selected: !it.selected } : it)); }; const moveDetailImage = (id, direction) => { setDetailImages(prev => { const idx = prev.findIndex(it => it.id === id); if (idx < 0) return prev; const newIdx = direction === 'up' ? idx - 1 : idx + 1; if (newIdx < 0 || newIdx >= prev.length) return prev; const next = [...prev]; [next[idx], next[newIdx]] = [next[newIdx], next[idx]]; return next; }); }; const removeDetailImage = (id) => { setDetailImages(prev => prev.filter(it => it.id !== id)); }; const addPcFiles = (files) => { const additions = Array.from(files).map((f, i) => ({ id: 'pc-' + Date.now() + '-' + i, source: 'pc', file: f, url: URL.createObjectURL(f), // 미리보기용 blob URL kind: 'pc', selected: true, })); setDetailImages(prev => [...prev, ...additions]); }; const previewNaverPayload = async () => { if (!isLive) { // mock 모드: 구버전 빌더로 추측 페이로드 const p = window.API.buildNaverProductPayload({ product_code: registerForm.product_code, name: registerForm.name, brand: registerForm.brand, desc: registerForm.desc, image_url: registerForm.image_url, salePrice: parseInt(registerForm.sale_price) || 0, stock: parseInt(registerForm.stock) || 0, }, { leafCategoryId: registerForm.leafCategoryId, baseFee: parseInt(registerForm.baseFee) || 3000, }); setNaverPreview(p); return; } // 라이브 모드: template 기반 (자동 선택 — 매장의 첫 라이브 상품 사용) if (!templateProductId) { window.toast("매장에 라이브 상품이 1개 이상 있어야 등록 가능"); return; } try { const tpl = await window.API.naverFetchOriginProduct(parseInt(templateProductId)); const payload = JSON.parse(JSON.stringify(tpl)); delete payload.originProductNo; delete payload.smartstoreChannelProductNo; payload.originProduct.name = registerForm.name; payload.originProduct.salePrice = parseInt(registerForm.sale_price) || 0; payload.originProduct.stockQuantity = parseInt(registerForm.stock) || 0; payload.originProduct.leafCategoryId = registerForm.leafCategoryId; if (registerForm.image_url) { payload.originProduct.images = { representativeImage: { url: registerForm.image_url }, optionalImages: [], }; } payload.originProduct.detailContent = `
${(registerForm.desc || registerForm.name).replace(/[<>]/g, "")}
`; payload.originProduct.detailAttribute = payload.originProduct.detailAttribute || {}; payload.originProduct.detailAttribute.sellerCodeInfo = payload.originProduct.detailAttribute.sellerCodeInfo || {}; payload.originProduct.detailAttribute.sellerCodeInfo.sellerManagementCode = registerForm.product_code; if (payload.originProduct.customerBenefit?.immediateDiscountPolicy) { delete payload.originProduct.customerBenefit.immediateDiscountPolicy; } if (payload.smartstoreChannelProduct) { payload.smartstoreChannelProduct.channelProductName = registerForm.name; } setNaverPreview(payload); } catch (err) { window.toast("미리보기 실패: " + err.message); } }; const submitRegister = async () => { if (!isLive) { // mock 모드: 시뮬레이션 const newId = "P" + (10300 + products.length); const newProduct = { id: newId, name: registerForm.name, category: "fashion", price: parseInt(registerForm.sale_price) || 0, costPrice: parseInt(registerForm.purchase_price) || 0, stock: parseInt(registerForm.stock) || 0, sales30d: 0, rating: 0, reviews: 0, status: "active", views30d: 0, sku: registerForm.product_code, brand: registerForm.brand || "ISVM", tone: parseInt(registerForm.product_code, 10) % 9 || 0, desc: registerForm.desc, image_url: registerForm.image_url, }; setProducts(prev => [...prev, newProduct]); setRegistered(prev => new Set(prev).add(registerForm.product_code)); setRegisterForm(null); setNaverPreview(null); window.toast(`'${newProduct.name.slice(0, 24)}…' 등록 (시뮬레이션)`); return; } // 라이브 모드: 실제 POST if (!templateProductId) { window.toast("매장에 라이브 상품이 1개 이상 있어야 등록 가능 (카테고리 참조용)"); return; } setRegistering(true); setRegisterResult(null); try { // 1) 사용할 이미지 결정 (자동: 모든 ISVM, 수동: 선택된 것만 순서대로) let sourceImages = []; if (detailMode === "auto") { // 자동 — 미리 로드한 detailImages 전체 사용 (혹시 비어있으면 다시 조회) if (detailImages.length === 0) { try { const imgs = await window.API.isvmFetchProductImages(registerForm.product_code); sourceImages = imgs.map(img => ({ source: 'isvm', url: img.url })); } catch (err) { console.warn("[ISVM 이미지 재조회 실패]", err.message); } } else { sourceImages = detailImages.map(it => ({ source: it.source, url: it.url, file: it.file })); } } else { // 수동 — 선택된 것만, 그리드 순서대로 sourceImages = detailImages.filter(it => it.selected).map(it => ({ source: it.source, url: it.url, file: it.file })); } // 메인 이미지 URL을 첫 번째로 강제 (대표 이미지가 representativeImage에 들어가도록) if (registerForm.image_url) { const mainIdx = sourceImages.findIndex(s => s.url === registerForm.image_url); if (mainIdx > 0) { const [main] = sourceImages.splice(mainIdx, 1); sourceImages.unshift(main); } else if (mainIdx < 0) { sourceImages.unshift({ source: 'isvm', url: registerForm.image_url }); } } if (sourceImages.length === 0) { window.toast("등록할 이미지가 없습니다 (수동 모드면 1장 이상 선택 필요)"); setRegistering(false); return; } // 2) 순차 업로드 — 진행 표시 const uploadedUrls = []; setUploadProgress({ done: 0, total: sourceImages.length, label: "이미지 업로드 중" }); for (let i = 0; i < sourceImages.length; i++) { const src = sourceImages[i]; try { let naverUrl; if (src.source === 'pc' && src.file) { naverUrl = await window.API.naverUploadImageFile(src.file); } else if (src.url) { // 이미 네이버 URL이면 그대로 사용 if (/(shop-phinf\.pstatic\.net|phinf\.pstatic\.net|naver\.com)/.test(src.url)) { naverUrl = src.url; } else { naverUrl = await window.API.naverUploadImageFromUrl(src.url); } } if (naverUrl) uploadedUrls.push(naverUrl); } catch (err) { console.warn("[이미지 " + (i + 1) + "/" + sourceImages.length + " 업로드 실패]", err.message); } setUploadProgress({ done: i + 1, total: sourceImages.length, label: "이미지 업로드 중" }); } if (uploadedUrls.length === 0) { throw new Error("모든 이미지 업로드 실패"); } // 3) detailContent SmartEditor HTML 생성 (모든 업로드된 이미지) const detailHtml = window.API.buildSmartEditorImagesHtml(uploadedUrls); // 4) 등록 호출 (representativeImage = 첫 URL, optionalImages는 자동으로 다음 9장) setUploadProgress({ done: 0, total: 1, label: "네이버에 등록 중" }); const overrides = { // 등록 직전 한번 더 sanitize — 사용자가 폼에서 직접 수정 시 * 들어갈 수 있음 name: cleanProductName(registerForm.name), salePrice: parseInt(registerForm.sale_price) || 0, stockQuantity: parseInt(registerForm.stock) || 0, leafCategoryId: registerForm.leafCategoryId, imageUrl: uploadedUrls[0], optionalImageUrls: uploadedUrls.slice(1, 10), // 최대 9장 detailContent: detailHtml, sellerCode: registerForm.product_code, }; const result = await window.API.naverRegisterFromTemplate(parseInt(templateProductId), overrides); setUploadProgress(null); setRegisterResult({ ok: true, response: result }); setRegistered(prev => new Set(prev).add(registerForm.product_code)); window.toast(`'${registerForm.name.slice(0, 24)}…' 네이버에 등록되었습니다`); } catch (err) { console.error("[ISVM register]", err); setRegisterResult({ ok: false, error: err.message }); window.toast("등록 실패: " + err.message); } finally { setRegistering(false); } }; const corsWarning = error && error.includes("CORS"); return (

ISVM → 네이버 스마트스토어 연동

ISVM 도매 사이트에서 상품을 검색하고 네이버 스마트스토어에 등록합니다

{/* Connection bar */}
IS
ISVM Crawler API
{window.API.ISVM_BASE}
{health === true ? "연결됨" : health === false ? "연결 실패" : "확인 중..."}
N
네이버 커머스 API
{creds.naver_client_id ? `client_id: ${creds.naver_client_id.slice(0, 6)}••••••` : "자격증명 미설정"}
{creds.naver_client_id && creds.proxy_url ? "프록시 연결" : "프록시 필요"}
{/* Search bar */}
setKeyword(e.target.value)} onKeyDown={e => e.key === "Enter" && doSearch()} />
{error && (
{corsWarning ? "⚠️ CORS 차단" : "❌ 오류"}: {error} {corsWarning && (
ISVM 서버에 이 도메인을 화이트리스트에 등록하거나, 자체 백엔드를 통해 프록시하세요.
)}
)} {results.length === 0 && !loading && !error && (
키워드를 입력하고 검색하세요
ISVM 도매 사이트의 실시간 매입가/이미지가 조회됩니다
)} {results.length > 0 && ( <>
검색 결과 {results.length}개 · 페이지 {currentPage}/{totalPages} · {pageStart + 1}~{Math.min(pageStart + PAGE_SIZE, results.length)} 표시 · 선택: {selected.size} {selected.size > 0 && ( )}
{currentPage} / {totalPages}
{pagedResults.map(item => { const isReg = registered.has(item.product_code) || products.some(p => p.sku === item.product_code); const margin = ((item.purchase_price * 1.6 - item.purchase_price) / (item.purchase_price * 1.6) * 100).toFixed(0); return (
{item.image_url ? { e.target.style.display = "none"; }} /> : } {item.product_code} {isReg && ( 등록됨 )}
{item.name}
{item.brand || "-"}
매입가
{formatKRW(item.purchase_price)}
권장 판매가 (마진 {margin}%)
{formatKRW(Math.round(item.purchase_price * 1.6 / 10) * 10)}
); })}
)} {/* Register modal */} {registerForm && ( { if (!registering) { setRegisterForm(null); setNaverPreview(null); setRegisterResult(null); } }} size="lg" title="네이버 스마트스토어 상품 등록" footer={ registerResult?.ok ? ( ) : ( <>
) }>
{registerForm.image_url ? : }
ISVM 출처
{registerForm.product_code}
매입가 {formatKRW(registerForm.purchase_price)}
setRegisterForm({ ...registerForm, name: e.target.value })} /> setRegisterForm({ ...registerForm, brand: e.target.value })} />
setRegisterForm({ ...registerForm, sale_price: parseInt(e.target.value.replace(/[^0-9]/g, "")) || 0 })} /> setRegisterForm({ ...registerForm, stock: parseInt(e.target.value) || 0 })} />
setRegisterForm({ ...registerForm, leafCategoryId: e.target.value })} /> setRegisterForm({ ...registerForm, baseFee: parseInt(e.target.value) || 0 })} />