// src/renderer/components/support/support-contact.ts // 고객지원 문의 정책(순수 함수)과 IO 포트. // // 앱에는 환불 자격 조회·결제 취소·티켓 접수 백엔드가 없다. 그래서 지원 모달은 "처리됐다"고 // 주장하지 않고, 사용자가 직접 보내는 이메일(mailto:)로만 문의를 넘긴다. 서버가 확인한 성공이 // 없으므로 성공 문구·티켓 번호·환불 처리 결과는 만들지 않는다. import type { IPCResult } from '@d3ro/core/errors' /** 지원 문의 수신 주소. 사이트의 개인정보·약관 문의 주소와 같다 (site/public/terms). */ export const SUPPORT_EMAIL = 'yunchan8804@gmail.com' export const SUPPORT_CATEGORIES = ['audio', 'gpu', 'billing', 'general'] as const export type SupportCategory = (typeof SUPPORT_CATEGORIES)[number] export interface SupportDraft { category: SupportCategory subject: string details: string } export interface SupportDiagnostics { appVersion: string | null platform: string | null } export type SupportDraftField = 'subject' | 'details' export type SupportDraftValidation = | { ok: true } | { ok: false; missing: SupportDraftField[] } /** Windows 셸이 mailto 인자를 잘라내지 않도록 본문 길이를 제한한다. */ export const MAX_MAILTO_BODY_CHARS = 1500 export function isSupportCategory(value: string): value is SupportCategory { return (SUPPORT_CATEGORIES as readonly string[]).includes(value) } export function validateSupportDraft(draft: SupportDraft): SupportDraftValidation { const missing: SupportDraftField[] = [] if (!draft.subject.trim()) missing.push('subject') if (!draft.details.trim()) missing.push('details') return missing.length === 0 ? { ok: true } : { ok: false, missing } } export function formatSupportDiagnostics(diag: SupportDiagnostics): string { const version = diag.appVersion?.trim() || 'unknown' const platform = diag.platform?.trim() || 'unknown' return `D3RO Voice ${version} • ${platform}` } /** * 문의 초안 → mailto: URL. 수신·전송은 사용자의 메일 앱이 한다. * 본문 라벨은 지원 담당자가 읽는 기술 정보라 영어로 고정한다. */ export function buildSupportMailto(draft: SupportDraft, diag: SupportDiagnostics): string { const subject = `[D3RO Voice][${draft.category}] ${draft.subject.trim()}` const details = draft.details.trim() const trimmedDetails = details.length > MAX_MAILTO_BODY_CHARS ? `${details.slice(0, MAX_MAILTO_BODY_CHARS)}…` : details const body = [ trimmedDetails, '', '---', `Category: ${draft.category}`, `Diagnostics: ${formatSupportDiagnostics(diag)}`, ].join('\n') return `mailto:${SUPPORT_EMAIL}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}` } // ── 포트 ──────────────────────────────────────────────── /** 지원 모달이 의존하는 IO 경계. UI는 이 인터페이스만 안다. */ export interface SupportChannel { /** 메일 앱 열기를 시도한다. 메인 프로세스가 성공을 돌려줄 때만 true. */ openMail(url: string): Promise getDiagnostics(): Promise } /** preload `electronAPI.system` 중 이 기능이 쓰는 최소 표면. */ export interface SupportSystemApi { openExternal(params: { url: string }): Promise> getVersion(): Promise> getPlatform(): Promise> } async function readResult(call: () => Promise>): Promise { try { const result = await call() return result.success ? result.data : null } catch { return null } } export function createElectronSupportChannel(api: SupportSystemApi): SupportChannel { return { async openMail(url) { try { const result = await api.openExternal({ url }) return result.success } catch { return false } }, async getDiagnostics() { const [appVersion, platform] = await Promise.all([ readResult(() => api.getVersion()), readResult(() => api.getPlatform()), ]) return { appVersion, platform } }, } }