feat(subscription): DistrictTierEditor — 자치구 5티어 드래그앤드롭
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
167
src/pages/subscription/components/DistrictTierEditor.jsx
Normal file
167
src/pages/subscription/components/DistrictTierEditor.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user