Files
sam-react-prod/src/components/board/BoardManagement/BoardDetailClientV2.tsx
유병철 f6551c7e8b feat(WEB): 전체 페이지 ?mode= URL 네비게이션 패턴 적용
- 등록(?mode=new), 상세(?mode=view), 수정(?mode=edit) URL 패턴 일괄 적용
- 중복 패턴 제거: /edit?mode=edit → ?mode=edit (16개 파일)
- 제목 일관성: {기능} 등록/상세/수정 패턴 적용
- 검수 체크리스트 문서 추가 (79개 페이지)
- UniversalListPage, IntegratedDetailTemplate 공통 컴포넌트 개선

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 12:27:43 +09:00

306 lines
9.2 KiB
TypeScript

'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 { 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 { 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}?mode=view`);
} 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 <DetailPageSkeleton sections={1} fieldsPerSection={6} />;
}
// 에러 발생 (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}
/>
<DeleteConfirmDialog
open={deleteDialogOpen}
onOpenChange={setDeleteDialogOpen}
onConfirm={confirmDelete}
title="게시판 삭제"
description={
<>
&quot;{boardData.boardName}&quot; ?
<br />
<span className="text-destructive">
.
</span>
</>
}
loading={isDeleting}
/>
</>
);
}
// 데이터 없음 (should not reach here)
return (
<ErrorCard
type="not-found"
title="게시판을 찾을 수 없습니다"
description="요청하신 게시판 정보가 존재하지 않습니다."
homeButtonLabel="목록으로 이동"
homeButtonHref={BASE_PATH}
/>
);
}