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:
@@ -1,58 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { use, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { getBadDebtById } from '@/components/accounting/BadDebtCollection/actions';
|
||||
import { BadDebtDetail } from '@/components/accounting/BadDebtCollection/BadDebtDetail';
|
||||
import type { BadDebtRecord } from '@/components/accounting/BadDebtCollection/types';
|
||||
|
||||
interface EditBadDebtPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 하위 호환성을 위한 리다이렉트 페이지
|
||||
* /bad-debt-collection/[id]/edit → /bad-debt-collection/[id]?mode=edit
|
||||
*/
|
||||
export default function EditBadDebtPage({ params }: EditBadDebtPageProps) {
|
||||
const { id } = use(params);
|
||||
const router = useRouter();
|
||||
const [data, setData] = useState<BadDebtRecord | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getBadDebtById(id)
|
||||
.then(result => {
|
||||
if (result) {
|
||||
setData(result);
|
||||
} else {
|
||||
setError('데이터를 찾을 수 없습니다.');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setError('데이터를 불러오는 중 오류가 발생했습니다.');
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [id]);
|
||||
router.replace(`/ko/accounting/bad-debt-collection/${id}?mode=edit`);
|
||||
}, [id, router]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !data) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
|
||||
<div className="text-muted-foreground">{error || '데이터를 찾을 수 없습니다.'}</div>
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
뒤로 가기
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <BadDebtDetail mode="edit" recordId={id} initialData={data} />;
|
||||
}
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">리다이렉트 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { getBadDebtById } from '@/components/accounting/BadDebtCollection/actions';
|
||||
import { BadDebtDetail } from '@/components/accounting/BadDebtCollection/BadDebtDetail';
|
||||
import type { BadDebtRecord } from '@/components/accounting/BadDebtCollection/types';
|
||||
import { use } from 'react';
|
||||
import { BadDebtDetailClientV2 } from '@/components/accounting/BadDebtCollection';
|
||||
|
||||
interface BadDebtDetailPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
@@ -12,47 +9,5 @@ interface BadDebtDetailPageProps {
|
||||
|
||||
export default function BadDebtDetailPage({ params }: BadDebtDetailPageProps) {
|
||||
const { id } = use(params);
|
||||
const router = useRouter();
|
||||
const [data, setData] = useState<BadDebtRecord | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getBadDebtById(id)
|
||||
.then(result => {
|
||||
if (result) {
|
||||
setData(result);
|
||||
} else {
|
||||
setError('데이터를 찾을 수 없습니다.');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setError('데이터를 불러오는 중 오류가 발생했습니다.');
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [id]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !data) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
|
||||
<div className="text-muted-foreground">{error || '데이터를 찾을 수 없습니다.'}</div>
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
뒤로 가기
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <BadDebtDetail mode="view" recordId={id} initialData={data} />;
|
||||
}
|
||||
return <BadDebtDetailClientV2 recordId={id} />;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { BadDebtDetail } from '@/components/accounting/BadDebtCollection/BadDebtDetail';
|
||||
import { BadDebtDetailClientV2 } from '@/components/accounting/BadDebtCollection';
|
||||
|
||||
export default function NewBadDebtPage() {
|
||||
return <BadDebtDetail mode="new" />;
|
||||
}
|
||||
return <BadDebtDetailClientV2 />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { use } from 'react';
|
||||
import DepositDetailClientV2 from '@/components/accounting/DepositManagement/DepositDetailClientV2';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default function DepositEditPage({ params }: PageProps) {
|
||||
const { id } = use(params);
|
||||
|
||||
return <DepositDetailClientV2 depositId={id} initialMode="edit" />;
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { useParams, useSearchParams } from 'next/navigation';
|
||||
import { DepositDetail } from '@/components/accounting/DepositManagement/DepositDetail';
|
||||
import { use } from 'react';
|
||||
import DepositDetailClientV2 from '@/components/accounting/DepositManagement/DepositDetailClientV2';
|
||||
|
||||
export default function DepositDetailPage() {
|
||||
const params = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
const depositId = params.id as string;
|
||||
const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view';
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
return <DepositDetail depositId={depositId} mode={mode} />;
|
||||
export default function DepositDetailPage({ params }: PageProps) {
|
||||
const { id } = use(params);
|
||||
|
||||
return <DepositDetailClientV2 depositId={id} initialMode="view" />;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import DepositDetailClientV2 from '@/components/accounting/DepositManagement/DepositDetailClientV2';
|
||||
|
||||
export default function DepositNewPage() {
|
||||
return <DepositDetailClientV2 initialMode="create" />;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { use } from 'react';
|
||||
import WithdrawalDetailClientV2 from '@/components/accounting/WithdrawalManagement/WithdrawalDetailClientV2';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default function WithdrawalEditPage({ params }: PageProps) {
|
||||
const { id } = use(params);
|
||||
|
||||
return <WithdrawalDetailClientV2 withdrawalId={id} initialMode="edit" />;
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { useParams, useSearchParams } from 'next/navigation';
|
||||
import { WithdrawalDetail } from '@/components/accounting/WithdrawalManagement/WithdrawalDetail';
|
||||
import { use } from 'react';
|
||||
import WithdrawalDetailClientV2 from '@/components/accounting/WithdrawalManagement/WithdrawalDetailClientV2';
|
||||
|
||||
export default function WithdrawalDetailPage() {
|
||||
const params = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
const withdrawalId = params.id as string;
|
||||
const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view';
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
return <WithdrawalDetail withdrawalId={withdrawalId} mode={mode} />;
|
||||
export default function WithdrawalDetailPage({ params }: PageProps) {
|
||||
const { id } = use(params);
|
||||
|
||||
return <WithdrawalDetailClientV2 withdrawalId={id} initialMode="view" />;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import WithdrawalDetailClientV2 from '@/components/accounting/WithdrawalManagement/WithdrawalDetailClientV2';
|
||||
|
||||
export default function WithdrawalNewPage() {
|
||||
return <WithdrawalDetailClientV2 initialMode="create" />;
|
||||
}
|
||||
@@ -1,116 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
|
||||
import { BoardForm } from '@/components/board/BoardManagement/BoardForm';
|
||||
import { getBoardById, updateBoard } from '@/components/board/BoardManagement/actions';
|
||||
import { forceRefreshMenus } from '@/lib/utils/menuRefresh';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import type { Board, BoardFormData } from '@/components/board/BoardManagement/types';
|
||||
/**
|
||||
* 게시판관리 수정 페이지 (리다이렉트)
|
||||
*
|
||||
* 기존 URL 호환성을 위해 유지
|
||||
* /[id]/edit → /[id]?mode=edit 로 리다이렉트
|
||||
*/
|
||||
|
||||
export default function BoardEditPage() {
|
||||
import { useEffect } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
|
||||
|
||||
export default function BoardEditRedirectPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const [board, setBoard] = useState<Board | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const fetchBoard = useCallback(async () => {
|
||||
const id = params.id as string;
|
||||
if (!id) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const result = await getBoardById(id);
|
||||
|
||||
if (result.success && result.data) {
|
||||
setBoard(result.data);
|
||||
} else {
|
||||
setError(result.error || '게시판 정보를 불러오는데 실패했습니다.');
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
}, [params.id]);
|
||||
const id = params.id as string;
|
||||
|
||||
useEffect(() => {
|
||||
fetchBoard();
|
||||
}, [fetchBoard]);
|
||||
router.replace(`/ko/board/board-management/${id}?mode=edit`);
|
||||
}, [router, id]);
|
||||
|
||||
const handleSubmit = async (data: BoardFormData) => {
|
||||
if (!board) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
setError(null);
|
||||
|
||||
const result = await updateBoard(board.id, {
|
||||
...data,
|
||||
boardCode: board.boardCode,
|
||||
description: board.description,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
// 게시판 수정 성공 시 메뉴 즉시 갱신
|
||||
await forceRefreshMenus();
|
||||
router.push(`/ko/board/board-management/${board.id}`);
|
||||
} else {
|
||||
setError(result.error || '수정에 실패했습니다.');
|
||||
}
|
||||
|
||||
setIsSubmitting(false);
|
||||
};
|
||||
|
||||
// 로딩 상태
|
||||
if (isLoading) {
|
||||
return <ContentLoadingSpinner text="게시판 정보를 불러오는 중..." />;
|
||||
}
|
||||
|
||||
// 에러 상태
|
||||
if (error && !board) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
|
||||
<p className="text-destructive">{error}</p>
|
||||
<Button onClick={() => router.push('/ko/board/board-management')} variant="outline">
|
||||
목록으로
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!board) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
|
||||
<p className="text-destructive">게시판을 찾을 수 없습니다.</p>
|
||||
<Button onClick={() => router.push('/ko/board/board-management')} variant="outline">
|
||||
목록으로
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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={board}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
{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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
return <ContentLoadingSpinner text="페이지 이동 중..." />;
|
||||
}
|
||||
|
||||
@@ -1,134 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
|
||||
import { BoardDetail } from '@/components/board/BoardManagement/BoardDetail';
|
||||
import { getBoardById, deleteBoard } from '@/components/board/BoardManagement/actions';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import type { Board } from '@/components/board/BoardManagement/types';
|
||||
/**
|
||||
* 게시판관리 상세/수정 페이지
|
||||
*
|
||||
* IntegratedDetailTemplate V2 마이그레이션
|
||||
* - /[id] → 상세 보기 (view 모드)
|
||||
* - /[id]?mode=edit → 수정 (edit 모드)
|
||||
*/
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import { BoardDetailClientV2 } from '@/components/board/BoardManagement/BoardDetailClientV2';
|
||||
|
||||
export default function BoardDetailPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const [board, setBoard] = useState<Board | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const id = params.id as string;
|
||||
|
||||
const fetchBoard = useCallback(async () => {
|
||||
const id = params.id as string;
|
||||
if (!id) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const result = await getBoardById(id);
|
||||
|
||||
if (result.success && result.data) {
|
||||
setBoard(result.data);
|
||||
} else {
|
||||
setError(result.error || '게시판 정보를 불러오는데 실패했습니다.');
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
}, [params.id]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchBoard();
|
||||
}, [fetchBoard]);
|
||||
|
||||
const handleEdit = () => {
|
||||
router.push(`/ko/board/board-management/${params.id}/edit`);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
setDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
const confirmDelete = async () => {
|
||||
if (!board) return;
|
||||
|
||||
setIsDeleting(true);
|
||||
const result = await deleteBoard(board.id);
|
||||
|
||||
if (result.success) {
|
||||
router.push('/ko/board/board-management');
|
||||
} else {
|
||||
setError(result.error || '삭제에 실패했습니다.');
|
||||
setDeleteDialogOpen(false);
|
||||
}
|
||||
setIsDeleting(false);
|
||||
};
|
||||
|
||||
// 로딩 상태
|
||||
if (isLoading) {
|
||||
return <ContentLoadingSpinner text="게시판 정보를 불러오는 중..." />;
|
||||
}
|
||||
|
||||
// 에러 상태
|
||||
if (error || !board) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
|
||||
<p className="text-destructive">{error || '게시판을 찾을 수 없습니다.'}</p>
|
||||
<Button onClick={() => router.push('/ko/board/board-management')} variant="outline">
|
||||
목록으로
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<BoardDetail
|
||||
board={board}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>게시판 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
"{board.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>
|
||||
</>
|
||||
);
|
||||
return <BoardDetailClientV2 boardId={id} />;
|
||||
}
|
||||
|
||||
@@ -1,64 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { BoardForm } from '@/components/board/BoardManagement/BoardForm';
|
||||
import { createBoard } from '@/components/board/BoardManagement/actions';
|
||||
import { forceRefreshMenus } from '@/lib/utils/menuRefresh';
|
||||
import type { BoardFormData } from '@/components/board/BoardManagement/types';
|
||||
/**
|
||||
* 게시판관리 등록 페이지
|
||||
*
|
||||
* IntegratedDetailTemplate V2 마이그레이션
|
||||
*/
|
||||
|
||||
// 게시판 코드 생성 (임시: 타임스탬프 기반)
|
||||
const generateBoardCode = (): string => {
|
||||
const timestamp = Date.now().toString(36);
|
||||
const random = Math.random().toString(36).substring(2, 6);
|
||||
return `board_${timestamp}_${random}`;
|
||||
};
|
||||
import { BoardDetailClientV2 } from '@/components/board/BoardManagement/BoardDetailClientV2';
|
||||
|
||||
export default function BoardNewPage() {
|
||||
const router = useRouter();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const handleSubmit = async (data: BoardFormData) => {
|
||||
setIsSubmitting(true);
|
||||
setError(null);
|
||||
|
||||
const result = await createBoard({
|
||||
...data,
|
||||
boardCode: generateBoardCode(),
|
||||
});
|
||||
|
||||
if (result.success && result.data) {
|
||||
// 게시판 생성 성공 시 메뉴 즉시 갱신
|
||||
await forceRefreshMenus();
|
||||
router.push('/ko/board/board-management');
|
||||
} else {
|
||||
setError(result.error || '게시판 생성에 실패했습니다.');
|
||||
}
|
||||
|
||||
setIsSubmitting(false);
|
||||
};
|
||||
|
||||
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={handleSubmit}
|
||||
/>
|
||||
{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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
export default function BoardCreatePage() {
|
||||
return <BoardDetailClientV2 boardId="new" />;
|
||||
}
|
||||
|
||||
@@ -1,57 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { use, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ProgressBillingDetailForm } from '@/components/business/construction/progress-billing';
|
||||
import { getProgressBillingDetail } from '@/components/business/construction/progress-billing/actions';
|
||||
|
||||
interface ProgressBillingEditPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 하위 호환성을 위한 리다이렉트 페이지
|
||||
* /progress-billing-management/[id]/edit → /progress-billing-management/[id]?mode=edit
|
||||
*/
|
||||
export default function ProgressBillingEditPage({ params }: ProgressBillingEditPageProps) {
|
||||
const { id } = use(params);
|
||||
const router = useRouter();
|
||||
const [data, setData] = useState<Awaited<ReturnType<typeof getProgressBillingDetail>>['data']>(undefined);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getProgressBillingDetail(id)
|
||||
.then(result => {
|
||||
if (result.success && result.data) {
|
||||
setData(result.data);
|
||||
} else {
|
||||
setError('기성청구 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setError('기성청구 정보를 불러오는 중 오류가 발생했습니다.');
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [id]);
|
||||
router.replace(`/ko/construction/billing/progress-billing-management/${id}?mode=edit`);
|
||||
}, [id, router]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !data) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
|
||||
<div className="text-muted-foreground">{error || '기성청구 정보를 찾을 수 없습니다.'}</div>
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
뒤로 가기
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <ProgressBillingDetailForm mode="edit" billingId={id} initialData={data} />;
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">리다이렉트 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { use, useEffect, useState, useCallback } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { ProgressBillingDetailForm } from '@/components/business/construction/progress-billing';
|
||||
import { getProgressBillingDetail } from '@/components/business/construction/progress-billing/actions';
|
||||
import { ServerErrorPage } from '@/components/common/ServerErrorPage';
|
||||
|
||||
interface ProgressBillingDetailPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
@@ -11,12 +12,19 @@ interface ProgressBillingDetailPageProps {
|
||||
|
||||
export default function ProgressBillingDetailPage({ params }: ProgressBillingDetailPageProps) {
|
||||
const { id } = use(params);
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
// V2 패턴: mode 체크
|
||||
const mode = searchParams.get('mode') || 'view';
|
||||
const isEditMode = mode === 'edit';
|
||||
|
||||
const [data, setData] = useState<Awaited<ReturnType<typeof getProgressBillingDetail>>['data']>(undefined);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = useCallback(() => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
getProgressBillingDetail(id)
|
||||
.then(result => {
|
||||
if (result.success && result.data) {
|
||||
@@ -31,6 +39,10 @@ export default function ProgressBillingDetailPage({ params }: ProgressBillingDet
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
@@ -41,17 +53,21 @@ export default function ProgressBillingDetailPage({ params }: ProgressBillingDet
|
||||
|
||||
if (error || !data) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
|
||||
<div className="text-muted-foreground">{error || '기성청구 정보를 찾을 수 없습니다.'}</div>
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
뒤로 가기
|
||||
</button>
|
||||
</div>
|
||||
<ServerErrorPage
|
||||
title="기성청구 정보를 불러올 수 없습니다"
|
||||
message={error || '기성청구 정보를 찾을 수 없습니다.'}
|
||||
onRetry={fetchData}
|
||||
showBackButton={true}
|
||||
showHomeButton={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <ProgressBillingDetailForm mode="view" billingId={id} initialData={data} />;
|
||||
return (
|
||||
<ProgressBillingDetailForm
|
||||
mode={isEditMode ? 'edit' : 'view'}
|
||||
billingId={id}
|
||||
initialData={data}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
import { use } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { LaborDetailClient } from '@/components/business/construction/labor-management';
|
||||
import { LaborDetailClientV2 } from '@/components/business/construction/labor-management';
|
||||
import type { DetailMode } from '@/components/templates/IntegratedDetailTemplate';
|
||||
|
||||
interface LaborDetailPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
@@ -12,7 +13,7 @@ export default function LaborDetailPage({ params }: LaborDetailPageProps) {
|
||||
const { id } = use(params);
|
||||
const searchParams = useSearchParams();
|
||||
const mode = searchParams.get('mode');
|
||||
const isEditMode = mode === 'edit';
|
||||
const initialMode: DetailMode = mode === 'edit' ? 'edit' : 'view';
|
||||
|
||||
return <LaborDetailClient laborId={id} isEditMode={isEditMode} />;
|
||||
return <LaborDetailClientV2 laborId={id} initialMode={initialMode} />;
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { LaborDetailClient } from '@/components/business/construction/labor-management';
|
||||
'use client';
|
||||
|
||||
import { LaborDetailClientV2 } from '@/components/business/construction/labor-management';
|
||||
|
||||
export default function LaborNewPage() {
|
||||
return <LaborDetailClient isNewMode />;
|
||||
return <LaborDetailClientV2 initialMode="create" />;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { use } from 'react';
|
||||
import PricingDetailClient from '@/components/business/construction/pricing-management/PricingDetailClient';
|
||||
import { PricingDetailClientV2 } from '@/components/business/construction/pricing-management';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
@@ -10,5 +10,5 @@ interface PageProps {
|
||||
export default function PricingEditPage({ params }: PageProps) {
|
||||
const { id } = use(params);
|
||||
|
||||
return <PricingDetailClient id={id} mode="edit" />;
|
||||
return <PricingDetailClientV2 pricingId={id} initialMode="edit" />;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { use } from 'react';
|
||||
import PricingDetailClient from '@/components/business/construction/pricing-management/PricingDetailClient';
|
||||
import { PricingDetailClientV2 } from '@/components/business/construction/pricing-management';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
@@ -10,5 +10,5 @@ interface PageProps {
|
||||
export default function PricingDetailPage({ params }: PageProps) {
|
||||
const { id } = use(params);
|
||||
|
||||
return <PricingDetailClient id={id} mode="view" />;
|
||||
return <PricingDetailClientV2 pricingId={id} initialMode="view" />;
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import PricingDetailClient from '@/components/business/construction/pricing-management/PricingDetailClient';
|
||||
'use client';
|
||||
|
||||
import { PricingDetailClientV2 } from '@/components/business/construction/pricing-management';
|
||||
|
||||
export default function PricingNewPage() {
|
||||
return <PricingDetailClient mode="create" />;
|
||||
return <PricingDetailClientV2 initialMode="create" />;
|
||||
}
|
||||
@@ -1,43 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import SiteDetailForm from '@/components/business/construction/site-management/SiteDetailForm';
|
||||
import { use, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
// 목업 데이터
|
||||
const MOCK_SITE = {
|
||||
id: '1',
|
||||
siteCode: '123-12-12345',
|
||||
partnerId: '1',
|
||||
partnerName: '거래처명',
|
||||
siteName: '현장명',
|
||||
address: '',
|
||||
status: 'active' as const,
|
||||
createdAt: '2025-09-01T00:00:00Z',
|
||||
updatedAt: '2025-09-01T00:00:00Z',
|
||||
};
|
||||
|
||||
interface PageProps {
|
||||
interface EditSitePageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default function SiteEditPage({ params }: PageProps) {
|
||||
/**
|
||||
* 하위 호환성을 위한 리다이렉트 페이지
|
||||
* /site-management/[id]/edit → /site-management/[id]?mode=edit
|
||||
*/
|
||||
export default function EditSitePage({ params }: EditSitePageProps) {
|
||||
const { id } = use(params);
|
||||
const [site, setSite] = useState<typeof MOCK_SITE | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: API에서 현장 정보 조회
|
||||
setSite({ ...MOCK_SITE, id });
|
||||
setIsLoading(false);
|
||||
}, [id]);
|
||||
router.replace(`/ko/construction/order/site-management/${id}?mode=edit`);
|
||||
}, [id, router]);
|
||||
|
||||
if (isLoading || !site) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <SiteDetailForm site={site} mode="edit" />;
|
||||
}
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">리다이렉트 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,43 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import SiteDetailForm from '@/components/business/construction/site-management/SiteDetailForm';
|
||||
/**
|
||||
* 현장관리 상세 페이지 (V2)
|
||||
*
|
||||
* /site-management/[id] → 조회 모드
|
||||
* /site-management/[id]?mode=edit → 수정 모드
|
||||
*/
|
||||
|
||||
// 목업 데이터
|
||||
const MOCK_SITE = {
|
||||
id: '1',
|
||||
siteCode: '123-12-12345',
|
||||
partnerId: '1',
|
||||
partnerName: '거래처명',
|
||||
siteName: '현장명',
|
||||
address: '',
|
||||
status: 'active' as const,
|
||||
createdAt: '2025-09-01T00:00:00Z',
|
||||
updatedAt: '2025-09-01T00:00:00Z',
|
||||
};
|
||||
import { use } from 'react';
|
||||
import { SiteDetailClientV2 } from '@/components/business/construction/site-management/SiteDetailClientV2';
|
||||
|
||||
interface PageProps {
|
||||
interface SiteDetailPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default function SiteDetailPage({ params }: PageProps) {
|
||||
export default function SiteDetailPage({ params }: SiteDetailPageProps) {
|
||||
const { id } = use(params);
|
||||
const [site, setSite] = useState<typeof MOCK_SITE | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: API에서 현장 정보 조회
|
||||
setSite({ ...MOCK_SITE, id });
|
||||
setIsLoading(false);
|
||||
}, [id]);
|
||||
|
||||
if (isLoading || !site) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <SiteDetailForm site={site} mode="view" />;
|
||||
}
|
||||
return <SiteDetailClientV2 siteId={id} />;
|
||||
}
|
||||
|
||||
@@ -1,48 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import StructureReviewDetailForm from '@/components/business/construction/structure-review/StructureReviewDetailForm';
|
||||
import { use, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
// 목업 데이터
|
||||
const MOCK_REVIEW = {
|
||||
id: '1',
|
||||
reviewNumber: '123123',
|
||||
partnerId: '1',
|
||||
partnerName: '거래처명A',
|
||||
siteId: '1',
|
||||
siteName: '현장A',
|
||||
requestDate: '2025-12-12',
|
||||
reviewCompany: '회사명',
|
||||
reviewerName: '홍길동',
|
||||
reviewDate: '2025-12-15',
|
||||
completionDate: null,
|
||||
status: 'pending' as const,
|
||||
createdAt: '2025-12-01T00:00:00Z',
|
||||
updatedAt: '2025-12-01T00:00:00Z',
|
||||
};
|
||||
|
||||
interface PageProps {
|
||||
interface EditStructureReviewPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default function StructureReviewEditPage({ params }: PageProps) {
|
||||
/**
|
||||
* 하위 호환성을 위한 리다이렉트 페이지
|
||||
* /structure-review/[id]/edit → /structure-review/[id]?mode=edit
|
||||
*/
|
||||
export default function EditStructureReviewPage({ params }: EditStructureReviewPageProps) {
|
||||
const { id } = use(params);
|
||||
const [review, setReview] = useState<typeof MOCK_REVIEW | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: API에서 구조검토 정보 조회
|
||||
setReview({ ...MOCK_REVIEW, id });
|
||||
setIsLoading(false);
|
||||
}, [id]);
|
||||
router.replace(`/ko/construction/order/structure-review/${id}?mode=edit`);
|
||||
}, [id, router]);
|
||||
|
||||
if (isLoading || !review) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <StructureReviewDetailForm review={review} mode="edit" />;
|
||||
}
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">리다이렉트 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,48 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import StructureReviewDetailForm from '@/components/business/construction/structure-review/StructureReviewDetailForm';
|
||||
/**
|
||||
* 구조검토 상세 페이지 (V2)
|
||||
*
|
||||
* /structure-review/[id] → 조회 모드
|
||||
* /structure-review/[id]?mode=edit → 수정 모드
|
||||
*/
|
||||
|
||||
// 목업 데이터
|
||||
const MOCK_REVIEW = {
|
||||
id: '1',
|
||||
reviewNumber: '123123',
|
||||
partnerId: '1',
|
||||
partnerName: '거래처명A',
|
||||
siteId: '1',
|
||||
siteName: '현장A',
|
||||
requestDate: '2025-12-12',
|
||||
reviewCompany: '회사명',
|
||||
reviewerName: '홍길동',
|
||||
reviewDate: '2025-12-15',
|
||||
completionDate: null,
|
||||
status: 'pending' as const,
|
||||
createdAt: '2025-12-01T00:00:00Z',
|
||||
updatedAt: '2025-12-01T00:00:00Z',
|
||||
};
|
||||
import { use } from 'react';
|
||||
import { StructureReviewDetailClientV2 } from '@/components/business/construction/structure-review/StructureReviewDetailClientV2';
|
||||
|
||||
interface PageProps {
|
||||
interface StructureReviewDetailPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default function StructureReviewDetailPage({ params }: PageProps) {
|
||||
export default function StructureReviewDetailPage({ params }: StructureReviewDetailPageProps) {
|
||||
const { id } = use(params);
|
||||
const [review, setReview] = useState<typeof MOCK_REVIEW | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: API에서 구조검토 정보 조회
|
||||
setReview({ ...MOCK_REVIEW, id });
|
||||
setIsLoading(false);
|
||||
}, [id]);
|
||||
|
||||
if (isLoading || !review) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <StructureReviewDetailForm review={review} mode="view" />;
|
||||
return <StructureReviewDetailClientV2 reviewId={id} />;
|
||||
}
|
||||
|
||||
@@ -1,38 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { BiddingDetailForm, getBiddingDetail } from '@/components/business/construction/bidding';
|
||||
import { use, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
interface BiddingEditPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 하위 호환성을 위한 리다이렉트 페이지
|
||||
* /bidding/[id]/edit → /bidding/[id]?mode=edit
|
||||
*/
|
||||
export default function BiddingEditPage({ params }: BiddingEditPageProps) {
|
||||
const { id } = use(params);
|
||||
const [data, setData] = useState<Awaited<ReturnType<typeof getBiddingDetail>>['data']>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
getBiddingDetail(id)
|
||||
.then(result => {
|
||||
setData(result.data);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [id]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
router.replace(`/ko/construction/project/bidding/${id}?mode=edit`);
|
||||
}, [id, router]);
|
||||
|
||||
return (
|
||||
<BiddingDetailForm
|
||||
mode="edit"
|
||||
biddingId={id}
|
||||
initialData={data}
|
||||
/>
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">리다이렉트 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { use, useEffect, useState, useCallback } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { BiddingDetailForm, getBiddingDetail } from '@/components/business/construction/bidding';
|
||||
import { ServerErrorPage } from '@/components/common/ServerErrorPage';
|
||||
|
||||
interface BiddingDetailPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
@@ -9,17 +11,37 @@ interface BiddingDetailPageProps {
|
||||
|
||||
export default function BiddingDetailPage({ params }: BiddingDetailPageProps) {
|
||||
const { id } = use(params);
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
// V2 패턴: mode 체크
|
||||
const mode = searchParams.get('mode') || 'view';
|
||||
const isEditMode = mode === 'edit';
|
||||
|
||||
const [data, setData] = useState<Awaited<ReturnType<typeof getBiddingDetail>>['data']>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = useCallback(() => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
getBiddingDetail(id)
|
||||
.then(result => {
|
||||
setData(result.data);
|
||||
if (result.success && result.data) {
|
||||
setData(result.data);
|
||||
} else {
|
||||
setError('입찰 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setError('입찰 정보를 불러오는 중 오류가 발생했습니다.');
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
@@ -28,9 +50,21 @@ export default function BiddingDetailPage({ params }: BiddingDetailPageProps) {
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ServerErrorPage
|
||||
title="입찰 정보를 불러올 수 없습니다"
|
||||
message={error}
|
||||
onRetry={fetchData}
|
||||
showBackButton={true}
|
||||
showHomeButton={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BiddingDetailForm
|
||||
mode="view"
|
||||
mode={isEditMode ? 'edit' : 'view'}
|
||||
biddingId={id}
|
||||
initialData={data}
|
||||
/>
|
||||
|
||||
@@ -1,61 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { EstimateDetailForm } from '@/components/business/construction/estimates';
|
||||
import type { EstimateDetail } from '@/components/business/construction/estimates';
|
||||
import { getEstimateDetail } from '@/components/business/construction/estimates/actions';
|
||||
import { use, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
interface EstimateEditPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 하위 호환성을 위한 리다이렉트 페이지
|
||||
* /estimates/[id]/edit → /estimates/[id]?mode=edit
|
||||
*/
|
||||
export default function EstimateEditPage({ params }: EstimateEditPageProps) {
|
||||
const { id } = use(params);
|
||||
const [data, setData] = useState<EstimateDetail | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
const result = await getEstimateDetail(id);
|
||||
if (result.success && result.data) {
|
||||
setData(result.data);
|
||||
} else {
|
||||
setError(result.error || '견적 정보를 불러오는데 실패했습니다.');
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
fetchData();
|
||||
}, [id]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-red-500">{error}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
router.replace(`/ko/construction/project/bidding/estimates/${id}?mode=edit`);
|
||||
}, [id, router]);
|
||||
|
||||
return (
|
||||
<EstimateDetailForm
|
||||
mode="edit"
|
||||
estimateId={id}
|
||||
initialData={data || undefined}
|
||||
/>
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">리다이렉트 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { use, useEffect, useState, useCallback } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { EstimateDetailForm } from '@/components/business/construction/estimates';
|
||||
import type { EstimateDetail } from '@/components/business/construction/estimates';
|
||||
import { getEstimateDetail } from '@/components/business/construction/estimates/actions';
|
||||
import { ServerErrorPage } from '@/components/common/ServerErrorPage';
|
||||
|
||||
interface EstimateDetailPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
@@ -11,30 +13,37 @@ interface EstimateDetailPageProps {
|
||||
|
||||
export default function EstimateDetailPage({ params }: EstimateDetailPageProps) {
|
||||
const { id } = use(params);
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
// V2 패턴: mode 체크
|
||||
const mode = searchParams.get('mode') || 'view';
|
||||
const isEditMode = mode === 'edit';
|
||||
|
||||
const [data, setData] = useState<EstimateDetail | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
const result = await getEstimateDetail(id);
|
||||
if (result.success && result.data) {
|
||||
setData(result.data);
|
||||
} else {
|
||||
setError(result.error || '견적 정보를 불러오는데 실패했습니다.');
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
const result = await getEstimateDetail(id);
|
||||
if (result.success && result.data) {
|
||||
setData(result.data);
|
||||
} else {
|
||||
setError(result.error || '견적 정보를 불러오는데 실패했습니다.');
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
fetchData();
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
@@ -45,15 +54,19 @@ export default function EstimateDetailPage({ params }: EstimateDetailPageProps)
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-red-500">{error}</div>
|
||||
</div>
|
||||
<ServerErrorPage
|
||||
title="견적 정보를 불러올 수 없습니다"
|
||||
message={error}
|
||||
onRetry={fetchData}
|
||||
showBackButton={true}
|
||||
showHomeButton={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EstimateDetailForm
|
||||
mode="view"
|
||||
mode={isEditMode ? 'edit' : 'view'}
|
||||
estimateId={id}
|
||||
initialData={data || undefined}
|
||||
/>
|
||||
|
||||
@@ -1,60 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { use, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import PartnerForm from '@/components/business/construction/partners/PartnerForm';
|
||||
import { getPartner } from '@/components/business/construction/partners/actions';
|
||||
|
||||
interface PartnerEditPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 하위 호환성을 위한 리다이렉트 페이지
|
||||
* /partners/[id]/edit → /partners/[id]?mode=edit
|
||||
*/
|
||||
export default function PartnerEditPage({ params }: PartnerEditPageProps) {
|
||||
const { id } = use(params);
|
||||
const router = useRouter();
|
||||
const [data, setData] = useState<Awaited<ReturnType<typeof getPartner>>['data']>(undefined);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getPartner(id)
|
||||
.then(result => {
|
||||
if (result.success && result.data) {
|
||||
setData(result.data);
|
||||
} else {
|
||||
setError('협력업체 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setError('협력업체 정보를 불러오는 중 오류가 발생했습니다.');
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [id]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
|
||||
<div className="text-muted-foreground">{error}</div>
|
||||
<button onClick={() => router.back()} className="text-primary hover:underline">
|
||||
뒤로 가기
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
router.replace(`/ko/construction/project/bidding/partners/${id}?mode=edit`);
|
||||
}, [id, router]);
|
||||
|
||||
return (
|
||||
<PartnerForm
|
||||
mode="edit"
|
||||
partnerId={id}
|
||||
initialData={data}
|
||||
/>
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">리다이렉트 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { use, useEffect, useState, useCallback } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import PartnerForm from '@/components/business/construction/partners/PartnerForm';
|
||||
import { getPartner } from '@/components/business/construction/partners/actions';
|
||||
import { ServerErrorPage } from '@/components/common/ServerErrorPage';
|
||||
|
||||
interface PartnerDetailPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
@@ -11,12 +12,19 @@ interface PartnerDetailPageProps {
|
||||
|
||||
export default function PartnerDetailPage({ params }: PartnerDetailPageProps) {
|
||||
const { id } = use(params);
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
// V2 패턴: mode 체크
|
||||
const mode = searchParams.get('mode') || 'view';
|
||||
const isEditMode = mode === 'edit';
|
||||
|
||||
const [data, setData] = useState<Awaited<ReturnType<typeof getPartner>>['data']>(undefined);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = useCallback(() => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
getPartner(id)
|
||||
.then(result => {
|
||||
if (result.success && result.data) {
|
||||
@@ -31,6 +39,10 @@ export default function PartnerDetailPage({ params }: PartnerDetailPageProps) {
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
@@ -41,18 +53,19 @@ export default function PartnerDetailPage({ params }: PartnerDetailPageProps) {
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
|
||||
<div className="text-muted-foreground">{error}</div>
|
||||
<button onClick={() => router.back()} className="text-primary hover:underline">
|
||||
뒤로 가기
|
||||
</button>
|
||||
</div>
|
||||
<ServerErrorPage
|
||||
title="협력업체 정보를 불러올 수 없습니다"
|
||||
message={error}
|
||||
onRetry={fetchData}
|
||||
showBackButton={true}
|
||||
showHomeButton={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PartnerForm
|
||||
mode="view"
|
||||
mode={isEditMode ? 'edit' : 'view'}
|
||||
partnerId={id}
|
||||
initialData={data}
|
||||
/>
|
||||
|
||||
@@ -1,59 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { use, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { SiteBriefingForm, getSiteBriefing } from '@/components/business/construction/site-briefings';
|
||||
|
||||
interface SiteBriefingEditPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 하위 호환성을 위한 리다이렉트 페이지
|
||||
* /site-briefings/[id]/edit → /site-briefings/[id]?mode=edit
|
||||
*/
|
||||
export default function SiteBriefingEditPage({ params }: SiteBriefingEditPageProps) {
|
||||
const { id } = use(params);
|
||||
const router = useRouter();
|
||||
const [data, setData] = useState<Awaited<ReturnType<typeof getSiteBriefing>>['data']>(undefined);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getSiteBriefing(id)
|
||||
.then(result => {
|
||||
if (result.success && result.data) {
|
||||
setData(result.data);
|
||||
} else {
|
||||
setError('현장 설명회 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setError('현장 설명회 정보를 불러오는 중 오류가 발생했습니다.');
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [id]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
|
||||
<div className="text-muted-foreground">{error}</div>
|
||||
<button onClick={() => router.back()} className="text-primary hover:underline">
|
||||
뒤로 가기
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
router.replace(`/ko/construction/project/bidding/site-briefings/${id}?mode=edit`);
|
||||
}, [id, router]);
|
||||
|
||||
return (
|
||||
<SiteBriefingForm
|
||||
mode="edit"
|
||||
briefingId={id}
|
||||
initialData={data}
|
||||
/>
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">리다이렉트 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { use, useEffect, useState, useCallback } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { SiteBriefingForm, getSiteBriefing } from '@/components/business/construction/site-briefings';
|
||||
import { ServerErrorPage } from '@/components/common/ServerErrorPage';
|
||||
|
||||
interface SiteBriefingDetailPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
@@ -10,12 +11,19 @@ interface SiteBriefingDetailPageProps {
|
||||
|
||||
export default function SiteBriefingDetailPage({ params }: SiteBriefingDetailPageProps) {
|
||||
const { id } = use(params);
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
// V2 패턴: mode 체크
|
||||
const mode = searchParams.get('mode') || 'view';
|
||||
const isEditMode = mode === 'edit';
|
||||
|
||||
const [data, setData] = useState<Awaited<ReturnType<typeof getSiteBriefing>>['data']>(undefined);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = useCallback(() => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
getSiteBriefing(id)
|
||||
.then(result => {
|
||||
if (result.success && result.data) {
|
||||
@@ -30,6 +38,10 @@ export default function SiteBriefingDetailPage({ params }: SiteBriefingDetailPag
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
@@ -40,18 +52,19 @@ export default function SiteBriefingDetailPage({ params }: SiteBriefingDetailPag
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
|
||||
<div className="text-muted-foreground">{error}</div>
|
||||
<button onClick={() => router.back()} className="text-primary hover:underline">
|
||||
뒤로 가기
|
||||
</button>
|
||||
</div>
|
||||
<ServerErrorPage
|
||||
title="현장 설명회 정보를 불러올 수 없습니다"
|
||||
message={error}
|
||||
onRetry={fetchData}
|
||||
showBackButton={true}
|
||||
showHomeButton={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SiteBriefingForm
|
||||
mode="view"
|
||||
mode={isEditMode ? 'edit' : 'view'}
|
||||
briefingId={id}
|
||||
initialData={data}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { use } from 'react';
|
||||
import ConstructionDetailClient from '@/components/business/construction/management/ConstructionDetailClient';
|
||||
import { use, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{
|
||||
@@ -9,8 +9,21 @@ interface PageProps {
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 하위 호환성을 위한 리다이렉트 페이지
|
||||
* /construction-management/[id]/edit → /construction-management/[id]?mode=edit
|
||||
*/
|
||||
export default function ConstructionManagementEditPage({ params }: PageProps) {
|
||||
const { id } = use(params);
|
||||
const router = useRouter();
|
||||
|
||||
return <ConstructionDetailClient id={id} mode="edit" />;
|
||||
}
|
||||
useEffect(() => {
|
||||
router.replace(`/ko/construction/project/construction-management/${id}?mode=edit`);
|
||||
}, [id, router]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">리다이렉트 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { use } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import ConstructionDetailClient from '@/components/business/construction/management/ConstructionDetailClient';
|
||||
|
||||
interface PageProps {
|
||||
@@ -11,6 +12,11 @@ interface PageProps {
|
||||
|
||||
export default function ConstructionManagementDetailPage({ params }: PageProps) {
|
||||
const { id } = use(params);
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
return <ConstructionDetailClient id={id} mode="view" />;
|
||||
// V2 패턴: mode 체크
|
||||
const mode = searchParams.get('mode') || 'view';
|
||||
const isEditMode = mode === 'edit';
|
||||
|
||||
return <ConstructionDetailClient id={id} mode={isEditMode ? 'edit' : 'view'} />;
|
||||
}
|
||||
|
||||
@@ -1,53 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import IssueDetailForm from '@/components/business/construction/issue-management/IssueDetailForm';
|
||||
import { getIssue } from '@/components/business/construction/issue-management/actions';
|
||||
import type { Issue } from '@/components/business/construction/issue-management/types';
|
||||
import { useEffect } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
|
||||
/**
|
||||
* 하위 호환성을 위한 리다이렉트 페이지
|
||||
* /issue-management/[id]/edit → /issue-management/[id]?mode=edit
|
||||
*/
|
||||
export default function IssueEditPage() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const id = params.id as string;
|
||||
|
||||
const [issue, setIssue] = useState<Issue | undefined>(undefined);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const result = await getIssue(id);
|
||||
if (result.success && result.data) {
|
||||
setIssue(result.data);
|
||||
} else {
|
||||
setError(result.error || '이슈를 찾을 수 없습니다.');
|
||||
}
|
||||
} catch {
|
||||
setError('이슈 조회에 실패했습니다.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
router.replace(`/ko/construction/project/issue-management/${id}?mode=edit`);
|
||||
}, [id, router]);
|
||||
|
||||
loadData();
|
||||
}, [id]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-red-500">{error}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <IssueDetailForm issue={issue} mode="edit" />;
|
||||
}
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">리다이렉트 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,38 +1,46 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { useParams, useSearchParams } from 'next/navigation';
|
||||
import IssueDetailForm from '@/components/business/construction/issue-management/IssueDetailForm';
|
||||
import { getIssue } from '@/components/business/construction/issue-management/actions';
|
||||
import type { Issue } from '@/components/business/construction/issue-management/types';
|
||||
import { ServerErrorPage } from '@/components/common/ServerErrorPage';
|
||||
|
||||
export default function IssueDetailPage() {
|
||||
const params = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
const id = params.id as string;
|
||||
|
||||
// V2 패턴: mode 체크
|
||||
const mode = searchParams.get('mode') || 'view';
|
||||
const isEditMode = mode === 'edit';
|
||||
|
||||
const [issue, setIssue] = useState<Issue | undefined>(undefined);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const result = await getIssue(id);
|
||||
if (result.success && result.data) {
|
||||
setIssue(result.data);
|
||||
} else {
|
||||
setError(result.error || '이슈를 찾을 수 없습니다.');
|
||||
}
|
||||
} catch {
|
||||
setError('이슈 조회에 실패했습니다.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
const result = await getIssue(id);
|
||||
if (result.success && result.data) {
|
||||
setIssue(result.data);
|
||||
} else {
|
||||
setError(result.error || '이슈를 찾을 수 없습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
loadData();
|
||||
} catch {
|
||||
setError('이슈 조회에 실패했습니다.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
@@ -43,11 +51,15 @@ export default function IssueDetailPage() {
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-red-500">{error}</div>
|
||||
</div>
|
||||
<ServerErrorPage
|
||||
title="이슈 정보를 불러올 수 없습니다"
|
||||
message={error}
|
||||
onRetry={fetchData}
|
||||
showBackButton={true}
|
||||
showHomeButton={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <IssueDetailForm issue={issue} mode="view" />;
|
||||
return <IssueDetailForm issue={issue} mode={isEditMode ? 'edit' : 'view'} />;
|
||||
}
|
||||
@@ -1,57 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
interface EditInquiryPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1:1 문의 수정 페이지
|
||||
* 하위 호환성을 위한 리다이렉트 페이지
|
||||
* /qna/[id]/edit → /qna/[id]?mode=edit
|
||||
*/
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { InquiryForm } from '@/components/customer-center/InquiryManagement';
|
||||
import { transformPostToInquiry, type Inquiry } from '@/components/customer-center/InquiryManagement/types';
|
||||
import { getPost } from '@/components/customer-center/shared/actions';
|
||||
|
||||
export default function InquiryEditPage() {
|
||||
const params = useParams();
|
||||
const inquiryId = params.id as string;
|
||||
|
||||
const [inquiry, setInquiry] = useState<Inquiry | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
export default function EditInquiryPage({ params }: EditInquiryPageProps) {
|
||||
const { id } = use(params);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchInquiry() {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
router.replace(`/ko/customer-center/qna/${id}?mode=edit`);
|
||||
}, [id, router]);
|
||||
|
||||
const result = await getPost('qna', inquiryId);
|
||||
|
||||
if (result.success && result.data) {
|
||||
setInquiry(transformPostToInquiry(result.data));
|
||||
} else {
|
||||
setError(result.error || '문의를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
fetchInquiry();
|
||||
}, [inquiryId]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<p className="text-muted-foreground">로딩 중...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !inquiry) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<p className="text-muted-foreground">{error || '문의를 찾을 수 없습니다.'}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <InquiryForm mode="edit" initialData={inquiry} />;
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">리다이렉트 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,135 +1,20 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 1:1 문의 상세 페이지
|
||||
* 1:1 문의 상세 페이지 (V2)
|
||||
*
|
||||
* /qna/[id] → 조회 모드
|
||||
* /qna/[id]?mode=edit → 수정 모드
|
||||
*/
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { InquiryDetail } from '@/components/customer-center/InquiryManagement';
|
||||
import { transformPostToInquiry, type Inquiry, type Comment } from '@/components/customer-center/InquiryManagement/types';
|
||||
import { getPost, getComments, createComment, updateComment, deleteComment, deletePost } from '@/components/customer-center/shared/actions';
|
||||
import { transformApiToComment } from '@/components/customer-center/shared/types';
|
||||
import { use } from 'react';
|
||||
import { InquiryDetailClientV2 } from '@/components/customer-center/InquiryManagement';
|
||||
|
||||
export default function InquiryDetailPage() {
|
||||
const params = useParams();
|
||||
const inquiryId = params.id as string;
|
||||
|
||||
const [inquiry, setInquiry] = useState<Inquiry | null>(null);
|
||||
const [comments, setComments] = useState<Comment[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [currentUserId, setCurrentUserId] = useState<string>('');
|
||||
|
||||
// 현재 사용자 ID 가져오기 (localStorage에서)
|
||||
useEffect(() => {
|
||||
const userStr = localStorage.getItem('user');
|
||||
if (userStr) {
|
||||
try {
|
||||
const user = JSON.parse(userStr);
|
||||
// user.id는 실제 DB user ID (숫자)
|
||||
setCurrentUserId(String(user.id || ''));
|
||||
} catch {
|
||||
setCurrentUserId('');
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 데이터 로드
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
// 게시글과 댓글 동시 로드
|
||||
const [postResult, commentsResult] = await Promise.all([
|
||||
getPost('qna', inquiryId),
|
||||
getComments('qna', inquiryId),
|
||||
]);
|
||||
|
||||
if (postResult.success && postResult.data) {
|
||||
setInquiry(transformPostToInquiry(postResult.data));
|
||||
} else {
|
||||
setError(postResult.error || '문의를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
if (commentsResult.success && commentsResult.data) {
|
||||
setComments(commentsResult.data.map(transformApiToComment));
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
fetchData();
|
||||
}, [inquiryId]);
|
||||
|
||||
// 댓글 추가
|
||||
const handleAddComment = useCallback(async (content: string) => {
|
||||
const result = await createComment('qna', inquiryId, content);
|
||||
if (result.success && result.data) {
|
||||
setComments((prev) => [...prev, transformApiToComment(result.data!)]);
|
||||
} else {
|
||||
console.error('댓글 등록 실패:', result.error);
|
||||
}
|
||||
}, [inquiryId]);
|
||||
|
||||
// 댓글 수정
|
||||
const handleUpdateComment = useCallback(async (commentId: string, content: string) => {
|
||||
const result = await updateComment('qna', inquiryId, commentId, content);
|
||||
if (result.success && result.data) {
|
||||
setComments((prev) =>
|
||||
prev.map((c) => (c.id === commentId ? transformApiToComment(result.data!) : c))
|
||||
);
|
||||
} else {
|
||||
console.error('댓글 수정 실패:', result.error);
|
||||
}
|
||||
}, [inquiryId]);
|
||||
|
||||
// 댓글 삭제
|
||||
const handleDeleteComment = useCallback(async (commentId: string) => {
|
||||
const result = await deleteComment('qna', inquiryId, commentId);
|
||||
if (result.success) {
|
||||
setComments((prev) => prev.filter((c) => c.id !== commentId));
|
||||
} else {
|
||||
console.error('댓글 삭제 실패:', result.error);
|
||||
}
|
||||
}, [inquiryId]);
|
||||
|
||||
// 문의 삭제
|
||||
const handleDeleteInquiry = useCallback(async () => {
|
||||
const result = await deletePost('qna', inquiryId);
|
||||
return result.success;
|
||||
}, [inquiryId]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<p className="text-muted-foreground">로딩 중...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !inquiry) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<p className="text-muted-foreground">{error || '문의를 찾을 수 없습니다.'}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 답변은 추후 API 추가 시 구현
|
||||
const reply = undefined;
|
||||
|
||||
return (
|
||||
<InquiryDetail
|
||||
inquiry={inquiry}
|
||||
reply={reply}
|
||||
comments={comments}
|
||||
currentUserId={currentUserId}
|
||||
onAddComment={handleAddComment}
|
||||
onUpdateComment={handleUpdateComment}
|
||||
onDeleteComment={handleDeleteComment}
|
||||
onDeleteInquiry={handleDeleteInquiry}
|
||||
/>
|
||||
);
|
||||
interface InquiryDetailPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default function InquiryDetailPage({ params }: InquiryDetailPageProps) {
|
||||
const { id } = use(params);
|
||||
return <InquiryDetailClientV2 inquiryId={id} />;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 1:1 문의 등록 페이지
|
||||
* 1:1 문의 등록 페이지 (V2)
|
||||
*/
|
||||
|
||||
import { InquiryForm } from '@/components/customer-center/InquiryManagement';
|
||||
import { InquiryDetailClientV2 } from '@/components/customer-center/InquiryManagement';
|
||||
|
||||
export default function InquiryCreatePage() {
|
||||
return <InquiryForm mode="create" />;
|
||||
return <InquiryDetailClientV2 />;
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
title: '1:1 문의 등록',
|
||||
description: '1:1 문의를 등록합니다.',
|
||||
};
|
||||
@@ -1,62 +1,27 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 공정 수정 페이지 (Client Component)
|
||||
* 공정관리 수정 페이지 (리다이렉트)
|
||||
*
|
||||
* 기존 URL 호환성을 위해 유지
|
||||
* /[id]/edit → /[id]?mode=edit 로 리다이렉트
|
||||
*/
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { use, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ProcessForm } from '@/components/process-management';
|
||||
import { getProcessById } from '@/components/process-management/actions';
|
||||
import type { Process } from '@/components/process-management/types';
|
||||
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
|
||||
|
||||
export default function EditProcessPage({
|
||||
export default function ProcessEditRedirectPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const { id } = use(params);
|
||||
const router = useRouter();
|
||||
const [data, setData] = useState<Process | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getProcessById(id)
|
||||
.then(result => {
|
||||
if (result.success && result.data) {
|
||||
setData(result.data);
|
||||
} else {
|
||||
setError('공정 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setError('공정 정보를 불러오는 중 오류가 발생했습니다.');
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [id]);
|
||||
router.replace(`/ko/master-data/process-management/${id}?mode=edit`);
|
||||
}, [router, id]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">공정 정보를 불러오는 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !data) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
|
||||
<div className="text-muted-foreground">{error || '공정을 찾을 수 없습니다.'}</div>
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
뒤로 가기
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <ProcessForm mode="edit" initialData={data} />;
|
||||
}
|
||||
return <ContentLoadingSpinner text="페이지 이동 중..." />;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 공정 상세 페이지 (Client Component)
|
||||
* 공정관리 상세/수정 페이지
|
||||
*
|
||||
* IntegratedDetailTemplate V2 마이그레이션
|
||||
* - /[id] → 상세 보기 (view 모드)
|
||||
* - /[id]?mode=edit → 수정 (edit 모드)
|
||||
*/
|
||||
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ProcessDetail } from '@/components/process-management';
|
||||
import { getProcessById } from '@/components/process-management/actions';
|
||||
import type { Process } from '@/components/process-management/types';
|
||||
import { use } from 'react';
|
||||
import { ProcessDetailClientV2 } from '@/components/process-management';
|
||||
|
||||
export default function ProcessDetailPage({
|
||||
params,
|
||||
@@ -16,47 +17,6 @@ export default function ProcessDetailPage({
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const { id } = use(params);
|
||||
const router = useRouter();
|
||||
const [data, setData] = useState<Process | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getProcessById(id)
|
||||
.then(result => {
|
||||
if (result.success && result.data) {
|
||||
setData(result.data);
|
||||
} else {
|
||||
setError('공정 정보를 찾을 수 없습니다.');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setError('공정 정보를 불러오는 중 오류가 발생했습니다.');
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [id]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">공정 정보를 불러오는 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !data) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
|
||||
<div className="text-muted-foreground">{error || '공정을 찾을 수 없습니다.'}</div>
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
뒤로 가기
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <ProcessDetail process={data} />;
|
||||
}
|
||||
return <ProcessDetailClientV2 processId={id} />;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
/**
|
||||
* 공정 등록 페이지
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { ProcessForm } from '@/components/process-management';
|
||||
/**
|
||||
* 공정관리 등록 페이지
|
||||
*
|
||||
* IntegratedDetailTemplate V2 마이그레이션
|
||||
*/
|
||||
|
||||
export default function CreateProcessPage() {
|
||||
return <ProcessForm mode="create" />;
|
||||
}
|
||||
import { ProcessDetailClientV2 } from '@/components/process-management';
|
||||
|
||||
export default function ProcessCreatePage() {
|
||||
return <ProcessDetailClientV2 processId="new" />;
|
||||
}
|
||||
|
||||
@@ -1,78 +1,24 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 거래처 수정 페이지
|
||||
* 거래처(영업) 수정 페이지 (리다이렉트)
|
||||
*
|
||||
* 기존 URL 호환성을 위해 유지
|
||||
* /[id]/edit → /[id]?mode=edit 로 리다이렉트
|
||||
*/
|
||||
|
||||
"use client";
|
||||
import { useEffect } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter, useParams } from "next/navigation";
|
||||
import { ClientRegistration } from "@/components/clients/ClientRegistration";
|
||||
import {
|
||||
useClientList,
|
||||
ClientFormData,
|
||||
clientToFormData,
|
||||
} from "@/hooks/useClientList";
|
||||
import { toast } from "sonner";
|
||||
import { ContentLoadingSpinner } from "@/components/ui/loading-spinner";
|
||||
|
||||
export default function ClientEditPage() {
|
||||
export default function ClientEditRedirectPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const id = params.id as string;
|
||||
|
||||
const { fetchClient, updateClient, isLoading: hookLoading } = useClientList();
|
||||
const [editingClient, setEditingClient] = useState<ClientFormData | null>(
|
||||
null
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// 데이터 로드
|
||||
useEffect(() => {
|
||||
const loadClient = async () => {
|
||||
if (!id) return;
|
||||
router.replace(`/ko/sales/client-management-sales-admin/${id}?mode=edit`);
|
||||
}, [router, id]);
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const data = await fetchClient(id);
|
||||
if (data) {
|
||||
setEditingClient(clientToFormData(data));
|
||||
} else {
|
||||
toast.error("거래처를 찾을 수 없습니다.");
|
||||
router.push("/sales/client-management-sales-admin");
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("데이터 로드 중 오류가 발생했습니다.");
|
||||
router.push("/sales/client-management-sales-admin");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadClient();
|
||||
}, [id, fetchClient, router]);
|
||||
|
||||
const handleBack = () => {
|
||||
router.push(`/sales/client-management-sales-admin/${id}`);
|
||||
};
|
||||
|
||||
const handleSave = async (formData: ClientFormData) => {
|
||||
await updateClient(id, formData);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <ContentLoadingSpinner text="거래처 정보를 불러오는 중..." />;
|
||||
}
|
||||
|
||||
if (!editingClient) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ClientRegistration
|
||||
onBack={handleBack}
|
||||
onSave={handleSave}
|
||||
editingClient={editingClient}
|
||||
isLoading={hookLoading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <ContentLoadingSpinner text="페이지 이동 중..." />;
|
||||
}
|
||||
|
||||
@@ -1,124 +1,20 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 거래처 상세 페이지
|
||||
* 거래처(영업) 상세/수정 페이지
|
||||
* IntegratedDetailTemplate V2 마이그레이션
|
||||
*
|
||||
* URL 패턴:
|
||||
* - /[id] : view 모드
|
||||
* - /[id]?mode=edit : edit 모드
|
||||
*/
|
||||
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter, useParams } from "next/navigation";
|
||||
import { ClientDetail } from "@/components/clients/ClientDetail";
|
||||
import { useClientList, Client } from "@/hooks/useClientList";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { ContentLoadingSpinner } from "@/components/ui/loading-spinner";
|
||||
import { useParams } from 'next/navigation';
|
||||
import { ClientDetailClientV2 } from '@/components/clients/ClientDetailClientV2';
|
||||
|
||||
export default function ClientDetailPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const id = params.id as string;
|
||||
|
||||
const { fetchClient, deleteClient } = useClientList();
|
||||
const [client, setClient] = useState<Client | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
|
||||
// 데이터 로드
|
||||
useEffect(() => {
|
||||
const loadClient = async () => {
|
||||
if (!id) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const data = await fetchClient(id);
|
||||
if (data) {
|
||||
setClient(data);
|
||||
} else {
|
||||
toast.error("거래처를 찾을 수 없습니다.");
|
||||
router.push("/sales/client-management-sales-admin");
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("데이터 로드 중 오류가 발생했습니다.");
|
||||
router.push("/sales/client-management-sales-admin");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadClient();
|
||||
}, [id, fetchClient, router]);
|
||||
|
||||
const handleBack = () => {
|
||||
router.push("/sales/client-management-sales-admin");
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
router.push(`/sales/client-management-sales-admin/${id}/edit`);
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
await deleteClient(id);
|
||||
toast.success("거래처가 삭제되었습니다.");
|
||||
router.push("/sales/client-management-sales-admin");
|
||||
} catch (error) {
|
||||
toast.error("삭제 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
setShowDeleteDialog(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <ContentLoadingSpinner text="거래처 정보를 불러오는 중..." />;
|
||||
}
|
||||
|
||||
if (!client) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ClientDetail
|
||||
client={client}
|
||||
onBack={handleBack}
|
||||
onEdit={handleEdit}
|
||||
onDelete={() => setShowDeleteDialog(true)}
|
||||
/>
|
||||
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>거래처 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
'{client.name}' 거래처를 삭제하시겠습니까?
|
||||
<br />
|
||||
이 작업은 되돌릴 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isDeleting}>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleDelete}
|
||||
disabled={isDeleting}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
{isDeleting ? "삭제 중..." : "삭제"}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <ClientDetailClientV2 clientId={id} />;
|
||||
}
|
||||
|
||||
@@ -1,30 +1,12 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 거래처 등록 페이지
|
||||
* 거래처(영업) 등록 페이지
|
||||
* IntegratedDetailTemplate V2 마이그레이션
|
||||
*/
|
||||
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ClientRegistration } from "@/components/clients/ClientRegistration";
|
||||
import { useClientList, ClientFormData } from "@/hooks/useClientList";
|
||||
import { ClientDetailClientV2 } from '@/components/clients/ClientDetailClientV2';
|
||||
|
||||
export default function ClientNewPage() {
|
||||
const router = useRouter();
|
||||
const { createClient, isLoading } = useClientList();
|
||||
|
||||
const handleBack = () => {
|
||||
router.push("/sales/client-management-sales-admin");
|
||||
};
|
||||
|
||||
const handleSave = async (formData: ClientFormData) => {
|
||||
await createClient(formData);
|
||||
};
|
||||
|
||||
return (
|
||||
<ClientRegistration
|
||||
onBack={handleBack}
|
||||
onSave={handleSave}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <ClientDetailClientV2 />;
|
||||
}
|
||||
|
||||
@@ -1,100 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
interface EditQuotePageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 견적 수정 페이지
|
||||
* 하위 호환성을 위한 리다이렉트 페이지
|
||||
* /quote-management/[id]/edit → /quote-management/[id]?mode=edit
|
||||
*/
|
||||
|
||||
"use client";
|
||||
|
||||
import { useRouter, useParams } from "next/navigation";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { QuoteRegistration, QuoteFormData } from "@/components/quotes/QuoteRegistration";
|
||||
import {
|
||||
getQuoteById,
|
||||
updateQuote,
|
||||
transformQuoteToFormData,
|
||||
transformFormDataToApi,
|
||||
} from "@/components/quotes";
|
||||
import { toast } from "sonner";
|
||||
import { ContentLoadingSpinner } from "@/components/ui/loading-spinner";
|
||||
|
||||
export default function QuoteEditPage() {
|
||||
export default function EditQuotePage({ params }: EditQuotePageProps) {
|
||||
const { id } = use(params);
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const quoteId = params.id as string;
|
||||
|
||||
const [quote, setQuote] = useState<QuoteFormData | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
// 견적 데이터 조회
|
||||
const fetchQuote = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await getQuoteById(quoteId);
|
||||
console.log('[EditPage] API 응답 result.data:', JSON.stringify({
|
||||
calculationInputs: result.data?.calculationInputs,
|
||||
items: result.data?.items?.map(i => ({ quantity: i.quantity, unitPrice: i.unitPrice }))
|
||||
}, null, 2));
|
||||
|
||||
if (result.success && result.data) {
|
||||
const formData = transformQuoteToFormData(result.data);
|
||||
console.log('[EditPage] 변환된 formData.items[0]:', JSON.stringify({
|
||||
quantity: formData.items[0]?.quantity,
|
||||
wingSize: formData.items[0]?.wingSize,
|
||||
inspectionFee: formData.items[0]?.inspectionFee,
|
||||
}, null, 2));
|
||||
setQuote(formData);
|
||||
} else {
|
||||
toast.error(result.error || "견적 정보를 불러오는데 실패했습니다.");
|
||||
router.push("/sales/quote-management");
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("견적 정보를 불러오는데 실패했습니다.");
|
||||
router.push("/sales/quote-management");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [quoteId, router]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchQuote();
|
||||
}, [fetchQuote]);
|
||||
|
||||
const handleBack = () => {
|
||||
router.push("/sales/quote-management");
|
||||
};
|
||||
|
||||
const handleSave = async (formData: QuoteFormData) => {
|
||||
if (isSaving) return;
|
||||
setIsSaving(true);
|
||||
|
||||
try {
|
||||
// FormData를 API 요청 형식으로 변환
|
||||
const apiData = transformFormDataToApi(formData);
|
||||
|
||||
const result = await updateQuote(quoteId, apiData as any);
|
||||
|
||||
if (result.success) {
|
||||
toast.success("견적이 수정되었습니다.");
|
||||
router.push(`/sales/quote-management/${quoteId}`);
|
||||
} else {
|
||||
toast.error(result.error || "견적 수정에 실패했습니다.");
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("견적 수정에 실패했습니다.");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <ContentLoadingSpinner text="견적 정보를 불러오는 중..." />;
|
||||
}
|
||||
router.replace(`/ko/sales/quote-management/${id}?mode=edit`);
|
||||
}, [id, router]);
|
||||
|
||||
return (
|
||||
<QuoteRegistration
|
||||
onBack={handleBack}
|
||||
onSave={handleSave}
|
||||
editingQuote={quote}
|
||||
/>
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">리다이렉트 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
/**
|
||||
* 견적 상세 페이지
|
||||
* - 기본 정보 표시
|
||||
* 견적 상세/수정 페이지 (V2 통합)
|
||||
* - 기본 정보 표시 (view mode)
|
||||
* - 자동 견적 산출 정보
|
||||
* - 견적서 / 산출내역서 / 발주서 모달
|
||||
* - 수정 모드 (edit mode)
|
||||
*
|
||||
* URL 패턴:
|
||||
* - /quote-management/[id] → 상세 보기 (view)
|
||||
* - /quote-management/[id]?mode=edit → 수정 모드 (edit)
|
||||
*/
|
||||
|
||||
"use client";
|
||||
|
||||
import { useRouter, useParams } from "next/navigation";
|
||||
import { useRouter, useParams, useSearchParams } from "next/navigation";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { QuoteFormData } from "@/components/quotes/QuoteRegistration";
|
||||
import { QuoteRegistration, QuoteFormData } from "@/components/quotes/QuoteRegistration";
|
||||
import { QuoteDocument } from "@/components/quotes/QuoteDocument";
|
||||
import { QuoteCalculationReport } from "@/components/quotes/QuoteCalculationReport";
|
||||
import { PurchaseOrderDocument } from "@/components/quotes/PurchaseOrderDocument";
|
||||
@@ -20,6 +25,8 @@ import {
|
||||
sendQuoteEmail,
|
||||
sendQuoteKakao,
|
||||
transformQuoteToFormData,
|
||||
updateQuote,
|
||||
transformFormDataToApi,
|
||||
} from "@/components/quotes";
|
||||
import { getCompanyInfo } from "@/components/settings/CompanyInfoManagement/actions";
|
||||
import type { CompanyFormData } from "@/components/settings/CompanyInfoManagement/types";
|
||||
@@ -56,12 +63,18 @@ import { ContentLoadingSpinner } from "@/components/ui/loading-spinner";
|
||||
export default function QuoteDetailPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
const quoteId = params.id as string;
|
||||
|
||||
// V2 패턴: mode 체크
|
||||
const mode = searchParams.get("mode") || "view";
|
||||
const isEditMode = mode === "edit";
|
||||
|
||||
const [quote, setQuote] = useState<QuoteFormData | null>(null);
|
||||
const [companyInfo, setCompanyInfo] = useState<CompanyFormData | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
// 다이얼로그 상태
|
||||
const [isQuoteDocumentOpen, setIsQuoteDocumentOpen] = useState(false);
|
||||
@@ -143,7 +156,29 @@ export default function QuoteDetailPage() {
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
router.push(`/sales/quote-management/${quoteId}/edit`);
|
||||
router.push(`/sales/quote-management/${quoteId}?mode=edit`);
|
||||
};
|
||||
|
||||
// V2 패턴: 수정 저장 핸들러
|
||||
const handleSave = async (formData: QuoteFormData) => {
|
||||
if (isSaving) return;
|
||||
setIsSaving(true);
|
||||
|
||||
try {
|
||||
const apiData = transformFormDataToApi(formData);
|
||||
const result = await updateQuote(quoteId, apiData as any);
|
||||
|
||||
if (result.success) {
|
||||
toast.success("견적이 수정되었습니다.");
|
||||
router.push(`/sales/quote-management/${quoteId}`);
|
||||
} else {
|
||||
toast.error(result.error || "견적 수정에 실패했습니다.");
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("견적 수정에 실패했습니다.");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFinalize = async () => {
|
||||
@@ -261,6 +296,18 @@ export default function QuoteDetailPage() {
|
||||
);
|
||||
}
|
||||
|
||||
// V2 패턴: Edit 모드일 때 QuoteRegistration 컴포넌트 렌더링
|
||||
if (isEditMode) {
|
||||
return (
|
||||
<QuoteRegistration
|
||||
onBack={handleBack}
|
||||
onSave={handleSave}
|
||||
editingQuote={quote}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// View 모드: 상세 보기
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
{/* 헤더 */}
|
||||
|
||||
@@ -1,152 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import { use, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
interface EditQuoteTestPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 견적 수정 테스트 페이지 (V2 UI)
|
||||
*
|
||||
* 새로운 자동 견적 산출 UI 테스트용
|
||||
* 기존 견적 수정 페이지는 수정하지 않음
|
||||
* 하위 호환성을 위한 리다이렉트 페이지
|
||||
* /quote-management/test/[id]/edit → /quote-management/test/[id]?mode=edit
|
||||
*/
|
||||
|
||||
"use client";
|
||||
|
||||
import { useRouter, useParams } from "next/navigation";
|
||||
import { useState, useEffect } from "react";
|
||||
import { QuoteRegistrationV2, QuoteFormDataV2 } from "@/components/quotes/QuoteRegistrationV2";
|
||||
import { ContentLoadingSpinner } from "@/components/ui/loading-spinner";
|
||||
import { toast } from "sonner";
|
||||
|
||||
// 테스트용 목업 데이터
|
||||
const MOCK_DATA: QuoteFormDataV2 = {
|
||||
id: "1",
|
||||
registrationDate: "2026-01-12",
|
||||
writer: "드미트리",
|
||||
clientId: "1",
|
||||
clientName: "아크다이레드",
|
||||
siteName: "강남 테스트 현장",
|
||||
manager: "김담당",
|
||||
contact: "010-1234-5678",
|
||||
dueDate: "2026-02-01",
|
||||
remarks: "테스트 비고 내용입니다.",
|
||||
status: "draft",
|
||||
locations: [
|
||||
{
|
||||
id: "loc-1",
|
||||
floor: "1층",
|
||||
code: "FSS-01",
|
||||
openWidth: 5000,
|
||||
openHeight: 3000,
|
||||
productCode: "KSS01",
|
||||
productName: "방화스크린",
|
||||
quantity: 1,
|
||||
guideRailType: "wall",
|
||||
motorPower: "single",
|
||||
controller: "basic",
|
||||
wingSize: 50,
|
||||
inspectionFee: 50000,
|
||||
unitPrice: 1645200,
|
||||
totalPrice: 1645200,
|
||||
},
|
||||
{
|
||||
id: "loc-2",
|
||||
floor: "3층",
|
||||
code: "FST-30",
|
||||
openWidth: 7500,
|
||||
openHeight: 3300,
|
||||
productCode: "KSS02",
|
||||
productName: "방화스크린2",
|
||||
quantity: 1,
|
||||
guideRailType: "wall",
|
||||
motorPower: "single",
|
||||
controller: "smart",
|
||||
wingSize: 50,
|
||||
inspectionFee: 50000,
|
||||
unitPrice: 2589198,
|
||||
totalPrice: 2589198,
|
||||
},
|
||||
{
|
||||
id: "loc-3",
|
||||
floor: "5층",
|
||||
code: "FSS-50",
|
||||
openWidth: 6000,
|
||||
openHeight: 2800,
|
||||
productCode: "KSS01",
|
||||
productName: "방화스크린",
|
||||
quantity: 2,
|
||||
guideRailType: "floor",
|
||||
motorPower: "three",
|
||||
controller: "premium",
|
||||
wingSize: 50,
|
||||
inspectionFee: 50000,
|
||||
unitPrice: 1721214,
|
||||
totalPrice: 3442428,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default function QuoteTestEditPage() {
|
||||
export default function EditQuoteTestPage({ params }: EditQuoteTestPageProps) {
|
||||
const { id } = use(params);
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const quoteId = params.id as string;
|
||||
|
||||
const [quote, setQuote] = useState<QuoteFormDataV2 | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// 테스트용 데이터 로드 시뮬레이션
|
||||
const loadQuote = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// 실제로는 API 호출
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
setQuote({ ...MOCK_DATA, id: quoteId });
|
||||
} catch (error) {
|
||||
toast.error("견적 정보를 불러오는데 실패했습니다.");
|
||||
router.push("/sales/quote-management");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadQuote();
|
||||
}, [quoteId, router]);
|
||||
|
||||
const handleBack = () => {
|
||||
router.push("/sales/quote-management");
|
||||
};
|
||||
|
||||
const handleSave = async (data: QuoteFormDataV2, saveType: "temporary" | "final") => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
// TODO: API 연동 시 실제 저장 로직 구현
|
||||
console.log("[테스트] 수정 데이터:", data);
|
||||
console.log("[테스트] 저장 타입:", saveType);
|
||||
|
||||
// 테스트용 지연
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
toast.success(`[테스트] ${saveType === "temporary" ? "임시" : "최종"} 저장 완료`);
|
||||
|
||||
// 저장 후 상세 페이지로 이동
|
||||
if (saveType === "final") {
|
||||
router.push(`/sales/quote-management/test/${quoteId}`);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("저장 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <ContentLoadingSpinner text="견적 정보를 불러오는 중..." />;
|
||||
}
|
||||
router.replace(`/ko/sales/quote-management/test/${id}?mode=edit`);
|
||||
}, [id, router]);
|
||||
|
||||
return (
|
||||
<QuoteRegistrationV2
|
||||
mode="edit"
|
||||
onBack={handleBack}
|
||||
onSave={handleSave}
|
||||
initialData={quote}
|
||||
isLoading={isSaving}
|
||||
/>
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">리다이렉트 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
/**
|
||||
* 견적 상세 테스트 페이지 (V2 UI)
|
||||
* 견적 상세/수정 테스트 페이지 (V2 UI 통합)
|
||||
*
|
||||
* 새로운 자동 견적 산출 UI 테스트용
|
||||
* 기존 견적 상세 페이지는 수정하지 않음
|
||||
* URL 패턴:
|
||||
* - /quote-management/test/[id] → 상세 보기 (view)
|
||||
* - /quote-management/test/[id]?mode=edit → 수정 모드 (edit)
|
||||
*/
|
||||
|
||||
"use client";
|
||||
|
||||
import { useRouter, useParams } from "next/navigation";
|
||||
import { useRouter, useParams, useSearchParams } from "next/navigation";
|
||||
import { useState, useEffect } from "react";
|
||||
import { QuoteRegistrationV2, QuoteFormDataV2, LocationItem } from "@/components/quotes/QuoteRegistrationV2";
|
||||
import { ContentLoadingSpinner } from "@/components/ui/loading-spinner";
|
||||
@@ -84,10 +86,16 @@ const MOCK_DATA: QuoteFormDataV2 = {
|
||||
export default function QuoteTestDetailPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
const quoteId = params.id as string;
|
||||
|
||||
// V2 패턴: mode 체크
|
||||
const mode = searchParams.get("mode") || "view";
|
||||
const isEditMode = mode === "edit";
|
||||
|
||||
const [quote, setQuote] = useState<QuoteFormDataV2 | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// 테스트용 데이터 로드 시뮬레이션
|
||||
@@ -112,15 +120,42 @@ export default function QuoteTestDetailPage() {
|
||||
router.push("/sales/quote-management");
|
||||
};
|
||||
|
||||
// V2 패턴: 수정 저장 핸들러
|
||||
const handleSave = async (data: QuoteFormDataV2, saveType: "temporary" | "final") => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
// TODO: API 연동 시 실제 저장 로직 구현
|
||||
console.log("[테스트] 수정 데이터:", data);
|
||||
console.log("[테스트] 저장 타입:", saveType);
|
||||
|
||||
// 테스트용 지연
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
toast.success(`[테스트] ${saveType === "temporary" ? "임시" : "최종"} 저장 완료`);
|
||||
|
||||
// 저장 후 상세 페이지로 이동
|
||||
if (saveType === "final") {
|
||||
router.push(`/sales/quote-management/test/${quoteId}`);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("저장 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <ContentLoadingSpinner text="견적 정보를 불러오는 중..." />;
|
||||
}
|
||||
|
||||
// V2 패턴: mode에 따라 view/edit 렌더링
|
||||
return (
|
||||
<QuoteRegistrationV2
|
||||
mode="view"
|
||||
mode={isEditMode ? "edit" : "view"}
|
||||
onBack={handleBack}
|
||||
onSave={isEditMode ? handleSave : undefined}
|
||||
initialData={quote}
|
||||
isLoading={isSaving}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,38 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { PopupForm } from '@/components/settings/PopupManagement';
|
||||
import { getPopupById } from '@/components/settings/PopupManagement/actions';
|
||||
import type { Popup } from '@/components/settings/PopupManagement/types';
|
||||
/**
|
||||
* 팝업관리 수정 페이지 (리다이렉트)
|
||||
*
|
||||
* 기존 URL 호환성을 위해 유지
|
||||
* /[id]/edit → /[id]?mode=edit 로 리다이렉트
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
|
||||
|
||||
export default function PopupEditPage() {
|
||||
export default function PopupEditRedirectPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const id = params.id as string;
|
||||
const [popup, setPopup] = useState<Popup | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPopup = async () => {
|
||||
const data = await getPopupById(id);
|
||||
setPopup(data);
|
||||
setIsLoading(false);
|
||||
};
|
||||
fetchPopup();
|
||||
}, [id]);
|
||||
router.replace(`/ko/settings/popup-management/${id}?mode=edit`);
|
||||
}, [router, id]);
|
||||
|
||||
if (isLoading) {
|
||||
return <ContentLoadingSpinner text="팝업 정보를 불러오는 중..." />;
|
||||
}
|
||||
|
||||
if (!popup) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<p className="text-muted-foreground">팝업을 찾을 수 없습니다.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <PopupForm mode="edit" initialData={popup} />;
|
||||
}
|
||||
return <ContentLoadingSpinner text="페이지 이동 중..." />;
|
||||
}
|
||||
|
||||
@@ -1,93 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { PopupDetail } from '@/components/settings/PopupManagement';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
|
||||
import { getPopupById, deletePopup } from '@/components/settings/PopupManagement/actions';
|
||||
import type { Popup } from '@/components/settings/PopupManagement/types';
|
||||
/**
|
||||
* 팝업관리 상세/수정 페이지
|
||||
*
|
||||
* IntegratedDetailTemplate V2 마이그레이션
|
||||
* - /[id] → 상세 보기 (view 모드)
|
||||
* - /[id]?mode=edit → 수정 (edit 모드)
|
||||
*/
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import { PopupDetailClientV2 } from '@/components/settings/PopupManagement/PopupDetailClientV2';
|
||||
|
||||
export default function PopupDetailPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const [popup, setPopup] = useState<Popup | null>(null);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const id = params.id as string;
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPopup = async () => {
|
||||
const id = params.id as string;
|
||||
const data = await getPopupById(id);
|
||||
setPopup(data);
|
||||
};
|
||||
fetchPopup();
|
||||
}, [params.id]);
|
||||
|
||||
const handleEdit = () => {
|
||||
router.push(`/ko/settings/popup-management/${params.id}/edit`);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
setDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
const confirmDelete = async () => {
|
||||
setIsDeleting(true);
|
||||
const result = await deletePopup(params.id as string);
|
||||
if (result.success) {
|
||||
router.push('/ko/settings/popup-management');
|
||||
} else {
|
||||
console.error('Delete failed:', result.error);
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!popup) {
|
||||
return <ContentLoadingSpinner text="팝업 정보를 불러오는 중..." />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PopupDetail
|
||||
popup={popup}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>팝업 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
"{popup.title}" 팝업을 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="text-destructive">
|
||||
삭제된 팝업 정보는 복구할 수 없습니다.
|
||||
</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDelete}
|
||||
disabled={isDeleting}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
{isDeleting ? '삭제 중...' : '삭제'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
return <PopupDetailClientV2 popupId={id} />;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { PopupForm } from '@/components/settings/PopupManagement';
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 팝업관리 등록 페이지
|
||||
*
|
||||
* IntegratedDetailTemplate V2 마이그레이션
|
||||
*/
|
||||
|
||||
import { PopupDetailClientV2 } from '@/components/settings/PopupManagement/PopupDetailClientV2';
|
||||
|
||||
export default function PopupCreatePage() {
|
||||
return <PopupForm mode="create" />;
|
||||
return <PopupDetailClientV2 popupId="new" />;
|
||||
}
|
||||
|
||||
@@ -132,9 +132,20 @@ async function proxyRequest(
|
||||
method: string
|
||||
) {
|
||||
try {
|
||||
// 1. HttpOnly 쿠키에서 토큰 읽기 (서버에서만 가능!)
|
||||
let token = request.cookies.get('access_token')?.value;
|
||||
const refreshToken = request.cookies.get('refresh_token')?.value;
|
||||
// 1. 🆕 미들웨어에서 전달한 새 토큰 먼저 확인
|
||||
// Set-Cookie는 응답 헤더에만 설정되어 같은 요청 내 cookies()로 읽을 수 없음
|
||||
// 따라서 request headers로 전달된 새 토큰을 먼저 사용
|
||||
const refreshedAccessToken = request.headers.get('x-refreshed-access-token');
|
||||
const refreshedRefreshToken = request.headers.get('x-refreshed-refresh-token');
|
||||
|
||||
// 2. HttpOnly 쿠키에서 토큰 읽기 (서버에서만 가능!)
|
||||
let token = refreshedAccessToken || request.cookies.get('access_token')?.value;
|
||||
const refreshToken = refreshedRefreshToken || request.cookies.get('refresh_token')?.value;
|
||||
|
||||
// 디버깅: 어떤 토큰을 사용하는지 로그
|
||||
if (refreshedAccessToken) {
|
||||
console.log('🔵 [PROXY] Using refreshed token from middleware headers');
|
||||
}
|
||||
|
||||
// 2. 백엔드 URL 구성
|
||||
const backendUrl = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/${params.path.join('/')}`;
|
||||
|
||||
Reference in New Issue
Block a user