수신자 그룹:
- DB: momo_recipient_groups + momo_recipient_group_members (auto-ensure).
- API: 그룹 list/save/delete + members get/save + all-users picker.
- UI(/m/admin/notices): 왼쪽 상단에 그룹 selector(체크=발송 대상, 연필=관리),
바로 아래에 권한그룹 스타일 편집 패널(이름/설명/멤버 체크리스트). 기존 개별
선택 패널은 그대로 유지. 발송 = 그룹 멤버 ∪ 개별 선택 유니온.
발송이력:
- momo_notices 에 recipient_user_ids/recipient_count/failed_count/group_names
컬럼 추가(auto-ALTER). send-push 가 발송 시 함께 기록.
- 신규 페이지 /m/admin/notice-history: 시간/제목/그룹/대상수/성공/실패. 펼치면
본문 + 수신자 이름 칩 + 공지 페이지 링크.
- 사이드바 메뉴: 마스터 관리 > 푸시알림 발송이력 (menu_info 9000298).
이전엔 FITO PLM(Java→Next 컨버전 시절) 내용이 그대로 남아 있었음.
현 상태(유통/물류 ERP, momotogether.com, com.momotogether.app TWA)에 맞춰
주요 기능·기술 스택·디렉토리·환경변수·Gitea 자동배포·TWA 빌드·코딩 컨벤션
모두 갱신.
- localStorage('momo-push-intent') 로 사용자 의도(켜기/끄기) 영속화.
- 마운트 시: pushManager 가 sub 를 갖고 있으면 ON + 서버에 endpoint 재동기화.
sub 가 없는데 의도='on' + 권한=granted 면 조용히 재구독해 ON 유지.
- SW 업데이트(v1→v2) 직후 getSubscription 이 일시적으로 null 을 반환해
토글이 잘못 OFF 표시되던 케이스 방지.
- turnOff 는 의도를 먼저 'off' 로 기록해서 도중 실패해도 자동 재구독 안 함.
관리자가 공지(제목·본문·이미지+선택적 외부링크)를 작성하고 푸시 구독자 중
원하는 사람에게 발송. 사용자가 알림 탭하면 자체 공지 페이지(/m/notices/[id])
또는 지정 URL 로 이동.
- lib/notices: momo_notices 테이블 자동 생성.
- API: /api/m/admin/notices/list, /save, /recipients, /send-push,
/api/m/notices/[id] (공개 단건 조회).
- Admin UI(/m/admin/notices): 좌 수신자 다중선택+검색+거래처/관리자 필터,
우 제목/본문/이미지 업로드/외부링크. [N명에게 발송] 한 번으로 공지 저장+푸시.
- 공개 페이지(/m/notices/[id]): 이미지+제목+본문 렌더.
- 이미지 업로드는 기존 /api/m/items/upload-image 재사용.
- 사이드바: 마스터 관리 > 푸시알림 게시판 (menu_info 9000299) 신규 등록.
관리자(user_type='A') 가 본인 구독으로 테스트해도 알람 미수신이던 문제 —
notifyItemSale 의 generalOnly 옵션을 해제. 관리자도 본인의 변경분으로 발송 확인 가능.
items/save 와 bulk-sale-range 양쪽에 sent/failed/스킵 사유 로그 추가.
- <tr title=...> 의 lock 메시지 hover 툴팁이 다음 행 위로 떠다니던 문제 해결 — title 제거.
- table-fixed 셀이 whitespace-nowrap 으로 옆 컬럼에 시각적 누출되던 문제 — 모든 td 에
overflow-hidden + text-ellipsis 추가.
- 컬럼 폭 확장: 발주번호 100→112, 발주일 82→100, 합계 100→110, 상태 72→78.
- 좌측 패널 최소폭 560→640 으로 키워 업체 컬럼이 화면에 꽉 차게 한다.
- 그리드 비율 2:3 → minmax(560px,1fr) : 1.4fr 로 좌측 패널을 더 넓게 (clamp 560 floor).
- 셀 padding px-1 → px-2 / py-1.5 → py-2 로 여백 확대.
- 컬럼 폭 재조정: 발주번호 88→100, 발주일 72→82, 합계 82→100, 상태 62→72.
업체 컬럼은 flex 로 남는 공간 모두 사용.
- title 속성 제거 — truncate 시 떠다니던 hover 툴팁이 다음 행 위로 겹쳐
보이던 문제 해결.
- bulk-sale-range API: alwaysSale 모드 추가 — 선택 품목들을 is_always_sale='Y'
로 설정하면서 날짜는 모두 NULL 로 초기화.
- 품목 관리 일괄 패널: [상시 판매로 설정] 버튼 추가. 안내 문구도 갱신
(상시=항상 노출/날짜 초기화, 해제=미노출/날짜 초기화).
- 목록 판매기간 컬럼: 상시(초록 배지)/날짜범위/미노출(빨강 배지) 3종 명확 표시.
요구 정정: 기존엔 날짜 없으면 자동 "상시" 였으나, 이제 명시적 [상시 판매] 체크가
있어야 출고요청에 노출되고 날짜 없으면 미노출(=거래처 화면에서 안 보임).
- DB: momo_items.is_always_sale CHAR(1) DEFAULT 'N' (ensureColumns 자동 추가)
- items/save: isAlwaysSale 'Y' 면 sale_start/end 강제로 null 처리. INSERT/UPDATE
에 is_always_sale 컬럼 반영. 알림 트리거에 상시 플래그 변경도 포함.
- items/list forSale 필터: is_always_sale='Y' 이거나, 시작/종료 중 하나 설정 +
현재 기간 안 일 때만 노출. (둘 다 NULL + 상시 미체크 = 미노출)
- orders/save on_sale 재판정: items/list 와 동일 규칙으로 마감 차단.
- 품목 관리 편집 폼: [상시 판매] 체크박스 + 체크 시 날짜 입력 비활성/비움.
- 품목 관리 목록: 상시(초록) / 날짜범위 / 미노출(빨강) 3가지 구분 표시.
체크 / 발주번호 / 발주일 / 업체 / 합계 / 상태 6개 컬럼 분리.
table-fixed + 고정 너비(88/72/82/62) 로 가로 스크롤 없이 480px 컨테이너에 맞춤.
업체명: text-sm font-bold(검정) — 가장 크고 굵게.
발주번호/발주일: text-xs tabular-nums 정렬.
부분입고(1000개 중 999개 입고) 시, 입금해야 할 금액은 입고분(999×단가)이지
발주 총액(1000×단가)이 아님. 이를 정확히 반영:
- list API: 발주/입고/미입고 수량(qty)·금액(price) 4개 필드 추가.
- 목록 표시(데스크탑/모바일): 발주 ₩총액(수량) / 입고 ₩입고금액(입고수량) /
미입고 ₩잔액(미입고수량) 3행 정리.
- 입금처리 모달 기본 금액 = 입고금액(있으면)으로 자동 채움 (입고 전이면 발주금액 폴백).
- 입금수정 모달도 동일 정보 표시 + 권장 금액 안내.
데스크탑 좌측 발주 리스트가 좁아서 가로 스크롤이 보이던 문제 + 업체명이 작던 문제.
- 발주번호/발주일을 업체 셀로 합쳐 4열 구조(체크/업체·발주/합계/상태)로 단순화
- 업체명: font-bold text-sm (강조), 그 아래 작은 회색으로 날짜·발주번호
- table-fixed + overflow-x-hidden 로 가로 스크롤 없이 한 화면에
기준 창고에 재고 row 가 없는 품목 출고 시 INSERT INTO momo_stocks(...regdate)
가 실패('column regdate does not exist')해 트랜잭션 롤백 → "승인 중 오류".
momo_stocks 실제 컬럼은 update_date 뿐이라 update_date 로 수정.
(기존 row 가 있던 품목은 UPDATE 경로라 정상 → 그래서 일부만 실패했음)
html-to-image 가 높이를 약간 짧게 잡아 발주서 하단 날짜 줄이 잘리던 문제.
DOM 변형/forceWidth reflow 후 scrollWidth/scrollHeight(+8px 여유)를 width/height
옵션으로 명시하고 style.overflow=visible 로 클립 방지.
입금완료(PAID) 행 동작에 [수정] 버튼 추가 — 입금일/입금액/방법/메모 수정,
또는 [입금 취소]로 입금완료 해제(입고 진행 상태로 복원 + 입금정보 삭제).
- 신규 /api/m/admin/proc-payments/update (action: edit | cancel).
- REQUESTED/PARTIAL/RECEIVED 행 동작은 기존 그대로 유지.
- bulk-sale-range: 리스트에서 판매기간 일괄 적용 시에도 일반 사용자 푸시.
1건이면 품목명, 여러 건이면 'N개 품목 판매' 요약. 해제(clear)는 알림 제외.
- 알림 아이콘: 큰 아이콘은 모모 로고(icon-192), 상태바 작은 배지는 흰 M
단색 투명 PNG(badge-96) — 기존엔 컬러 PNG라 크롬이 지구본 기본 배지로 대체했음.
- sw.js: CACHE v2 로 올려 갱신 강제 + badge-96 precache, push 핸들러가
payload icon/badge 우선 사용.
푸시:
- 구독 직후 '환영 푸시' 자동 발송 — 서버→푸시서비스→기기 경로 즉시 확인.
- /api/m/push/test (GET 구독 카운트, POST 본인 기기 테스트 발송).
- PushOptIn: 허용 결과 안내 + '알림 켜짐' 옆 [테스트] 버튼.
- sendPush 발송 로그(targets/sent/failed) 추가.
프로필:
- 회원정보 수정 페이지에 [닫기] 버튼 — 앱(standalone)은 브라우저 뒤로가기가
없어 모달처럼 갇히던 문제. history 있으면 back, 없으면 /m/orders/new.
요구 정정 — 트리거는 품목 마스터 저장(items/save) 이며, '지금 출고 가능'
전환뿐 아니라 미래 판매예정(시작일이 오늘 이후)도 알림 대상.
- getSaleInfo(): 판매 일정 유무 + 마감 미경과(sellable) + 현재 출고가능(orderableNow).
- 등록: 판매 일정이 잡혀 있으면 알림. 수정: 판매 시작/마감일이 바뀌고
그 일정이 아직 유효(오늘/미래)할 때만 알림 (단가 등 단순수정·과거날짜 제외).
- 메시지: 지금 가능 → "지금 출고요청 가능", 미래 → "{시작일} 판매 예정".
- 수신 대상: sendPush(generalOnly) — 관리자(user_type='A') 제외, 일반 거래처만.
- lib/push.ts: web-push + VAPID(env 우선/하드코딩 폴백) + momo_push_subscriptions
자동 생성. sendPush() 는 만료(404/410) 구독 자동 정리.
- API: GET /api/m/push/vapid (공개키), POST /api/m/push/subscribe (구독 저장).
- sw.js: push / notificationclick 핸들러 추가 (클릭 시 /m/orders/new 열기).
- components/PushOptIn: 출고요청 페이지에 '새 품목 알림 받기' 버튼. 권한 허용 시
구독 저장, 이미 허용이면 조용히 갱신. iOS<16.4 등 미지원 환경은 자동 숨김.
- items/save: 품목이 '출고요청 불가 → 가능' 으로 전환되면(신규 등록 포함, KST 기준
판매기간/ACTIVE/비숨김) 구독자에게 푸시 발송. 단순 수정은 알림 안 함.
운영에서 VAPID 키 교체 원하면 .env.production 에 VAPID_* 설정(없으면 기본키 사용).
페이지를 띄워둔 채 마감 시각이 지나면 목록은 그대로라 담기/발주가 됐던 문제.
- isSaleClosed(): SALE_END_DATE(KST 벽시계) 기준 마감 판정 (자정정각=종일 규칙 동일).
- 담기(addManyToCart)/발주요청(submitOrder) 직전 마감 재확인 후 경고+차단.
- 카드/리스트에 '판매 마감' 상태 표시 + 30초 틱으로 idle 중에도 자동 전환.
- 백엔드 orders/save 의 마감 재검증과 합쳐 2중 차단.
직전 커밋에서 관리자/무제한은 총 재고도 초과 가능하게 했으나, 요구사항은
"총 재고보다 많이는 못 나가되 기준 창고가 비어도 총 재고가 충분하면 출고 가능".
- orders/save: 재고 차단을 다시 전체 창고 합(stock_qty) 기준으로 모두에게 적용.
기준 창고(거래처 default_wh)가 0 이어도 총 재고가 충분하면 통과.
- orders/new(카드/리스트): 담기 한도/품절 표시를 전체 창고 합 기준으로 환원.
unlimitedQty 는 1회 발주 한도(maxQ)만 무시, 총 재고는 못 넘김.
실제 차감(approve)은 기준 창고에서 빼며 부족분은 음수로 떨어지고(제약 없음 확인),
관리자가 재고 이동으로 정리. 판매 마감 KST 재판정/타임존 수정은 유지.
- items/list: 마감 비교를 NOW() → (NOW() AT TIME ZONE 'Asia/Seoul') 로 변경.
DB 서버 TZ 가 UTC 면 마감 지난 품목이 9시간 더 노출되던 문제 해결.
- orders/save: 출고요청 시 판매기간 KST 기준 서버 재체크 — 마감 지난 품목이
장바구니에 남아 전송돼도 차단 + 경고 메시지.
- orders/save & orders/new: 관리자/무제한(unlimited_qty='Y') 은 재고 초과(음수)
출고 허용. 총 재고가 남아 있으면 기준 창고가 비어도 출고 후 재고이동으로 정리.
(실제 차감 approve 는 이미 음수 허용) 카드/리스트 품절표시도 unlimited 는 해제.
증상: sale_end_date='2026-05-22 00:00:00' 인 품목들이 5월 22일 0시 1분부터
출고요청 화면에서 사라짐. 사용자 의도는 "5월 22일 종일 마감".
원인: NOW() <= sale_end_date 비교가 자정 정각을 그 날의 끝이 아니라
그 날의 시작으로 해석.
수정: CASE 로 종료시각이 자정 정각이면 (= 시간 명시 안 함)
그 날 23:59:59 까지 노출. 시간 명시(예: 22:00)는 그 시각까지 정확히.
NOW() <= CASE
WHEN sale_end_date = date_trunc('day', sale_end_date)
THEN sale_end_date + INTERVAL '1 day' - INTERVAL '1 second'
ELSE sale_end_date
END
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
요구: 거래처 default 창고에 재고가 모자라도 그대로 차감해서 출고 진행.
일자별 발주/재고 + 창고별 재고 현황에서 음수(-) 로 표시되면 관리자가
다른 창고에서 부족 창고로 수동 재고 이동 처리하는 운영 정책.
변경:
- "재고 부족: 현재고 N, 요청 M" 차단 + ROLLBACK 제거 → 그대로 차감
- 재고 row 자체가 없던 품목은 새 row(qty=-N) INSERT
- itemsRes SQL 에 kind='ITEM' AND item_objid IS NOT NULL 가드 추가
(택배/용차/환불 라인이 잘못 차감되는 잠재 버그도 같이 차단)
- stock_moves OUT 이력은 동일하게 음수 qty 로 기록
음수 재고 발생 시 운영 흐름:
1) 다른 창고에 같은 품목 재고 확인
2) 관리자 패널 → 재고 이동 (오프라인 물리 이동 + 시스템 등록)
3) 부족 창고 재고가 0 이상으로 복구
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
이전 commit(474cf79)에서 PAID 도 수정 가능해졌으므로, 기본 노출 대상에 포함.
- statuses: ["REQUESTED", "APPROVED"] → ["REQUESTED", "APPROVED", "PAID"]
- select option 라벨: "출고요청+출고완료" → "출고요청+출고완료+입금완료"
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
요구: 출고처리(/m/admin/orders) 에서 입금완료 후에도 admin 이 품목과
택배/용차 라인을 추가/수정할 수 있어야 함. 계산서 발행 전 일괄 정정 케이스.
변경:
- /api/m/orders/items/add: admin 분기 신설 → REQUESTED/APPROVED/PAID 허용
(USER 는 기존대로 REQUESTED/APPROVED 까지)
- /api/m/orders/items/update: admin 분기에 PAID 포함
- items/update 의 재고 ± 동기화 분기에 PAID 도 포함 — APPROVED 와 동일하게
momo_stock_moves 의 OUT 이력으로 wh_objid 찾아 차이만큼 재고 조정
- /m/admin/orders StatementPreview: editable 에 PAID 도 true 처리
→ [+ 품목 추가] / 택배·용차 / 수량 입력칸 노출
INVOICED(계산서 발행) / CANCELLED 는 여전히 잠금 — 한 번 더 단계 진행하면
정정 불가.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
증상: 발주 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>
증상: 발주 15 → 16 클릭 시 본인 화면에 15, 16 모두 "내가 수정 중" 표시.
(다른 사용자 화면에는 16만 정상 표시 — DB 가 잠시 둘 다 me 로 오염됐다가
TTL 2분 후 알아서 풀림)
원인: heartbeat 와 acquire 가 같은 분기를 공유.
"빈 락이거나 자기 락 → UPDATE editing_by=me, editing_at=NOW()" 로 처리.
release(15) 직전 발사된 heartbeat(15) 가 release 응답보다 늦게 도착하면
빈 락을 자기 락으로 다시 잡아버림.
수정: heartbeat 액션을 분리.
- "UPDATE editing_at = NOW() WHERE objid = $1 AND editing_by = $2 AND alive"
- 자기 락이 살아있을 때만 갱신, 빈 락은 절대 잡지 않음
- rowCount=0 이면 409 + 현재 락 보유자 정보 반환
acquire 분기는 그대로 — 빈 락이거나 자기 락이면 신규 잡기.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
문제: 발주 A 클릭 → B 클릭 시 A 의 release/acquire 응답을 기다리지 않아
리스트는 30초 자동 갱신 전까지 stale (A 에 여전히 "내가 수정 중" 표시)
수정:
- 이전 락 release 시 setOrders 로 previousLocked 행의 EDITING_BY 즉시 null
- 새 락 acquire 성공 시 activeId 행의 EDITING_BY 즉시 본인으로 세팅
- 다른 사람 락이라 거부된 경우도 그 정보로 즉시 업데이트
- 30초 자동 갱신은 안전망(누락된 변화 동기화)으로 유지
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- load() 를 30초 setInterval 로 주기적 호출 → 누가 새로 락을 잡았는지 실시간 반영
- 카드/테이블 row 모두:
• 본인 락 (EDITING_BY === myUserId): 초록 배경 + ✏️ "내가 수정 중"
• 다른 사람 락: 빨강 배경 + 🔒 보유자명 (기존)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
이전 커밋(6be1633)에서 SQL이 momo_orders.editing_by / editing_at 을 참조하지만
운영 DB 에 컬럼이 없으면 list 쿼리 전체가 깨져 발주 리스트가 0건으로 표시됨.
(컬럼 자동 증설은 lock 라우트에만 있었는데, list 가 먼저 호출되니 시점이 안 맞음)
해결: list/detail/items.add/items.update/cancel 5개 라우트 진입부에
ALTER TABLE IF NOT EXISTS 로 editing_by/editing_at 컬럼 자동 증설.
컬럼이 이미 있으면 no-op. 첫 호출 1회만 ALTER 호출.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DB:
- momo_orders.editing_by (TEXT), editing_at (TIMESTAMP) — 자동 증설
- /api/m/admin/orders/lock 신규: action=acquire|heartbeat|release
- TTL 2분 (페이지가 죽거나 비정상 종료되어도 2분 후 자동 해제)
거래처 측 보호 (양방향 락):
- /api/m/orders/items/add, items/update, cancel : 락이 살아있고 본인 락이 아니면 409
"○○ 담당자가 수정 중입니다" 메시지 반환
- /m/orders DetailModal: editable=false + 상단 적색 배너로 차단 안내
출고관리 (/m/admin/orders):
- activeId 변경 시 이전 락 release → 새 락 acquire (useEffect)
- 30초마다 heartbeat — 락 갱신
- 헤더 옆에 [✏️ 편집 가능] / [🔒 ○○ 수정 중] 배지
- 다른 사람 락이면 우측 미리보기 영역 pointer-events-none + opacity-60
- 발주 리스트 (테이블/카드) 행에 🔒 + 보유자명 표시
- beforeunload + 컴포넌트 언마운트 시 sendBeacon 으로 release
list/detail API: 편집중 컬럼 두 개 노출 (EDITING_BY / EDITING_BY_NAME) — 2분 TTL CASE 절로 만료된 락은 자동 NULL
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DB:
- user_info.default_charter_use (CHAR 'Y'/'N'), default_charter_price (INTEGER)
- /api/admin/users/detail: ALTER TABLE IF NOT EXISTS 로 자동 증설 + SELECT 노출
- /api/admin/users/save: 두 필드 UPDATE
- /api/auth/me: 로그인 사용자의 defaultCharterUse/Price 응답에 포함
UI:
- admin-panel/user-form: [기본 용차비 사용] 체크박스 + [금액] 입력 (사용 체크 시만 활성)
- /m/orders/new: 카트에 품목이 들어오는 순간 default_charter_use='Y' 거래처는 용차 라인 자동 추가
- /m/orders/new: 거래처는 카트 안 택배/용차 라인 수정/삭제 불가 (read-only 표시)
[+ 택배 추가] [+ 용차 추가] 버튼도 admin 만 노출
- /m/orders DetailModal: canEditExtra=false 로 거래처 택배/용차 수정/삭제 차단
(출고관리 /m/admin/orders 에서만 수정 가능)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>