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:
유병철
2026-02-23 17:17:13 +09:00
parent 6c3572e568
commit 07374c826c
75 changed files with 1704 additions and 1376 deletions

View File

@@ -1,109 +0,0 @@
'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 type { PermissionDialogProps } from './types';
/**
* 권한 추가/수정 다이얼로그
*/
export function PermissionDialog({
isOpen,
onOpenChange,
mode,
permission,
onSubmit
}: PermissionDialogProps) {
const [name, setName] = useState('');
const [status, setStatus] = useState<'active' | 'hidden'>('active');
// 다이얼로그 열릴 때 초기값 설정
useEffect(() => {
if (isOpen) {
if (mode === 'edit' && permission) {
setName(permission.name);
setStatus(permission.status);
} else {
setName('');
setStatus('active');
}
}
}, [isOpen, mode, permission]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (name.trim()) {
onSubmit({ name: name.trim(), status });
setName('');
setStatus('active');
}
};
const dialogTitle = mode === 'add' ? '권한 등록' : '권한 수정';
const submitText = mode === 'add' ? '등록' : '수정';
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{dialogTitle}</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit}>
<div className="space-y-4 py-4">
{/* 권한명 입력 */}
<div className="space-y-2">
<Label htmlFor="permission-name"></Label>
<Input
id="permission-name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="권한명을 입력하세요"
autoFocus
/>
</div>
{/* 상태 선택 */}
<div className="space-y-2">
<Label htmlFor="permission-status"></Label>
<Select value={status} onValueChange={(value: 'active' | 'hidden') => setStatus(value)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="active"></SelectItem>
<SelectItem value="hidden"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button type="submit" disabled={!name.trim()}>
{submitText}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}

View File

@@ -1,19 +1,13 @@
'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 { Loader2 } from 'lucide-react';
import { useMemo } from 'react';
import { GenericCRUDDialog, type CRUDFieldDefinition } from '@/components/molecules/GenericCRUDDialog';
import type { RankDialogProps } from './types';
const RANK_FIELDS: CRUDFieldDefinition[] = [
{ key: 'name', label: '직급명', type: 'text', placeholder: '직급명을 입력하세요' },
];
/**
* 직급 추가/수정 다이얼로그
*/
@@ -25,65 +19,21 @@ export function RankDialog({
onSubmit,
isLoading = false,
}: RankDialogProps) {
const [name, setName] = useState('');
// 다이얼로그 열릴 때 초기값 설정
useEffect(() => {
if (isOpen) {
if (mode === 'edit' && rank) {
setName(rank.name);
} else {
setName('');
}
}
}, [isOpen, mode, rank]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (name.trim()) {
onSubmit(name.trim());
setName('');
}
};
const title = mode === 'add' ? '직급 추가' : '직급 수정';
const submitText = mode === 'add' ? '등록' : '수정';
const initialData = useMemo(
() => (rank ? { name: rank.name } : undefined),
[rank]
);
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">
{/* 직급명 입력 */}
<div className="space-y-2">
<Label htmlFor="rank-name"></Label>
<Input
id="rank-name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="직급명을 입력하세요"
autoFocus
/>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={isLoading}>
</Button>
<Button type="submit" disabled={!name.trim() || isLoading}>
{isLoading ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : null}
{submitText}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<GenericCRUDDialog
isOpen={isOpen}
onOpenChange={onOpenChange}
mode={mode}
entityName="직급"
fields={RANK_FIELDS}
initialData={initialData}
onSubmit={(data) => onSubmit(data.name)}
isLoading={isLoading}
/>
);
}

View File

@@ -1,19 +1,13 @@
'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 { Loader2 } from 'lucide-react';
import { useMemo } from 'react';
import { GenericCRUDDialog, type CRUDFieldDefinition } from '@/components/molecules/GenericCRUDDialog';
import type { TitleDialogProps } from './types';
const TITLE_FIELDS: CRUDFieldDefinition[] = [
{ key: 'name', label: '직책명', type: 'text', placeholder: '직책명을 입력하세요' },
];
/**
* 직책 추가/수정 다이얼로그
*/
@@ -25,66 +19,21 @@ export function TitleDialog({
onSubmit,
isLoading = false,
}: TitleDialogProps) {
const [name, setName] = useState('');
// 다이얼로그 열릴 때 초기값 설정
useEffect(() => {
if (isOpen) {
if (mode === 'edit' && title) {
setName(title.name);
} else {
setName('');
}
}
}, [isOpen, mode, title]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (name.trim()) {
onSubmit(name.trim());
setName('');
}
};
const dialogTitle = mode === 'add' ? '직책 추가' : '직책 수정';
const submitText = mode === 'add' ? '등록' : '수정';
const initialData = useMemo(
() => (title ? { name: title.name } : undefined),
[title]
);
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{dialogTitle}</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit}>
<div className="space-y-4 py-4">
{/* 직책명 입력 */}
<div className="space-y-2">
<Label htmlFor="title-name"></Label>
<Input
id="title-name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="직책명을 입력하세요"
autoFocus
disabled={isLoading}
/>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={isLoading}>
</Button>
<Button type="submit" disabled={!name.trim() || isLoading}>
{isLoading ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : null}
{submitText}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<GenericCRUDDialog
isOpen={isOpen}
onOpenChange={onOpenChange}
mode={mode}
entityName="직책"
fields={TITLE_FIELDS}
initialData={initialData}
onSubmit={(data) => onSubmit(data.name)}
isLoading={isLoading}
/>
);
}