Files
sam-react-prod/src/components/molecules/GenericCRUDDialog.tsx
유병철 07374c826c refactor(WEB): claudedocs 재정리 및 AuthContext/Zustand/유틸 코드 개선
- claudedocs 폴더 구조 재정리: archive/sessions, guides/migration·mobile·universal-list, refactoring 분류
- 오래된 세션 컨텍스트/체크리스트 문서 정리 (아카이브 이동 또는 삭제)
- AuthContext → authStore(Zustand) 전환 시작, RootProvider 간소화
- GenericCRUDDialog 공통 다이얼로그 컴포넌트 추가
- PermissionDialog 삭제 → GenericCRUDDialog로 대체
- RankDialog/TitleDialog GenericCRUDDialog 기반으로 리팩토링
- toast-utils.ts 삭제 (미사용)
- fileDownload.ts 개선, excel-download.ts 정리
- menuStore/themeStore Zustand 셀렉터 최적화
- useColumnSettings/useTableColumnStore 기능 보강
- 세금계산서/견적/작업자화면/결재 등 소규모 개선

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 17:17:13 +09:00

172 lines
4.9 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Loader2 } from 'lucide-react';
/**
* 필드 정의
*/
export interface CRUDFieldDefinition {
key: string;
label: string;
type: 'text' | 'select';
placeholder?: string;
options?: { value: string; label: string }[];
defaultValue?: string;
}
export interface GenericCRUDDialogProps {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
mode: 'add' | 'edit';
entityName: string;
fields: CRUDFieldDefinition[];
initialData?: Record<string, string>;
onSubmit: (data: Record<string, string>) => void;
isLoading?: boolean;
addLabel?: string;
editLabel?: string;
}
/**
* 단순 CRUD 다이얼로그 공통 컴포넌트
*
* 텍스트 입력 + Select 조합의 단순 폼 다이얼로그를 생성합니다.
* RankDialog, TitleDialog 등 동일 패턴의 다이얼로그를 대체합니다.
*/
export function GenericCRUDDialog({
isOpen,
onOpenChange,
mode,
entityName,
fields,
initialData,
onSubmit,
isLoading = false,
addLabel = '등록',
editLabel = '수정',
}: GenericCRUDDialogProps) {
const [formData, setFormData] = useState<Record<string, string>>({});
useEffect(() => {
if (isOpen) {
if (mode === 'edit' && initialData) {
setFormData({ ...initialData });
} else {
const defaults: Record<string, string> = {};
fields.forEach((f) => {
defaults[f.key] = f.defaultValue ?? '';
});
setFormData(defaults);
}
}
}, [isOpen, mode, initialData, fields]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const firstTextField = fields.find((f) => f.type === 'text');
if (firstTextField && !formData[firstTextField.key]?.trim()) return;
const trimmed: Record<string, string> = {};
Object.entries(formData).forEach(([k, v]) => {
trimmed[k] = v.trim();
});
onSubmit(trimmed);
const defaults: Record<string, string> = {};
fields.forEach((f) => {
defaults[f.key] = f.defaultValue ?? '';
});
setFormData(defaults);
};
const title = mode === 'add' ? `${entityName} 추가` : `${entityName} 수정`;
const submitText = mode === 'add' ? addLabel : editLabel;
const firstTextField = fields.find((f) => f.type === 'text');
const isSubmitDisabled =
isLoading || (firstTextField ? !formData[firstTextField.key]?.trim() : false);
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit}>
<div className="space-y-4 py-4">
{fields.map((field, idx) => (
<div key={field.key} className="space-y-2">
<Label htmlFor={`crud-${field.key}`}>{field.label}</Label>
{field.type === 'text' ? (
<Input
id={`crud-${field.key}`}
value={formData[field.key] ?? ''}
onChange={(e) =>
setFormData((prev) => ({ ...prev, [field.key]: e.target.value }))
}
placeholder={field.placeholder}
autoFocus={idx === 0}
disabled={isLoading}
/>
) : (
<Select
value={formData[field.key] ?? ''}
onValueChange={(value) =>
setFormData((prev) => ({ ...prev, [field.key]: value }))
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{field.options?.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
))}
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isLoading}
>
</Button>
<Button type="submit" disabled={isSubmitDisabled}>
{isLoading ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : null}
{submitText}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}