diff --git a/src/app/[locale]/(protected)/quality/qms/page.tsx b/src/app/[locale]/(protected)/quality/qms/page.tsx
index 195db457..f2a0ebb8 100644
--- a/src/app/[locale]/(protected)/quality/qms/page.tsx
+++ b/src/app/[locale]/(protected)/quality/qms/page.tsx
@@ -270,6 +270,16 @@ export default function QualityInspectionPage() {
@@ -284,6 +294,7 @@ export default function QualityInspectionPage() {
@@ -315,17 +326,7 @@ export default function QualityInspectionPage() {
+
- >
)}
{/* 설정 패널 */}
diff --git a/src/app/[locale]/(protected)/sales/quote-management/test-new/page.tsx b/src/app/[locale]/(protected)/sales/quote-management/test-new/page.tsx
new file mode 100644
index 00000000..872bc423
--- /dev/null
+++ b/src/app/[locale]/(protected)/sales/quote-management/test-new/page.tsx
@@ -0,0 +1,54 @@
+/**
+ * 견적 등록 테스트 페이지 (V2 UI)
+ *
+ * 새로운 자동 견적 산출 UI 테스트용
+ * 기존 견적 등록 페이지는 수정하지 않음
+ */
+
+"use client";
+
+import { useRouter } from "next/navigation";
+import { useState } from "react";
+import { QuoteRegistrationV2, QuoteFormDataV2 } from "@/components/quotes/QuoteRegistrationV2";
+import { toast } from "sonner";
+
+export default function QuoteTestNewPage() {
+ const router = useRouter();
+ const [isSaving, setIsSaving] = useState(false);
+
+ 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" ? "임시" : "최종"} 저장 완료`);
+
+ // 저장 후 상세 페이지로 이동 (테스트용으로 ID=1 사용)
+ if (saveType === "final") {
+ router.push("/sales/quote-management/test/1");
+ }
+ } catch (error) {
+ toast.error("저장 중 오류가 발생했습니다.");
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/src/app/[locale]/(protected)/sales/quote-management/test/[id]/edit/page.tsx b/src/app/[locale]/(protected)/sales/quote-management/test/[id]/edit/page.tsx
new file mode 100644
index 00000000..afb22c59
--- /dev/null
+++ b/src/app/[locale]/(protected)/sales/quote-management/test/[id]/edit/page.tsx
@@ -0,0 +1,152 @@
+/**
+ * 견적 수정 테스트 페이지 (V2 UI)
+ *
+ * 새로운 자동 견적 산출 UI 테스트용
+ * 기존 견적 수정 페이지는 수정하지 않음
+ */
+
+"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() {
+ const router = useRouter();
+ const params = useParams();
+ const quoteId = params.id as string;
+
+ const [quote, setQuote] = useState
(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 ;
+ }
+
+ return (
+
+ );
+}
diff --git a/src/app/[locale]/(protected)/sales/quote-management/test/[id]/page.tsx b/src/app/[locale]/(protected)/sales/quote-management/test/[id]/page.tsx
new file mode 100644
index 00000000..4e8eadb6
--- /dev/null
+++ b/src/app/[locale]/(protected)/sales/quote-management/test/[id]/page.tsx
@@ -0,0 +1,126 @@
+/**
+ * 견적 상세 테스트 페이지 (V2 UI)
+ *
+ * 새로운 자동 견적 산출 UI 테스트용
+ * 기존 견적 상세 페이지는 수정하지 않음
+ */
+
+"use client";
+
+import { useRouter, useParams } from "next/navigation";
+import { useState, useEffect } from "react";
+import { QuoteRegistrationV2, QuoteFormDataV2, LocationItem } 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 QuoteTestDetailPage() {
+ const router = useRouter();
+ const params = useParams();
+ const quoteId = params.id as string;
+
+ const [quote, setQuote] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+
+ 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");
+ };
+
+ if (isLoading) {
+ return ;
+ }
+
+ return (
+
+ );
+}
diff --git a/src/components/business/construction/contract/ContractDetailForm.tsx b/src/components/business/construction/contract/ContractDetailForm.tsx
index e9f12e4e..f2b269ce 100644
--- a/src/components/business/construction/contract/ContractDetailForm.tsx
+++ b/src/components/business/construction/contract/ContractDetailForm.tsx
@@ -36,7 +36,7 @@ import {
getEmptyContractFormData,
contractDetailToFormData,
} from './types';
-import { updateContract, deleteContract } from './actions';
+import { updateContract, deleteContract, createContract } from './actions';
import { downloadFileById } from '@/lib/utils/fileDownload';
import { ContractDocumentModal } from './modals/ContractDocumentModal';
import {
@@ -59,19 +59,22 @@ function formatFileSize(bytes: number): string {
}
interface ContractDetailFormProps {
- mode: 'view' | 'edit';
+ mode: 'view' | 'edit' | 'create';
contractId: string;
initialData?: ContractDetail;
+ isChangeContract?: boolean; // 변경 계약서 생성 여부
}
export default function ContractDetailForm({
mode,
contractId,
initialData,
+ isChangeContract = false,
}: ContractDetailFormProps) {
const router = useRouter();
const isViewMode = mode === 'view';
const isEditMode = mode === 'edit';
+ const isCreateMode = mode === 'create';
// 폼 데이터
const [formData, setFormData] = useState(
@@ -121,10 +124,19 @@ export default function ContractDetailForm({
router.push(`/ko/construction/project/contract/${contractId}/edit`);
}, [router, contractId]);
- const handleCancel = useCallback(() => {
- router.push(`/ko/construction/project/contract/${contractId}`);
+ // 변경 계약서 생성 핸들러
+ const handleCreateChangeContract = useCallback(() => {
+ router.push(`/ko/construction/project/contract/create?baseContractId=${contractId}`);
}, [router, contractId]);
+ const handleCancel = useCallback(() => {
+ if (isCreateMode) {
+ router.push('/ko/construction/project/contract');
+ } else {
+ router.push(`/ko/construction/project/contract/${contractId}`);
+ }
+ }, [router, contractId, isCreateMode]);
+
// 폼 필드 변경
const handleFieldChange = useCallback(
(field: keyof ContractFormData, value: string | number) => {
@@ -141,14 +153,28 @@ export default function ContractDetailForm({
const handleConfirmSave = useCallback(async () => {
setIsLoading(true);
try {
- const result = await updateContract(contractId, formData);
- if (result.success) {
- toast.success('수정이 완료되었습니다.');
- setShowSaveDialog(false);
- router.push(`/ko/construction/project/contract/${contractId}`);
- router.refresh();
+ if (isCreateMode) {
+ // 새 계약 생성 (변경 계약서 포함)
+ const result = await createContract(formData);
+ if (result.success && result.data) {
+ toast.success(isChangeContract ? '변경 계약서가 생성되었습니다.' : '계약이 생성되었습니다.');
+ setShowSaveDialog(false);
+ router.push(`/ko/construction/project/contract/${result.data.id}`);
+ router.refresh();
+ } else {
+ toast.error(result.error || '저장에 실패했습니다.');
+ }
} else {
- toast.error(result.error || '저장에 실패했습니다.');
+ // 기존 계약 수정
+ const result = await updateContract(contractId, formData);
+ if (result.success) {
+ toast.success('수정이 완료되었습니다.');
+ setShowSaveDialog(false);
+ router.push(`/ko/construction/project/contract/${contractId}`);
+ router.refresh();
+ } else {
+ toast.error(result.error || '저장에 실패했습니다.');
+ }
}
} catch (error) {
if (isNextRedirectError(error)) throw error;
@@ -156,7 +182,7 @@ export default function ContractDetailForm({
} finally {
setIsLoading(false);
}
- }, [router, contractId, formData]);
+ }, [router, contractId, formData, isCreateMode, isChangeContract]);
// 삭제 핸들러
const handleDelete = useCallback(() => {
@@ -280,6 +306,9 @@ export default function ContractDetailForm({
// 헤더 액션 버튼
const headerActions = isViewMode ? (
+
+ ) : isCreateMode ? (
+
+
+
+
) : (
);
+ // 페이지 타이틀
+ const pageTitle = isCreateMode
+ ? (isChangeContract ? '변경 계약서 생성' : '계약 등록')
+ : '계약 상세';
+
return (
- {/* 파일 선택 버튼 (수정 모드에서만) */}
- {isEditMode && (
+ {/* 파일 선택 버튼 (수정/생성 모드에서만) */}
+ {(isEditMode || isCreateMode) && (
@@ -498,7 +541,7 @@ export default function ContractDetailForm({
{formData.contractFile.name}
(새 파일)
- {isEditMode && (
+ {(isEditMode || isCreateMode) && (
다운로드
- {isEditMode && (
+ {(isEditMode || isCreateMode) && (
- {isEditMode && (
+ {(isEditMode || isCreateMode) && (