fix(dictionary): let an edit clear the pronunciation and report duplicate renames
This commit is contained in:
parent
9d5d043e8b
commit
de1e8a82a4
6 changed files with 350 additions and 24 deletions
|
|
@ -12,6 +12,12 @@ import { getLogger } from './LoggerService'
|
|||
import { getCloudSyncService } from './CloudSyncService'
|
||||
import { getMainWindow } from '../windows/WindowManager'
|
||||
import { D3ROError, ErrorCode } from '@d3ro/core/errors'
|
||||
import {
|
||||
buildDictionaryUpdatePatch,
|
||||
isUniqueConstraintViolation,
|
||||
normalizeDictionaryPronunciation,
|
||||
normalizeDictionaryWord
|
||||
} from '@d3ro/core/dictionary-policy'
|
||||
import type {
|
||||
DictionaryEntry,
|
||||
DictionaryQueryParams,
|
||||
|
|
@ -114,7 +120,7 @@ function normalizeCategory(value: string | null): DictionaryEntry['category'] {
|
|||
|
||||
class DictionaryService {
|
||||
add(params: DictionaryAddParams): DictionaryEntry {
|
||||
const word = params.word.trim()
|
||||
const word = normalizeDictionaryWord(params.word)
|
||||
if (!word) {
|
||||
throw new D3ROError(ErrorCode.DictionaryImportInvalidFormat, 'Dictionary word is empty')
|
||||
}
|
||||
|
|
@ -126,7 +132,7 @@ class DictionaryService {
|
|||
const entry: NewDictionary = {
|
||||
id,
|
||||
word,
|
||||
pronunciation: params.pronunciation ?? null,
|
||||
pronunciation: normalizeDictionaryPronunciation(params.pronunciation) ?? null,
|
||||
category: params.category ?? 'user',
|
||||
usageCount: 0,
|
||||
lastUsedAt: null,
|
||||
|
|
@ -137,8 +143,7 @@ class DictionaryService {
|
|||
try {
|
||||
db.insert(dictionary).values(entry).run()
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
if (message.includes('UNIQUE') || message.includes('unique')) {
|
||||
if (isUniqueConstraintViolation(err)) {
|
||||
throw new D3ROError(ErrorCode.DictionaryDuplicate, `Duplicate word: ${word}`)
|
||||
}
|
||||
throw err
|
||||
|
|
@ -150,16 +155,27 @@ class DictionaryService {
|
|||
}
|
||||
|
||||
update(params: DictionaryUpdateParams): DictionaryEntry | null {
|
||||
const result = buildDictionaryUpdatePatch(params)
|
||||
if (!result.ok) {
|
||||
throw new D3ROError(ErrorCode.DictionaryImportInvalidFormat, 'Dictionary word is empty')
|
||||
}
|
||||
|
||||
const db = getDatabase()
|
||||
const existing = db.select().from(dictionary).where(eq(dictionary.id, params.id)).get()
|
||||
if (!existing) return null
|
||||
|
||||
const updates: Partial<NewDictionary> = { updatedAt: Date.now() }
|
||||
if (params.word !== undefined) updates.word = params.word
|
||||
if (params.pronunciation !== undefined) updates.pronunciation = params.pronunciation
|
||||
if (params.category !== undefined) updates.category = params.category
|
||||
|
||||
const updates: Partial<NewDictionary> = { ...result.patch, updatedAt: Date.now() }
|
||||
try {
|
||||
db.update(dictionary).set(updates).where(eq(dictionary.id, params.id)).run()
|
||||
} catch (err) {
|
||||
if (isUniqueConstraintViolation(err)) {
|
||||
throw new D3ROError(
|
||||
ErrorCode.DictionaryDuplicate,
|
||||
`Duplicate word: ${result.patch.word ?? existing.word}`
|
||||
)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
||||
const updated = db.select().from(dictionary).where(eq(dictionary.id, params.id)).get()
|
||||
// Phase 3.3: 자동 push (fire-and-forget)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@ import { PageHeader, SearchInput, EmptyStateCard } from '../components/shared'
|
|||
import { d3roPalette, d3roFontSans, d3roTypo, d3roRadius, d3roShadow } from '@d3ro/ui/theme'
|
||||
import { useI18n } from '@d3ro/i18n'
|
||||
import type { DictionaryEntry, DictionaryPage as DictPageData } from '@d3ro/core/types'
|
||||
import {
|
||||
dictionarySaveErrorKey,
|
||||
toDictionaryAddParams,
|
||||
toDictionaryUpdateParams,
|
||||
} from '@d3ro/core/dictionary-policy'
|
||||
|
||||
export function DictionaryPage(): React.ReactElement {
|
||||
const { t } = useI18n()
|
||||
|
|
@ -20,6 +25,7 @@ export function DictionaryPage(): React.ReactElement {
|
|||
const [formWord, setFormWord] = useState('')
|
||||
const [formPronunciation, setFormPronunciation] = useState('')
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [formError, setFormError] = useState<string | null>(null)
|
||||
const isSavingRef = useRef(false)
|
||||
const [ioBusy, setIoBusy] = useState(false)
|
||||
const [ioMessage, setIoMessage] = useState<string | null>(null)
|
||||
|
|
@ -47,6 +53,7 @@ export function DictionaryPage(): React.ReactElement {
|
|||
setEditId(null)
|
||||
setFormWord('')
|
||||
setFormPronunciation('')
|
||||
setFormError(null)
|
||||
setDialogOpen(true)
|
||||
}
|
||||
|
||||
|
|
@ -55,6 +62,7 @@ export function DictionaryPage(): React.ReactElement {
|
|||
setEditId(entry.id)
|
||||
setFormWord(entry.word)
|
||||
setFormPronunciation(entry.pronunciation ?? '')
|
||||
setFormError(null)
|
||||
setDialogOpen(true)
|
||||
}
|
||||
|
||||
|
|
@ -62,18 +70,16 @@ export function DictionaryPage(): React.ReactElement {
|
|||
if (!formWord.trim() || saving || isSavingRef.current) return
|
||||
isSavingRef.current = true
|
||||
setSaving(true)
|
||||
setFormError(null)
|
||||
try {
|
||||
if (editId) {
|
||||
await window.electronAPI.dictionary.update({
|
||||
id: editId,
|
||||
word: formWord.trim(),
|
||||
pronunciation: formPronunciation.trim() || undefined,
|
||||
})
|
||||
} else {
|
||||
await window.electronAPI.dictionary.add({
|
||||
word: formWord.trim(),
|
||||
pronunciation: formPronunciation.trim() || undefined,
|
||||
})
|
||||
const form = { word: formWord, pronunciation: formPronunciation }
|
||||
const result = editId
|
||||
? await window.electronAPI.dictionary.update(toDictionaryUpdateParams(editId, form))
|
||||
: await window.electronAPI.dictionary.add(toDictionaryAddParams(form))
|
||||
if (!result.success) {
|
||||
// 다이얼로그를 유지해 사용자가 입력을 고칠 수 있게 한다 (예: 같은 단어로 이름 변경).
|
||||
setFormError(t(dictionarySaveErrorKey(result.error.code)))
|
||||
return
|
||||
}
|
||||
setDialogOpen(false)
|
||||
loadData()
|
||||
|
|
@ -308,7 +314,11 @@ export function DictionaryPage(): React.ReactElement {
|
|||
<TextField
|
||||
label={t('dictionary.word')}
|
||||
value={formWord}
|
||||
onChange={(e) => setFormWord(e.target.value)}
|
||||
onChange={(e) => {
|
||||
setFormWord(e.target.value)
|
||||
setFormError(null)
|
||||
}}
|
||||
error={formError !== null}
|
||||
fullWidth
|
||||
autoFocus
|
||||
sx={{ mt: 1.5 }}
|
||||
|
|
@ -321,13 +331,18 @@ export function DictionaryPage(): React.ReactElement {
|
|||
sx={{ mt: 2.5 }}
|
||||
helperText="e.g. 디쓰리오"
|
||||
/>
|
||||
{formError && (
|
||||
<PhosphorText variant="meta" role="alert" sx={{ display: 'block', mt: 1.5, color: d3roPalette.tag.red }}>
|
||||
{formError}
|
||||
</PhosphorText>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 2.5, pb: 2, pt: 1, gap: 1 }}>
|
||||
<PhysicalButton tone="glass" onClick={() => setDialogOpen(false)}>
|
||||
{t('common.cancel')}
|
||||
</PhysicalButton>
|
||||
<PhysicalButton tone="accent" onClick={handleSave} disabled={!formWord.trim() || saving}>
|
||||
{saving ? t('common.saving') || '저장 중...' : t('common.save')}
|
||||
{saving ? t('common.saving') : t('common.save')}
|
||||
</PhysicalButton>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
|
|
|||
75
apps/desktop/tests/red/dictionary-redteam-r1-12.test.ts
Normal file
75
apps/desktop/tests/red/dictionary-redteam-r1-12.test.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
// tests/red/dictionary-redteam-r1-12.test.ts
|
||||
// 회귀: 사전 편집에서 발음을 지울 수 없던 문제, 같은 단어로 이름을 바꿀 때
|
||||
// DictionaryDuplicate 대신 일반 DB 오류로 조용히 실패하던 문제.
|
||||
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { D3ROError, ErrorCode } from '@d3ro/core/errors'
|
||||
import { IPC_CHANNELS } from '@d3ro/core/ipc-channels'
|
||||
import { toDictionaryUpdateParams } from '@d3ro/core/dictionary-policy'
|
||||
import { getDictionaryService } from '../../src/main/services/DictionaryService'
|
||||
import { registerDictionaryHandlers } from '../../src/main/ipc/dictionary-handlers'
|
||||
import { invokeIpc, useRedHarness } from './harness'
|
||||
|
||||
useRedHarness()
|
||||
|
||||
describe('사전 편집 회귀 (redteam r1 #12)', () => {
|
||||
it('편집에서 발음 입력을 비우면 저장된 발음이 지워진다', () => {
|
||||
const svc = getDictionaryService()
|
||||
const entry = svc.add({ word: 'GPT', pronunciation: 'gee-pee-tee' })
|
||||
|
||||
const updated = svc.update(toDictionaryUpdateParams(entry.id, { word: 'GPT', pronunciation: '' }))
|
||||
|
||||
expect(updated?.pronunciation).toBeNull()
|
||||
const reloaded = svc.list({ page: 0, pageSize: 10 }).entries.find((e) => e.id === entry.id)
|
||||
expect(reloaded?.pronunciation).toBeNull()
|
||||
})
|
||||
|
||||
it('pronunciation: null 은 발음을 지우고, 생략하면 유지한다', () => {
|
||||
const svc = getDictionaryService()
|
||||
const entry = svc.add({ word: 'Whisper', pronunciation: '위스퍼' })
|
||||
|
||||
expect(svc.update({ id: entry.id, word: 'Whisper2' })?.pronunciation).toBe('위스퍼')
|
||||
expect(svc.update({ id: entry.id, pronunciation: null })?.pronunciation).toBeNull()
|
||||
})
|
||||
|
||||
it('공백뿐인 발음은 빈 문자열로 저장하지 않고 지운다', () => {
|
||||
const svc = getDictionaryService()
|
||||
const entry = svc.add({ word: 'Blank', pronunciation: '블랭크' })
|
||||
|
||||
expect(svc.update({ id: entry.id, pronunciation: ' ' })?.pronunciation).toBeNull()
|
||||
})
|
||||
|
||||
it('같은 카테고리의 기존 단어로 이름을 바꾸면 DictionaryDuplicate 이다', () => {
|
||||
const svc = getDictionaryService()
|
||||
svc.add({ word: 'Ollama' })
|
||||
const other = svc.add({ word: 'Llama' })
|
||||
|
||||
try {
|
||||
svc.update({ id: other.id, word: 'Ollama' })
|
||||
expect.unreachable()
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(D3ROError)
|
||||
expect((err as D3ROError).code).toBe(ErrorCode.DictionaryDuplicate)
|
||||
}
|
||||
})
|
||||
|
||||
it('IPC update 도 이름 중복을 DictionaryDuplicate 로 돌려준다', async () => {
|
||||
registerDictionaryHandlers()
|
||||
const svc = getDictionaryService()
|
||||
svc.add({ word: 'dup-target' })
|
||||
const other = svc.add({ word: 'dup-source' })
|
||||
|
||||
const res = await invokeIpc(IPC_CHANNELS.DICTIONARY.UPDATE, { id: other.id, word: 'dup-target' })
|
||||
|
||||
expect(res.success).toBe(false)
|
||||
if (!res.success) expect(res.error.code).toBe(ErrorCode.DictionaryDuplicate)
|
||||
})
|
||||
|
||||
it('편집에서 단어를 비우면 거부하고, 단어는 trim 해 저장한다', () => {
|
||||
const svc = getDictionaryService()
|
||||
const entry = svc.add({ word: 'Trim' })
|
||||
|
||||
expect(() => svc.update({ id: entry.id, word: ' ' })).toThrow(D3ROError)
|
||||
expect(svc.update({ id: entry.id, word: ' Trimmed ' })?.word).toBe('Trimmed')
|
||||
})
|
||||
})
|
||||
110
packages/core/__tests__/dictionary-policy.test.ts
Normal file
110
packages/core/__tests__/dictionary-policy.test.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
// packages/core/__tests__/dictionary-policy.test.ts
|
||||
// 사전 편집 정책 순수 함수 테스트 — 발음 지우기, 단어 검증, UNIQUE 판별, 폼 → 요청 변환.
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { ErrorCode } from '../src/errors'
|
||||
import {
|
||||
buildDictionaryUpdatePatch,
|
||||
dictionarySaveErrorKey,
|
||||
isUniqueConstraintViolation,
|
||||
normalizeDictionaryPronunciation,
|
||||
normalizeDictionaryWord,
|
||||
toDictionaryAddParams,
|
||||
toDictionaryUpdateParams
|
||||
} from '../src/dictionary-policy'
|
||||
|
||||
describe('normalizeDictionaryWord', () => {
|
||||
it('앞뒤 공백을 제거한다', () => {
|
||||
expect(normalizeDictionaryWord(' GPT ')).toBe('GPT')
|
||||
})
|
||||
it('공백뿐이면 null이다', () => {
|
||||
expect(normalizeDictionaryWord(' ')).toBeNull()
|
||||
expect(normalizeDictionaryWord('')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizeDictionaryPronunciation', () => {
|
||||
it('undefined는 변경 없음으로 유지한다', () => {
|
||||
expect(normalizeDictionaryPronunciation(undefined)).toBeUndefined()
|
||||
})
|
||||
it('null과 빈 문자열은 지우기(null)다', () => {
|
||||
expect(normalizeDictionaryPronunciation(null)).toBeNull()
|
||||
expect(normalizeDictionaryPronunciation('')).toBeNull()
|
||||
expect(normalizeDictionaryPronunciation(' ')).toBeNull()
|
||||
})
|
||||
it('값은 trim 한다', () => {
|
||||
expect(normalizeDictionaryPronunciation(' gee-pee-tee ')).toBe('gee-pee-tee')
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildDictionaryUpdatePatch', () => {
|
||||
it('발음 null은 패치에 null로 들어간다 (지우기)', () => {
|
||||
const result = buildDictionaryUpdatePatch({ id: 'a', pronunciation: null })
|
||||
expect(result).toEqual({ ok: true, patch: { pronunciation: null } })
|
||||
})
|
||||
it('빈 발음 문자열도 null로 지운다', () => {
|
||||
const result = buildDictionaryUpdatePatch({ id: 'a', pronunciation: ' ' })
|
||||
expect(result).toEqual({ ok: true, patch: { pronunciation: null } })
|
||||
})
|
||||
it('지정하지 않은 필드는 패치에 없다', () => {
|
||||
const result = buildDictionaryUpdatePatch({ id: 'a' })
|
||||
expect(result).toEqual({ ok: true, patch: {} })
|
||||
})
|
||||
it('단어는 trim 하고, 비면 거부한다', () => {
|
||||
expect(buildDictionaryUpdatePatch({ id: 'a', word: ' GPT ', category: 'technical' })).toEqual({
|
||||
ok: true,
|
||||
patch: { word: 'GPT', category: 'technical' }
|
||||
})
|
||||
expect(buildDictionaryUpdatePatch({ id: 'a', word: ' ' })).toEqual({
|
||||
ok: false,
|
||||
reason: 'empty-word'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('isUniqueConstraintViolation', () => {
|
||||
it('SQLite UNIQUE 오류를 판별한다', () => {
|
||||
expect(
|
||||
isUniqueConstraintViolation(
|
||||
new Error('UNIQUE constraint failed: dictionary.word, dictionary.category')
|
||||
)
|
||||
).toBe(true)
|
||||
expect(isUniqueConstraintViolation('unique violation')).toBe(true)
|
||||
expect(isUniqueConstraintViolation(new Error('disk I/O error'))).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('폼 → 요청 변환', () => {
|
||||
it('추가: 빈 발음은 보내지 않는다', () => {
|
||||
expect(toDictionaryAddParams({ word: ' GPT ', pronunciation: ' ' })).toEqual({ word: 'GPT' })
|
||||
expect(toDictionaryAddParams({ word: 'GPT', pronunciation: ' 지피티 ' })).toEqual({
|
||||
word: 'GPT',
|
||||
pronunciation: '지피티'
|
||||
})
|
||||
})
|
||||
it('편집: 비운 발음은 null로 보내 지우기를 명시한다', () => {
|
||||
expect(toDictionaryUpdateParams('id-1', { word: 'GPT', pronunciation: '' })).toEqual({
|
||||
id: 'id-1',
|
||||
word: 'GPT',
|
||||
pronunciation: null
|
||||
})
|
||||
expect(toDictionaryUpdateParams('id-1', { word: 'GPT', pronunciation: 'x' }).pronunciation).toBe(
|
||||
'x'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('dictionarySaveErrorKey', () => {
|
||||
it('오류 코드를 i18n 키로 바꾼다', () => {
|
||||
expect(dictionarySaveErrorKey(ErrorCode.DictionaryDuplicate)).toBe(
|
||||
'mobile.dictionary.error.duplicate'
|
||||
)
|
||||
expect(dictionarySaveErrorKey(ErrorCode.DictionaryNotFound)).toBe(
|
||||
'mobile.dictionary.error.notFound'
|
||||
)
|
||||
expect(dictionarySaveErrorKey(ErrorCode.DictionaryImportInvalidFormat)).toBe(
|
||||
'mobile.dictionary.error.validation'
|
||||
)
|
||||
expect(dictionarySaveErrorKey(ErrorCode.DBWriteFailed)).toBe('mobile.dictionary.error.server')
|
||||
})
|
||||
})
|
||||
109
packages/core/src/dictionary-policy.ts
Normal file
109
packages/core/src/dictionary-policy.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
// packages/core/src/dictionary-policy.ts
|
||||
// 사용자 사전 편집 정책 — 순수 함수만 둔다 (DB/IPC/UI 의존 없음).
|
||||
// 데스크톱 DictionaryService(쓰기 규칙)와 DictionaryPage(폼 → 요청 변환)가 같은 규칙을 공유한다.
|
||||
|
||||
import { ErrorCode } from './errors'
|
||||
import type { DictionaryAddParams, DictionaryEntry, DictionaryUpdateParams } from './types'
|
||||
|
||||
export type DictionaryCategory = DictionaryEntry['category']
|
||||
|
||||
/** 단어를 정규화한다. 공백뿐이면 null (= 유효하지 않음). */
|
||||
export function normalizeDictionaryWord(raw: string): string | null {
|
||||
const word = raw.trim()
|
||||
return word.length > 0 ? word : null
|
||||
}
|
||||
|
||||
/**
|
||||
* 발음 입력을 정규화한다.
|
||||
* - undefined → undefined: "변경 없음"
|
||||
* - null 또는 공백뿐인 문자열 → null: "발음 지우기"
|
||||
* - 그 외 → 앞뒤 공백을 제거한 값
|
||||
*/
|
||||
export function normalizeDictionaryPronunciation(
|
||||
raw: string | null | undefined
|
||||
): string | null | undefined {
|
||||
if (raw === undefined) return undefined
|
||||
if (raw === null) return null
|
||||
const pronunciation = raw.trim()
|
||||
return pronunciation.length > 0 ? pronunciation : null
|
||||
}
|
||||
|
||||
/** DB에 그대로 쓸 수 있는 부분 갱신 값. 키가 없으면 해당 필드는 바꾸지 않는다. */
|
||||
export interface DictionaryUpdatePatch {
|
||||
word?: string
|
||||
pronunciation?: string | null
|
||||
category?: DictionaryCategory
|
||||
}
|
||||
|
||||
export type DictionaryUpdatePatchResult =
|
||||
| { ok: true; patch: DictionaryUpdatePatch }
|
||||
| { ok: false; reason: 'empty-word' }
|
||||
|
||||
/** 갱신 요청을 검증·정규화해 쓰기 패치로 바꾼다. */
|
||||
export function buildDictionaryUpdatePatch(
|
||||
params: DictionaryUpdateParams
|
||||
): DictionaryUpdatePatchResult {
|
||||
const patch: DictionaryUpdatePatch = {}
|
||||
if (params.word !== undefined) {
|
||||
const word = normalizeDictionaryWord(params.word)
|
||||
if (word === null) return { ok: false, reason: 'empty-word' }
|
||||
patch.word = word
|
||||
}
|
||||
const pronunciation = normalizeDictionaryPronunciation(params.pronunciation)
|
||||
if (pronunciation !== undefined) patch.pronunciation = pronunciation
|
||||
if (params.category !== undefined) patch.category = params.category
|
||||
return { ok: true, patch }
|
||||
}
|
||||
|
||||
/** SQLite UNIQUE 제약 위반 여부 (better-sqlite3 오류 메시지 기준). */
|
||||
export function isUniqueConstraintViolation(err: unknown): boolean {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
return /unique/i.test(message)
|
||||
}
|
||||
|
||||
/** 추가/편집 다이얼로그의 입력값. */
|
||||
export interface DictionaryFormValues {
|
||||
word: string
|
||||
pronunciation: string
|
||||
}
|
||||
|
||||
/** 새 단어 추가 요청: 빈 발음은 보내지 않는다. */
|
||||
export function toDictionaryAddParams(form: DictionaryFormValues): DictionaryAddParams {
|
||||
const pronunciation = normalizeDictionaryPronunciation(form.pronunciation)
|
||||
return {
|
||||
word: form.word.trim(),
|
||||
...(pronunciation ? { pronunciation } : {})
|
||||
}
|
||||
}
|
||||
|
||||
/** 편집 요청: 비운 발음은 null로 보내 "지우기"를 명시한다. */
|
||||
export function toDictionaryUpdateParams(
|
||||
id: string,
|
||||
form: DictionaryFormValues
|
||||
): DictionaryUpdateParams {
|
||||
return {
|
||||
id,
|
||||
word: form.word.trim(),
|
||||
pronunciation: normalizeDictionaryPronunciation(form.pronunciation) ?? null
|
||||
}
|
||||
}
|
||||
|
||||
/** 사전 저장 실패를 사용자 메시지 i18n 키로 바꾼다. */
|
||||
export type DictionarySaveErrorKey =
|
||||
| 'mobile.dictionary.error.duplicate'
|
||||
| 'mobile.dictionary.error.notFound'
|
||||
| 'mobile.dictionary.error.validation'
|
||||
| 'mobile.dictionary.error.server'
|
||||
|
||||
export function dictionarySaveErrorKey(code: ErrorCode): DictionarySaveErrorKey {
|
||||
switch (code) {
|
||||
case ErrorCode.DictionaryDuplicate:
|
||||
return 'mobile.dictionary.error.duplicate'
|
||||
case ErrorCode.DictionaryNotFound:
|
||||
return 'mobile.dictionary.error.notFound'
|
||||
case ErrorCode.DictionaryImportInvalidFormat:
|
||||
return 'mobile.dictionary.error.validation'
|
||||
default:
|
||||
return 'mobile.dictionary.error.server'
|
||||
}
|
||||
}
|
||||
|
|
@ -718,7 +718,8 @@ export interface DictionaryAddParams {
|
|||
export interface DictionaryUpdateParams {
|
||||
id: string
|
||||
word?: string
|
||||
pronunciation?: string
|
||||
/** undefined = 변경 없음, null(또는 빈 문자열) = 발음 지우기 */
|
||||
pronunciation?: string | null
|
||||
category?: 'user' | 'auto' | 'technical'
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue