// === 고객 응대 (문의 / 리뷰) === // 탭: 문의 / 리뷰. 답변 작성 → 확인 모달 → 즉시 네이버 전송. // 네이버 endpoint path는 서버에서 후보 여러 개 자동 시도 (X-Naver-Path-Used 헤더로 확인). function CustomerScreen() { const isConfigured = window.API.naverConfigured(); const [tab, setTab] = useState("qa"); // "qa" | "reviews" const [items, setItems] = useState([]); const [meta, setMeta] = useState(null); // { page, size, totalElements, totalPages, pathUsed } const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [page, setPage] = useState(1); const [filter, setFilter] = useState("unanswered"); // "all" | "answered" | "unanswered" const [days, setDays] = useState(30); // 7 / 30 / 90 // 답변 작성 모달 const [composing, setComposing] = useState(null); // { item, text } const [sending, setSending] = useState(false); const [confirmSend, setConfirmSend] = useState(false); const load = (opts = {}) => { // 리뷰는 네이버 커머스 API에서 공식 미지원 (2024-08-30 네이버 답변). // 호출하면 404만 반복되므로 fetch 자체를 skip하고 안내 표시. if (tab === "reviews") { setItems([]); setMeta(null); setError(null); setLoading(false); return; } if (!isConfigured) { setError("API 자격증명이 설정되지 않았습니다 (API 설정 메뉴에서 입력)"); return; } const nextPage = opts.page || page; const nextFilter = opts.filter || filter; const nextDays = opts.days || days; setLoading(true); setError(null); const ymd = (d) => d.toISOString().slice(0, 10); const from = new Date(); from.setDate(from.getDate() - nextDays); const params = { page: nextPage, size: 30, from: ymd(from), to: ymd(new Date()) }; if (nextFilter === "answered") { if (tab === "qa") params.answered = "true"; else params.hasComment = "true"; } else if (nextFilter === "unanswered") { if (tab === "qa") params.answered = "false"; else params.hasComment = "false"; } const fetcher = tab === "qa" ? window.API.naverFetchQA : window.API.naverFetchReviews; fetcher(params) .then(result => { setItems(result.items || []); setMeta(result); if (opts.page) setPage(opts.page); if (opts.filter) setFilter(opts.filter); if (opts.days) setDays(opts.days); }) .catch(err => { console.error("[customer load]", err); setError(err.message); toast((tab === "qa" ? "문의" : "리뷰") + " 조회 실패: " + err.message); }) .finally(() => setLoading(false)); }; useEffect(() => { load(); }, [tab]); // 탭 바뀌면 새로 로드 const openCompose = (item) => { // 기존 답변/답글이 있으면 수정용으로 미리 채워넣기 const existing = item.answer || item.answerContent || item.sellerComment || item.comment || item.commentContent || ""; setComposing({ item, text: existing }); }; const closeCompose = () => { if (!sending) { setComposing(null); setConfirmSend(false); } }; const doSend = async () => { if (!composing || !composing.text.trim()) { toast("답변 내용을 입력하세요"); return; } setSending(true); try { const id = tab === "qa" ? (composing.item.inquiryNo || composing.item.qnaId || composing.item.id || composing.item.qaId) : (composing.item.reviewId || composing.item.id || composing.item.reviewNo); if (!id) throw new Error("ID 필드를 찾을 수 없음 — 응답 구조 확인 필요"); if (tab === "qa") await window.API.naverAnswerQA(id, composing.text); else await window.API.naverCommentReview(id, composing.text); toast("답변이 네이버에 전송되었습니다"); setComposing(null); setConfirmSend(false); load(); // 새로고침 } catch (err) { console.error("[doSend]", err); toast("전송 실패: " + err.message); } finally { setSending(false); } }; return (

고객 응대

네이버 스마트스토어 문의(상품Q&A)와 리뷰를 한 화면에서 조회 · 답변 {meta?.pathUsed && · endpoint: {meta.pathUsed}}

{tab !== "reviews" && ( )}
{/* 탭 */}
{/* 필터 — 리뷰 탭은 API 미지원이라 필터 숨김 */} {tab === "reviews" ? null : (
{meta && (
{meta.totalElements != null ? `${meta.totalElements}건` : `${items.length}건 표시`}
)}
)} {error && (
{error}
)} {/* 목록 (카드형) */} {tab === "reviews" ? ( ) : loading && items.length === 0 ? (
불러오는 중...
) : items.length === 0 ? (
문의가 없습니다
{filter === "unanswered" && (
"전체" 또는 "답변 완료" 탭으로 바꿔보세요
)}
) : ( )} {/* 페이지네이션 (리뷰 탭은 데이터 없으므로 숨김) */} {tab !== "reviews" && meta && meta.totalPages > 1 && (
페이지 {meta.page} / {meta.totalPages}
)} {/* 답변 작성 모달 */} ) : ( <> ) }> {composing && ( <>
{tab === "qa" ? "원본 문의" : "원본 리뷰"}
{tab === "qa" ? (composing.item.question || composing.item.content || composing.item.inquiryContent || "(내용 없음)") : (composing.item.reviewContent || composing.item.content || composing.item.body || "(내용 없음)")}
{tab === "qa" && (composing.item.answer || composing.item.answerContent) && (
기존 답변: {composing.item.answer || composing.item.answerContent}
)} {tab === "reviews" && (
평점: {composing.item.reviewScore || composing.item.rating || "?"}
)}
{!confirmSend ? ( <>