refactor(WEB): 회계/견적/설정/생산 등 전반적 코드 개선 및 공통화 2차

- 회계 모듈 전면 개선: 청구/입금/출금/매입/매출/세금계산서/일반전표/거래처원장 등
- 견적 모듈 금액 포맷/할인/수식/미리보기 등 코드 정리
- 설정 모듈: 계정관리/직급/직책/권한 상세 간소화
- 생산 모듈: 작업지시서/작업자화면/검수 문서 코드 정리
- UniversalListPage 엑셀 다운로드 및 필터 기능 확장
- 대시보드/게시판/수주 등 날짜 유틸 공통화 적용
- claudedocs 문서 인덱스 업데이트

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-02-20 10:45:47 +09:00
parent 71352923c8
commit f344dc7d00
123 changed files with 877 additions and 789 deletions

View File

@@ -19,10 +19,12 @@ import {
SelectValue,
} from '@/components/ui/select';
import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog';
import { useDeleteDialog } from '@/hooks/useDeleteDialog';
import { PageLayout } from '@/components/organisms/PageLayout';
import { PageHeader } from '@/components/organisms/PageHeader';
import { ContentSkeleton } from '@/components/ui/skeleton';
import { toast } from 'sonner';
import { formatAmountWon as formatCurrency } from '@/lib/utils/amount';
import type { Card as CardType, CardFormData, CardStatus } from './types';
import {
CARD_COMPANIES,
@@ -40,10 +42,6 @@ import {
getApprovalFormUrl,
} from './actions';
function formatCurrency(value: number): string {
return value.toLocaleString('ko-KR') + '원';
}
function formatExpiryDate(value: string): string {
if (value && value.length === 4) {
return `${value.slice(0, 2)}/${value.slice(2)}`;
@@ -71,7 +69,6 @@ export function CardDetail({ card, mode: initialMode, isLoading }: CardDetailPro
const router = useRouter();
const searchParams = useSearchParams();
const [mode, setMode] = useState(initialMode);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [isLoadingApproval, setIsLoadingApproval] = useState(false);
const [employees, setEmployees] = useState<Array<{ id: string; label: string }>>([]);
@@ -154,16 +151,11 @@ export function CardDetail({ card, mode: initialMode, isLoading }: CardDetailPro
}
};
const handleConfirmDelete = async () => {
if (!card?.id) return;
const result = await deleteCard(card.id);
if (result.success) {
toast.success('카드가 삭제되었습니다.');
router.push('/ko/hr/card-management');
} else {
toast.error(result.error || '카드 삭제에 실패했습니다.');
}
};
const deleteDialog = useDeleteDialog({
onDelete: async (id) => deleteCard(id),
onSuccess: () => router.push('/ko/hr/card-management'),
entityName: '카드',
});
const handleCancel = () => {
if (isCreateMode) {
@@ -352,7 +344,7 @@ export function CardDetail({ card, mode: initialMode, isLoading }: CardDetailPro
{/* 하단 버튼 */}
<div className="flex items-center justify-end gap-2">
<Button variant="outline" onClick={() => setShowDeleteDialog(true)} className="text-destructive hover:bg-destructive hover:text-destructive-foreground">
<Button variant="outline" onClick={() => deleteDialog.single.open(card!.id)} className="text-destructive hover:bg-destructive hover:text-destructive-foreground">
<Trash2 className="w-4 h-4 mr-2" />
</Button>
@@ -364,8 +356,8 @@ export function CardDetail({ card, mode: initialMode, isLoading }: CardDetailPro
</div>
<DeleteConfirmDialog
open={showDeleteDialog}
onOpenChange={setShowDeleteDialog}
open={deleteDialog.single.isOpen}
onOpenChange={deleteDialog.single.onOpenChange}
description={
<>
?
@@ -375,7 +367,8 @@ export function CardDetail({ card, mode: initialMode, isLoading }: CardDetailPro
</span>
</>
}
onConfirm={handleConfirmDelete}
onConfirm={deleteDialog.single.confirm}
loading={deleteDialog.isPending}
/>
</PageLayout>
);

View File

@@ -41,12 +41,9 @@ import {
CARD_STATUS_COLORS,
getCardCompanyLabel,
} from './types';
import { formatAmountWon as formatCurrency } from '@/lib/utils/amount';
import { getCards, getCardStats } from './actions';
function formatCurrency(value: number): string {
return value.toLocaleString('ko-KR') + '원';
}
export function CardManagement() {
const router = useRouter();
const itemsPerPage = 20;

View File

@@ -23,6 +23,7 @@ import {
USER_ROLE_LABELS,
USER_ACCOUNT_STATUS_LABELS,
} from './types';
import { formatNumber } from '@/lib/utils/amount';
interface EmployeeDetailProps {
employee: Employee;
@@ -87,7 +88,7 @@ export function EmployeeDetail({ employee, onEdit, onDelete }: EmployeeDetailPro
{employee.salary && (
<div>
<dt className="text-sm font-medium text-muted-foreground"></dt>
<dd className="text-sm mt-1">{employee.salary.toLocaleString()}</dd>
<dd className="text-sm mt-1">{formatNumber(employee.salary)}</dd>
</div>
)}
{employee.bankAccount && (

View File

@@ -255,17 +255,17 @@ export function EmployeeManagement() {
// 테이블 컬럼 정의
const tableColumns = useMemo(() => [
{ key: 'rowNumber', label: '번호', className: 'w-[60px] text-center' },
{ key: 'employeeCode', label: '사원코드', className: 'min-w-[100px]' },
{ key: 'department', label: '부서', className: 'min-w-[100px]' },
{ key: 'position', label: '직책', className: 'min-w-[100px]' },
{ key: 'name', label: '이름', className: 'min-w-[80px]' },
{ key: 'rank', label: '직급', className: 'min-w-[80px]' },
{ key: 'phone', label: '휴대폰', className: 'min-w-[120px]' },
{ key: 'email', label: '이메일', className: 'min-w-[150px]' },
{ key: 'hireDate', label: '입사일', className: 'min-w-[100px]' },
{ key: 'status', label: '상태', className: 'min-w-[80px]' },
{ key: 'userId', label: '사용자아이디', className: 'min-w-[100px]' },
{ key: 'userRole', label: '권한', className: 'min-w-[80px]' },
{ key: 'employeeCode', label: '사원코드', className: 'min-w-[100px]', sortable: true },
{ key: 'department', label: '부서', className: 'min-w-[100px]', sortable: true },
{ key: 'position', label: '직책', className: 'min-w-[100px]', sortable: true },
{ key: 'name', label: '이름', className: 'min-w-[80px]', sortable: true },
{ key: 'rank', label: '직급', className: 'min-w-[80px]', sortable: true },
{ key: 'phone', label: '휴대폰', className: 'min-w-[120px]', sortable: true },
{ key: 'email', label: '이메일', className: 'min-w-[150px]', sortable: true },
{ key: 'hireDate', label: '입사일', className: 'min-w-[100px]', sortable: true },
{ key: 'status', label: '상태', className: 'min-w-[80px]', sortable: true },
{ key: 'userId', label: '사용자아이디', className: 'min-w-[100px]', sortable: true },
{ key: 'userRole', label: '권한', className: 'min-w-[80px]', sortable: true },
{ key: 'actions', label: '작업', className: 'w-[100px] text-right' },
], []);

View File

@@ -62,6 +62,7 @@ import {
} from './types';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
import { toast } from 'sonner';
import { formatDate } from '@/lib/utils/date';
// ===== Mock 데이터 생성 (request 탭용 - 신청 현황은 leaves API 사용 예정) =====
@@ -188,7 +189,7 @@ export function VacationManagement() {
position: item.jobTitle || '-', // job_title_label → 직책
rank: item.rank || '-', // json_extra.rank → 직급
vacationType: item.grantType as VacationType,
grantDate: item.grantDate.split('T')[0],
grantDate: formatDate(item.grantDate),
grantDays: item.grantDays,
reason: item.reason || undefined,
createdAt: item.createdAt,
@@ -235,7 +236,7 @@ export function VacationManagement() {
endDate: item.endDate,
vacationDays: item.days,
status: item.status as RequestStatus,
requestDate: item.createdAt.split('T')[0],
requestDate: formatDate(item.createdAt),
createdAt: item.createdAt,
updatedAt: item.updatedAt,
};