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>
This commit is contained in:
171
src/components/molecules/GenericCRUDDialog.tsx
Normal file
171
src/components/molecules/GenericCRUDDialog.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -10,4 +10,7 @@ export { StandardDialog } from "./StandardDialog";
|
||||
export type { StandardDialogProps } from "./StandardDialog";
|
||||
|
||||
export { YearQuarterFilter } from "./YearQuarterFilter";
|
||||
export type { Quarter } from "./YearQuarterFilter";
|
||||
export type { Quarter } from "./YearQuarterFilter";
|
||||
|
||||
export { GenericCRUDDialog } from "./GenericCRUDDialog";
|
||||
export type { GenericCRUDDialogProps, CRUDFieldDefinition } from "./GenericCRUDDialog";
|
||||
Reference in New Issue
Block a user