115 lines
4.2 KiB
TypeScript
115 lines
4.2 KiB
TypeScript
// 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<boolean>
|
|
getDiagnostics(): Promise<SupportDiagnostics>
|
|
}
|
|
|
|
/** preload `electronAPI.system` 중 이 기능이 쓰는 최소 표면. */
|
|
export interface SupportSystemApi {
|
|
openExternal(params: { url: string }): Promise<IPCResult<void>>
|
|
getVersion(): Promise<IPCResult<string>>
|
|
getPlatform(): Promise<IPCResult<string>>
|
|
}
|
|
|
|
async function readResult<T>(call: () => Promise<IPCResult<T>>): Promise<T | null> {
|
|
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 }
|
|
},
|
|
}
|
|
}
|