fix(WEB): 건설 견적 관리 개선

- 견적 상세/수정 페이지 타입 수정
- actions.ts API 연동 개선
This commit is contained in:
2026-01-13 19:47:51 +09:00
parent fab7d669d5
commit 8083c0e015
3 changed files with 93 additions and 384 deletions

View File

@@ -3,202 +3,36 @@
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';
interface EstimateEditPageProps {
params: Promise<{ id: string }>;
}
// 목업 데이터 - 추후 API 연동
function getEstimateDetail(id: string): EstimateDetail {
// TODO: 실제 API 연동
const mockData: EstimateDetail = {
id,
estimateCode: '123123',
partnerId: '1',
partnerName: '회사명',
projectName: '현장명',
estimatorId: 'hong',
estimatorName: '이름',
itemCount: 21,
estimateAmount: 1420000,
completedDate: null,
bidDate: '2025-12-12',
status: 'pending',
createdAt: '2025-12-01',
updatedAt: '2025-12-01',
createdBy: 'hong',
siteBriefing: {
briefingCode: '123123',
partnerName: '회사명',
companyName: '회사명',
briefingDate: '2025-12-12',
attendee: '이름',
},
bidInfo: {
projectName: '현장명',
bidDate: '2025-12-12',
siteCount: 21,
constructionPeriod: '2026-01-01 ~ 2026-12-10',
constructionStartDate: '2026-01-01',
constructionEndDate: '2026-12-10',
vatType: 'excluded',
workReport: '업무 보고 내용',
documents: [
{
id: '1',
fileName: 'abc.zip',
fileUrl: '#',
fileSize: 1024000,
},
],
},
summaryItems: [
{
id: '1',
name: '서터 심창측공사',
quantity: 1,
unit: '식',
materialCost: 78540000,
laborCost: 15410000,
totalCost: 93950000,
remarks: '',
},
],
expenseItems: [
{
id: '1',
name: 'public_1',
amount: 10000,
},
],
priceAdjustments: [
{
id: '1',
category: '배합비',
unitPrice: 10000,
coating: 10000,
batting: 10000,
boxReinforce: 10500,
painting: 10500,
total: 51000,
},
{
id: '2',
category: '재단비',
unitPrice: 1375,
coating: 0,
batting: 0,
boxReinforce: 0,
painting: 0,
total: 1375,
},
{
id: '3',
category: '판매단가',
unitPrice: 0,
coating: 10000,
batting: 10000,
boxReinforce: 10500,
painting: 10500,
total: 41000,
},
{
id: '4',
category: '조립단가',
unitPrice: 10300,
coating: 10300,
batting: 10300,
boxReinforce: 10500,
painting: 10200,
total: 51600,
},
],
detailItems: [
{
id: '1',
no: 1,
name: 'FS530외/주차',
material: 'screen',
width: 2350,
height: 2500,
quantity: 1,
box: 1,
assembly: 0,
coating: 0,
batting: 0,
mounting: 0,
fitting: 0,
controller: 0,
widthConstruction: 0,
heightConstruction: 0,
materialCost: 1420000,
laborCost: 510000,
quantityPrice: 1930000,
expenseQuantity: 5500,
expenseTotal: 5500,
totalCost: 1930000,
otherCost: 0,
marginCost: 0,
totalPrice: 1930000,
unitPrice: 1420000,
expense: 0,
marginRate: 0,
unitQuantity: 1,
expenseResult: 0,
marginActual: 0,
},
{
id: '2',
no: 2,
name: 'FS530외/주차',
material: 'screen',
width: 7500,
height: 2500,
quantity: 1,
box: 1,
assembly: 0,
coating: 0,
batting: 0,
mounting: 0,
fitting: 0,
controller: 0,
widthConstruction: 0,
heightConstruction: 0,
materialCost: 4720000,
laborCost: 780000,
quantityPrice: 5500000,
expenseQuantity: 5500,
expenseTotal: 5500,
totalCost: 5500000,
otherCost: 0,
marginCost: 0,
totalPrice: 5500000,
unitPrice: 4720000,
expense: 0,
marginRate: 0,
unitQuantity: 1,
expenseResult: 0,
marginActual: 0,
},
],
approval: {
approvers: [],
references: [],
},
};
return mockData;
}
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);
useEffect(() => {
const detail = getEstimateDetail(id);
setData(detail);
setIsLoading(false);
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) {
@@ -209,6 +43,14 @@ export default function EstimateEditPage({ params }: EstimateEditPageProps) {
);
}
if (error) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-red-500">{error}</div>
</div>
);
}
return (
<EstimateDetailForm
mode="edit"

View File

@@ -3,202 +3,36 @@
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';
interface EstimateDetailPageProps {
params: Promise<{ id: string }>;
}
// 목업 데이터 - 추후 API 연동
function getEstimateDetail(id: string): EstimateDetail {
// TODO: 실제 API 연동
const mockData: EstimateDetail = {
id,
estimateCode: '123123',
partnerId: '1',
partnerName: '회사명',
projectName: '현장명',
estimatorId: 'hong',
estimatorName: '이름',
itemCount: 21,
estimateAmount: 1420000,
completedDate: null,
bidDate: '2025-12-12',
status: 'pending',
createdAt: '2025-12-01',
updatedAt: '2025-12-01',
createdBy: 'hong',
siteBriefing: {
briefingCode: '123123',
partnerName: '회사명',
companyName: '회사명',
briefingDate: '2025-12-12',
attendee: '이름',
},
bidInfo: {
projectName: '현장명',
bidDate: '2025-12-12',
siteCount: 21,
constructionPeriod: '2026-01-01 ~ 2026-12-10',
constructionStartDate: '2026-01-01',
constructionEndDate: '2026-12-10',
vatType: 'excluded',
workReport: '업무 보고 내용',
documents: [
{
id: '1',
fileName: 'abc.zip',
fileUrl: '#',
fileSize: 1024000,
},
],
},
summaryItems: [
{
id: '1',
name: '서터 심창측공사',
quantity: 1,
unit: '식',
materialCost: 78540000,
laborCost: 15410000,
totalCost: 93950000,
remarks: '',
},
],
expenseItems: [
{
id: '1',
name: 'public_1',
amount: 10000,
},
],
priceAdjustments: [
{
id: '1',
category: '배합비',
unitPrice: 10000,
coating: 10000,
batting: 10000,
boxReinforce: 10500,
painting: 10500,
total: 51000,
},
{
id: '2',
category: '재단비',
unitPrice: 1375,
coating: 0,
batting: 0,
boxReinforce: 0,
painting: 0,
total: 1375,
},
{
id: '3',
category: '판매단가',
unitPrice: 0,
coating: 10000,
batting: 10000,
boxReinforce: 10500,
painting: 10500,
total: 41000,
},
{
id: '4',
category: '조립단가',
unitPrice: 10300,
coating: 10300,
batting: 10300,
boxReinforce: 10500,
painting: 10200,
total: 51600,
},
],
detailItems: [
{
id: '1',
no: 1,
name: 'FS530외/주차',
material: 'screen',
width: 2350,
height: 2500,
quantity: 1,
box: 1,
assembly: 0,
coating: 0,
batting: 0,
mounting: 0,
fitting: 0,
controller: 0,
widthConstruction: 0,
heightConstruction: 0,
materialCost: 1420000,
laborCost: 510000,
quantityPrice: 1930000,
expenseQuantity: 5500,
expenseTotal: 5500,
totalCost: 1930000,
otherCost: 0,
marginCost: 0,
totalPrice: 1930000,
unitPrice: 1420000,
expense: 0,
marginRate: 0,
unitQuantity: 1,
expenseResult: 0,
marginActual: 0,
},
{
id: '2',
no: 2,
name: 'FS530외/주차',
material: 'screen',
width: 7500,
height: 2500,
quantity: 1,
box: 1,
assembly: 0,
coating: 0,
batting: 0,
mounting: 0,
fitting: 0,
controller: 0,
widthConstruction: 0,
heightConstruction: 0,
materialCost: 4720000,
laborCost: 780000,
quantityPrice: 5500000,
expenseQuantity: 5500,
expenseTotal: 5500,
totalCost: 5500000,
otherCost: 0,
marginCost: 0,
totalPrice: 5500000,
unitPrice: 4720000,
expense: 0,
marginRate: 0,
unitQuantity: 1,
expenseResult: 0,
marginActual: 0,
},
],
approval: {
approvers: [],
references: [],
},
};
return mockData;
}
export default function EstimateDetailPage({ params }: EstimateDetailPageProps) {
const { id } = use(params);
const [data, setData] = useState<EstimateDetail | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const detail = getEstimateDetail(id);
setData(detail);
setIsLoading(false);
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) {
@@ -209,6 +43,14 @@ 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>
);
}
return (
<EstimateDetailForm
mode="view"