fix(orders/admin): 자동 list 갱신의 stale 락 결과를 클라이언트에서 보정
Deploy momo-erp / deploy (push) Successful in 1m54s

증상: 발주 15 → 16 → 17 빠른 클릭 후 본인 화면에 16(또는 더 이전 발주)에 여전히
      "내가 수정 중" 표시. 다른 사용자 화면은 17만 락으로 정상.

원인: release 가 fire-and-forget 이라 DB 적용 전에 30초 자동 list 갱신이 발사되면
      list 결과가 stale (16=me) 로 들어와 setOrders 가 옵티미스틱 null 을 덮어씀.

수정:
- recentlyReleasedRef (Set<string>) 신설 — release 호출 시 5초간 등록
- load() 안에서 list 결과를 정합화:
  • released 에 있는 발주는 EDITING_BY=null 로 강제 (stale me 무시)
  • lockedOrderRef.current 와 일치하는 발주는 me 로 보정 (자기 락 보호)
- 5초 후 자동으로 set 에서 제거 → 그 이후엔 정상 DB 반영

이전 heartbeat fix(bdccaa0) 와 합쳐 race 양쪽 다 차단.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
chpark
2026-05-21 00:06:55 +09:00
parent bdccaa05c1
commit e1618fa9d2
+29 -7
View File
@@ -96,6 +96,8 @@ export default function AdminOrdersPage() {
const [myUserId, setMyUserId] = useState<string>("");
const heartbeatRef = useRef<NodeJS.Timeout | null>(null);
const lockedOrderRef = useRef<string>(""); // 현재 락 보유 중인 발주 id
// 최근 release 한 발주 5초간 추적 — 자동 list 갱신이 가져오는 stale me 결과를 무시하기 위함
const recentlyReleasedRef = useRef<Set<string>>(new Set());
// 본인 user_id 1회 확보
useEffect(() => {
@@ -104,9 +106,11 @@ export default function AdminOrdersPage() {
}).catch(() => {});
}, []);
// 락 해제 헬퍼 (best-effort)
// 락 해제 헬퍼 — recentlyReleased 에 5초간 등록해서 자동 갱신의 stale 결과를 무시
const releaseLock = useCallback(async (orderObjid: string) => {
if (!orderObjid) return;
recentlyReleasedRef.current.add(orderObjid);
setTimeout(() => recentlyReleasedRef.current.delete(orderObjid), 5000);
try {
await fetch("/api/m/admin/orders/lock", {
method: "POST", headers: { "Content-Type": "application/json" },
@@ -131,18 +135,36 @@ export default function AdminOrdersPage() {
});
const j = await res.json();
const list: Order[] = j.RESULTLIST ?? [];
setOrders(list);
setSelected((prev) => new Set(Array.from(prev).filter((id) => list.some((o) => o.OBJID === id))));
if (list.length && !list.some((o) => o.OBJID === activeId)) {
setActiveId(list[0].OBJID);
} else if (!list.length) {
// 클라이언트 락 추적과 list 결과 정합화:
// - 최근 release 한 발주는 DB stale (me 로 보이는 경우) 무시 → null 처리
// - 내가 보유 중인 발주는 me 로 보정 (혹시 누락된 경우)
const released = recentlyReleasedRef.current;
const myLocked = lockedOrderRef.current;
const adjusted: Order[] = list.map((o) => {
if (released.has(o.OBJID)) {
return { ...o, EDITING_BY: null, EDITING_BY_NAME: null };
}
if (myLocked && o.OBJID === myLocked) {
return {
...o,
EDITING_BY: myUserId || (o.EDITING_BY ?? "me"),
EDITING_BY_NAME: o.EDITING_BY_NAME || "(나)",
};
}
return o;
});
setOrders(adjusted);
setSelected((prev) => new Set(Array.from(prev).filter((id) => adjusted.some((o) => o.OBJID === id))));
if (adjusted.length && !adjusted.some((o) => o.OBJID === activeId)) {
setActiveId(adjusted[0].OBJID);
} else if (!adjusted.length) {
setActiveId("");
setDetail(null);
}
} finally {
setLoading(false);
}
}, [status, dateFrom, dateTo, keyword, activeId]);
}, [status, dateFrom, dateTo, keyword, activeId, myUserId]);
// 최초 로드만 자동, 검색 조건 변경은 [조회] 버튼으로
// eslint-disable-next-line react-hooks/exhaustive-deps