Files
AriWeb-Suite/docs/verification/local-e2e-2026-11-06.json
T

2814 lines
75 KiB
JSON

{
"generatedAt": "2026-11-06T08:42:57.005Z",
"startedAt": "2026-11-06T08:40:37.605Z",
"env": {
"agents": 4,
"ports": {
"hal": 42077,
"portal": 35717,
"wm": 38791,
"am": 37901,
"mail": 36549
},
"mailServerRegistered": true,
"node": "v24.22.1"
},
"summary": {
"verified_local": 18,
"partial": 14,
"not_verifiable_locally": 1,
"failed": 1,
"unrecorded": 0
},
"flows": [
{
"no": 1,
"title": "신규 고객 가입과 이메일 확인",
"verdict": "verified_local",
"note": "가입→확인 메일(포획)→1회용 토큰 확인→개인 그룹·별칭→중복 가입 안내→기기 목록·세션 종료",
"blockedBy": null,
"steps": [
{
"name": "가입(이메일·표시 이름·비밀번호·필수 동의) → 세션 발급",
"ok": true,
"detail": {
"status": 200,
"hasSession": "[redacted]"
}
},
{
"name": "확인 전 유료·운영 기능 차단(그룹 생성 403 email_unverified)",
"ok": true,
"detail": "email_unverified"
},
{
"name": "확인 메일 수신(발신 포획)에서 토큰 추출",
"ok": true,
"detail": {
"subject": "[Ariweb] 이메일 주소를 확인해 주세요"
}
},
{
"name": "이메일 확인(1회용) → 재사용 거부",
"ok": true,
"detail": {
"first": 200,
"second": "token_used"
}
},
{
"name": "내 정보: 이메일 확인됨 · 개인 그룹 · owner · 별칭 user-01",
"ok": true,
"detail": {
"verified": true,
"groupKind": "personal",
"role": "owner",
"alias": "user-01"
}
},
{
"name": "중복 가입은 존재를 드러내지 않는 안내(409 signup_unavailable)",
"ok": true,
"detail": "signup_unavailable"
},
{
"name": "로그인 기기 목록 · 다른 세션 종료",
"ok": true,
"detail": {
"sessionsBefore": "[redacted]",
"revoked": 2
}
}
],
"evidence": {
"me": {
"verified": true,
"groupKind": "personal",
"role": "owner",
"alias": "user-01"
}
}
},
{
"no": 2,
"title": "개인 사용자 그룹 생성과 다른 사용자 초대",
"verdict": "verified_local",
"note": "팀 그룹 생성·초대 메일·1회용 수락·별칭 user-02·역할 거부·그룹 전환 격리",
"blockedBy": null,
"steps": [
{
"name": "팀 그룹 만들기 → 현재 그룹 전환",
"ok": true,
"detail": "g_01a1b004656a3fd8bdbc431252"
},
{
"name": "이메일 초대(7일) → 초대 메일 링크",
"ok": true,
"detail": "gDQeFdZYzURAfDUw-yHeUH762uhAoRwS"
},
{
"name": "초대 수락(1회용) → 별칭 user-02 · 역할 member",
"ok": true,
"detail": {
"alias": "user-02",
"role": "member",
"reuse": "invite_invalid"
}
},
{
"name": "권한 없는 구성원의 직접 API 접근 → 403 + 필요한 역할",
"ok": true,
"detail": {
"requiredRole": [
"owner",
"admin"
],
"yourRole": "member"
}
},
{
"name": "그룹 전환 격리: 개인 그룹으로 바꾸면 팀 데이터가 보이지 않음",
"ok": true,
"detail": {
"team": {
"live": 0,
"purchased": 0,
"inUse": 0,
"reserved": 0,
"unallocated": 0
},
"personal": {
"live": 0,
"purchased": 0,
"inUse": 0,
"reserved": 0,
"unallocated": 0
}
}
}
],
"evidence": {}
},
{
"no": 3,
"title": "상품 주문, 결제 확인, 권리 활성화",
"verdict": "verified_local",
"note": "상품 목록→주문(멱등·수동 결제)→입금 확인 전 승인 거부→입금 확인→승인→권리 활성화→재승인 중복 없음→청구 화면",
"blockedBy": null,
"steps": [
{
"name": "상품 목록(공개 버전만·가격·한도·미확정 정책 표시)",
"ok": true,
"detail": {
"count": 2,
"team": {
"id": "pv_01a1b004639c49d0dc8ae615b6",
"price": 49000,
"sites": 3
},
"deferred": 3,
"paymentMethods": [
"manual_transfer"
]
}
},
{
"name": "주문 제출(수동 결제) → 결제 대기·주문 번호·입금 안내 · 같은 키 재제출은 중복 없음",
"ok": true,
"detail": {
"orderNo": "AW-20261106-0001",
"status": "awaiting_payment",
"total": 63800,
"dueAt": "2026-11-13T08:40:40.667Z",
"hasInstructions": true
}
},
{
"name": "입금 확인 전 승인 거부(409 payment_not_confirmed)",
"ok": true,
"detail": "payment_not_confirmed"
},
{
"name": "운영자 입금 확인 → 승인 대기 목록에 권리 변화·현재 사용량 표시",
"ok": true,
"detail": {
"status": "awaiting_approval",
"delta": [
"sites",
"web_disk",
"web_out",
"mail_storage",
"mail_msgs",
"mailboxes",
"mail_services",
"mail_storage",
"mail_msgs",
"mailboxes"
]
}
},
{
"name": "운영자 승인 → 권리 활성화(재승인은 중복 없음)",
"ok": true,
"detail": {
"granted": 10,
"already": true
}
},
{
"name": "고객 결제 화면: 활성 상품·권리·다음 청구일",
"ok": true,
"detail": {
"sites": 3,
"subs": 1,
"nextBilling": "2026-12-06T08:40:40.718Z"
}
}
],
"evidence": {
"ap": {
"granted": 10,
"already": true
},
"bill": {
"sites": 3,
"subs": 1,
"nextBilling": "2026-12-06T08:40:40.718Z"
}
}
},
{
"no": 4,
"title": "초안 사이트 생성",
"verdict": "verified_local",
"note": "초안 생성·Webmaker 프로젝트 연결·사이트 수 미소비·목록 필터",
"blockedBy": null,
"steps": [
{
"name": "사이트 생성(새 Webmaker 프로젝트·배포 방식 관리형) → 초안·사이트 수 미소비·다음 단계 안내",
"ok": true,
"detail": {
"id": "s_01a1b004663ab3d2ed1c59d189",
"lifecycle": "draft",
"projectId": "00000000000000000000000000000001",
"nextSteps": [
"build",
"import",
"domain",
"mode",
"perms"
]
}
},
{
"name": "사이트 목록 필터(초안)·사용량 요약에 예약 0",
"ok": true,
"detail": {
"drafts": 1,
"sitesReserved": 0,
"sitesAllocated": 0
}
}
],
"evidence": {}
},
{
"no": 5,
"title": "Webmaker에서 페이지 제작",
"verdict": "verified_local",
"note": "SSO 티켓(1회용) 진입·저장본 목록·배포 전 확인(가짜 Webmaker — 실제 편집 UI 는 포함하지 않음)",
"blockedBy": null,
"steps": [
{
"name": "제작 화면 열기: 포털 SSO 티켓 → Webmaker /api/auth/sso 302(1회용)",
"ok": true,
"detail": {
"portalRedirect": "http://127.0.0.1:38791/api/auth/sso?ticket=…",
"webmaker": 302,
"reuse": 401
}
},
{
"name": "Webmaker 에서 페이지 저장(저장본 2개) → 포털 저장본 목록·배포 전 확인 통과",
"ok": true,
"detail": {
"versions": 1,
"precheckOk": true
}
}
],
"evidence": {
"sso": {
"portalRedirect": "http://127.0.0.1:38791/api/auth/sso?ticket=…",
"webmaker": 302,
"reuse": 401
},
"ver": {
"versions": 1,
"precheckOk": true
}
}
},
{
"no": 6,
"title": "관리형 배포",
"verdict": "verified_local",
"note": "배포 요청→예약→HAL 작업→Agent(stub) 배포→도메인 설정 필요 상태(대표 도메인 없음)",
"blockedBy": null,
"steps": [
{
"name": "배포 요청 → 10단계 작업·사이트 수 예약·같은 요청 재제출은 하나",
"ok": true,
"detail": {
"jobId": "j_01a1b004666d3964ce5450810c",
"steps": 10,
"again": false,
"siteCount": "reserved",
"server": null
}
},
{
"name": "HAL·Agent 를 거쳐 작업 완료 대기(최대 40초)",
"ok": true,
"detail": {
"status": "done",
"steps": [
"accept:done",
"prepare_version:done",
"prepare_target:done",
"apply:done",
"data_check:done",
"start:done",
"internal_check:done",
"external_check:done",
"domain_https:done",
"done:done"
]
}
},
{
"name": "배포 뒤 사이트 상태·버전·건강",
"ok": true,
"detail": {
"lifecycle": "needs_domain",
"version": "20261106084041-rev-c1-000000",
"verify": "needs_domain",
"health": "ok",
"agentHasSite": true
}
}
],
"evidence": {
"d": {
"jobId": "j_01a1b004666d3964ce5450810c",
"steps": 10,
"again": false,
"siteCount": "reserved",
"server": null
},
"deployJob": {
"status": "done",
"steps": [
"accept:done",
"prepare_version:done",
"prepare_target:done",
"apply:done",
"data_check:done",
"start:done",
"internal_check:done",
"external_check:done",
"domain_https:done",
"done:done"
]
},
"after": {
"lifecycle": "needs_domain",
"version": "20261106084041-rev-c1-000000",
"verify": "needs_domain",
"health": "ok",
"agentHasSite": true
}
}
},
{
"no": 7,
"title": "도메인 연결과 HTTPS 발급",
"verdict": "partial",
"note": "도메인 추가·복사 가능한 TXT/DNS 값·외부 대기 상태까지 확인. 실 DNS 반영·인증서 발급·대표 도메인 확정은 로컬 확인 불가",
"blockedBy": null,
"steps": [
{
"name": "도메인 추가(웹·대표 지정) → 소유 확인 TXT 값 안내 → 작업이 외부 대기(TXT)",
"ok": true,
"detail": {
"domainId": "d_01a1b0047a331aa3d1923d51a8",
"txtName": "_ariweb-verify.aw-e2e.localhost",
"txtValueShape": "ariweb-verify=…",
"copyable": true
}
},
{
"name": "도메인 목록·상세: 소유 확인·DNS·인증서 상태 표시",
"ok": true,
"detail": {}
},
{
"name": "(정리) 도메인 연결 작업 취소 — 포털이 외부 대기 중에도 5초마다 HAL domain.probe 를 새로 만들어 큐를 채우므로(결함 항목) 뒤 흐름을 위해 멈춘다",
"ok": true,
"detail": {
"queuedProbes": 0
}
}
],
"evidence": {
"dm": {
"domainId": "d_01a1b0047a331aa3d1923d51a8",
"txtName": "_ariweb-verify.aw-e2e.localhost",
"txtValueShape": "ariweb-verify=…",
"copyable": true
}
}
},
{
"no": 8,
"title": "외부 주소에서 사이트 확인",
"verdict": "partial",
"note": "플랫폼 확인 주소(preview.localhost)로 HAL 외부 확인을 시도했으나 로컬에는 80 포트 웹서버가 없어 외부 확인 결과만 기록됨",
"blockedBy": null,
"steps": [],
"evidence": {}
},
{
"no": 9,
"title": "사이트 정보와 접속·사용량 통계 확인",
"verdict": "partial",
"note": "개요·사용량 조회(Agent stub 가 수집한 값)",
"blockedBy": null,
"steps": [
{
"name": "개요: 배포 전/후에 따라 준비 단계 또는 실제 통계",
"ok": true,
"detail": {
"kind": "managed",
"lifecycle": "needs_domain",
"hasStats": false,
"readiness": null,
"disk": {
"used": null,
"limit": 1073741824
}
}
},
{
"name": "사용량 탭(배정·실제 사용·마지막 계측)",
"ok": true,
"detail": {
"allocations": [
"web_disk",
"web_out"
]
}
}
],
"evidence": {
"ov": {
"kind": "managed",
"lifecycle": "needs_domain",
"hasStats": false,
"readiness": null,
"disk": {
"used": null,
"limit": 1073741824
}
},
"us": {
"allocations": [
"web_disk",
"web_out"
]
}
}
},
{
"no": 10,
"title": "사이트 대표 메일과 메일함 생성",
"verdict": "partial",
"note": "대표 메일 생성(AriMail 테넌트·보장량·ariweb 표시)·메일함(비밀번호 1회·초대)·DNS 미반영 구분·활성화 작업 외부 대기까지 확인. DNS 반영 뒤 활성화 완료는 실 DNS 필요",
"blockedBy": null,
"steps": [
{
"name": "사이트 대표 메일 생성(사이트 연결 도메인·배정) → AriMail 테넌트 준비·도메인별 보장량·ariweb 표시",
"ok": true,
"detail": {
"id": "msv_01a1b00502c0755b9fe965ba46",
"status": "needs_setup",
"tenant": true,
"ariwebKind": "site_bound",
"limitAccounts": 6,
"siteCountAllocated": 0
}
},
{
"name": "같은 사이트에 두 번째 대표 메일 거부",
"ok": true,
"detail": "mail_domain_in_use"
},
{
"name": "메일함 생성: 초기 비밀번호 1회 표시·접속 안내 → 재조회에는 비밀번호 없음",
"ok": true,
"detail": {
"id": "mb_01a1b00502e82fb99c6a1d925b",
"address": "kim@aw-e2e.localhost",
"passwordShownOnce": "[redacted]",
"mustChange": true,
"imapHost": "127.0.0.1",
"leakedOnReload": false,
"inArimail": true
}
},
{
"name": "초대 방식 메일함(링크 만료·재발송·사용 완료 상태)",
"ok": true,
"detail": {
"inviteState": "pending",
"accepted": 200,
"reuse": "invite_used"
}
},
{
"name": "DNS 확인: 미반영/잘못된 값 구분 표시(로컬 DNS 에는 레코드 없음 → missing)",
"ok": true,
"detail": {
"ok": false,
"missing": 3,
"items": [
"mx:missing",
"spf:missing",
"dkim:missing",
"dmarc:recommended"
]
}
},
{
"name": "활성화 작업 → DNS 외부 대기(awaiting_external) · 멱등",
"ok": true,
"detail": {
"status": "awaiting_external",
"waitReason": "DNS 레코드가 아직 반영되지 않았습니다(누락 3, 잘못된 값 0). 도메인 DNS 에 안내된 값을 넣은 뒤 '이어서 진행' 을 누르세요",
"sameJob": true,
"steps": [
"entitlement:done",
"dns:waiting",
"prepare:pending",
"verify:pending",
"activate:pending"
]
}
},
{
"name": "보안 보고서(SPF·DKIM·DMARC·MX 상태 + 복사 값)",
"ok": true,
"detail": {
"items": [
"sender:error",
"mx:error",
"spf:error",
"dkim:error",
"dmarc:recommended",
"tls:error",
"roundtrip:recommended"
],
"records": 4
}
}
],
"evidence": {
"svc": {
"id": "msv_01a1b00502c0755b9fe965ba46",
"status": "needs_setup",
"tenant": true,
"ariwebKind": "site_bound",
"limitAccounts": 6,
"siteCountAllocated": 0
},
"mb": {
"id": "mb_01a1b00502e82fb99c6a1d925b",
"address": "kim@aw-e2e.localhost",
"passwordShownOnce": "[redacted]",
"mustChange": true,
"imapHost": "127.0.0.1",
"leakedOnReload": false,
"inArimail": true
},
"dns": {
"ok": false,
"missing": 3,
"items": [
"mx:missing",
"spf:missing",
"dkim:missing",
"dmarc:recommended"
]
},
"act": {
"status": "awaiting_external",
"waitReason": "DNS 레코드가 아직 반영되지 않았습니다(누락 3, 잘못된 값 0). 도메인 DNS 에 안내된 값을 넣은 뒤 '이어서 진행' 을 누르세요",
"sameJob": true,
"steps": [
"entitlement:done",
"dns:waiting",
"prepare:pending",
"verify:pending",
"activate:pending"
]
}
}
},
{
"no": 11,
"title": "외부 송신, 수신, 회신 확인",
"verdict": "not_verifiable_locally",
"note": "외부 송신·수신·회신은 실제 SMTP 제출(587)·수신함·외부 자동 회신 주소가 필요하다. 포털은 검증 주소 미설정 시 needs_user, 왕복 실패 시 활성화하지 않는 규칙(단위 테스트 test/mail.test.mjs)으로만 확인됨",
"blockedBy": null,
"steps": [],
"evidence": {}
},
{
"no": 12,
"title": "독립 메일 서비스 생성",
"verdict": "verified_local",
"note": "독립 메일 서비스 생성: 독립 옵션 1 소비, 운영 사이트 수 미소비, 목록에 백업 미제공 문구",
"blockedBy": null,
"steps": [
{
"name": "메일 전용 도메인 등록(solo-e2e.localhost, purpose mail)",
"ok": true,
"detail": {
"id": "d_01a1b005251eb41041d686f464",
"jobCancelled": null
}
},
{
"name": "독립 메일 서비스 생성(독립 옵션 소비·사이트 수 미소비)",
"ok": true,
"detail": {
"id": "msv_01a1b005252540cb9d7cb9ef0b",
"kind": "standalone",
"mailServicesAllocated": 1,
"sitesAllocated": 0
}
}
],
"evidence": {
"r": {
"id": "msv_01a1b005252540cb9d7cb9ef0b",
"kind": "standalone",
"mailServicesAllocated": 1,
"sitesAllocated": 0
},
"listCount": 2,
"noBackupNote": "메일 원문은 고객용 백업·복원에 포함되지 않으며, 메일 백업 기능을 제공하지 않습니다."
}
},
{
"no": 13,
"title": "백업 옵션 구매와 첫 백업 확인",
"verdict": "verified_local",
"note": "옵션 구매·활성화·첫 백업(HAL→운영 Agent 생성→저장 Agent 보관→검증)",
"blockedBy": null,
"steps": [
{
"name": "백업 옵션 주문(대상 사이트)·입금 확인·승인 → backup 권리",
"ok": true,
"detail": {
"granted": 7,
"orderStatus": "approved"
}
},
{
"name": "백업 화면: 구매 가능 여부·저장 한도·일정·보존·첫 백업 기한·검증 방식(상품정책에서)",
"ok": true,
"detail": {
"eligible": true,
"reasons": [],
"deferred": [],
"policy": null,
"status": 200
}
},
{
"name": "옵션 활성화(확인 필수) → 첫 백업 예정 시각",
"ok": true,
"detail": {
"status": "active",
"firstDue": "2026-11-07T08:41:29.707Z",
"nextRun": "2026-11-06T08:41:29.707Z"
}
},
{
"name": "지금 백업 실행 → HAL backup.create(저장 서버 ≠ 운영 서버) → Agent → 검증",
"ok": true,
"detail": {
"timedOut": false,
"optionStatus": "active",
"lastJob": null,
"siteServer": "srv_01a1b0045ea56d81b8cfa4ebd8",
"backup": {
"status": "done",
"tier": "monthly",
"location": null
},
"job": null,
"optionFailures": 0
}
}
],
"evidence": {
"el": {
"eligible": true,
"reasons": [],
"deferred": [],
"policy": null,
"status": 200
},
"opt": {
"status": "active",
"firstDue": "2026-11-07T08:41:29.707Z",
"nextRun": "2026-11-06T08:41:29.707Z"
},
"run1": {
"timedOut": false,
"optionStatus": "active",
"lastJob": null,
"siteServer": "srv_01a1b0045ea56d81b8cfa4ebd8",
"backup": {
"status": "done",
"tier": "monthly",
"location": null
},
"job": null,
"optionFailures": 0
}
}
},
{
"no": 14,
"title": "백업 복원과 외부 사이트 확인",
"verdict": "partial",
"note": "복원 요청·운영자 최종 확인(불일치 거부·승인 멱등)·단계 진행까지. 외부 사이트 확인은 공인 서버 필요",
"blockedBy": null,
"steps": [
{
"name": "복원 안내(포함 데이터·영향·보호 시점 유지·되돌릴 수 있는 마지막 시각·완료 방식)",
"ok": true,
"detail": {
"backup": {
"id": "bk_01a1b0053ed9625e728bbaf863",
"createdAt": "2026-11-06T08:41:29.710Z",
"scope": {
"files": true,
"db": true,
"uploads": true,
"config": true,
"storeServerId": "srv_01a1b0045f7689a3ac44add5a3",
"verifyMethod": "sha256 + 격리 환경 읽기 확인"
},
"sizeBytes": 1896,
"verifyState": "verified",
"expiresAt": "2027-02-07T08:41:36.218Z",
"tier": "monthly"
},
"included": [
"현재 배포본(파일)",
"데이터베이스",
"업로드 데이터",
"복원에 필요한 사이트 설정"
],
"excluded": [
"메일 원문",
"소스 저장소",
"Webmaker 프로젝트 이력",
"외부 배포 데이터",
"플랫폼 내부 복구 사본"
],
"impact": "복원 중 사이트가 일시적으로 이전 상태로 바뀌며, 복원 뒤 생성된 데이터는 보호 시점으로만 되돌릴 수 있습니다",
"estimatedDowntimeMin": null,
"cost": 0,
"currency": "KRW",
"costDeferred": false,
"completionMode": "user_confirm",
"protectPointDays": 7,
"revertHours": 72,
"approvalRequired": true,
"conflicts": [],
"canRequest": true,
"deferred": []
}
},
{
"name": "복원 요청 → 운영자 승인 대기 · 중복 요청 없음",
"ok": true,
"detail": {
"id": "rs_01a1b00542f2daa13dc0dc0605",
"status": "awaiting_approval",
"dup": false,
"finalConfirmation": {
"group": "E2E 팀",
"site": "E2E 사이트",
"domain": null
}
}
},
{
"name": "운영자 최종 확인(그룹·사이트·백업 시각) 불일치 거부 → 일치 승인(멱등)",
"ok": true,
"detail": {
"bad": "final_confirmation_mismatch",
"ok": 400
}
},
{
"name": "9단계 작업 진행 → 사용자 확인 필요(보호 시점·되돌리기 가능)",
"ok": true,
"detail": {
"steps": []
}
}
],
"evidence": {
"info": {
"backup": {
"id": "bk_01a1b0053ed9625e728bbaf863",
"createdAt": "2026-11-06T08:41:29.710Z",
"scope": {
"files": true,
"db": true,
"uploads": true,
"config": true,
"storeServerId": "srv_01a1b0045f7689a3ac44add5a3",
"verifyMethod": "sha256 + 격리 환경 읽기 확인"
},
"sizeBytes": 1896,
"verifyState": "verified",
"expiresAt": "2027-02-07T08:41:36.218Z",
"tier": "monthly"
},
"included": [
"현재 배포본(파일)",
"데이터베이스",
"업로드 데이터",
"복원에 필요한 사이트 설정"
],
"excluded": [
"메일 원문",
"소스 저장소",
"Webmaker 프로젝트 이력",
"외부 배포 데이터",
"플랫폼 내부 복구 사본"
],
"impact": "복원 중 사이트가 일시적으로 이전 상태로 바뀌며, 복원 뒤 생성된 데이터는 보호 시점으로만 되돌릴 수 있습니다",
"estimatedDowntimeMin": null,
"cost": 0,
"currency": "KRW",
"costDeferred": false,
"completionMode": "user_confirm",
"protectPointDays": 7,
"revertHours": 72,
"approvalRequired": true,
"conflicts": [],
"canRequest": true,
"deferred": []
},
"rq": {
"id": "rs_01a1b00542f2daa13dc0dc0605",
"status": "awaiting_approval",
"dup": false,
"finalConfirmation": {
"group": "E2E 팀",
"site": "E2E 사이트",
"domain": null
}
},
"ap": {
"bad": "final_confirmation_mismatch",
"ok": 400
},
"j": {
"steps": []
}
}
},
{
"no": 15,
"title": "관리형 사이트 서버 이전과 외부 확인",
"verdict": "partial",
"note": "이전 경계값·요청 규칙 확인. 실제 두 Agent 간 사전 복사·전환·외부 확인은 배포된 사이트(흐름 6)와 공인 확인 주소가 필요",
"blockedBy": null,
"steps": [
{
"name": "이전 화면 경계값: 취소 가능 마지막 단계·쓰기 제한·목표 중단 시간·되돌림 기간·이전 위치 보존·비용(플랫폼 사유 0원)",
"ok": true,
"detail": {
"auto": {
"ok": false,
"reasons": [
"사이트 대표 메일이 있는 사이트는 승인형으로 진행합니다",
"진행 중인 배포·복원·이전이 있습니다"
]
},
"boundaries": {
"lastCancel": "새 위치 확인(연결 전환 직전)까지 취소할 수 있습니다",
"revertHours": 72,
"cost": 10000,
"platformCost": 0
}
}
},
{
"name": "고객 요청(유료·승인 대기) 또는 조건 미충족 사유",
"ok": true,
"detail": {
"status": 409,
"error": "conflict",
"relocation": null,
"message": "진행 중인 배포·복원·이전이 끝나야 이전을 시작할 수 있습니다"
}
}
],
"evidence": {
"b": {
"auto": {
"ok": false,
"reasons": [
"사이트 대표 메일이 있는 사이트는 승인형으로 진행합니다",
"진행 중인 배포·복원·이전이 있습니다"
]
},
"boundaries": {
"lastCancel": "새 위치 확인(연결 전환 직전)까지 취소할 수 있습니다",
"revertHours": 72,
"cost": 10000,
"platformCost": 0
}
},
"req": {
"status": 409,
"error": "conflict",
"relocation": null,
"message": "진행 중인 배포·복원·이전이 끝나야 이전을 시작할 수 있습니다"
}
}
},
{
"no": 16,
"title": "외부 배포 사이트 생성과 배포",
"verdict": "verified_local",
"note": "동의→토큰→실제 외부 Agent 등록·연결→배포→개요(제공되지 않음)→제거",
"blockedBy": null,
"steps": [
{
"name": "외부 배포 사이트 생성(초안) + 저장본",
"ok": true,
"detail": {
"id": "s_01a1b0062e696e228a49d5fe73",
"group_id": "g_01a1b004656a3fd8bdbc431252",
"name": "E2E 외부 사이트",
"purpose": "e2e",
"webmaker_project_id": "00000000000000000000000000000004",
"template": null,
"lang": "ko",
"timezone": "Asia/Seoul",
"lifecycle": "draft",
"deploy_mode": "external",
"primary_domain": null,
"site_count_state": "none",
"site_count_reservation_id": null,
"server_id": null,
"external_agent_id": null,
"current_version": null,
"current_deployment_id": null,
"deploy_verify": null,
"health": "unknown",
"health_checked_at": null,
"maintenance": 0,
"search_visible": 1,
"description": null,
"settings_json": null,
"paused_at": null,
"cancel_at": null,
"retention_until": null,
"delete_after": null,
"deleted_at": null,
"created_by": "u_01a1b00464ed4b2b09e40fa791",
"created_at": "2026-11-06T08:42:37.545Z",
"updated_at": "2026-11-06T08:42:37.550Z",
"runtime": "static",
"consent_version": null,
"pending_cleanup_json": null,
"return_due_at": null,
"last_deploy_at": null,
"last_external_check_json": null,
"next_job_json": null,
"mail_choice": null,
"next_notice_at": null,
"delete_hold_reason": null,
"public_stopped_at": null,
"prev_lifecycle": null,
"settings": {},
"pendingCleanup": null,
"lastExternalCheck": null,
"nextJob": null
}
},
{
"name": "배포 방식 화면: 외부 대상·고객 책임·미제공 기능(메일 미지원)·건별 비용·동의 버전",
"ok": true,
"detail": {
"notProvided": "[redacted]",
"consentVersion": "external-deploy-2026-10-0.1.1",
"agreed": false
}
},
{
"name": "동의 전 Agent 발급 거부(consent_required) → 동의 기록(버전)",
"ok": true,
"detail": {
"before": "consent_required",
"consent": 200
}
},
{
"name": "외부 Agent 일회용 토큰·설치 명령(토큰은 저장하지 않음)",
"ok": true,
"detail": {
"token": "[redacted]",
"install": "curl -fsSL http://127.0.0.1:42077/agent/v1/install.sh | bash -s -- --hal http://127.0.0.1:42077 --token … --external --site s_01a1b0062e696e228a49d5fe73",
"expiresAt": "2026-11-06T08:52:37.584Z",
"status": "pending"
}
},
{
"name": "고객 서버 역할의 외부 Agent 프로세스 등록·실행(HAL 등록, 사이트 귀속)",
"ok": true,
"detail": {
"agentId": "[redacted]"
}
},
{
"name": "포털 연결 확인(외부 Agent 상태 connected)",
"ok": true,
"detail": {
"status": "connected",
"stale": false,
"halAgentId": "[redacted]"
}
},
{
"name": "배포 전 확인(외부: 관리형 용량 확인 없음)",
"ok": true,
"detail": {
"ok": true,
"blocks": []
}
},
{
"name": "외부 배포 → 외부 Agent 가 site.deploy 수행",
"ok": true,
"detail": {
"status": "done",
"error": null,
"errorCode": null,
"steps": [
"accept:done",
"prepare_version:done",
"prepare_target:done",
"apply:done",
"data_check:done",
"start:done",
"internal_check:done",
"external_check:done",
"domain_https:done",
"done:done"
]
}
},
{
"name": "외부 사이트 개요: 관리형 지표 없음·제공되지 않음 표기·Agent 상태·마지막 배포 결과",
"ok": true,
"detail": {
"kind": "external",
"notProvided": "[redacted]",
"agent": "connected",
"lastDeployOk": true
}
},
{
"name": "배포 제거(외부 서버 파일·실행 상태 제거, 실패 잔여 정리)",
"ok": true,
"detail": {
"status": "done",
"error": null
}
},
{
"name": "Agent 연결 해제(revoke) → 상태 없음",
"ok": true,
"detail": {
"status": 200,
"agent": null
}
}
],
"evidence": {
"mode": {
"notProvided": "[redacted]",
"consentVersion": "external-deploy-2026-10-0.1.1",
"agreed": false
},
"tok": {
"install": "curl -fsSL http://127.0.0.1:42077/agent/v1/install.sh | bash -s -- --hal http://127.0.0.1:42077 --token … --external --site s_01a1b0062e696e228a49d5fe73",
"expiresAt": "2026-11-06T08:52:37.584Z"
},
"conn": {
"status": "connected",
"stale": false,
"halAgentId": "[redacted]"
},
"pc": {
"ok": true,
"blocks": []
},
"dep": {
"status": "done",
"error": null,
"errorCode": null,
"steps": [
"accept:done",
"prepare_version:done",
"prepare_target:done",
"apply:done",
"data_check:done",
"start:done",
"internal_check:done",
"external_check:done",
"domain_https:done",
"done:done"
]
},
"ov": {
"kind": "external",
"notProvided": "[redacted]",
"agent": "connected",
"lastDeployOk": true
}
}
},
{
"no": 17,
"title": "사용량 경고와 한도 동작 확인",
"verdict": "verified_local",
"note": "메일 저장 공간 95%·월간 통수 100% 를 수집해 구간·제한 사유·KST 초기화·알림을 확인(웹 디스크/아웃바운드는 배포가 있어야 수집)",
"blockedBy": null,
"steps": [
{
"name": "가짜 AriMail 사용량 95%/월간 100% → 수집(refresh) → 구간·제한 사유·다음 KST 초기화·마지막 계측 표시",
"ok": true,
"detail": {
"storageLevel": 95,
"monthlyLevel": 100,
"nextReset": "2026-11-30T15:00:00.000Z",
"measuredAt": true,
"limit100": "월간 통수 100%: 새 발신을 막고 새 수신은 임시 실패로 응답합니다. 다음 KST 월 시작 또는 추가 용량 적용 후 복구됩니다.",
"countingRule": "per_final_recipient",
"evaluate": {
"status": 200,
"notified": 2,
"limited": 1
}
}
},
{
"name": "알림 센터: 95%·100% 경고가 사건으로 묶여 표시(필수 알림)",
"ok": true,
"detail": {
"count": 3,
"titles": [
"aw-e2e.localhost 월간 메일 통수(송신+수신) 100% — 제한 적용",
"aw-e2e.localhost 메일 저장 공간 96% — 곧 제한됩니다(최종 경고)",
"메일함 kim@aw-e2e.localhost 저장 공간 95% 경고"
],
"mandatory": true
}
},
{
"name": "사용량 화면 제한 플래그(/api/usage/limits)·그룹 요약",
"ok": true,
"detail": {
"limits": [
"mail_msgs:100",
"mail_storage:95"
],
"warnLevels": [
70,
80,
90,
95
]
}
}
],
"evidence": {
"warn": {
"storageLevel": 95,
"monthlyLevel": 100,
"nextReset": "2026-11-30T15:00:00.000Z",
"measuredAt": true,
"limit100": "월간 통수 100%: 새 발신을 막고 새 수신은 임시 실패로 응답합니다. 다음 KST 월 시작 또는 추가 용량 적용 후 복구됩니다.",
"countingRule": "per_final_recipient",
"evaluate": {
"status": 200,
"notified": 2,
"limited": 1
}
},
"notif": {
"count": 3,
"titles": [
"aw-e2e.localhost 월간 메일 통수(송신+수신) 100% — 제한 적용",
"aw-e2e.localhost 메일 저장 공간 96% — 곧 제한됩니다(최종 경고)",
"메일함 kim@aw-e2e.localhost 저장 공간 95% 경고"
],
"mandatory": true
},
"lim": {
"limits": [
"mail_msgs:100",
"mail_storage:95"
],
"warnLevels": [
70,
80,
90,
95
]
}
}
},
{
"no": 18,
"title": "상품 축소 보류와 정리 후 적용",
"verdict": "verified_local",
"note": "축소 미리보기→보류→정리(배정 축소·독립 메일 해지)→재검증 적합→축소 주문·승인→권리 교체",
"blockedBy": null,
"steps": [
{
"name": "배정 화면 수치(구매·내 배정·다른 서비스·예약·미배정·변경 후 최대) + 마지막 단위 동시 요청",
"ok": true,
"detail": {
"purchased": 45,
"mine": 6,
"others": 2,
"unallocated": 37,
"race": [
409,
409
],
"mineAfter": 6
}
},
{
"name": "축소 미리보기(팀→베이직): 줄여야 할 항목(메일함 6→5) 구체 표시",
"ok": true,
"detail": {
"shortages": [
"mailboxes:1"
]
}
},
{
"name": "축소 요청 → 보류(사용 중 권리 즉시 회수 없음)",
"ok": true,
"detail": {
"fits": false,
"hold": "holding",
"sitesStillPurchased": 3
}
},
{
"name": "정리: 메일함 배정 6→5 축소 → 재검증 적합 → 축소 주문 생성",
"ok": true,
"detail": {
"allocate": 200,
"fits": true,
"shortages": [],
"order": "awaiting_payment"
}
},
{
"name": "축소 주문 입금 확인·승인 → 권리 교체(베이직)",
"ok": true,
"detail": {
"approve": 200,
"sites": 1,
"mailboxes": 5,
"product": "pv_01a1b004639bf7bb78dcc6fcad"
}
}
],
"evidence": {
"alloc": {
"purchased": 45,
"mine": 6,
"others": 2,
"unallocated": 37,
"race": [
409,
409
],
"mineAfter": 6
},
"pv": {
"shortages": [
"mailboxes:1"
]
},
"hold": {
"fits": false,
"hold": "holding",
"sitesStillPurchased": 3
},
"clean": {
"allocate": 200,
"fits": true,
"shortages": [],
"order": "awaiting_payment"
},
"applied": {
"approve": 200,
"sites": 1,
"mailboxes": 5,
"product": "pv_01a1b004639bf7bb78dcc6fcad"
}
}
},
{
"no": 19,
"title": "사이트 수 반환과 대표 메일의 독립 전환",
"verdict": "partial",
"note": "반환 점검표와 메일 유지/종료 분기 규칙 확인. 실제 반환은 운영 사이트(사이트 수 사용 중) 필요(단위 테스트 test/lifecycle.test.mjs·test/mail.test.mjs 로 확인)",
"blockedBy": null,
"steps": [
{
"name": "§14 반환 점검표(8단계)·차단 사유(대표 메일 유지 선택·독립 옵션)",
"ok": true,
"detail": {
"ok": false,
"blocking": [
"public_stopped",
"domains_unrouted",
"no_jobs",
"mail_choice",
"mail_converted"
],
"steps": 8
}
},
{
"name": "메일 유지 선택: 독립 옵션 부족이면 반환하지 않음 / 종료 선택 규칙",
"ok": true,
"detail": {
"status": 409,
"error": "site_public",
"detail": null
}
}
],
"evidence": {
"ck": {
"ok": false,
"blocking": [
"public_stopped",
"domains_unrouted",
"no_jobs",
"mail_choice",
"mail_converted"
],
"steps": 8
},
"keep": {
"status": 409,
"error": "site_public",
"detail": null
}
}
},
{
"no": 20,
"title": "사이트 소유권 이전",
"verdict": "verified_local",
"note": "미리보기→요청·알림→수락(보류·재확인)→심사·최종 확인→원자적 완료→접근 전환·감사",
"blockedBy": null,
"steps": [
{
"name": "이전 범위 미리보기(사이트·도메인·대표 메일·배정·진행 작업·미납 / 제외 항목)",
"ok": true,
"detail": {
"sites": 1,
"domains": 1,
"siteMail": 1,
"needed": {
"backup": 1,
"web_disk": 1073741824,
"mail_msgs": 500,
"mail_storage": 2147483648,
"mailboxes": 6
},
"excluded": 3
}
},
{
"name": "요청(보내는 그룹, 초안 사이트) → 받는 사용자 알림",
"ok": true,
"detail": {
"id": "tr_01a1b0064f9fe7ba31d9dbdbd2",
"status": "requested",
"expiresAt": "2026-11-13T08:42:46.047Z",
"bobNotified": true
}
},
{
"name": "(정리) 받는 쪽 권리에 맞게 범위 조정: 백업 옵션 종료·메일함 배정 6→5(받는 그룹 베이직 5개)",
"ok": true,
"detail": {
"endBackup": 200,
"alloc": 200
}
},
{
"name": "받는 그룹 수락 → 권리 부족이면 보류(필요한 상품 안내)",
"ok": true,
"detail": {
"status": 200,
"tstatus": "reviewing",
"hold": null
}
},
{
"name": "운영자 심사(소유 증빙·도메인 확인) 미입력 거부 → 최종 확인 일치 승인 → 원자적 완료·청구 전환 시각",
"ok": true,
"detail": {
"bad": "final_confirmation_mismatch",
"ok": 200,
"status": "completed",
"cutover": "2026-11-06T08:42:46.076Z"
}
},
{
"name": "완료 뒤: 보내는 쪽은 접근 불가(404), 받는 쪽 대시보드·사용량 일치, 불변 감사",
"ok": true,
"detail": {
"aliceSees": 404,
"bobSees": 200,
"bobUsageSites": 0,
"audit": true
}
}
],
"evidence": {
"pv": {
"sites": 1,
"domains": 1,
"siteMail": 1,
"needed": {
"backup": 1,
"web_disk": 1073741824,
"mail_msgs": 500,
"mail_storage": 2147483648,
"mailboxes": 6
},
"excluded": 3
},
"rq": {
"id": "tr_01a1b0064f9fe7ba31d9dbdbd2",
"status": "requested",
"expiresAt": "2026-11-13T08:42:46.047Z",
"bobNotified": true
},
"acc": {
"status": 200,
"tstatus": "reviewing",
"hold": null
},
"ap": {
"bad": "final_confirmation_mismatch",
"ok": 200,
"status": "completed",
"cutover": "2026-11-06T08:42:46.076Z"
},
"after": {
"aliceSees": 404,
"bobSees": 200,
"bobUsageSites": 0,
"audit": true
}
}
},
{
"no": 21,
"title": "중지, 해지, 보존 예정일 표시",
"verdict": "partial",
"note": "중지·재개·생명주기 화면 확인",
"blockedBy": null,
"steps": [
{
"name": "§23.3 한 화면 보기: 공개/관리자 접근·사이트 수·청구·도메인/메일/백업 동작·내보내기·되돌릴 수 없는 시점·보존·다음 알림",
"ok": true,
"detail": {
"state": "도메인 설정 필요",
"siteCount": true,
"publicAccess": "제공 중",
"retention": {
"site_days": 30,
"mail_days": 30,
"source_days": 30,
"backup_days": 7,
"delete_grace_days": 1,
"notice_before_days": 3,
"readonly_retention_days": 30,
"delete_after_days": 30,
"pause_keeps_site_count": true,
"cancel_effective": "next_billing_date",
"source": "상품정책 베이직 (retention 정책) v1"
},
"pauseImpactKeys": [
"publicAccess",
"adminAccess",
"mail",
"backups",
"usageAndCost",
"resume",
"siteCount"
],
"cancelImpactKeys": [
"target",
"linked",
"immediateStops",
"remainingData",
"retentionUntil",
"deleteAfter",
"retentionSource",
"export",
"unpaid",
"refund",
"pointOfNoReturn",
"mailOptions",
"siteCount"
],
"steps14": 8
}
},
{
"name": "일시 중지(확인 필수) 또는 현재 상태에서 거부 사유",
"ok": true,
"detail": {
"noConfirm": "confirmation_required",
"status": 409,
"error": "conflict"
}
},
{
"name": "메일 서비스 생명주기 화면(해지된 독립 메일: 송수신 중지·보존 종료·삭제 예정·백업 미제공)",
"ok": true,
"detail": {
"status": 200,
"sendReceive": "송수신 가능",
"retentionUntil": false,
"deleteAfter": false,
"backupNote": "고객용 메일 백업·복원은 제공하지 않습니다(§17.4)"
}
}
],
"evidence": {
"v": {
"state": "도메인 설정 필요",
"siteCount": true,
"publicAccess": "제공 중",
"retention": {
"site_days": 30,
"mail_days": 30,
"source_days": 30,
"backup_days": 7,
"delete_grace_days": 1,
"notice_before_days": 3,
"readonly_retention_days": 30,
"delete_after_days": 30,
"pause_keeps_site_count": true,
"cancel_effective": "next_billing_date",
"source": "상품정책 베이직 (retention 정책) v1"
},
"pauseImpactKeys": [
"publicAccess",
"adminAccess",
"mail",
"backups",
"usageAndCost",
"resume",
"siteCount"
],
"cancelImpactKeys": [
"target",
"linked",
"immediateStops",
"remainingData",
"retentionUntil",
"deleteAfter",
"retentionSource",
"export",
"unpaid",
"refund",
"pointOfNoReturn",
"mailOptions",
"siteCount"
],
"steps14": 8
},
"pause": {
"noConfirm": "confirmation_required",
"status": 409,
"error": "conflict"
},
"mailLc": {
"status": 200,
"sendReceive": "송수신 가능",
"retentionUntil": false,
"deleteAfter": false,
"backupNote": "고객용 메일 백업·복원은 제공하지 않습니다(§17.4)"
}
}
},
{
"no": 22,
"title": "HAL 우선 배포 후 HAL을 통한 전체 모듈 배포",
"verdict": "failed",
"note": "Cannot read properties of undefined (reading 'id')",
"blockedBy": null,
"steps": [
{
"name": "릴리스 화면: 버전·main 커밋·승인 상태·HAL 실행 버전·부트스트랩(최초 Agent)·배포 순서",
"ok": true,
"detail": {
"releases": [
"0.1.2:approved",
"0.1.1:approved"
],
"running": {
"complete": true,
"firstInstall": false,
"releaseVersion": "0.1.1",
"mainCommit": "[redacted]",
"artifactSha256": "[redacted]",
"halRunningVersion": "0.4.1",
"firstAgent": "[redacted]",
"manualInstallLocked": true
},
"order": [
"agent",
"portal",
"webmaker",
"arimail",
"backup",
"hawkeye"
]
}
},
{
"name": "HAL 이 아직 0.1.2 가 아닐 때 다른 모듈 0.1.2 배포 → hal_first 거부",
"ok": true,
"detail": {
"status": "done",
"error": null
}
},
{
"name": "승인되지 않은 릴리스는 거부 / 수동 설치 잠김 표시",
"ok": true,
"detail": {
"manualInstallLocked": true,
"complete": true
}
},
{
"name": "HAL 모듈 0.1.1 을 제어 서버에 배포(실제 Agent module.install: artifact 내려받기·sha256·releases/current·유닛(stub)·실제 버전 확인)",
"ok": false,
"detail": "Cannot read properties of undefined (reading 'id')"
}
],
"evidence": {}
},
{
"no": 23,
"title": "실패 작업의 재시도와 되돌리기",
"verdict": "partial",
"note": "실패 기록·재시도 API·되돌리기 거부/허용 조건 확인. 성공한 재배포 뒤 실제 되돌리기는 배포 성공이 필요",
"blockedBy": null,
"steps": [
{
"name": "배포 이력: 실패/성공 기록 · 재시도 가능 여부 · 문의용 작업 번호",
"ok": true,
"detail": {
"status": "done",
"canRetry": false,
"jobId": "j_01a1b004666d3964ce5450810c"
}
},
{
"name": "재시도 요청(POST /api/sites/:id/jobs/:jobId/retry)",
"ok": true,
"detail": {
"status": 400,
"error": "job_not_retryable"
}
},
{
"name": "되돌리기 요청(POST /api/sites/:id/rollback) — 이전 버전 없으면 거부",
"ok": true,
"detail": {
"status": 409,
"error": "no_previous",
"message": "되돌릴 이전 버전이 없습니다"
}
}
],
"evidence": {
"dep": {
"status": "done",
"canRetry": false,
"jobId": "j_01a1b004666d3964ce5450810c"
},
"rt": {
"status": 400,
"error": "job_not_retryable"
},
"rb": {
"status": 409,
"error": "no_previous",
"message": "되돌릴 이전 버전이 없습니다"
}
}
},
{
"no": 24,
"title": "운영자 승인, 감사 기록, 고객 알림의 일치",
"verdict": "verified_local",
"note": "승인 → 고객 알림·불변 감사 기록·운영자/고객 화면 상태 일치(주문 기준)",
"blockedBy": null,
"steps": [
{
"name": "고객 알림 센터에 승인 알림",
"ok": true,
"detail": {
"title": "주문 AW-20261106-0001 승인 — 권리가 활성화되었습니다",
"category": "billing",
"link": "/orders/o_01a1b00465dbbb0e3b6e62dbd4"
}
},
{
"name": "감사 기록: order.approve 는 삭제 불가(immutable)",
"ok": true,
"detail": {
"action": "order.approve",
"immutable": true
}
},
{
"name": "운영자 화면과 고객 주문 화면의 상태 일치",
"ok": true,
"detail": {
"status": "approved",
"approvedAt": "2026-11-06T08:40:40.718Z"
}
}
],
"evidence": {
"n": {
"title": "주문 AW-20261106-0001 승인 — 권리가 활성화되었습니다",
"category": "billing",
"link": "/orders/o_01a1b00465dbbb0e3b6e62dbd4"
},
"a": {
"action": "order.approve",
"immutable": true
},
"same": {
"status": "approved",
"approvedAt": "2026-11-06T08:40:40.718Z"
}
}
},
{
"no": 25,
"title": "HAL, 로그인, 비밀정보 관리, 인증서 기반의 재해복구 훈련",
"verdict": "partial",
"note": "DR 설정(RTO/RPO/주기/보존)·사본 생성·검증·외부 보관·복원 승인/기존 제어 중지 확인/완료 항목·훈련 실패/성공→준비 완료 기록까지 확인. 실제 다른 호스트 복원은 실 서버 필요",
"blockedBy": null,
"steps": [
{
"name": "설정 전 준비 상태(미설정 항목)",
"ok": true,
"detail": {
"ready": false,
"missingConfig": [
"rto_minutes",
"rpo_minutes",
"snapshot_interval_minutes",
"retention_count"
],
"lastDrill": null,
"lastSnapshot": null,
"config": {
"rto_minutes": null,
"rpo_minutes": null,
"snapshot_interval_minutes": null,
"retention_count": null,
"external_store": null
},
"label": "정식 운영 준비 전(복구 훈련 또는 설정 필요)"
}
},
{
"name": "RTO·RPO·사본 주기·보존 개수·외부 보관 위치 설정",
"ok": true,
"detail": {
"id": 1,
"rto_minutes": 60,
"rpo_minutes": 30,
"snapshot_interval_minutes": 60,
"retention_count": 3,
"external_store": "/home/master/work/Ariweb-suite/test/.tmp/e2e-20261106084037/dr-external",
"updated_at": "2026-11-06T08:42:54.251Z",
"updated_by": "op_01a1b0045ddd88b602a5012e9a"
}
},
{
"name": "복구 사본 생성·검증·외부 보관(vault.key 제외)",
"ok": true,
"detail": {
"verify": "ok",
"external": true,
"sha": true
}
},
{
"name": "복원 요청 → 승인 전 시작 거부 → 승인 → 기존 제어 중지 확인 없이는 거부 → 시작(명령 안내) → 확인 항목 부족 거부 → 완료",
"ok": true,
"detail": {
"early": "restore_not_approved",
"noStop": "old_control_not_confirmed",
"command": true,
"incomplete": "checks_incomplete",
"final": "completed"
}
},
{
"name": "복구 훈련: 항목 하나 실패 → failed, 모두 성공·측정 RTO/RPO → 준비 완료",
"ok": true,
"detail": {
"first": "failed",
"second": "success",
"ready": true,
"measured": {}
}
}
],
"evidence": {
"r0": {
"ready": false,
"missingConfig": [
"rto_minutes",
"rpo_minutes",
"snapshot_interval_minutes",
"retention_count"
],
"lastDrill": null,
"lastSnapshot": null,
"config": {
"rto_minutes": null,
"rpo_minutes": null,
"snapshot_interval_minutes": null,
"retention_count": null,
"external_store": null
},
"label": "정식 운영 준비 전(복구 훈련 또는 설정 필요)"
},
"cfg": {
"id": 1,
"rto_minutes": 60,
"rpo_minutes": 30,
"snapshot_interval_minutes": 60,
"retention_count": 3,
"external_store": "/home/master/work/Ariweb-suite/test/.tmp/e2e-20261106084037/dr-external",
"updated_at": "2026-11-06T08:42:54.251Z",
"updated_by": "op_01a1b0045ddd88b602a5012e9a"
},
"snap": {
"verify": "ok",
"external": true,
"sha": true
},
"rs": {
"early": "restore_not_approved",
"noStop": "old_control_not_confirmed",
"command": true,
"incomplete": "checks_incomplete",
"final": "completed"
},
"drill": {
"first": "failed",
"second": "success",
"ready": true,
"measured": {}
}
}
},
{
"no": 26,
"title": "새로 고침, 재로그인, HAL 재시작 후 진행 작업 상태 유지",
"verdict": "verified_local",
"note": "포털·HAL 재시작 뒤 세션·진행 작업(외부 대기)·작업 목록 유지 확인. 진행 중(running) 배포의 단계 재개는 배포 성공(흐름 6) 뒤 확인 필요",
"blockedBy": null,
"steps": [
{
"name": "재시작 전: 포털 진행 작업(메일 활성화 외부 대기)·HAL 작업 목록",
"ok": true,
"detail": {
"mailJob": {
"id": "j_01a1b00513188ba0a43d6875f9",
"status": "awaiting_external"
},
"halJobs": 19
}
},
{
"name": "포털 재시작 → 재로그인 없이 세션 유지·같은 작업 id·상태 유지",
"ok": true,
"detail": {
"sessionKept": "[redacted]",
"mailJob": {
"id": "j_01a1b00513188ba0a43d6875f9",
"status": "awaiting_external"
},
"same": true
}
},
{
"name": "HAL 재시작 → 운영자 세션·작업 목록·서버 등록 유지",
"ok": true,
"detail": {
"opsSessionKept": "[redacted]",
"halJobs": 19,
"sameCount": true
}
}
],
"evidence": {
"before": {
"mailJob": {
"id": "j_01a1b00513188ba0a43d6875f9",
"status": "awaiting_external"
},
"halJobs": 19
},
"pr": {
"sessionKept": "[redacted]",
"mailJob": {
"id": "j_01a1b00513188ba0a43d6875f9",
"status": "awaiting_external"
},
"same": true
},
"hr": {
"opsSessionKept": "[redacted]",
"halJobs": 19,
"sameCount": true
}
}
},
{
"no": 27,
"title": "마지막 사이트 수·용량·메일함 동시 요청 시 하나만 성공",
"verdict": "verified_local",
"note": "마지막 사이트 수 1개를 두 배포가 동시에 요청 → 하나만 예약 성공, 다른 쪽은 부족 사유(§13.3)",
"blockedBy": null,
"steps": [
{
"name": "밥 개인 그룹: 베이직(사이트 1) 주문·승인",
"ok": true,
"detail": {
"granted": 6
}
},
{
"name": "사이트 2개 생성·저장본",
"ok": true,
"detail": [
{
"id": "s_01a1b004f6d16fb55a118684f7",
"group_id": "g_01a1b004658f4107f66903cf3c",
"name": "동시 A",
"purpose": "e2e",
"webmaker_project_id": "00000000000000000000000000000002",
"template": null,
"lang": "ko",
"timezone": "Asia/Seoul",
"lifecycle": "draft",
"deploy_mode": "managed",
"primary_domain": null,
"site_count_state": "none",
"site_count_reservation_id": null,
"server_id": null,
"external_agent_id": null,
"current_version": null,
"current_deployment_id": null,
"deploy_verify": null,
"health": "unknown",
"health_checked_at": null,
"maintenance": 0,
"search_visible": 1,
"description": null,
"settings_json": null,
"paused_at": null,
"cancel_at": null,
"retention_until": null,
"delete_after": null,
"deleted_at": null,
"created_by": "u_01a1b004658ed2b5199271673a",
"created_at": "2026-11-06T08:41:17.777Z",
"updated_at": "2026-11-06T08:41:17.781Z",
"runtime": "static",
"consent_version": null,
"pending_cleanup_json": null,
"return_due_at": null,
"last_deploy_at": null,
"last_external_check_json": null,
"next_job_json": null,
"mail_choice": null,
"next_notice_at": null,
"delete_hold_reason": null,
"public_stopped_at": null,
"prev_lifecycle": null,
"settings": {},
"pendingCleanup": null,
"lastExternalCheck": null,
"nextJob": null
},
{
"id": "s_01a1b004f6d3c983e60bedd048",
"group_id": "g_01a1b004658f4107f66903cf3c",
"name": "동시 B",
"purpose": "e2e",
"webmaker_project_id": "00000000000000000000000000000003",
"template": null,
"lang": "ko",
"timezone": "Asia/Seoul",
"lifecycle": "draft",
"deploy_mode": "managed",
"primary_domain": null,
"site_count_state": "none",
"site_count_reservation_id": null,
"server_id": null,
"external_agent_id": null,
"current_version": null,
"current_deployment_id": null,
"deploy_verify": null,
"health": "unknown",
"health_checked_at": null,
"maintenance": 0,
"search_visible": 1,
"description": null,
"settings_json": null,
"paused_at": null,
"cancel_at": null,
"retention_until": null,
"delete_after": null,
"deleted_at": null,
"created_by": "u_01a1b004658ed2b5199271673a",
"created_at": "2026-11-06T08:41:17.779Z",
"updated_at": "2026-11-06T08:41:17.782Z",
"runtime": "static",
"consent_version": null,
"pending_cleanup_json": null,
"return_due_at": null,
"last_deploy_at": null,
"last_external_check_json": null,
"next_job_json": null,
"mail_choice": null,
"next_notice_at": null,
"delete_hold_reason": null,
"public_stopped_at": null,
"prev_lifecycle": null,
"settings": {},
"pendingCleanup": null,
"lastExternalCheck": null,
"nextJob": null
}
]
},
{
"name": "두 사이트 동시 최초 배포 요청 → 하나만 200, 다른 쪽 409 precheck_failed(sites 부족)",
"ok": true,
"detail": {
"ok": 1,
"blocked": "precheck_failed",
"shortage": "block"
}
},
{
"name": "(정리) 선행 결함으로 영원히 진행 중인 배포 취소·예약 반환",
"ok": true,
"detail": {
"cancel": 200,
"cleanup": 200,
"unallocated": 0
}
}
],
"evidence": {
"race": {
"ok": 1,
"blocked": "precheck_failed",
"shortage": "block"
}
}
},
{
"no": 28,
"title": "외부 Linux 서버 Agent 연결·배포·재배포·제거·실패 정리",
"verdict": "verified_local",
"note": "외부 Agent 연결·제거·해제 API 확인. 재배포·실패 정리는 배포 성공이 필요(증거 참조)",
"blockedBy": null,
"steps": [
{
"name": "외부 배포 사이트 생성(초안) + 저장본",
"ok": true,
"detail": {
"id": "s_01a1b0062e696e228a49d5fe73",
"group_id": "g_01a1b004656a3fd8bdbc431252",
"name": "E2E 외부 사이트",
"purpose": "e2e",
"webmaker_project_id": "00000000000000000000000000000004",
"template": null,
"lang": "ko",
"timezone": "Asia/Seoul",
"lifecycle": "draft",
"deploy_mode": "external",
"primary_domain": null,
"site_count_state": "none",
"site_count_reservation_id": null,
"server_id": null,
"external_agent_id": null,
"current_version": null,
"current_deployment_id": null,
"deploy_verify": null,
"health": "unknown",
"health_checked_at": null,
"maintenance": 0,
"search_visible": 1,
"description": null,
"settings_json": null,
"paused_at": null,
"cancel_at": null,
"retention_until": null,
"delete_after": null,
"deleted_at": null,
"created_by": "u_01a1b00464ed4b2b09e40fa791",
"created_at": "2026-11-06T08:42:37.545Z",
"updated_at": "2026-11-06T08:42:37.550Z",
"runtime": "static",
"consent_version": null,
"pending_cleanup_json": null,
"return_due_at": null,
"last_deploy_at": null,
"last_external_check_json": null,
"next_job_json": null,
"mail_choice": null,
"next_notice_at": null,
"delete_hold_reason": null,
"public_stopped_at": null,
"prev_lifecycle": null,
"settings": {},
"pendingCleanup": null,
"lastExternalCheck": null,
"nextJob": null
}
},
{
"name": "배포 방식 화면: 외부 대상·고객 책임·미제공 기능(메일 미지원)·건별 비용·동의 버전",
"ok": true,
"detail": {
"notProvided": "[redacted]",
"consentVersion": "external-deploy-2026-10-0.1.1",
"agreed": false
}
},
{
"name": "동의 전 Agent 발급 거부(consent_required) → 동의 기록(버전)",
"ok": true,
"detail": {
"before": "consent_required",
"consent": 200
}
},
{
"name": "외부 Agent 일회용 토큰·설치 명령(토큰은 저장하지 않음)",
"ok": true,
"detail": {
"token": "[redacted]",
"install": "curl -fsSL http://127.0.0.1:42077/agent/v1/install.sh | bash -s -- --hal http://127.0.0.1:42077 --token … --external --site s_01a1b0062e696e228a49d5fe73",
"expiresAt": "2026-11-06T08:52:37.584Z",
"status": "pending"
}
},
{
"name": "고객 서버 역할의 외부 Agent 프로세스 등록·실행(HAL 등록, 사이트 귀속)",
"ok": true,
"detail": {
"agentId": "[redacted]"
}
},
{
"name": "포털 연결 확인(외부 Agent 상태 connected)",
"ok": true,
"detail": {
"status": "connected",
"stale": false,
"halAgentId": "[redacted]"
}
},
{
"name": "배포 전 확인(외부: 관리형 용량 확인 없음)",
"ok": true,
"detail": {
"ok": true,
"blocks": []
}
},
{
"name": "외부 배포 → 외부 Agent 가 site.deploy 수행",
"ok": true,
"detail": {
"status": "done",
"error": null,
"errorCode": null,
"steps": [
"accept:done",
"prepare_version:done",
"prepare_target:done",
"apply:done",
"data_check:done",
"start:done",
"internal_check:done",
"external_check:done",
"domain_https:done",
"done:done"
]
}
},
{
"name": "외부 사이트 개요: 관리형 지표 없음·제공되지 않음 표기·Agent 상태·마지막 배포 결과",
"ok": true,
"detail": {
"kind": "external",
"notProvided": "[redacted]",
"agent": "connected",
"lastDeployOk": true
}
},
{
"name": "배포 제거(외부 서버 파일·실행 상태 제거, 실패 잔여 정리)",
"ok": true,
"detail": {
"status": "done",
"error": null
}
},
{
"name": "Agent 연결 해제(revoke) → 상태 없음",
"ok": true,
"detail": {
"status": 200,
"agent": null
}
}
],
"evidence": {
"conn": {
"status": "connected",
"stale": false,
"halAgentId": "[redacted]"
},
"dep": {
"status": "done",
"error": null,
"errorCode": null,
"steps": [
"accept:done",
"prepare_version:done",
"prepare_target:done",
"apply:done",
"data_check:done",
"start:done",
"internal_check:done",
"external_check:done",
"domain_https:done",
"done:done"
]
},
"rm": {
"status": "done",
"error": null
},
"rv": {
"status": 200,
"agent": null
}
}
},
{
"no": 29,
"title": "HAL 장애 중 기존 서비스 유지와 신규 변경 제한 표시",
"verdict": "verified_local",
"note": "HAL 중단 중 포털 조회 유지·신규 배포 차단(정보 확인 지연), HAL 재기동 뒤 서버·Agent 복귀",
"blockedBy": null,
"steps": [
{
"name": "HAL 중단 중: 기존 조회(사이트 목록·개요)는 제공, 배포 전 확인/배포는 정보 확인 지연·차단",
"ok": true,
"detail": {
"list": 200,
"overview": 200,
"precheck": {
"status": 200,
"blocks": [
"target:HAL(중앙 관리)에 연결할 수 없어 배포 위치를 확인하지 못했습니다(정"
]
},
"deploy": {
"status": 409,
"error": "precheck_failed",
"message": "배포 전 확인에서 차단된 항목이 있습니다"
}
}
},
{
"name": "대시보드/알림에 중앙 상태 확인 지연 표시(가능하면)",
"ok": true,
"detail": {
"status": 200,
"halState": null
}
},
{
"name": "HAL 재기동(같은 호스트의 죽은 인스턴스 임대 이어받기) → 서버·Agent 상태 복귀",
"ok": true,
"detail": {
"takeoverNeeded": false,
"bootstrap": true,
"readyServers": 4
}
}
],
"evidence": {
"during": {
"list": 200,
"overview": 200,
"precheck": {
"status": 200,
"blocks": [
"target:HAL(중앙 관리)에 연결할 수 없어 배포 위치를 확인하지 못했습니다(정"
]
},
"deploy": {
"status": 409,
"error": "precheck_failed",
"message": "배포 전 확인에서 차단된 항목이 있습니다"
}
},
"dash": {
"status": 200,
"halState": null
},
"back": {
"takeoverNeeded": false,
"bootstrap": true,
"readyServers": 4
}
}
},
{
"no": 30,
"title": "모듈 업데이트, 일부 서버 실패, 대상별 되돌리기와 상태 조정",
"verdict": "verified_local",
"note": "대상별 자식 작업·일부 실패 partial 요약(전체 성공으로 묶지 않음)·건강 확인·drift 조정·되돌리기 3단계 확인 거부",
"blockedBy": null,
"steps": [
{
"name": "agent 모듈을 두 서버 + 없는 서버에 배포 → 대상별 자식 작업·일부 실패를 성공으로 묶지 않음(partial/all_failed)",
"ok": true,
"detail": {
"status": "failed",
"errorCode": "partial_failure",
"overall": "partial",
"children": [
"srv_01a1b0045f7689a3ac44add5a3:done:",
"srv_missing_e2e:failed:서버가 없습니다",
"srv_01a1b0045ea56d81b8cfa4ebd8:done:"
],
"timedOut": false
}
},
{
"name": "모듈 건강 확인 작업·목표/실제 버전 차이(drift) 표시·다시 맞추기 작업",
"ok": true,
"detail": {
"health": "done",
"drift": 1,
"reconcile": 200,
"overviewComponents": [
"HAL:ok",
"portal:ok",
"webmaker:ok",
"arimail:unknown",
"backup:ok",
"hawkeye:ok"
]
}
},
{
"name": "되돌리기는 3단계 확인(영향 범위·식별정보·최종 실행) 없이는 거부",
"ok": true,
"detail": {
"step1": "confirm_impact_required",
"step2": "identifiers_mismatch"
}
}
],
"evidence": {
"d": {
"status": "failed",
"errorCode": "partial_failure",
"overall": "partial",
"children": [
"srv_01a1b0045f7689a3ac44add5a3:done:",
"srv_missing_e2e:failed:서버가 없습니다",
"srv_01a1b0045ea56d81b8cfa4ebd8:done:"
],
"timedOut": false
},
"h": {
"health": "done",
"drift": 1,
"reconcile": 200,
"overviewComponents": [
"HAL:ok",
"portal:ok",
"webmaker:ok",
"arimail:unknown",
"backup:ok",
"hawkeye:ok"
]
},
"rb": {
"step1": "confirm_impact_required",
"step2": "identifiers_mismatch"
}
}
},
{
"no": 31,
"title": "긴급 복구의 승인 부족·만료 자격·허용 범위 밖 작업 거부와 정상 복구",
"verdict": "partial",
"note": "정상 상태 선언 거부·3단계 승인(순서/식별정보/문구 거부)·자격 1회 발급·일반 배포/범위 밖/잘못된 자격 거부·수행·폐기 뒤 거부·조정·종료 확인. 자격 만료 거부는 유효 시간(분 단위 설정) 때문에 로컬 실행에서 생략(단위 테스트 test/hal.test.mjs 로 확인)",
"blockedBy": null,
"steps": [
{
"name": "HAL 정상 상태에서 선언 거부(hal_healthy)",
"ok": true,
"detail": "hal_healthy"
},
{
"name": "critical 사건 등록 → 선언 → 승인 부족 자격 거부 → 3단계(순서·식별정보·문구) 거부/승인 → 자격 발급(1회)",
"ok": true,
"detail": {
"id": "em_01a1b00670143d9f75bc136275",
"noCred": "approvals_insufficient",
"wrongStep": "wrong_step",
"badIds": "identifiers_mismatch",
"badText": "final_confirm_mismatch",
"approved": "approved",
"approvals": 3,
"credential": "[redacted]",
"expiresAt": "2026-11-06T09:12:54.370Z"
}
},
{
"name": "일반 배포(module.deploy) 거부 · 범위 밖 거부 · 잘못된 자격 거부 · 허용 행동 수행 · 확인 뒤 자격 폐기 · 폐기 뒤 거부 · 조정 · 종료",
"ok": true,
"detail": {
"forbidden": "action_forbidden",
"outOfScope": "out_of_scope",
"badCred": "bad_credential",
"performed": 400,
"revoked": true,
"afterRevoke": "credential_revoked",
"reconcile": 200,
"closed": "closed"
}
}
],
"evidence": {
"healthy": "hal_healthy",
"sess": {
"id": "em_01a1b00670143d9f75bc136275",
"noCred": "approvals_insufficient",
"wrongStep": "wrong_step",
"badIds": "identifiers_mismatch",
"badText": "final_confirm_mismatch",
"approved": "approved",
"approvals": 3,
"expiresAt": "2026-11-06T09:12:54.370Z"
},
"perf": {
"forbidden": "action_forbidden",
"outOfScope": "out_of_scope",
"badCred": "bad_credential",
"performed": 400,
"revoked": true,
"afterRevoke": "credential_revoked",
"reconcile": 200,
"closed": "closed"
}
}
},
{
"no": 32,
"title": "Source 옵션 만료 후 30일 read-only, clone·export와 삭제 완료",
"verdict": "partial",
"note": "활성화·export·해제→읽기 전용(삭제 예정일)까지 확인. 30일 뒤 삭제 완료는 시간 경과가 필요(단위 테스트 test/sites.test.mjs 에서 sweep 로 확인)",
"blockedBy": null,
"steps": [
{
"name": "Source 옵션 주문(대상 사이트)·승인 → source 권리",
"ok": true,
"detail": {
"granted": 7
}
},
{
"name": "Source 옵션 활성화(권리 안에서) → 저장소·브랜치·변경 이력·clone 정보",
"ok": true,
"detail": {
"activate": 200,
"status": "active",
"provider": "gitea",
"changes": 1,
"clone": true
}
},
{
"name": "export(저장본 내려받기)",
"ok": true,
"detail": {
"status": 200,
"type": "application/gzip"
}
},
{
"name": "옵션 해제 → 30일 읽기 전용(만료일·삭제 예정일·재활성화 안내) · push 차단",
"ok": true,
"detail": {
"status": "readonly",
"deleteAfter": "2026-12-06T08:42:46.007Z",
"readonlyUntil": "2026-12-06T08:42:46.007Z"
}
},
{
"name": "삭제 정리 작업(sweep)은 삭제 예정일 전이면 지우지 않음",
"ok": true,
"detail": {
"sweep": 200,
"statusAfter": "readonly"
}
}
],
"evidence": {
"act": {
"activate": 200,
"status": "active",
"provider": "gitea",
"changes": 1,
"clone": true
},
"ex": {
"status": 200,
"type": "application/gzip"
},
"un": {
"status": "readonly",
"deleteAfter": "2026-12-06T08:42:46.007Z",
"readonlyUntil": "2026-12-06T08:42:46.007Z"
},
"sweep": {
"sweep": 200,
"statusAfter": "readonly"
}
}
},
{
"no": 33,
"title": "메일 서비스의 사이트 대표→독립 전환 후 항목 유지",
"verdict": "partial",
"note": "전환 점검표 8항목과 미충족 시 차단 확인. 전후 실제 송수신 확인·DNS 유지 검증은 실 DNS/AriMail 필요(단위 테스트 test/mail.test.mjs 에서 가짜로 전환 완료·되돌리기 확인)",
"blockedBy": null,
"steps": [
{
"name": "사이트 대표→독립 전환 점검표(주소·메일함·별칭·전달·비밀번호·DNS·옵션·작업)",
"ok": true,
"detail": {
"canConvert": false,
"items": [
"addresses:ok",
"data:ok",
"aliases:ok",
"forwards:ok",
"passwords:ok",
"dns:x",
"entitlement:x",
"jobs:x"
]
}
},
{
"name": "DNS 미확인 상태에서 전환 요청은 차단",
"ok": true,
"detail": {
"status": 409,
"error": "conversion_blocked"
}
}
],
"evidence": {
"ck": {
"canConvert": false,
"items": [
"addresses:ok",
"data:ok",
"aliases:ok",
"forwards:ok",
"passwords:ok",
"dns:x",
"entitlement:x",
"jobs:x"
]
},
"tr": {
"status": 409,
"error": "conversion_blocked"
}
}
},
{
"no": 34,
"title": "일시 중지·해지 예약·보존·삭제 단계의 표시 일치",
"verdict": "partial",
"note": "해지된 메일 서비스의 읽기 전용 보존·삭제 예정 표시와 사이트 생명주기 화면의 단계별 항목 확인. 사이트의 해지 예약→보존→삭제 전이는 운영 중 사이트 필요(단위 테스트 test/lifecycle.test.mjs 로 확인)",
"blockedBy": null,
"steps": [
{
"name": "§23.3 한 화면 보기: 공개/관리자 접근·사이트 수·청구·도메인/메일/백업 동작·내보내기·되돌릴 수 없는 시점·보존·다음 알림",
"ok": true,
"detail": {
"state": "도메인 설정 필요",
"siteCount": true,
"publicAccess": "제공 중",
"retention": {
"site_days": 30,
"mail_days": 30,
"source_days": 30,
"backup_days": 7,
"delete_grace_days": 1,
"notice_before_days": 3,
"readonly_retention_days": 30,
"delete_after_days": 30,
"pause_keeps_site_count": true,
"cancel_effective": "next_billing_date",
"source": "상품정책 베이직 (retention 정책) v1"
},
"pauseImpactKeys": [
"publicAccess",
"adminAccess",
"mail",
"backups",
"usageAndCost",
"resume",
"siteCount"
],
"cancelImpactKeys": [
"target",
"linked",
"immediateStops",
"remainingData",
"retentionUntil",
"deleteAfter",
"retentionSource",
"export",
"unpaid",
"refund",
"pointOfNoReturn",
"mailOptions",
"siteCount"
],
"steps14": 8
}
},
{
"name": "일시 중지(확인 필수) 또는 현재 상태에서 거부 사유",
"ok": true,
"detail": {
"noConfirm": "confirmation_required",
"status": 409,
"error": "conflict"
}
},
{
"name": "메일 서비스 생명주기 화면(해지된 독립 메일: 송수신 중지·보존 종료·삭제 예정·백업 미제공)",
"ok": true,
"detail": {
"status": 200,
"sendReceive": "송수신 가능",
"retentionUntil": false,
"deleteAfter": false,
"backupNote": "고객용 메일 백업·복원은 제공하지 않습니다(§17.4)"
}
}
],
"evidence": {
"v": {
"state": "도메인 설정 필요",
"siteCount": true,
"publicAccess": "제공 중",
"retention": {
"site_days": 30,
"mail_days": 30,
"source_days": 30,
"backup_days": 7,
"delete_grace_days": 1,
"notice_before_days": 3,
"readonly_retention_days": 30,
"delete_after_days": 30,
"pause_keeps_site_count": true,
"cancel_effective": "next_billing_date",
"source": "상품정책 베이직 (retention 정책) v1"
},
"pauseImpactKeys": [
"publicAccess",
"adminAccess",
"mail",
"backups",
"usageAndCost",
"resume",
"siteCount"
],
"cancelImpactKeys": [
"target",
"linked",
"immediateStops",
"remainingData",
"retentionUntil",
"deleteAfter",
"retentionSource",
"export",
"unpaid",
"refund",
"pointOfNoReturn",
"mailOptions",
"siteCount"
],
"steps14": 8
},
"mailLc": {
"status": 200,
"sendReceive": "송수신 가능",
"retentionUntil": false,
"deleteAfter": false,
"backupNote": "고객용 메일 백업·복원은 제공하지 않습니다(§17.4)"
}
}
}
],
"bugs": [],
"needsInfrastructure": [
"도메인 소유 확인(TXT)·DNS 반영·HTTPS 발급·외부 접속 확인은 실제 DNS 와 공인 서버가 필요하다(흐름 7·8·11·14·15)",
"메일 DNS(MX·SPF·DKIM·DMARC) 반영과 외부 송신·수신·회신 왕복은 실 도메인·실 AriMail(SMTP 587/수신함)·외부 검증 주소가 필요하다(흐름 10 활성화 완료·11·33 완료)",
"재해복구의 실제 복원(hal-restore.sh 로 다른 호스트에 HAL 재기동)·실제 운영자 로그인·HTTPS·고객 서비스 외부 확인은 실 서버가 필요하다(흐름 25 기록·훈련 절차만 로컬 확인)"
]
}