fix(WEB): 견적 상세 페이지 상태 변경 및 견적완료일 표시 기능 추가

- 견적 상세 페이지에서 상태 변경 가능하도록 Select 컴포넌트 추가
- 견적완료일(completedDate) 필드를 FormData 타입에 추가
- 견적완료 상태일 때 완료일자 표시
- 저장 시 상태를 강제로 'completed'로 변경하던 로직 제거
- 목록 페이지에서 견적완료일이 표시되지 않던 문제 수정
  - updated_at을 완료 상태의 completedDate로 사용
- QuantityInput 컴포넌트 controlled component 에러 수정
  - defaultValue props 분리하여 spread 방지
This commit is contained in:
2026-01-23 21:09:18 +09:00
parent 750f50d953
commit 9fb5c171eb
5 changed files with 57 additions and 13 deletions

View File

@@ -113,11 +113,11 @@ export default function EstimateDetailForm({
console.log('🔍 [handleConfirmSave] formData.priceAdjustmentData:', formData.priceAdjustmentData);
console.log('🔍 [handleConfirmSave] formData 전체:', formData);
// 현재 사용자 이름을 견적자로 설정하고, 상태를 견적완료로 변경하여 저장
// 현재 사용자 이름을 견적자로 설정하고 저장 (상태는 사용자 선택값 유지)
const result = await updateEstimate(estimateId, {
...formData,
estimatorName: currentUser!.name,
status: 'completed', // 저장 시 견적완료 상태로 변경 (입찰에서 조회 가능)
// status는 formData에 포함되어 있으므로 사용자가 선택한 값 그대로 전송
});
if (result.success) {

View File

@@ -247,6 +247,14 @@ interface ApiBidDocument {
* 기존 프론트엔드 타입과 호환성 유지
*/
function transformQuoteToEstimate(apiData: ApiQuote): Estimate {
const status = mapQuoteStatusToEstimateStatus(apiData.status);
// 완료 상태인 경우 updated_at을 완료일로 사용
// (상태가 완료로 변경될 때 updated_at이 갱신되므로)
const completedDate = status === 'completed' && apiData.updated_at
? apiData.updated_at.split('T')[0] // ISO 형식에서 날짜만 추출
: null;
return {
id: String(apiData.id),
estimateCode: apiData.quote_number || '',
@@ -259,9 +267,9 @@ function transformQuoteToEstimate(apiData: ApiQuote): Estimate {
estimateCompanyManagerContact: '', // API에서 제공 시 매핑 필요
itemCount: apiData.items?.length || 0,
estimateAmount: Number(apiData.total_amount) || 0,
completedDate: null,
completedDate,
bidDate: apiData.registration_date || null,
status: mapQuoteStatusToEstimateStatus(apiData.status),
status,
createdAt: apiData.created_at || '',
updatedAt: apiData.updated_at || '',
createdBy: apiData.created_by ? String(apiData.created_by) : '',

View File

@@ -15,8 +15,8 @@ import {
} from '@/components/ui/select';
import { FileDropzone } from '@/components/ui/file-dropzone';
import { FileList, type ExistingFile } from '@/components/ui/file-list';
import type { EstimateDetailFormData } from '../types';
import { STATUS_STYLES, STATUS_LABELS, VAT_TYPE_OPTIONS } from '../types';
import type { EstimateDetailFormData, EstimateStatus } from '../types';
import { STATUS_STYLES, STATUS_LABELS, VAT_TYPE_OPTIONS, ESTIMATE_STATUS_OPTIONS } from '../types';
import { formatAmount } from '../utils';
interface EstimateInfoSectionProps {
@@ -66,8 +66,8 @@ export function EstimateInfoSection({
<Input value={formData.estimateCompanyManagerContact} disabled className="bg-gray-50" />
</div>
</div>
{/* 3행: 견적금액, 상태 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* 3행: 견적금액, 상태, 완료일자 */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label className="text-sm font-medium text-gray-700"></Label>
<Input
@@ -78,12 +78,43 @@ export function EstimateInfoSection({
</div>
<div className="space-y-2">
<Label className="text-sm font-medium text-gray-700"></Label>
<div className="flex items-center h-10 px-3 border rounded-md bg-gray-50">
<span className={STATUS_STYLES[formData.status]}>
{STATUS_LABELS[formData.status]}
</span>
</div>
{isViewMode ? (
<div className="flex items-center h-10 px-3 border rounded-md bg-gray-50">
<span className={STATUS_STYLES[formData.status]}>
{STATUS_LABELS[formData.status]}
</span>
</div>
) : (
<Select
value={formData.status}
onValueChange={(val) => onFormDataChange({ status: val as EstimateStatus })}
>
<SelectTrigger className="bg-white">
<SelectValue placeholder="상태 선택" />
</SelectTrigger>
<SelectContent>
{ESTIMATE_STATUS_OPTIONS.filter((opt) => opt.value !== 'all').map((option) => (
<SelectItem key={option.value} value={option.value}>
<span className={STATUS_STYLES[option.value as EstimateStatus]}>
{option.label}
</span>
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
{/* 완료일자: 견적완료 상태일 때만 표시 */}
{formData.status === 'completed' && formData.completedDate && (
<div className="space-y-2">
<Label className="text-sm font-medium text-gray-700"></Label>
<Input
value={formData.completedDate}
disabled
className="bg-gray-50"
/>
</div>
)}
</div>
</CardContent>
</Card>

View File

@@ -192,6 +192,7 @@ export interface EstimateDetailFormData {
estimateCompanyManagerContact: string; // 견적 회사 담당자 연락처
estimateAmount: number;
status: EstimateStatus;
completedDate: string | null; // 견적완료일
// 현장설명회 정보
siteBriefing: SiteBriefingInfo;
@@ -273,6 +274,7 @@ export function getEmptyEstimateDetailFormData(): EstimateDetailFormData {
estimateCompanyManagerContact: '',
estimateAmount: 0,
status: 'pending',
completedDate: null,
siteBriefing: {
briefingCode: '',
partnerName: '',
@@ -314,6 +316,7 @@ export function estimateDetailToFormData(detail: EstimateDetail): EstimateDetail
estimateCompanyManagerContact: detail.estimateCompanyManagerContact || '',
estimateAmount: detail.estimateAmount,
status: detail.status,
completedDate: detail.completedDate || null,
siteBriefing: {
...detail.siteBriefing,
briefingDate: normalizeDateValue(detail.siteBriefing?.briefingDate),

View File

@@ -63,6 +63,8 @@ const QuantityInput = React.forwardRef<HTMLInputElement, QuantityInputProps>(
disabled,
onFocus,
onBlur,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
defaultValue: _defaultValue, // controlled component이므로 defaultValue 무시
...props
},
ref