e70267f738
Build & Deploy to K8s / build-and-deploy (push) Failing after 1m14s
- 음성 인식 (scada-demo/js/voice.js) — 한국어 발화 → 키워드 매핑 → INVYONE_UI.select() · 사이드바 마이크 버튼 + transcript 라벨, 매칭 시 청록 펄스 · Chrome/Edge HTTPS 환경 (운영 siflex.invyone.com OK) - 경고시스템/다중경고 버튼을 음성 인식과 동일 톤 · 🚨 emoji → SVG 삼각형 아이콘, voice-btn 패턴 (다크 솔리드 + 컬러 액센트) · 정적 (반짝 펄스 애니메이션 제거) - client.ts stash pop conflict 정리 (DEV_TENANT_HOST + 도메인 정리 통합) - ui.js 다중 경고 시연 wiring + scada 작업 노트 2건 - 기타 syncthing 보류분 batch (대시보드/레이아웃/로그인 layout 정리) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
181 lines
7.1 KiB
TypeScript
181 lines
7.1 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import { X } from 'lucide-react';
|
|
import { getIconComponent } from '@/components/admin/MenuIconPicker';
|
|
|
|
interface CreateDashboardModalProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
onSubmit: (payload: { name: string; icon: string; is_personal: boolean }) => Promise<void> | void;
|
|
/** 'create' (기본) 는 새 대시보드 생성, 'edit' 은 기존 항목 수정 — 제목/버튼/공유범위 분기 */
|
|
mode?: 'create' | 'edit';
|
|
defaultName?: string;
|
|
defaultIcon?: string;
|
|
defaultIsPersonal?: boolean;
|
|
submitting?: boolean;
|
|
}
|
|
|
|
const ICON_PRESETS = [
|
|
'ClipboardList', 'BarChart3', 'TrendingUp', 'TrendingDown',
|
|
'Package', 'Truck', 'Factory', 'Compass',
|
|
'Map', 'Wrench', 'Settings', 'Folder',
|
|
'Boxes', 'Users', 'Calendar', 'LayoutDashboard',
|
|
];
|
|
|
|
export function CreateDashboardModal({
|
|
open,
|
|
onClose,
|
|
onSubmit,
|
|
mode = 'create',
|
|
defaultName = '',
|
|
defaultIcon = 'ClipboardList',
|
|
defaultIsPersonal = false,
|
|
submitting = false,
|
|
}: CreateDashboardModalProps) {
|
|
const isEdit = mode === 'edit';
|
|
const [name, setName] = useState(defaultName);
|
|
const [icon, setIcon] = useState(defaultIcon);
|
|
const [isPersonal, setIsPersonal] = useState(defaultIsPersonal);
|
|
const nameRef = useRef<HTMLInputElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (open) {
|
|
setName(defaultName);
|
|
setIcon(defaultIcon);
|
|
setIsPersonal(defaultIsPersonal);
|
|
setTimeout(() => nameRef.current?.focus(), 30);
|
|
}
|
|
}, [open, defaultName, defaultIcon, defaultIsPersonal]);
|
|
|
|
if (!open) return null;
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
const trimmed = name.trim();
|
|
if (!trimmed || submitting) return;
|
|
await onSubmit({ name: trimmed, icon, is_personal: isPersonal });
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/40"
|
|
onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}
|
|
>
|
|
<div className="w-[420px] max-w-[92vw] rounded-xl border border-border bg-[var(--v5-surface-solid)] p-5 shadow-[var(--v5-glow-md)]">
|
|
<div className="mb-4 flex items-center justify-between">
|
|
<h3 className="text-[0.95rem] font-bold text-foreground">
|
|
{isEdit ? '대시보드 수정' : '새 대시보드 만들기'}
|
|
</h3>
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
className="rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
>
|
|
<X size={16} />
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<div>
|
|
<label className="mb-1.5 block text-[0.7rem] font-semibold uppercase tracking-wide text-muted-foreground">
|
|
이름
|
|
</label>
|
|
<input
|
|
ref={nameRef}
|
|
type="text"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="예: 수주 관리"
|
|
className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground outline-none focus:border-[var(--v5-primary)]"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="mb-1.5 block text-[0.7rem] font-semibold uppercase tracking-wide text-muted-foreground">
|
|
아이콘
|
|
</label>
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{ICON_PRESETS.map((iconName) => {
|
|
const Ico = getIconComponent(iconName);
|
|
const selected = icon === iconName;
|
|
return (
|
|
<button
|
|
key={iconName}
|
|
type="button"
|
|
onClick={() => setIcon(iconName)}
|
|
className={`flex h-9 w-9 items-center justify-center rounded-md border transition-colors ${
|
|
selected
|
|
? 'border-[var(--v5-primary)] bg-[var(--v5-primary)]/10 text-[var(--v5-primary)]'
|
|
: 'border-border text-muted-foreground hover:bg-accent hover:text-foreground'
|
|
}`}
|
|
aria-label={`아이콘 ${iconName}`}
|
|
>
|
|
{Ico ? <Ico className="h-4 w-4" /> : null}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="mb-1.5 block text-[0.7rem] font-semibold uppercase tracking-wide text-muted-foreground">
|
|
공유 범위
|
|
{isEdit && <span className="ml-2 normal-case tracking-normal text-muted-foreground">(생성 후 변경 불가)</span>}
|
|
</label>
|
|
<div className={`space-y-1.5${isEdit ? ' opacity-60' : ''}`}>
|
|
<label className={`flex items-start gap-2 rounded-md border border-border p-2.5 ${isEdit ? 'cursor-not-allowed' : 'cursor-pointer hover:bg-accent/40'}`}>
|
|
<input
|
|
type="radio"
|
|
name="scope"
|
|
checked={!isPersonal}
|
|
onChange={() => setIsPersonal(false)}
|
|
disabled={isEdit}
|
|
className="mt-0.5 accent-[var(--v5-primary)]"
|
|
/>
|
|
<div className="flex-1">
|
|
<div className="text-sm font-medium text-foreground">회사 전체 공용</div>
|
|
<div className="text-xs text-muted-foreground">같은 회사 사용자 모두가 볼 수 있습니다 (기본)</div>
|
|
</div>
|
|
</label>
|
|
<label className={`flex items-start gap-2 rounded-md border border-border p-2.5 ${isEdit ? 'cursor-not-allowed' : 'cursor-pointer hover:bg-accent/40'}`}>
|
|
<input
|
|
type="radio"
|
|
name="scope"
|
|
checked={isPersonal}
|
|
onChange={() => setIsPersonal(true)}
|
|
disabled={isEdit}
|
|
className="mt-0.5 accent-[var(--v5-primary)]"
|
|
/>
|
|
<div className="flex-1">
|
|
<div className="text-sm font-medium text-foreground">나만 보기 (개인 대시보드)</div>
|
|
<div className="text-xs text-muted-foreground">내 사이드바에만 표시됩니다</div>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-2 pt-1">
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
disabled={submitting}
|
|
className="rounded-md border border-border bg-background px-3 py-1.5 text-sm hover:bg-accent disabled:opacity-50"
|
|
>
|
|
취소
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={submitting || !name.trim()}
|
|
className="rounded-md bg-[var(--v5-primary)] px-3 py-1.5 text-sm font-semibold text-white hover:opacity-90 disabled:opacity-50"
|
|
>
|
|
{submitting ? (isEdit ? '저장 중...' : '생성 중...') : isEdit ? '저장' : '만들기'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|