feat(WEB): Phase 2-3 V2 마이그레이션 완료 및 ServerErrorPage 적용
Phase 2 완료 (4개): - 노무관리, 단가관리(건설), 입금, 출금 Phase 3 라우팅 구조 변경 완료 (22개): - 거래처(영업), 팝업관리, 공정관리, 게시판관리, 대손추심, Q&A - 현장관리, 실행내역, 견적관리, 견적(테스트) - 입찰관리, 이슈관리, 현장설명회, 견적서(건설) - 협력업체, 시공관리, 기성관리, 품목관리(건설) - 회계 도메인: 거래처, 매출, 세금계산서, 매입 신규 컴포넌트: - ErrorCard: 에러 페이지 UI 통일 - ServerErrorPage: V2 페이지 에러 처리 필수 - V2 Client 컴포넌트 및 Config 파일들 총 47개 상세 페이지 중 28개 완료, 19개 제외/불필요 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
329
src/components/board/BoardManagement/BoardDetailClientV2.tsx
Normal file
329
src/components/board/BoardManagement/BoardDetailClientV2.tsx
Normal file
@@ -0,0 +1,329 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 게시판관리 상세 클라이언트 컴포넌트 V2
|
||||
*
|
||||
* 라우팅 구조 변경: /[id], /[id]/edit, /new → /[id]?mode=view|edit, /new
|
||||
* 기존 BoardDetail, BoardForm 컴포넌트 활용
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { BoardDetail } from './BoardDetail';
|
||||
import { BoardForm } from './BoardForm';
|
||||
import { getBoardById, createBoard, updateBoard, deleteBoard } from './actions';
|
||||
import { forceRefreshMenus } from '@/lib/utils/menuRefresh';
|
||||
import type { Board, BoardFormData } from './types';
|
||||
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
|
||||
import { ErrorCard } from '@/components/ui/error-card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
type DetailMode = 'view' | 'edit' | 'create';
|
||||
|
||||
interface BoardDetailClientV2Props {
|
||||
boardId?: string;
|
||||
initialMode?: DetailMode;
|
||||
}
|
||||
|
||||
const BASE_PATH = '/ko/board/board-management';
|
||||
|
||||
// 게시판 코드 생성 (타임스탬프 기반)
|
||||
const generateBoardCode = (): string => {
|
||||
const timestamp = Date.now().toString(36);
|
||||
const random = Math.random().toString(36).substring(2, 6);
|
||||
return `board_${timestamp}_${random}`;
|
||||
};
|
||||
|
||||
export function BoardDetailClientV2({ boardId, initialMode }: BoardDetailClientV2Props) {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
// URL 쿼리에서 모드 결정
|
||||
const modeFromQuery = searchParams.get('mode') as DetailMode | null;
|
||||
const isNewMode = !boardId || boardId === 'new';
|
||||
|
||||
const [mode, setMode] = useState<DetailMode>(() => {
|
||||
if (isNewMode) return 'create';
|
||||
if (initialMode) return initialMode;
|
||||
if (modeFromQuery === 'edit') return 'edit';
|
||||
return 'view';
|
||||
});
|
||||
|
||||
const [boardData, setBoardData] = useState<Board | null>(null);
|
||||
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(() => {
|
||||
const loadData = async () => {
|
||||
if (isNewMode) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const result = await getBoardById(boardId!);
|
||||
if (result.success && result.data) {
|
||||
setBoardData(result.data);
|
||||
} else {
|
||||
setError(result.error || '게시판 정보를 찾을 수 없습니다.');
|
||||
toast.error('게시판을 불러오는데 실패했습니다.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('게시판 조회 실패:', err);
|
||||
setError('게시판 정보를 불러오는 중 오류가 발생했습니다.');
|
||||
toast.error('게시판을 불러오는데 실패했습니다.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadData();
|
||||
}, [boardId, isNewMode]);
|
||||
|
||||
// URL 쿼리 변경 감지
|
||||
useEffect(() => {
|
||||
if (!isNewMode && modeFromQuery === 'edit') {
|
||||
setMode('edit');
|
||||
} else if (!isNewMode && !modeFromQuery) {
|
||||
setMode('view');
|
||||
}
|
||||
}, [modeFromQuery, isNewMode]);
|
||||
|
||||
// 등록 핸들러
|
||||
const handleCreate = async (data: BoardFormData) => {
|
||||
setIsSubmitting(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const result = await createBoard({
|
||||
...data,
|
||||
boardCode: generateBoardCode(),
|
||||
});
|
||||
|
||||
if (result.success && result.data) {
|
||||
await forceRefreshMenus();
|
||||
toast.success('게시판이 등록되었습니다.');
|
||||
router.push(BASE_PATH);
|
||||
} else {
|
||||
setError(result.error || '게시판 등록에 실패했습니다.');
|
||||
toast.error(result.error || '게시판 등록에 실패했습니다.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('게시판 등록 실패:', err);
|
||||
setError('게시판 등록 중 오류가 발생했습니다.');
|
||||
toast.error('게시판 등록 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 수정 핸들러
|
||||
const handleUpdate = async (data: BoardFormData) => {
|
||||
if (!boardData) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const result = await updateBoard(boardData.id, {
|
||||
...data,
|
||||
boardCode: boardData.boardCode,
|
||||
description: boardData.description,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
await forceRefreshMenus();
|
||||
toast.success('게시판이 수정되었습니다.');
|
||||
router.push(`${BASE_PATH}/${boardData.id}`);
|
||||
} else {
|
||||
setError(result.error || '게시판 수정에 실패했습니다.');
|
||||
toast.error(result.error || '게시판 수정에 실패했습니다.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('게시판 수정 실패:', err);
|
||||
setError('게시판 수정 중 오류가 발생했습니다.');
|
||||
toast.error('게시판 수정 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 삭제 핸들러
|
||||
const handleDelete = () => {
|
||||
setDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
const confirmDelete = async () => {
|
||||
if (!boardData) return;
|
||||
|
||||
setIsDeleting(true);
|
||||
|
||||
try {
|
||||
const result = await deleteBoard(boardData.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);
|
||||
}
|
||||
};
|
||||
|
||||
// 수정 모드 전환
|
||||
const handleEdit = () => {
|
||||
router.push(`${BASE_PATH}/${boardId}?mode=edit`);
|
||||
};
|
||||
|
||||
// 로딩 중
|
||||
if (isLoading) {
|
||||
return <ContentLoadingSpinner text="게시판 정보를 불러오는 중..." />;
|
||||
}
|
||||
|
||||
// 에러 발생 (view/edit 모드에서)
|
||||
if (error && !isNewMode) {
|
||||
return (
|
||||
<ErrorCard
|
||||
type="network"
|
||||
title="게시판 정보를 불러올 수 없습니다"
|
||||
description={error}
|
||||
tips={[
|
||||
'해당 게시판이 존재하는지 확인해주세요',
|
||||
'인터넷 연결 상태를 확인해주세요',
|
||||
'잠시 후 다시 시도해주세요',
|
||||
]}
|
||||
homeButtonLabel="목록으로 이동"
|
||||
homeButtonHref={BASE_PATH}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 등록 모드
|
||||
if (mode === 'create') {
|
||||
return (
|
||||
<>
|
||||
{error && (
|
||||
<div className="mb-4 p-4 bg-destructive/10 border border-destructive/20 rounded-md">
|
||||
<p className="text-sm text-destructive">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
<BoardForm mode="create" onSubmit={handleCreate} />
|
||||
{isSubmitting && (
|
||||
<div className="fixed inset-0 bg-background/50 flex items-center justify-center z-50">
|
||||
<div className="flex items-center gap-2 bg-background p-4 rounded-md shadow-lg">
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
<span>등록 중...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// 수정 모드
|
||||
if (mode === 'edit' && boardData) {
|
||||
return (
|
||||
<>
|
||||
{error && (
|
||||
<div className="mb-4 p-4 bg-destructive/10 border border-destructive/20 rounded-md">
|
||||
<p className="text-sm text-destructive">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
<BoardForm mode="edit" board={boardData} onSubmit={handleUpdate} />
|
||||
{isSubmitting && (
|
||||
<div className="fixed inset-0 bg-background/50 flex items-center justify-center z-50">
|
||||
<div className="flex items-center gap-2 bg-background p-4 rounded-md shadow-lg">
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
<span>저장 중...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// 상세 보기 모드
|
||||
if (mode === 'view' && boardData) {
|
||||
return (
|
||||
<>
|
||||
<BoardDetail
|
||||
board={boardData}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>게시판 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
"{boardData.boardName}" 게시판을 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="text-destructive">
|
||||
삭제된 게시판 정보는 복구할 수 없습니다.
|
||||
</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isDeleting}>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDelete}
|
||||
disabled={isDeleting}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
{isDeleting ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
삭제 중...
|
||||
</>
|
||||
) : (
|
||||
'삭제'
|
||||
)}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// 데이터 없음 (should not reach here)
|
||||
return (
|
||||
<ErrorCard
|
||||
type="not-found"
|
||||
title="게시판을 찾을 수 없습니다"
|
||||
description="요청하신 게시판 정보가 존재하지 않습니다."
|
||||
homeButtonLabel="목록으로 이동"
|
||||
homeButtonHref={BASE_PATH}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
'use client';
|
||||
|
||||
export { BoardDetailClientV2 } from './BoardDetailClientV2';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ClipboardList, Edit, Trash2, Plus } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
Reference in New Issue
Block a user