16. Office — ACN 마케팅 전산
원본 명세:
D:\www\kimduckbo\docs\ACN_마케팅_전산_기획메모.md(손글씨 메모). 사이트의 Office 탭(/{locale}/office)으로 구현되었으며, 이후acn202610.netlify.app (테스트 기간 표기; 정식 도메인 확정 시 변경)도메인을 이 경로에 연결할 수 있다.
원칙
- 수치 전용(오프체인 장부). Office는 노드 구매·추천·보상·출금 요청을 숫자로 기록하고 보여 줄 뿐, 블록체인을 호출하거나 토큰을 옮기지 않는다. 실제 토큰 이동은 dApp에서만 온체인으로 일어난다.
- 회원 계정은 아이디(이메일) + 비밀번호 + 닉네임 + Google OTP. 가입 직후 Google Authenticator 앱에 QR을 등록하고 첫 6자리 코드를 확인해야 가입이 완료되며, 로그인은 비밀번호 → OTP 코드 2단계다. 이메일 인증(메일 발송)은 없다.
- (선택, 현재 미사용) 이메일 인증 계정.
OFFICE_EMAIL_LOGIN_IDS에 아이디를 적으면 그 계정은 Google OTP 대신 메일로 받은 6자리 코드로 인증한다. 메일 전송 경로(Brevo HTTPS 또는 SMTP)가 필요하며, Render 무료 인스턴스는 SMTP 포트가 막혀 있다. 운영 설정에서는 비워 두어 모든 계정이 Google OTP를 쓴다. - 추천은 추천인 아이디(이메일)로 받는다. 가입 시 추천인 아이디를 입력하거나
?ref=<아이디>링크/QR로 가입하면 그 회원의 직접 추천으로 등록된다. - 관리자 승인 모델. 노드 구매 신청과 출금 요청은 관리자가 확인·승인한다. 결제 자체는 시스템 밖(dApp 전송, 계좌 이체 등)에서 이루어진다.
화면 구성 (명세 항목 ↔ 탭)
| 명세 | 탭 | 내용 |
|---|---|---|
| 대쉬보드 | 대시보드 | 엘리스 플랫폼·노드·디바이스 이미지, 노드 종류별 판매량/잔여량/총량, 회원 수, 활성 노드, 내 요약 |
| 노드 | 노드 | 스튜디오 노드 $500(ACN 10개×1,000일), 프리미엄 노드 $1,000(ACN 20개×1,000일), 엘리스 플랫폼 배당 표기, 구매 신청(메모 입력) |
| 내 노드 | 내 노드 | S/P 노드 개수, 노드별 상태(승인 대기/활성/거절/취소), 경과일, 적립 ACN, 승인 대기 신청 취소 |
| 지갑 | 지갑 | ACN·USDT 각각 누적/출금 가능/진행/완료, 항목별 내역, ACN → USDT 스왑(관리자 비율), 스왑 내역, 입금(활성화된 노드 구매) 내역, 출금 요청(USDT만, 받을 주소) |
| 내 추천 링크 | 내 추천 링크 | 내 아이디(추천인 아이디로 안내), 링크(/{locale}/office?ref=<아이디>), QR, 복사 |
| 내 추천 | 내 추천 | 직접 추천 회원 목록(닉네임, 가려진 이메일, 가입일, 직급, S/P 노드, 구매 금액) |
| 조직도 | 조직도 | 대라인/소라인/산하 합계(USD), 산하 인원, 트리형 차트(박스·연결선) / 목록 전환, 각 회원의 본인 구매·산하 합계 |
| 보상 내역 | 보상 내역 | 추천 보상, 직급 보상, 조정 내역 + 노드 일일 지급 합계 |
| 에어드랍 | 에어드랍 | 노드별 에어드랍 히스토리 |
| — | 관리자 | 관리자 이메일로 로그인 시에만 표시: 노드 승인/거절, 노드 직접 발급, 출금 처리, 에어드랍 등록, 보상 지급, 직급 설정, OTP 초기화, 스왑 비율, 조직도 편집(드래그앤드롭), 테스트 조직 생성 |
계산 규칙
- 노드 일일 지급: 활성화 시각부터 경과한 일수 × 일일 ACN, 최대
term_days(1,000일). 저장하지 않고 조회 시 계산한다. - 추천 보상: 노드가 활성화될 때 직접 추천인에게
가격(USD) × OFFICE_REFERRAL_ACN_PER_USDACN을 적립(기본 0.1 → $500 노드에 50 ACN). 보상 플랜 확정 전 임시값이다. - 직급 보상: 자동 계산 없이 관리자가 지급(직급도 관리자가 설정). 직급 산정 규칙이 정해지면 추가한다.
- 에어드랍: 관리자가 노드 종류(S/P/ALL)와 노드당 수량을 등록하면, 등록 시점에 활성 상태인 해당 종류 노드마다 배분 레코드(
office_airdrop_allocs)가 기록되고 적립에 합산된다. 이후 승인·소급 활성화된 노드는 지난 에어드랍에 포함되지 않는다. - 장부는 ACN과 USDT 두 개다. ACN = 노드 일일 지급 + 추천/직급/배당/조정 보상 + 에어드랍 − 스왑으로 전환한 ACN. USDT = USDT로 지급된 보상(플랫폼 배당, 직급, 조정) + 스왑으로 받은 USDT. 출금은 USDT로만 접수하며, 회원은 지갑 탭에서 ACN을 관리자가 정한 비율(
/admin/swap-rate, 기본OFFICE_SWAP_USDT_PER_ACN)로 USDT로 바꾼 뒤 출금 신청한다(최소 스왑 1 ACN, 최소 출금 10 USDT). 출금 가능액 = 누적 − 진행 중(대기·승인) 출금 − 지급 완료 출금. 음수 조정은 해당 자산의 출금 가능액을 넘지 못한다. - 입금 주소: 구매 신청 후
OFFICE_DEPOSIT_ADDRESS(USDT·BEP20)와 금액을 QR·복사 버튼과 함께 보여 준다. 입금 확인은 관리자가 한다. - 출금 상태 전이: 대기 → 승인 → 지급 완료 / 거절. 지급 완료에는 참조(tx 해시 등)가 필수.
- 대라인/소라인: 직접 추천 라인별 산하 구매 합계(활성 노드 가격 합) 중 최댓값이 대라인, 나머지 합이 소라인.
- 판매량/잔여량: 판매량 = 활성 노드, 예약 = 승인 대기(14일 이내) 신청, 잔여량 = 총량 − 판매량 − 예약. 총량을 초과해 신청·승인·발급할 수 없다. 14일이 지난 대기 신청은 자동 취소되어 재고를 반환하고, 회원도 대기 중 신청을 직접 취소할 수 있다. 관리자 직접 발급도 같은 재고 제한을 받는다.
- 조직도 편집(관리자): 회원 박스를 다른 회원 위로 드래그(PC)하거나 선택 후 대상 탭(모바일)하면 그 회원과 산하 전체가 새 추천인 아래로 이동한다(
POST /admin/org/move,office_org_moves에 감사 기록). 정산 규칙: (1) 이미 지급된 추천 보상은 노드에 귀속되어 그대로 남고 회수·재지급하지 않는다, (2) 이동 이후 활성화되는 노드부터 새 추천인 체인에 적립된다, (3) 대라인/소라인·직접 추천 수·조직도는 현재 추천 관계로 즉시 재계산된다, (4) 노드 적립·에어드랍·스왑·출금·장부는 회원/노드 단위라 영향이 없다, (5) 자기 산하 아래로 옮기는 순환은 거부되고, 이동은 조건부 UPDATE(이전 추천인이 그대로일 때만)로 동시 변경을 막는다. 조직도 조회는 스냅샷 트랜잭션(get_conn(snapshot=True))에서 한 번의 일관된 상태로 읽고, 활성 노드 합계는 요청당 1회만 집계한다. 편집 화면은 이동 요청 후 새 트리를 받을 때까지 잠겨 있어 오래된 화면 기준으로 두 번째 이동을 보낼 수 없다. 이동 중에 승인되는 노드는 승인 시점의 추천인에게 적립된다(의도된 동작). - 동시성: 장부를 읽고 쓰는 모든 요청은
BEGIN IMMEDIATE트랜잭션 + 조건부 UPDATE(상태가 그대로일 때만)로 처리하며, 추천 보상은 노드당 1회(부분 유니크 인덱스)로 보장된다.
인증
POST /api/office/auth/signup{email, password, nickname, referrerId?} → 계정 생성(미활성) + OTP 등록 정보(setupToken,otpauthUri,secret). 프론트가otpauthUri를 QR로 표시하고 Google Authenticator로 스캔(또는 키 수동 입력).POST /auth/otp/confirm{setupToken, code} → 첫 코드가 맞으면 계정 활성화 + 세션 발급(자동 로그인).POST /auth/login{email, password} → 비밀번호가 맞으면otp_required+preToken(10분). OTP를 아직 등록하지 않았거나 관리자가 초기화한 계정은otp_setup으로 1번의 등록 정보를 다시 준다.POST /auth/otp{preToken, code} → 세션 발급.
- 이메일 인증 계정(
OFFICE_EMAIL_LOGIN_IDS)은 1·3번에서email_required+preToken을 받고, 서버가 6자리 코드를 메일로 보낸다.POST /auth/email{preToken, code}로 가입 완료/로그인,POST /auth/email/resend{preToken}으로 재발송(1분에 1회). 코드는 10분·1회용이며 해시로만 저장하고, 오류 8회면 15분 잠금. - OTP는 RFC 6238 TOTP(SHA1·6자리·30초, 표준 라이브러리 구현). 시계 오차를 위해 ±1 스텝을 허용하고, 사용한 스텝 이하의 코드는 재사용할 수 없다(재생 공격 방지). 잘못된 코드 8회면 15분 잠금.
- 단계 토큰(
setup/otp)은 용도가 표시된 짧은 JWT(10분)로, 세션으로 쓸 수 없고 서로 바꿔 쓸 수도 없다. 관리자 OTP 초기화는 새 비밀키를 만들고 기존 세션·단계 토큰을 모두 무효화한다. 초기화된 회원도 회원 자격(추천·조직도·장부)은 그대로 유지된다. - 비밀번호는 PBKDF2-HMAC-SHA256(210,000회)로 해시하며 해시 계산·검증은 쓰기 트랜잭션 밖에서 수행한다. 로그인은 계정 유무와 무관하게 해시 검증을 정확히 1회 수행한다. 세션은 HS256 JWT(
OFFICE_JWT_SECRET, 기본 72시간). - 이메일 소유 확인이 없으므로 아이디는 먼저 등록한 사람의 것이다. OTP 등록을 한 번도 마치지 않은 가입은 하루 동안 아이디·닉네임을 점유하고, 그 뒤에는 다른 가입이 덮어쓸 수 있다. 한 번이라도 활성화된 계정(관리자가 OTP를 초기화한 회원 포함)은 절대 덮어쓰이지 않는다.
- 같은 이유로 관리자 아이디(
OFFICE_ADMIN_EMAILS)의 가입에는OFFICE_ADMIN_SIGNUP_KEY가 필요하다(키가 비어 있으면 관리자 아이디는 가입 불가). 프론트는 서버가 키를 요구하면 입력란을 보여 준다. - 비밀번호 오류는 계정별로 10회면 15분 잠금(IP 레이트리밋과 별개). 모든 토큰(세션·단계 토큰)은 계정의
session_epoch를 담고, 관리자 OTP 초기화가 이를 올려 기존 토큰을 모두 무효화한다. - 닉네임은 2~20자(한글·일본어·중국어·태국어·영숫자·공백·
_·-), 대소문자 무시 중복 불가. - 관리자 =
OFFICE_ADMIN_EMAILS에 있는 아이디로 OTP 등록을 마친 계정.
API 요약 (/api/office)
| 메서드 | 경로 | 권한 | 설명 |
|---|---|---|---|
| POST | /auth/signup, /auth/otp/confirm, /auth/login, /auth/otp, /auth/email, /auth/email/resend | 공개 | 위 인증 흐름 |
| GET | /dashboard | 공개 | 상품·판매량·회원 수 |
| GET | /config | 공개 | 배포 자가 점검(비밀값 없이 설정 여부만: 관리자 아이디 수, 이메일 인증 아이디 수, SMTP·JWT 설정 여부) |
| GET | /me | 회원 | 프로필, 내 아이디, 추천인, 노드 수, 장부 요약 |
| POST | /referrer | 회원 | 추천인(아이디) 미설정 시 1회 설정 |
| GET/POST | /nodes, /nodes/request | 회원 | 내 노드 / 구매 신청 |
| POST | /me/nickname | 회원 | 닉네임 변경 |
| POST | /swap | 회원 | ACN → USDT 스왑 |
| POST | /nodes/{id}/cancel | 회원 | 본인 승인 대기 신청 취소 |
| GET | /referrals, /org, /rewards, /wallet, /airdrops | 회원 | 각 탭 데이터 |
| POST | /withdrawals | 회원 | 출금 요청 |
| GET/POST | /admin/nodes, /admin/nodes/{id}, /admin/nodes(발급) | 관리자 | 노드 승인·거절·직접 발급 |
| GET/POST | /admin/withdrawals?status=, /admin/withdrawals/{id} | 관리자 | 출금 처리(pending→approved→paid/rejected, paid는 참조 필수) |
| POST | /admin/airdrops, /admin/rewards, /admin/rank, /admin/otp-reset, /admin/demo-seed, /admin/swap-rate, /admin/org(전체 조직), /admin/org/move | 관리자 | 에어드랍 등록, 보상 지급(직급/배당/조정 × ACN/USDT), 직급 설정, OTP 초기화, 테스트 조직 생성(OFFICE_DEMO), 스왑 비율 설정 |
레이트리밋: 인증 계열은 IP당 분당 OFFICE_AUTH_RATE_PER_MINUTE(기본 20), 쓰기 계열은 OFFICE_WRITE_RATE_PER_MINUTE(기본 30).
데이터 (SQLite, office_* 테이블)
office_users(계정: 아이디·비밀번호 해시·TOTP 비밀키/활성 여부/마지막 사용 카운터/실패 횟수·추천인·직급), office_nodes(노드, pending/active/rejected/cancelled, 활성화 시각), office_rewards(추천/직급/조정 적립, 추천 보상은 노드당 유니크), office_withdrawals(출금 요청·상태·참조), office_swaps(ACN→USDT 스왑, 비율 기록), office_org_moves(조직도 편집 감사 기록), office_airdrops(에어드랍 정의), office_airdrop_allocs(에어드랍 배분: 에어드랍×노드). 금액은 정수(센트, milli-ACN)로 저장한다.
기동 시 마이그레이션이 이전 버전 데이터를 정리한다: 중복 추천 보상 제거 후 유니크 인덱스 생성, 배분 기록이 없는 에어드랍 백필, 이메일 인증 방식 잔여 테이블 삭제. 이메일 인증 버전으로 만들어진 office_users(TOTP 컬럼 없음)는 외래키를 잠시 끄고 같은 id로 재구성된다: 인증 완료 회원은 비밀번호·노드·보상을 그대로 유지하고 다음 로그인에서 OTP를 등록하며, 미인증 행은 삭제된다. 에어드랍 배분 백필은 최초 1회만 실행된다(indexer_state에 기록).
환경 변수 (백엔드)
| 변수 | 설명 |
|---|---|
OFFICE_JWT_SECRET | 세션 서명 키(32자 이상). OFFICE_SITE_URL이 localhost가 아니면 필수(없으면 기동 거부). 개발에서는 비우면 기동마다 무작위 |
OFFICE_ADMIN_EMAILS | 관리자 아이디(콤마 구분) |
OFFICE_ADMIN_SIGNUP_KEY | 관리자 아이디로 가입할 때 요구되는 키(비우면 관리자 아이디 가입 불가) |
OFFICE_EMAIL_LOGIN_IDS | Google OTP 대신 이메일 코드로 인증하는 아이디(콤마 구분) |
BREVO_API_KEY | 이메일 코드 발송(HTTPS). Render 무료 인스턴스는 SMTP 포트가 막혀 있어 이 방식을 쓴다. SMTP_FROM은 Brevo에서 인증한 발신 주소 |
SMTP_HOST SMTP_PORT SMTP_USER SMTP_PASSWORD SMTP_FROM SMTP_STARTTLS | 이메일 코드 발송(SMTP, 포트가 열린 환경). Gmail이면 smtp.gmail.com/587/주소/앱 비밀번호 |
OFFICE_EMAIL_DEV_ECHO | 개발용(localhost 사이트 URL에서만): 응답에 코드를 포함 |
OFFICE_SITE_URL | 공개 사이트 주소(예: https://acn202610.netlify.app). localhost가 아니면 OFFICE_JWT_SECRET 필수 |
OFFICE_DEPOSIT_ADDRESS | 노드 구매 USDT 입금 주소(BEP20) |
OFFICE_DEMO | 테스트 사이트용: 관리자 탭의 "테스트 조직 생성" 활성화. 기본값 true(테스트 배포) — 운영에서는 반드시 false |
OFFICE_SWAP_USDT_PER_ACN, OFFICE_MIN_SWAP_ACN | 스왑 초기 비율(관리자가 화면에서 변경하면 DB 값 우선)과 최소 스왑 수량 |
OFFICE_NODE_*, OFFICE_REFERRAL_ACN_PER_USD, OFFICE_MIN_WITHDRAW_*, OFFICE_ORG_DEPTH, OFFICE_AUTH_RATE_PER_MINUTE, OFFICE_WRITE_RATE_PER_MINUTE | 상품·보상·출금·조직도·레이트리밋 설정 |
Render 무료 플랜은 디스크가 없어 재배포 시 SQLite가 초기화된다. Office 데이터는 재구축이 불가능하므로 운영 전 영구 디스크(유료) 또는 외부 DB로 옮겨야 한다.
프론트엔드
- 경로
/{locale}/office. 모바일 게이트 없이 모든 브라우저에서 동작한다. 지갑 라이브러리를 쓰지 않는다. components/office/:OfficeApp(셸·탭·세션, 세션 토큰마다 새 React Query 캐시),OfficeAuth(로그인/가입/OTP 등록·코드 입력),OfficeSections(9개 탭),OfficeAdmin(관리자).- API 클라이언트
lib/officeApi.ts(세션 토큰은 localStorage, 해당 토큰이 401을 받으면 삭제). OTP QR은otpauth://URI를react-qr-code로 그린다. - 문구는 각 언어 사전의
office네임스페이스(7개 언어).
운영 메모
- Render 무료 인스턴스는 디스크가 없어 배포(코드 푸시·환경변수 변경)마다 Office 데이터가 초기화된다. 실제 회원을 받기 전에 유료 디스크 또는 외부 DB로 옮겨야 한다.
- 운영 API에 실제 아이디로 가입 요청을 보내 점검하지 말 것: OTP 등록을 마치지 않은 가입 행이 24시간 동안 그 아이디를 점유한다. 점검은
/api/office/config와 관리자 아이디의 403 응답으로만 한다.
검증
backend/tests_office_smoke.py: 가입→OTP 등록→2단계 로그인(재사용·창 밖 코드·잘못된 단계 토큰 거부, 실패 잠금), 추천 아이디 체인·순환 방지, 관리자 OTP 초기화, 구매 신청→관리자 승인(활성화 시각 소급)→일일 적립·추천 보상, 조직도 대/소라인, 출금 요청→지급 처리, 에어드랍, 직급·보상, 관리자 권한 검사.
