feat(subscription): DistrictTierEditor — 자치구 5티어 드래그앤드롭

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-28 10:55:54 +09:00
parent 3b3e4a1ee1
commit 9e5521d784

View File

@@ -0,0 +1,167 @@
import { useEffect, useState } from "react";
const SEOUL_DISTRICTS = [
"강남구","강동구","강북구","강서구","관악구",
"광진구","구로구","금천구","노원구","도봉구",
"동대문구","동작구","마포구","서대문구","서초구",
"성동구","성북구","송파구","양천구","영등포구",
"용산구","은평구","종로구","중구","중랑구",
];
const TIERS = [
{ key: "S", label: "S", weight: "100%" },
{ key: "A", label: "A", weight: "80%" },
{ key: "B", label: "B", weight: "60%" },
{ key: "C", label: "C", weight: "40%" },
{ key: "D", label: "D", weight: "20%" },
];
const EMPTY_TIERS = { S: [], A: [], B: [], C: [], D: [] };
function useIsDesktop() {
const [isDesktop, setIsDesktop] = useState(
typeof window !== "undefined" && window.matchMedia("(min-width: 768px)").matches
);
useEffect(() => {
if (typeof window === "undefined") return;
const mq = window.matchMedia("(min-width: 768px)");
const handler = (e) => setIsDesktop(e.matches);
mq.addEventListener("change", handler);
return () => mq.removeEventListener("change", handler);
}, []);
return isDesktop;
}
export default function DistrictTierEditor({ value, onChange }) {
const isDesktop = useIsDesktop();
const [dragOver, setDragOver] = useState(null); // 현재 hover 중인 zone key
const current = value && Object.keys(value).length > 0 ? value : EMPTY_TIERS;
const unassigned = SEOUL_DISTRICTS.filter(
d => !TIERS.some(t => (current[t.key] || []).includes(d))
);
const moveDistrict = (district, targetTier /* null = 미할당 */) => {
const next = { S: [], A: [], B: [], C: [], D: [] };
for (const t of Object.keys(next)) {
next[t] = (current[t] || []).filter(d => d !== district);
}
if (targetTier) {
next[targetTier] = [...next[targetTier], district];
}
onChange(next);
};
const onDragStart = (e, district) => {
e.dataTransfer.setData("text/district", district);
e.dataTransfer.effectAllowed = "move";
};
const onDragOver = (e, key) => {
e.preventDefault();
e.dataTransfer.dropEffect = "move";
if (dragOver !== key) setDragOver(key);
};
const onDragLeave = () => setDragOver(null);
const onDrop = (e, targetTier /* null = 미할당 */) => {
e.preventDefault();
const district = e.dataTransfer.getData("text/district");
setDragOver(null);
if (district) moveDistrict(district, targetTier);
};
if (!isDesktop) {
return (
<div className="sub-panel">
<div className="sub-panel__head">
<p className="sub-panel__eyebrow">자치구 우선순위</p>
<h3>지역 5티어</h3>
</div>
<div className="sub-panel__body" style={{ display: "grid", gap: 10 }}>
{TIERS.map(t => (
<div key={t.key} className="dte-row dte-row--readonly">
<span className={`sub-chip sub-chip--tier sub-chip--tier-${t.key}`}>
{t.label} {t.weight}
</span>
<span className="dte-row__list">
{(current[t.key] || []).length === 0
? <span className="dte-empty">(없음)</span>
: (current[t.key] || []).join(", ")}
</span>
</div>
))}
<p className="dte-mobile-hint"> 자치구 분류는 PC에서 편집할 있어요</p>
</div>
</div>
);
}
return (
<div className="sub-panel">
<div className="sub-panel__head">
<p className="sub-panel__eyebrow">자치구 우선순위</p>
<h3>지역 5티어 (드래그해서 분류)</h3>
</div>
<div className="sub-panel__body" style={{ display: "grid", gap: 12 }}>
{/* 미할당 풀 */}
<div
className={`dte-pool ${dragOver === "_unassigned" ? "dte-pool--over" : ""}`}
onDragOver={(e) => onDragOver(e, "_unassigned")}
onDragLeave={onDragLeave}
onDrop={(e) => onDrop(e, null)}
>
<p className="dte-pool__title">미할당 ({unassigned.length})</p>
<div className="dte-chips">
{unassigned.map(d => (
<span
key={d}
draggable
onDragStart={(e) => onDragStart(e, d)}
className="sub-chip sub-chip--district dte-chip"
>
{d}
</span>
))}
</div>
</div>
{/* 5티어 그리드 */}
<div className="dte-grid">
{TIERS.map(t => (
<div
key={t.key}
className={`dte-zone ${dragOver === t.key ? "dte-zone--over" : ""}`}
onDragOver={(e) => onDragOver(e, t.key)}
onDragLeave={onDragLeave}
onDrop={(e) => onDrop(e, t.key)}
>
<div className={`dte-zone__head sub-chip--tier-${t.key}`}>
{t.label} <span className="dte-zone__weight">{t.weight}</span>
</div>
<div className="dte-zone__chips">
{(current[t.key] || []).map(d => (
<span
key={d}
draggable
onDragStart={(e) => onDragStart(e, d)}
className="sub-chip sub-chip--district dte-chip"
>
{d}
<button
type="button"
className="dte-chip__remove"
onClick={() => moveDistrict(d, null)}
aria-label={`${d} 미할당으로`}
>
×
</button>
</span>
))}
</div>
</div>
))}
</div>
</div>
</div>
);
}