// === Bulk price edit === function BulkEditScreen({ products, setProducts, productsSource }) { const { CATEGORIES } = window.MOCK; const isLive = productsSource === "live"; const [keyword, setKeyword] = useState(""); const [selectedCats, setSelectedCats] = useState(new Set()); const [mode, setMode] = useState("percent"); // percent / fixed / set const [direction, setDirection] = useState("up"); const [value, setValue] = useState(10); const [previewOpen, setPreviewOpen] = useState(false); const [naverSaving, setNaverSaving] = useState(false); const [saveProgress, setSaveProgress] = useState(null); const abortRef = useRef(false); // 라이브 모드면 카테고리 chip을 실제 상품 데이터에서 동적 생성 const effectiveCategories = useMemo(() => { if (!isLive) return CATEGORIES; const seen = new Map(); products.forEach(p => { if (p.category && !seen.has(p.category)) { seen.set(p.category, { id: p.category, name: p.category, icon: "" }); } }); return Array.from(seen.values()).sort((a, b) => a.name.localeCompare(b.name, "ko")); }, [products, isLive]); const matched = useMemo(() => { return products.filter(p => { if (keyword && !p.name.toLowerCase().includes(keyword.toLowerCase())) return false; if (selectedCats.size > 0 && !selectedCats.has(p.category)) return false; return true; }); }, [products, keyword, selectedCats]); // 가격 계산 — 네이버는 10원 단위만 받음 const calcNewPrice = (oldPrice) => { let np; if (mode === "set") np = value; else { const sign = direction === "up" ? 1 : -1; if (mode === "percent") np = oldPrice * (1 + sign * value / 100); else np = oldPrice + sign * value; } np = Math.max(0, Math.round(np / 10) * 10); // 10원 단위 내림 반올림 return np; }; // 즉시할인은 절대값이라 판매가 변경해도 그대로 유지 → 할인가 재계산 const previewRows = matched.map(p => { const newPrice = calcNewPrice(p.price); const oldDp = p.discountedPrice || p.price; const discount = p.price - oldDp; const newDiscountedPrice = Math.max(0, newPrice - discount); return { ...p, newPrice, oldDiscountedPrice: oldDp, newDiscountedPrice, discount }; }).filter(p => p.newPrice !== p.price); const totalDelta = previewRows.reduce((s, p) => s + (p.newPrice - p.price), 0); const toggleCat = (id) => { const s = new Set(selectedCats); if (s.has(id)) s.delete(id); else s.add(id); setSelectedCats(s); }; const apply = async () => { if (!isLive) { // mock 모드: 즉시 로컬 반영 setProducts(prev => prev.map(p => { const m = previewRows.find(r => r.id === p.id); if (!m) return p; return { ...p, price: m.newPrice }; })); toast(`${previewRows.length}개 상품의 판매가가 수정되었습니다`); setPreviewOpen(false); return; } // 라이브 모드: 순차 PUT abortRef.current = false; setNaverSaving(true); const succeeded = []; const failed = []; setSaveProgress({ done: 0, total: previewRows.length, succeeded: [], failed: [] }); for (let i = 0; i < previewRows.length; i++) { if (abortRef.current) break; const item = previewRows[i]; try { if (!item.originProductNo) throw new Error("originProductNo 누락"); await window.API.naverChangePrice(item.originProductNo, item.newPrice); succeeded.push(item); } catch (err) { console.error("[bulk-saveToNaver]", item.id, err); failed.push({ ...item, error: err.message }); } setSaveProgress({ done: i + 1, total: previewRows.length, succeeded: [...succeeded], failed: [...failed], aborted: abortRef.current }); } // 성공한 것만 products에 반영 (할인가도 재계산) if (succeeded.length > 0) { const okIds = new Set(succeeded.map(x => x.id)); setProducts(prev => prev.map(p => { if (!okIds.has(p.id)) return p; const item = succeeded.find(x => x.id === p.id); return { ...p, price: item.newPrice, discountedPrice: item.newDiscountedPrice }; })); } setNaverSaving(false); if (failed.length === 0 && !abortRef.current) { toast(`${succeeded.length}개 가격이 네이버에 반영되었습니다`); } else if (abortRef.current) { toast(`중지됨 — ${succeeded.length}개 적용, ${previewRows.length - succeeded.length - failed.length}개 미적용`); } else { toast(`${succeeded.length}개 성공, ${failed.length}개 실패`); } }; const abortBulk = () => { abortRef.current = true; }; const closePreview = () => { if (naverSaving) return; setPreviewOpen(false); setSaveProgress(null); }; return (
{isLive ? `네이버 라이브 ${products.length}개 상품에서 필터 → 미리보기 → 일괄 적용 (10원 단위 반올림)` : "키워드와 카테고리로 상품을 필터링한 뒤 일괄로 가격을 변경합니다"}
| 상품 | 현재가 | 변경가 | 차액 |
|---|---|---|---|
|
{p.name}
{hasDiscount && (
할인가 {formatKRW(p.oldDiscountedPrice)} → {formatKRW(p.newDiscountedPrice)}
)}
|
{formatKRW(p.price)} | {formatKRW(p.newPrice)} | = 0 ? "var(--accent)" : "var(--danger)" }}> {diff >= 0 ? "+" : ""}{formatKRW(diff)} |
| 상품 | 현재가 | 변경가 | 차액 |
|---|---|---|---|
|
{p.name}
{hasDiscount && (
할인가 {formatKRW(p.oldDiscountedPrice)} → {formatKRW(p.newDiscountedPrice)} (즉시할인 {formatNum(p.discount)}원 유지)
)}
|
{formatKRW(p.price)} | {formatKRW(p.newPrice)} | = 0 ? "var(--accent)" : "var(--danger)", fontSize: 12 }}> {diff >= 0 ? "+" : ""}{formatKRW(diff)} |
| ... 외 {previewRows.length - 50}개 (전체 적용됩니다) | |||