/** * portal/usage/limits.mjs — 100% 제한 조회(다른 영역이 쓰는 읽기 전용 API)와 자원별 제한 의미(§13.4). * * 자원별 100% 동작 * web_out 사이트 대신 제한 안내 화면(유지: 관리자 화면·조회·삭제·내보내기 / 제한: 공개 접속). 다음 KST 월 시작 또는 추가 용량 적용 후 복구. * web_disk 유지: 기존 사이트 조회·삭제·내보내기 / 제한: 새 업로드·데이터베이스 증가·새 배포. * mail_storage 유지: 기존 메일 조회·삭제 / 제한: 새 수신(발신 서버가 재시도하도록 임시 실패 4xx)·저장. * mail_msgs 유지: 조회·삭제 / 제한: 새 발신 차단 + 새 수신 임시 실패. 다음 KST 월 시작 또는 추가 용량 적용 후 복구. * mailboxes 유지: 기존 메일함 / 제한: 새 메일함 생성. * sites·mail_services·source·backup 은 개수 권리라 100% = "추가 생성 불가"(lib/quota.reserve 가 거부). * 초과 사용 허용(overage opt-in)이 유효하고 상한 안이면 limited=false 로 두되 overage=1 로 표시한다. * 고객 데이터는 어떤 경우에도 자동 삭제하지 않는다. */ export const LIMIT_SEMANTICS = Object.freeze({ web_out: { kept: ['관리 화면 접근', '파일 조회·내보내기', '삭제'], blocked: ['공개 사이트 접속(제한 안내 화면 표시)'], recovery: '다음 KST 월 시작 또는 추가 용량(옵션 구매·배정 증가) 적용 후 자동 복구', monthly: true }, web_disk: { kept: ['기존 사이트 조회', '삭제·내보내기'], blocked: ['새 업로드', '데이터베이스 증가', '새 배포'], recovery: '파일·데이터 정리 또는 추가 용량 적용 후 복구', monthly: false }, mail_storage: { kept: ['기존 메일 조회', '삭제'], blocked: ['새 수신(임시 실패 응답 — 발신 서버가 재시도)', '새 저장'], recovery: '메일 정리 또는 추가 용량 적용 후 복구', monthly: false }, mail_msgs: { kept: ['기존 메일 조회', '삭제'], blocked: ['새 발신', '새 수신(임시 실패 응답)'], recovery: '다음 KST 월 시작 또는 추가 통수 적용 후 복구', monthly: true }, mailboxes: { kept: ['기존 메일함'], blocked: ['새 메일함 생성'], recovery: '메일함 정리 또는 추가 메일함 옵션 적용 후 복구', monthly: false }, sites: { kept: ['운영 중 사이트'], blocked: ['새 공개 배포(사이트 수 확보)'], recovery: '사이트 수 반환(§14) 또는 상품 구매', monthly: false }, mail_services: { kept: ['기존 메일 서비스'], blocked: ['새 독립 메일 서비스'], recovery: '옵션 구매', monthly: false }, source: { kept: ['기존 저장소 조회'], blocked: ['새 Source 옵션 적용'], recovery: '옵션 구매', monthly: false }, backup: { kept: ['기존 백업 보관'], blocked: ['새 백업 옵션 활성화'], recovery: '옵션 구매', monthly: false }, }); /** 대상(사이트·메일 서비스)의 자원이 100% 제한 중인지 */ export function isLimited(store, resource, subjectType, subjectId) { const r = store.get('SELECT limited FROM usage_limits WHERE resource=? AND subject_type=? AND subject_id=?', resource, subjectType, subjectId); return !!(r && r.limited); } /** 제한 상세(화면용): { level, limited, used, limit, since, reason, semantics, overage } 또는 null */ export function limitInfo(store, resource, subjectType, subjectId) { const r = store.get('SELECT * FROM usage_limits WHERE resource=? AND subject_type=? AND subject_id=?', resource, subjectType, subjectId); if (!r) return null; return { resource, level: r.level, limited: !!r.limited, used: r.used, limit: r.limit, since: r.since, reason: r.reason, overage: !!r.overage, evaluatedAt: r.evaluated_at, semantics: LIMIT_SEMANTICS[resource] || null }; } /** 그룹 전체의 제한·경고 목록 */ export function listLimits(store, groupId) { return store.all('SELECT * FROM usage_limits WHERE group_id=? AND level>0 ORDER BY level DESC, resource', groupId).map((r) => ({ ...r, limited: !!r.limited, overage: !!r.overage, semantics: LIMIT_SEMANTICS[r.resource] || null })); } /** 대상에 걸린 모든 제한(배포 전 확인 등에서 한 번에) */ export function limitsFor(store, subjectType, subjectId) { return store.all('SELECT * FROM usage_limits WHERE subject_type=? AND subject_id=? AND level>0', subjectType, subjectId).map((r) => ({ ...r, limited: !!r.limited, semantics: LIMIT_SEMANTICS[r.resource] || null })); }