crm.crowny.org 복구 + crowny.org 디자인 통일
개요
crm.crowny.org(:9748)를
로그인 → 휴대폰 연락처 가져오기 → 4티어 고객관리 → 문자(DM) 대화 핵심 기능으로 재구축하고, crowny.org 금색 디자인(#C9A961 + Pretendard Variable + JetBrains Mono, Claude.ai 스타일)으로 통일했다. 기존 "12전략×4성향" v2 빌드는 폐기, 사용자 요청대로 새 스펙으로 전면 재작성.
무엇을 했는지
백엔드 재작성 (server.js, 외부 의존성 0 — http/fs/crypto/url):
로그인/가입 일체형: id(전화·이메일)+PIN(4자리+), salt+SHA256, 첫 로그인=자동가입. Bearer 토큰 세션.
owner 스코핑: 모든 데이터가 로그인 계정에만 저장.
POST /api/login, GET /api/state, POST /api/tiers, POST /api/contacts/import(전화번호 정규화 기준 중복제거), POST/PATCH/DELETE /api/customer[/:id], POST /api/customer/:id/message(dir in/out + ts 저장).
4티어 기본값(핵심VIP/단골/잠재/신규, 이름·색 변경 가능).
영속화: 디바운스(300ms) + 원자적 tmp→rename. SIGTERM/SIGINT flush 추가(배포·재시작 데이터 손실 방지 — 디바운스 미flush 버그 수정).
프론트엔드 재작성 (web/index.html, 모바일 우선 SPA):
crowny.org 디자인 토큰(--accent:#C9A961, Pretendard Variable, JetBrains Mono, 라이트/다크 자동).
연락처 가져오기 3경로: ①Contacts Picker API(Android Chrome) ②.vcf/.csv 파일 업로드 ③"이름,전화" 붙여넣기. 아이폰은 vCard 내보내기 안내.
티어 탭 필터 + 통계바 + 검색 + 고객카드(최근 메시지 미리보기).
고객 상세: 티어 즉시변경, 전화걸기(tel:), 편집/삭제, 메모, 문자 대화 스레드(내보냄/받음, 보내기 시 sms: 문자앱 연동 + 동시 기록).
한선씨 동반: 고객티어.한선(전화정규화·중복제거·티어분류·대화스레드 핵심 로직), hanseonc_high 컴파일 검증 통과(38KB toau).배포 (충돌 해소)
- 기존 :9748은 통합 프레임워크(
crowny-services/서비스서버.js)가 정적 서빙 중이었음 → 풀 기능엔 독립 서버 필요.
registry.psv에서 crm 줄 제거(프레임워크 데몬이 :9748 안 건드림).
- 프레임워크 crm 프로세스 종료 → 독립 LaunchAgent 전환.
org.crowny.crm.plist 수정: node 경로 /usr/local/bin/node(없음, exit78) → /opt/homebrew/bin/node, PORT=9748 추가.
- 게이트웨이는 이미 crm.crowny.org→:9748 결선됨(변경 불필요). 공개 HTTPS 200 확인.
검증
- 백엔드: 가입/로그인/잘못된PIN 401/연락처 import(2추가1중복스킵)/message(out·in)/tier PATCH/owner 스코핑 — 전부 통과.
- 영속성: SIGTERM flush 후 재시작 → 고객·티어·메시지 보존 확인.
- 공개:
https://crm.crowny.org/api/health 200 (TTFB 0.39s), 디자인 토큰(#C9A961/Pretendard/JetBrains) 적용 확인.
- 클린 리셋 완료(빈 db, 신규 가입만 동작).
관련 파일
/Users/ef/crowny-crm/server.js — 백엔드
/Users/ef/crowny-crm/web/index.html — 프론트(SPA)
/Users/ef/crowny-crm/고객티어.한선 — 한선씨 동반
/Users/ef/Library/LaunchAgents/org.crowny.crm.plist — 영속 기동
/Users/ef/crowny-services/registry.psv — crm 줄 제거됨(백업 .bak.*)
- 백업:
web/index.html.bak.*, server.js(이전 v2 git 없음), data/crm.json·users.json(옛 v2, 미사용)
잔여 이슈
- 연락처 휴대폰 직접선택은 Android Chrome 전용(Contacts Picker API). 아이폰/데스크탑은 vCard·CSV·붙여넣기 폴백.
- 실제 SMS 발송은 OS 문자앱 연동(
sms: 스킴) — 서버측 발송 게이트웨이는 미구현(통신사 연동 시 추가).
- 옛 v2 데이터 파일(crm.json 등) 아카이브 상태 — 필요시 삭제 가능.
2차 구현 (2026-06-22): SMS 자동발송 + 문자 자동화 + 잔여 정리
"셋다" 요청 — 세 가지 모두 구현 완료.
① 서버측 SMS 자동발송
발송 어댑터 sendSMS(cfg, to, text): provider별 분기
none: 서버 발송 안 함 → 클라이언트가 sms: 폰앱 폴백
aligo: apis.aligo.in/send/ (key/user_id/sender/receiver/msg, form-urlencoded)
custom: 임의 HTTP 엔드포인트 POST {to,text,sender} + Bearer
per-owner 설정 GET/POST /api/sms/config (API키 마스킹, hasKey만 노출), POST /api/sms/test(내게 테스트)
메시지 발송 시 send:true면 서버가 직접 발송하고 sent/via 기록. 미설정이면 폰앱 + 기록.
외부 의존성 0 (https 내장 모듈만).② 문자 대화 자동화 + 템플릿
- 템플릿 CRUD
GET/POST/DELETE /api/templates (티어별, 변수 {이름}{회사}{전화} 치환)
- 대량발송
POST /api/broadcast {tier, text, send}: 티어 대상 일괄, 변수 치환, send=true 서버발송/false 기록만
- 예약발송
POST /api/scheduled + 30초 주기 스케줄러(runScheduler): 도래 시 치환·발송·대화기록·flush
- 프론트: 헤더에 대량발송(메가폰)·설정(기어) 버튼, 설정 모달(발송방식+템플릿관리), 대량발송 모달(대상수 미리보기), composer에 템플릿 적용·서버발송 토글·⏰예약 버튼
③ 잔여 정리
- 옛 v2 데이터 파일(crm.json/users.json/sessions.json) →
data/_v2_archive/ 이동(새 서버 미사용)
검증
- 백엔드 임시포트: SMS설정(키마스킹)·메시지 send·템플릿·대량발송({이름}치환)·예약등록 전부 통과
- 라이브 E2E: 예약발송 스케줄러 30초 주기 실발화 확인({이름} 치환, 발화 후 목록 비움)
- 한선씨 동반(고객티어.한선)에 이름치환·서버발송판단 추가, 컴파일+실행 검증
- 한선씨 함정 발견:
포함()은 0-based 인덱스 반환(-1=없음) → 만약(포함()) 직접사용 시 인덱스0("0") 누락. 포함(...) > -1로 수정. (프로덕션 JS 정규식은 무관, 동반 데모만 영향)
발송 연동 안내 (운영자)
- 실제 SMS 발송하려면 설정→알리고 선택→발신번호·user_id·API Key 입력→"내게 테스트". 또는 커스텀 HTTP 엔드포인트.
- 미설정 시에도 폰 문자앱 연동 + 대화 기록은 그대로 동작.
3차 구현 (2026-06-22): 30명 무료 체험 + 크라우니달러 결제 확장
크라우니 회원 누구나 30명까지 무료, 초과 시 크라우니달러(CRN)로 인원 확장.
정책
- 무료 한도
FREE_LIMIT=30 (env CRM_FREE_LIMIT로 조정), 단가 1명 = 1 크라우니달러 (env CRM_PRICE_PER_SLOT)
- 한도 = 무료30 + 결제로 늘린 extra
- 신규 등록 한도 검사:
POST /api/customer(신규만), POST /api/contacts/import(초과분 차단, 중복/빈건은 한도 미소모)
크라우니달러 = 지갑 CRN 연동
- wallet.crowny.org:9410 — CRM 오너 id를 지갑 userId로 사용
- 잔액조회
GET /api/wallet/:userId (crn), 결제 POST /api/wallet/send {from:오너, to:재단, asset:CRN, amount}
- 재단 계정
crowny-crm-treasury (env CRM_TREASURY), 지갑 URL env WALLET_URL
GET /api/plan(한도/사용/잔액/단가), POST /api/plan/purchase {slots}(잔액검사→송금→extra 증가)
프론트
- 사용량 바(N/한도, 80%↑ 경고색, 한도초과 빨강) + "인원 늘리기" 버튼
- 업그레이드 모달: 잔액·한도 표시, 팩(+30/+100/+500) 또는 직접입력, 결제금액 미리보기, "크라우니달러로 결제"
- 고객 추가/연락처 가져오기에서 402 needUpgrade → 자동으로 업그레이드 모달
검증
- 한도 강제(무료3 테스트): 3명 OK→4번째 402 needUpgrade
- 결제: 잔액0 차단 → credit로 CRN 충전 → 결제 성공(차감·한도상승·재등록 가능) — 실 지갑(:9410) E2E
- 라이브: state.plan/api/plan 지갑연동, index 51KB, 공개 HTTPS 200
- 한선씨 동반에 등록한도·등록가능·결제금액 추가(컴파일+실행 검증)
운영 파라미터 (기본값, 조정 가능)
- 무료 30명 / 1명=1 크라우니달러. 다른 가격·무료한도는 LaunchAgent env(CRM_FREE_LIMIT, CRM_PRICE_PER_SLOT)로 변경.
4차 구현 (2026-06-22): 무료기간(~9/30) + 월 구독제로 전환 ★현행
3차의 "30명 무료+슬롯 단건구매" 모델을
시간기반 무료기간 + 월 구독제로 교체(사용자 재정의).
정책
- 2026-09-30 23:59:59 KST 까지 전면 무료 (전 기능, 인원 제한 없음). 테스트·재부팅 고려 — 설정은 db.json에 영속.
- 중간 유료화 토글: 관리자가 날짜 전에도 강제 유료화 가능(
forcePaid).
- 9/30 이후: 월 구독 필요. 미구독 시 쓰기(고객추가/가져오기/문자/대량/예약/티어/설정) 전부 402 차단, 조회·결제만 허용.
- 월 구독료(고객 수 구간): ≤50명 20 / ≤200명 50 / ≤1000명 150 / 그이상 400 크라우니달러. "20부터, 사용자수 따라 추가요금".
백엔드
- 설정
DB.config = { forcePaid, freeUntil } (영속). billingActive = forcePaid || now>freeUntil. needPayment = billingActive && !subActive.
- 오너별
subscription = { paidUntil, history }. 결제 시 paidUntil = max(now,paidUntil)+30일.
- 쓰기 게이트: needPayment & (POST/PATCH/DELETE) & path≠구독결제 → 402 needSubscription.
- 라우트:
GET /api/plan(billing/sub 상태+구간+잔액), POST /api/subscription/pay(월구독료 결제→1개월 연장), POST /api/admin/billing(x-admin-token, forcePaid/freeUntil 토글).
- 슬롯 단건구매(
/api/plan/purchase)·FREE_LIMIT 한도 제거.
관리자 토큰 (유료화 토글)
CRM_ADMIN_TOKEN env(plist) = 13c8dd575bf93142c8c656a2
- 유료화 ON:
curl -X POST https://crm.crowny.org/api/admin/billing -H 'x-admin-token: <토큰>' -H 'Content-Type: application/json' -d '{"forcePaid":true}'
- 무료 복귀:
... -d '{"forcePaid":false}'
- 무료기간 변경:
... -d '{"freeUntilDate":"2026-10-31"}'
프론트
- 사용량 바 → 상태 배너 3종: 무료기간(9/30까지)·구독중(만료일)·구독필요(경고색).
- 구독 모달: 잔액·고객수·이번달 구독료·만료일·요금 구간표(현재 구간 강조)·"1개월 구독 결제".
- 402 needSubscription는
api()에서 중앙 처리 → 자동으로 구독 모달.
검증
- 무료기간: 고객추가 자유, needPayment=false. 강제유료화 ON→고객추가 402. 구독 잔액0 차단→CRN 충전→결제 성공(월20 차감, +30일)→사용 재개.
- 관리자 토큰: 오답 401, 정답 토글(ON/OFF) 즉시 반영, db 영속(재부팅 안전).
- 라이브 state.plan/plan tiers, 공개 HTTPS 200. 한선씨 동반(유료기간·월구독료·결제필요) 컴파일+실행 검증.