fix(support): stop faking refund approvals and ticket submissions in the support modal
This commit is contained in:
parent
e243e5282c
commit
819bc9d789
6 changed files with 580 additions and 355 deletions
115
apps/desktop/src/renderer/components/support/support-contact.ts
Normal file
115
apps/desktop/src/renderer/components/support/support-contact.ts
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
// 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 }
|
||||
},
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue