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

@@ -29,7 +29,7 @@ import {
CardHeader,
} from '@/components/ui/card';
import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog';
import { toast } from 'sonner';
import { useDeleteDialog } from '@/hooks/useDeleteDialog';
import { CommentSection } from '../CommentSection';
import { deletePost } from '../actions';
import type { Post, Comment } from '../types';
@@ -46,8 +46,6 @@ interface BoardDetailProps {
export function BoardDetail({ post, comments: initialComments, currentUserId }: BoardDetailProps) {
const router = useRouter();
const sidebarCollapsed = useMenuStore((state) => state.sidebarCollapsed);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [comments, setComments] = useState<Comment[]>(initialComments);
const isMyPost = post.authorId === currentUserId;
@@ -61,25 +59,11 @@ export function BoardDetail({ post, comments: initialComments, currentUserId }:
router.push(`/ko/board/${post.boardCode}/${post.id}?mode=edit`);
}, [router, post.boardCode, post.id]);
const handleConfirmDelete = useCallback(async () => {
setIsDeleting(true);
try {
const result = await deletePost(post.boardCode, post.id);
if (result.success) {
toast.success('게시글이 삭제되었습니다.');
router.push('/ko/board');
} else {
toast.error(result.error || '게시글 삭제에 실패했습니다.');
}
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('게시글 삭제 오류:', error);
toast.error('게시글 삭제에 실패했습니다.');
} finally {
setIsDeleting(false);
setShowDeleteDialog(false);
}
}, [post.boardCode, post.id, router]);
const deleteDialog = useDeleteDialog({
onDelete: async () => deletePost(post.boardCode, post.id),
onSuccess: () => router.push('/ko/board'),
entityName: '게시글',
});
// ===== 댓글 핸들러 =====
// TODO: 댓글 API 연동 (별도 작업)
@@ -211,7 +195,7 @@ export function BoardDetail({ post, comments: initialComments, currentUserId }:
<div className="flex items-center gap-1 md:gap-2">
<Button
variant="outline"
onClick={() => setShowDeleteDialog(true)}
onClick={() => deleteDialog.single.open(post.id)}
size="sm"
className="text-destructive hover:bg-destructive hover:text-destructive-foreground md:size-default"
>
@@ -228,12 +212,12 @@ export function BoardDetail({ post, comments: initialComments, currentUserId }:
{/* 삭제 확인 다이얼로그 */}
<DeleteConfirmDialog
open={showDeleteDialog}
onOpenChange={setShowDeleteDialog}
onConfirm={handleConfirmDelete}
open={deleteDialog.single.isOpen}
onOpenChange={deleteDialog.single.onOpenChange}
onConfirm={deleteDialog.single.confirm}
title="게시글 삭제"
description="정말 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다."
loading={isDeleting}
loading={deleteDialog.isPending}
/>
</PageLayout>
);

View File

@@ -224,10 +224,10 @@ export function BoardList() {
columns: [
{ key: 'no', label: 'No.', className: 'w-[60px] text-center' },
{ key: 'title', label: '제목', className: 'min-w-[300px]' },
{ key: 'author', label: '작성자', className: 'w-[120px]' },
{ key: 'createdAt', label: '등록일', className: 'w-[120px]' },
{ key: 'viewCount', label: '조회수', className: 'w-[80px] text-center' },
{ key: 'title', label: '제목', className: 'min-w-[300px]', sortable: true },
{ key: 'author', label: '작성자', className: 'w-[120px]', sortable: true },
{ key: 'createdAt', label: '등록일', className: 'w-[120px]', sortable: true },
{ key: 'viewCount', label: '조회수', className: 'w-[80px] text-center', sortable: true },
{ key: 'actions', label: '작업', className: 'w-[100px] text-center' },
],

View File

@@ -19,6 +19,7 @@ import { DetailPageSkeleton } from '@/components/ui/skeleton';
import { ErrorCard } from '@/components/ui/error-card';
import { Button } from '@/components/ui/button';
import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog';
import { useDeleteDialog } from '@/hooks/useDeleteDialog';
import { toast } from 'sonner';
import { usePermission } from '@/hooks/usePermission';
@@ -58,8 +59,6 @@ export function BoardDetailClientV2({ boardId, initialMode }: BoardDetailClientV
const [isLoading, setIsLoading] = useState(!isNewMode);
const [error, setError] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
// 데이터 로드
useEffect(() => {
@@ -161,36 +160,17 @@ export function BoardDetailClientV2({ boardId, initialMode }: BoardDetailClientV
};
// 삭제 핸들러
const handleDelete = () => {
setDeleteDialogOpen(true);
};
const confirmDelete = async () => {
if (!boardData) return;
setIsDeleting(true);
try {
const result = await deleteBoard(boardData.id);
const deleteDialog = useDeleteDialog({
onDelete: async (id) => {
const result = await deleteBoard(id);
if (result.success) {
await forceRefreshMenus();
toast.success('게시판이 삭제되었습니다.');
router.push(BASE_PATH);
} else {
setError(result.error || '삭제에 실패했습니다.');
toast.error(result.error || '삭제에 실패했습니다.');
setDeleteDialogOpen(false);
}
} catch (err) {
console.error('게시판 삭제 실패:', err);
setError('게시판 삭제 중 오류가 발생했습니다.');
toast.error('게시판 삭제 중 오류가 발생했습니다.');
setDeleteDialogOpen(false);
} finally {
setIsDeleting(false);
}
};
return result;
},
onSuccess: () => router.push(BASE_PATH),
entityName: '게시판',
});
// 수정 모드 전환
const handleEdit = () => {
@@ -271,13 +251,13 @@ export function BoardDetailClientV2({ boardId, initialMode }: BoardDetailClientV
<BoardDetail
board={boardData}
onEdit={canUpdate ? handleEdit : undefined}
onDelete={canDelete ? handleDelete : undefined}
onDelete={canDelete ? () => deleteDialog.single.open(boardData!.id) : undefined}
/>
<DeleteConfirmDialog
open={deleteDialogOpen}
onOpenChange={setDeleteDialogOpen}
onConfirm={confirmDelete}
open={deleteDialog.single.isOpen}
onOpenChange={deleteDialog.single.onOpenChange}
onConfirm={deleteDialog.single.confirm}
title="게시판 삭제"
description={
<>
@@ -288,7 +268,7 @@ export function BoardDetailClientV2({ boardId, initialMode }: BoardDetailClientV
</span>
</>
}
loading={isDeleting}
loading={deleteDialog.isPending}
/>
</>
);