// === Product detail viewer ===
function ProductDetail({ productId, products, setProducts, setAllProducts, onClose, productsSource }) {
const product = products.find(p => p.id === productId);
if (!product) {
console.warn("[ProductDetail] 상품 못 찾음. productId=", productId, "products.length=", products?.length);
return (
닫기}>
ID {productId} 상품을 찾을 수 없습니다.
전체 상품 로드가 끝났는지 확인하거나, 새로고침 후 다시 시도해주세요.
);
}
const isLive = productsSource === "live";
const cat = window.MOCK.CATEGORIES.find(c => c.id === product.category);
const [tab, setTab] = useState("overview");
const margin = product.costPrice > 0
? ((product.price - product.costPrice) / product.price * 100).toFixed(1)
: null;
// 라이브 모드: GET origin-product로 전체 상세 데이터 가져옴
const [liveData, setLiveData] = useState(null);
const [loadingLive, setLoadingLive] = useState(false);
const [liveError, setLiveError] = useState(null);
const [edits, setEdits] = useState({}); // { salePrice?, stockQuantity?, immediateDiscount?, name? }
const [saving, setSaving] = useState(false);
useEffect(() => {
if (!isLive || !product.originProductNo) return;
setLoadingLive(true);
setLiveError(null);
window.API.naverFetchOriginProduct(product.originProductNo)
.then(data => setLiveData(data))
.catch(err => {
console.error("[detail-fetch]", err);
setLiveError(err.message);
})
.finally(() => setLoadingLive(false));
}, [isLive, product.originProductNo]);
// 표시할 값 — 편집 중이면 edits, 아니면 liveData(라이브) 또는 product(mock)
const liveOP = liveData?.originProduct;
const cur = isLive && liveOP ? {
name: liveOP.name,
salePrice: liveOP.salePrice,
stockQuantity: liveOP.stockQuantity,
immediateDiscount: liveOP.customerBenefit?.immediateDiscountPolicy?.discountMethod?.value || 0,
statusType: liveOP.statusType,
detailContent: liveOP.detailContent || "",
representativeImageUrl: liveOP.images?.representativeImage?.url || product.imageUrl,
optionalImages: liveOP.images?.optionalImages || [],
wholeCategoryName: product.categoryFull,
leafCategoryId: liveOP.leafCategoryId,
} : {
name: product.name,
salePrice: product.price,
stockQuantity: product.stock,
immediateDiscount: product.price - (product.discountedPrice || product.price),
statusType: product.status === "active" ? "SALE" : "OUTOFSTOCK",
detailContent: product.desc || "",
representativeImageUrl: product.imageUrl,
optionalImages: [],
wholeCategoryName: cat?.name || "",
leafCategoryId: product.category,
};
const get = (key) => edits[key] !== undefined ? edits[key] : cur[key];
const isDirty = (key) => edits[key] !== undefined && edits[key] !== cur[key];
const anyDirty = Object.keys(edits).some(k => edits[k] !== cur[k]);
const currentDiscountedPrice = Math.max(0, (get("salePrice") || 0) - (get("immediateDiscount") || 0));
const setEdit = (key, value) => setEdits(prev => ({ ...prev, [key]: value }));
const clearEdits = () => setEdits({});
// mock 저장 (기존 동작)
const saveMock = () => {
setProducts(prev => prev.map(p => {
if (p.id !== product.id) return p;
return {
...p,
name: get("name"),
price: get("salePrice"),
stock: get("stockQuantity"),
desc: get("detailContent"),
};
}));
toast("저장되었습니다");
onClose();
};
// 라이브 저장 — 변경된 필드만 PUT
const saveLive = async () => {
const changes = {};
if (isDirty("name")) changes.name = get("name");
if (isDirty("salePrice")) {
const v = parseInt(get("salePrice"), 10);
if (isNaN(v) || v < 0 || v % 10 !== 0) { toast("판매가는 10원 단위 정수여야 합니다"); return; }
changes.salePrice = v;
}
if (isDirty("immediateDiscount")) {
const v = parseInt(get("immediateDiscount"), 10);
if (isNaN(v) || v < 0 || v % 10 !== 0) { toast("즉시할인은 10원 단위 정수여야 합니다"); return; }
if (v > get("salePrice")) { toast("즉시할인이 판매가를 넘을 수 없습니다"); return; }
changes.immediateDiscount = v;
}
if (isDirty("stockQuantity")) {
const v = parseInt(get("stockQuantity"), 10);
if (isNaN(v) || v < 0) { toast("재고는 0 이상이어야 합니다"); return; }
changes.stockQuantity = v;
}
if (isDirty("detailContent")) {
changes.detailContent = get("detailContent") || "";
}
if (Object.keys(changes).length === 0) { toast("변경된 내용이 없습니다"); return; }
setSaving(true);
try {
await window.API.naverUpdateProduct(product.originProductNo, changes);
// 로컬 products state 업데이트
setProducts(prev => prev.map(p => {
if (p.id !== product.id) return p;
const newPrice = changes.salePrice !== undefined ? changes.salePrice : p.price;
const newDiscount = changes.immediateDiscount !== undefined ? changes.immediateDiscount : (p.price - (p.discountedPrice || p.price));
return {
...p,
name: changes.name !== undefined ? changes.name : p.name,
price: newPrice,
stock: changes.stockQuantity !== undefined ? changes.stockQuantity : p.stock,
discountedPrice: Math.max(0, newPrice - newDiscount),
status: (changes.stockQuantity !== undefined && changes.stockQuantity === 0) ? "outofstock" : p.status,
};
}));
toast("네이버에 저장되었습니다");
onClose();
} catch (err) {
console.error("[detail-save]", err);
toast("저장 실패: " + err.message);
} finally {
setSaving(false);
}
};
const onSave = isLive ? saveLive : saveMock;
const storeUrl = isLive && product.channelProductNo
? `https://smartstore.naver.com/main/products/${product.channelProductNo}`
: "https://smartstore.naver.com";
return (
{isLive ? `#${product.originProductNo}` : product.sku}
{get("name")}
{isLive && 라이브}
} footer={
<>
스마트스토어에서 보기
>
}>
{isLive && loadingLive && (
네이버에서 상세 데이터 불러오는 중...
)}
{isLive && liveError && (
상세 조회 실패: {liveError}
)}
{(!isLive || liveData) && (
{/* Left: gallery */}
{cur.representativeImageUrl ? (

) : (
)}
{isLive && cur.optionalImages.length > 0 && (
{cur.optionalImages.slice(0, 4).map((img, i) => (

))}
)}
최종가 (할인 적용)
{formatKRW(currentDiscountedPrice)}
판매가 {formatKRW(get("salePrice"))}
{get("immediateDiscount") > 0 && (
<> · 즉시할인 -{formatNum(get("immediateDiscount"))}원>
)}
재고
{formatNum(get("stockQuantity"))}
{isDirty("stockQuantity") && (미적용)}
{!isLive && (
평점
{product.rating}
({formatNum(product.reviews)})
)}
{/* Right: details */}
{cat?.icon} {cur.wholeCategoryName || cat?.name || "(카테고리 없음)"}
{isLive && cur.leafCategoryId && (
카테고리 ID: {cur.leafCategoryId}
)}
{[
["overview", "기본정보"],
["pricing", "가격/재고"],
["desc", "상세설명"],
["sales", "판매통계"],
].map(([k, l]) => (
))}
{tab === "overview" && (
)}
{tab === "pricing" && (
)}
{tab === "desc" && (
{isLive ? (
<>
블록 에디터 — 이미지를 추가/삭제/순서변경하고 텍스트와 섞을 수 있습니다.
저장 버튼을 누르면 네이버 SmartEditor 형식으로 변환되어 적용됩니다.
{
// 원본과 동일하면 dirty 표시 제거 (불필요한 PUT 방지)
if (html === cur.detailContent) {
setEdits(prev => {
if (prev.detailContent === undefined) return prev;
const { detailContent: _omit, ...rest } = prev;
return rest;
});
} else {
setEdit("detailContent", html);
}
}}
/>
>
) : (
)}
{tab === "sales" && (
{isLive ? (
판매 통계는 별도 API 연동 필요
현재는 상품 조회 API만 연동됨. 향후 주문 통계 endpoint 연동 시 표시됩니다.
) : (
<>
30일 판매량
{product.sales30d}건
30일 매출
{formatKRW(product.price * product.sales30d)}
전환율
{(product.sales30d / product.views30d * 100).toFixed(1)}%
Math.round(product.sales30d / 30 * (0.7 + Math.random() * 0.6)))} height={80} />
>
)}
)}
)}
);
}
function Field({ label, children }) {
return (
);
}
window.ProductDetail = ProductDetail;