d3ro-voice/apps/desktop/src/renderer/components/meeting/AddDocumentDialog.tsx
yunchan8804 a041f1b6a9 feat(V2-1c): packages/ui 추출 — DS 컴포넌트 + theme + theme-vars 분리
packages/ui (@d3ro/ui) 신규:
- src/theme.ts (d3roPalette/d3roTypo/d3roShadow/d3roRadius SSOT)
- src/theme-vars.ts (팝업/main 프로세스용 CSS 변수 맵)
- src/components/ds/ (CrtDisplay, InstrumentPanel, Led, MetalCard,
  MetalDial, PhosphorText, PhysicalButton, ScreenPanel, ButtonGroup)
- src/index.ts barrel
- subpath exports: ./theme, ./theme-vars, ./components/ds
- React/MUI/Emotion은 peerDependencies로 선언
- @d3ro/core만 직접 의존성

apps/desktop/src/shared/ 디렉토리 완전 제거:
- theme-vars가 마지막 남은 파일이었음
- tsconfig include에서 src/shared/**/* 제거

일괄 치환 (renderer 전역):
- ../theme, ../../theme, ./theme → @d3ro/ui/theme
- ../components/ds, ../../components/ds, ./ds, ../ds → @d3ro/ui/components/ds
- ../ds/<Component>, ../../ds/<Component> → @d3ro/ui/components/ds
  (세부 파일 import는 barrel로 통합)
- @shared/theme-vars → @d3ro/ui/theme-vars (WindowManager)

apps/desktop 설정:
- package.json: @d3ro/ui: '*' dep 추가
- tsconfig.node/web.json: @shared/* paths 완전 제거, @d3ro/ui,
  @d3ro/ui/* paths 추가
- electron.vite.config.ts: @shared alias 제거, @d3ro/ui alias 추가,
  externalize exclude에 @d3ro/ui 추가
- vitest.config.ts: alias 교체

DS 컴포넌트 내부의 '../../theme' 상대 경로는 packages/ui 구조에서
동일하게 해결되어 그대로 유효.

검증: typecheck + build + dev 런타임 모두 통과.
2026-04-08 14:50:33 +09:00

193 lines
5.8 KiB
TypeScript

// src/renderer/components/meeting/AddDocumentDialog.tsx
// Phase 14.5: 문서 생성 다이얼로그 — 템플릿 선택 + 커스텀 프롬프트
import { useState, useCallback } from 'react'
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Box,
TextField,
LinearProgress,
} from '@mui/material'
import { MetalCard } from '@d3ro/ui/components/ds'
import { PhosphorText } from '@d3ro/ui/components/ds'
import { PhysicalButton } from '@d3ro/ui/components/ds'
import { d3roPalette, d3roTypo, d3roRadius, d3roShadow } from '@d3ro/ui/theme'
import { useI18n } from '../../i18n'
interface TemplateItem {
id: string
name: string
description: string
templateType: string
isBuiltin: boolean
}
interface AddDocumentDialogProps {
open: boolean
onClose: () => void
onGenerate: (templateId: string, customPrompt?: string, customTitle?: string) => void
templates: TemplateItem[]
generating: boolean
}
export function AddDocumentDialog({
open,
onClose,
onGenerate,
templates,
generating,
}: AddDocumentDialogProps): React.ReactElement {
const { t } = useI18n()
const [selectedId, setSelectedId] = useState<string | null>(null)
const [customPrompt, setCustomPrompt] = useState('')
const [customTitle, setCustomTitle] = useState('')
const selectedTemplate = templates.find((tp) => tp.id === selectedId)
const isCustom = selectedTemplate?.templateType === 'custom'
const handleGenerate = useCallback(() => {
if (!selectedId) return
onGenerate(
selectedId,
isCustom && customPrompt.trim() ? customPrompt.trim() : undefined,
isCustom && customTitle.trim() ? customTitle.trim() : undefined,
)
}, [selectedId, isCustom, customPrompt, customTitle, onGenerate])
const handleClose = useCallback(() => {
if (!generating) {
setSelectedId(null)
setCustomPrompt('')
setCustomTitle('')
onClose()
}
}, [generating, onClose])
return (
<Dialog
open={open}
onClose={handleClose}
maxWidth="sm"
fullWidth
slotProps={{
paper: {
sx: {
bgcolor: d3roPalette.bg.card,
border: `1px solid ${d3roPalette.border.default}`,
borderRadius: d3roRadius.inner,
boxShadow: d3roShadow.tooltip,
},
},
}}
>
<DialogTitle>
<PhosphorText variant="label">{t('meeting.selectTemplate')}</PhosphorText>
</DialogTitle>
<DialogContent>
{generating && (
<Box sx={{ mb: 2 }}>
<LinearProgress
sx={{
height: 4,
borderRadius: 2,
bgcolor: d3roPalette.bg.inset,
'& .MuiLinearProgress-bar': { bgcolor: d3roPalette.accent.amber },
}}
/>
<PhosphorText variant="dim" sx={{ fontSize: 11, mt: 0.5 }}>
{t('meeting.generating')}
</PhosphorText>
</Box>
)}
{/* 템플릿 목록 */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
{templates.map((tp) => (
<Box
key={tp.id}
onClick={() => !generating && setSelectedId(tp.id)}
sx={{
cursor: generating ? 'default' : 'pointer',
opacity: generating ? 0.6 : 1,
border:
selectedId === tp.id
? `2px solid ${d3roPalette.accent.amber}`
: `2px solid transparent`,
borderRadius: d3roRadius.inner,
transition: 'border-color 0.15s',
'&:hover': {
borderColor: generating ? undefined : d3roPalette.accent.amberDim,
},
}}
>
<MetalCard>
<PhosphorText variant="compact">{tp.name}</PhosphorText>
<PhosphorText variant="dim" sx={{ fontSize: 11, mt: 0.25 }}>
{tp.description}
</PhosphorText>
</MetalCard>
</Box>
))}
</Box>
{/* 커스텀 템플릿 추가 입력 */}
{isCustom && (
<Box sx={{ mt: 2, display: 'flex', flexDirection: 'column', gap: 1.5 }}>
<TextField
label={t('meeting.templateName')}
size="small"
fullWidth
value={customTitle}
onChange={(e) => setCustomTitle(e.target.value)}
disabled={generating}
sx={{
'& .MuiInputBase-root': { fontSize: d3roTypo.compact.size },
}}
/>
<TextField
label={t('meeting.customPrompt')}
size="small"
fullWidth
multiline
rows={3}
value={customPrompt}
onChange={(e) => setCustomPrompt(e.target.value)}
disabled={generating}
placeholder={t('meeting.templatePrompt')}
sx={{
'& .MuiInputBase-root': { fontSize: d3roTypo.compact.size },
}}
/>
</Box>
)}
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2, gap: 1 }}>
<PhysicalButton
size="small"
onClick={handleClose}
disabled={generating}
sx={{ height: 36, fontSize: d3roTypo.engrave.size }}
>
{t('common.cancel')}
</PhysicalButton>
<PhysicalButton
size="small"
onClick={handleGenerate}
disabled={!selectedId || generating}
sx={{
height: 36,
fontSize: d3roTypo.engrave.size,
color: d3roPalette.accent.amber,
}}
>
{generating ? t('meeting.generating') : t('meeting.generate')}
</PhysicalButton>
</DialogActions>
</Dialog>
)
}