// === 고객 응대 (문의 / 리뷰) ===
// 탭: 문의 / 리뷰. 답변 작성 → 확인 모달 → 즉시 네이버 전송.
// 네이버 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" && (
load()} disabled={loading}>
{loading ? "조회 중..." : "새로고침"}
)}
{/* 탭 */}
setTab("qa")}>
문의 (상품 Q&A)
setTab("reviews")}>
리뷰
{/* 필터 — 리뷰 탭은 API 미지원이라 필터 숨김 */}
{tab === "reviews" ? null : (
load({ page: 1, filter: "unanswered" })}>
{tab === "qa" ? "미답변" : "답글 없음"}
load({ page: 1, filter: "all" })}>전체
load({ page: 1, filter: "answered" })}>
{tab === "qa" ? "답변 완료" : "답글 있음"}
load({ page: 1, days: 7 })}>최근 7일
load({ page: 1, days: 30 })}>30일
load({ page: 1, days: 90 })}>90일
{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}
load({ page: meta.page - 1 })}>이전
= meta.totalPages}
onClick={() => load({ page: meta.page + 1 })}>다음
)}
{/* 답변 작성 모달 */}
취소
setConfirmSend(true)}
disabled={!composing?.text?.trim() || sending}>
전송 미리보기
>
) : (
<>
setConfirmSend(false)} disabled={sending}>돌아가기
{sending ? "전송 중..." : (<> 네이버에 전송>)}
>
)
}>
{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 ? (
<>
글자 수: {composing.text.length}자
>
) : (
<>
전송 확인
아래 내용을 즉시 네이버에 전송합니다. 전송 후 수정/삭제는 네이버 판매자 센터에서 가능합니다.
{composing.text}
>
)}
>
)}
);
}
// 리뷰는 네이버 커머스 API가 공식 미지원 (2024-08-30 네이버 답변).
// 판매자 센터에서 직접 관리하도록 안내.
function ReviewsNotSupported() {
return (
리뷰 조회 API가 네이버에서 공식 미지원
네이버 커머스 API팀의 공식 답변(2024-08-30):
"안타깝게도 리뷰와 관련된 API 제공에 대해서 가까운 시일 내 제공 계획이 없습니다."
리뷰 조회와 답글 작성은 네이버 판매자 센터 에서 직접 진행하셔야 합니다.
네이버에서 API를 추가하면 이 화면도 자동 활성화됩니다.
);
}
// 문의 목록 (Q&A) — 네이버 실제 응답 필드: question, answer, createDate
// 카드형 레이아웃 — 한 row당 문의/답변 정보 많아서 표보다 가독성 좋음
function QAList({ items, onReply }) {
return (
{items.map((q, i) => {
const id = q.inquiryNo || q.id || q.qnaId || q.qaId || ("row-" + i);
const question = q.question || q.content || q.inquiryContent || q.title || q.inquiryTitle || "";
const answer = q.answer || q.answerContent || "";
const isAnswered = !!(answer && String(answer).trim());
const author = q.writerName || q.writer || q.writerId || q.memberId || "익명";
const date = q.createDate || q.inquiryRegistrationDateTime || q.registrationDate || q.regDate || q.createdAt || "";
const product = q.productName || q.productOrderProductName || q.product?.name || "";
const orderNo = q.productOrderId || q.orderId || "";
const dateStr = (date || "").replace("T", " ").slice(0, 16);
return (
{/* 헤더: 작성자 · 일시 · 상태 · 버튼 */}
{author}
{dateStr}
{(product || orderNo) && (
{product && <>· 📦 {product.length > 40 ? product.slice(0, 40) + "..." : product}>}
{orderNo && · 주문 {orderNo} }
)}
{isAnswered
? 답변 완료
: 미답변 }
onReply(q)}>
{isAnswered ? "답변 수정" : "답변 작성"}
{/* 문의 본문 */}
{question
? question
: (문의 내용 없음 — 응답 구조 확인 필요. F12 콘솔 로그 참고) }
{/* 답변 (있을 때만) */}
{isAnswered && (
)}
);
})}
);
}
// 리뷰 목록 — 카드형. 실제 응답 필드는 첫 로드 시 콘솔 로그로 확인.
function ReviewList({ items, onReply }) {
return (
{items.map((r, i) => {
const id = r.reviewId || r.id || r.reviewNo || ("row-" + i);
const comment = r.sellerComment || r.comment || r.commentContent || "";
const hasComment = !!(comment && String(comment).trim());
const content = r.reviewContent || r.content || r.body || r.review || "";
const author = r.writerName || r.writer || r.writerId || r.memberId || "익명";
const date = r.reviewRegistrationDateTime || r.createDate || r.registrationDate || r.regDate || r.createdAt || "";
const product = r.productName || r.productOrderProductName || r.product?.name || "";
const rating = parseInt(r.reviewScore || r.rating || r.score || 0, 10) || 0;
const dateStr = (date || "").replace("T", " ").slice(0, 16);
const stars = "★".repeat(Math.max(0, Math.min(5, rating))) + "☆".repeat(Math.max(0, 5 - rating));
return (
{stars}
{author}
{dateStr}
{product && (
· 📦 {product.length > 40 ? product.slice(0, 40) + "..." : product}
)}
{hasComment
? 답글 완료
: 답글 없음 }
onReply(r)}>
{hasComment ? "답글 수정" : "답글 작성"}
{content
? content
: (리뷰 내용 없음 — 응답 구조 확인 필요) }
{hasComment && (
)}
);
})}
);
}
window.CustomerScreen = CustomerScreen;