diff --git a/src/components/attendance/actions.ts b/src/components/attendance/actions.ts index 5bd2ea66..2e572cb3 100644 --- a/src/components/attendance/actions.ts +++ b/src/components/attendance/actions.ts @@ -12,6 +12,7 @@ import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch, getServerApiHeaders } from '@/lib/api/fetch-wrapper'; + import { getTodayString } from '@/utils/date'; // ============================================ // 타입 정의 @@ -217,7 +218,7 @@ export async function getTodayAttendance(): Promise<{ __authError?: boolean; }> { try { - const today = new Date().toISOString().split('T')[0]; + const today = getTodayString(); const { response, error } = await serverFetch( `${process.env.NEXT_PUBLIC_API_URL}/api/v1/attendances?date=${today}&per_page=1`, diff --git a/src/components/business/MainDashboard.tsx b/src/components/business/MainDashboard.tsx index d8baf9cb..e66994a8 100644 --- a/src/components/business/MainDashboard.tsx +++ b/src/components/business/MainDashboard.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useState } from "react"; +import { getLocalDateString, getTodayString } from "@/utils/date"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -537,7 +538,7 @@ export function MainDashboard() { }, // 오늘 일정 (금일) { - date: new Date().toISOString().split('T')[0], + date: getTodayString(), events: [ { time: "09:30", title: "일일 운영회의", location: "회의실 B", attendees: 6, priority: "high" }, { time: "11:00", title: "고객 컴플레인 대응", location: "CS실", attendees: 4, priority: "high" }, @@ -2310,8 +2311,8 @@ export function MainDashboard() { {calendarDate && (() => { - const selectedDateStr = calendarDate.toISOString().split('T')[0]; - const today = new Date().toISOString().split('T')[0]; + const selectedDateStr = getLocalDateString(calendarDate); + const today = getTodayString(); const isToday = selectedDateStr === today; const dayIncoming = ceoData.calendarData.incoming.find(item => item.date === selectedDateStr); diff --git a/src/components/business/construction/bidding/types.ts b/src/components/business/construction/bidding/types.ts index a0b765ad..07771581 100644 --- a/src/components/business/construction/bidding/types.ts +++ b/src/components/business/construction/bidding/types.ts @@ -5,6 +5,8 @@ * (별도 등록 기능 없음, 상세/수정만 가능) */ +import { getTodayString } from '@/utils/date'; + // 입찰 상태 export type BiddingStatus = | 'waiting' // 입찰대기 @@ -217,7 +219,7 @@ export const VAT_TYPE_OPTIONS = [ ]; // 오늘 날짜 (YYYY-MM-DD) -const getTodayDate = () => new Date().toISOString().split('T')[0]; +const getTodayDate = () => getTodayString(); // 날짜 값 정규화 (빈 값, '0', null이면 오늘 날짜 반환) const normalizeDateValue = (value: string | null | undefined): string => { diff --git a/src/components/business/construction/contract/types.ts b/src/components/business/construction/contract/types.ts index b91a17fa..9c8bd0ce 100644 --- a/src/components/business/construction/contract/types.ts +++ b/src/components/business/construction/contract/types.ts @@ -4,6 +4,8 @@ * 계약 데이터는 낙찰 후 자동 등록됨 */ +import { getTodayString } from '@/utils/date'; + // 계약 상태 export type ContractStatus = | 'pending' // 계약대기 @@ -192,7 +194,7 @@ export interface ContractFormData { } // 오늘 날짜 (YYYY-MM-DD) -const getTodayDate = () => new Date().toISOString().split('T')[0]; +const getTodayDate = () => getTodayString(); // 날짜 값 정규화 (빈 값, '0', null이면 오늘 날짜 반환) const normalizeDateValue = (value: string | null | undefined): string => { diff --git a/src/components/business/construction/estimates/types.ts b/src/components/business/construction/estimates/types.ts index d95bc6e8..2d26570b 100644 --- a/src/components/business/construction/estimates/types.ts +++ b/src/components/business/construction/estimates/types.ts @@ -2,6 +2,8 @@ * 주일 기업 - 견적관리 타입 정의 */ +import { getTodayString } from '@/utils/date'; + // 견적 상태 export type EstimateStatus = 'pending' | 'approval_waiting' | 'completed' | 'rejected' | 'hold'; @@ -253,7 +255,7 @@ export function getEmptyPriceAdjustmentData(): PriceAdjustmentData { } // 오늘 날짜 (YYYY-MM-DD) -const getTodayDate = () => new Date().toISOString().split('T')[0]; +const getTodayDate = () => getTodayString(); // 날짜 값 정규화 (빈 값, '0', null이면 오늘 날짜 반환) const normalizeDateValue = (value: string | null | undefined): string => { diff --git a/src/components/business/construction/issue-management/IssueDetailForm.tsx b/src/components/business/construction/issue-management/IssueDetailForm.tsx index 537faece..6b256803 100644 --- a/src/components/business/construction/issue-management/IssueDetailForm.tsx +++ b/src/components/business/construction/issue-management/IssueDetailForm.tsx @@ -2,6 +2,7 @@ import { useState, useCallback, useRef, useEffect } from 'react'; import { useRouter } from 'next/navigation'; +import { getTodayString } from '@/utils/date'; import { Mic, X, Upload } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -54,7 +55,7 @@ export default function IssueDetailForm({ issue, mode = 'view' }: IssueDetailFor constructionManagers: issue?.constructionManagers || '', reporter: issue?.reporter || '', assignee: issue?.assignee || '', - reportDate: issue?.reportDate || new Date().toISOString().split('T')[0], + reportDate: issue?.reportDate || getTodayString(), resolvedDate: issue?.resolvedDate || '', status: issue?.status || 'received', category: issue?.category || 'material', diff --git a/src/components/business/construction/management/ConstructionDetailClient.tsx b/src/components/business/construction/management/ConstructionDetailClient.tsx index bb294b9d..0316668b 100644 --- a/src/components/business/construction/management/ConstructionDetailClient.tsx +++ b/src/components/business/construction/management/ConstructionDetailClient.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useCallback, useMemo } from 'react'; import { useRouter } from 'next/navigation'; +import { getTodayString } from '@/utils/date'; import { Plus, Trash2, FileText, Upload, X } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -123,7 +124,7 @@ export default function ConstructionDetailClient({ id, mode }: ConstructionDetai const handleAddWorkerInfo = () => { const newWorkerInfo: WorkerInfo = { id: `worker-${Date.now()}`, - workDate: new Date().toISOString().split('T')[0], + workDate: getTodayString(), workers: [], }; setFormData((prev) => ({ diff --git a/src/components/business/construction/management/ProjectEndDialog.tsx b/src/components/business/construction/management/ProjectEndDialog.tsx index 2f432475..0a67ea98 100644 --- a/src/components/business/construction/management/ProjectEndDialog.tsx +++ b/src/components/business/construction/management/ProjectEndDialog.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState, useEffect } from 'react'; +import { getTodayString } from '@/utils/date'; import { Dialog, DialogContent, @@ -53,7 +54,7 @@ export default function ProjectEndDialog({ projectId: project.id, projectName: project.siteName, workDate: project.endDate, // 결선작업일은 프로젝트 종료일로 설정 - completionDate: new Date().toISOString().split('T')[0], // 오늘 날짜 + completionDate: getTodayString(), // 오늘 날짜 status: project.status === 'completed' ? 'completed' : 'in_progress', memo: '', }); diff --git a/src/components/business/construction/site-briefings/types.ts b/src/components/business/construction/site-briefings/types.ts index cdddfc36..56cc2ac3 100644 --- a/src/components/business/construction/site-briefings/types.ts +++ b/src/components/business/construction/site-briefings/types.ts @@ -2,6 +2,8 @@ * 주일 기업 - 현장설명회 관리 타입 정의 */ +import { getTodayString } from '@/utils/date'; + // 현장설명회 상태 export type SiteBriefingStatus = 'scheduled' | 'ongoing' | 'completed' | 'cancelled' | 'postponed'; @@ -218,7 +220,7 @@ export const VAT_TYPE_OPTIONS = [ ]; // 오늘 날짜 (YYYY-MM-DD) -const getTodayDate = () => new Date().toISOString().split('T')[0]; +const getTodayDate = () => getTodayString(); // 날짜 값 정규화 (빈 값, '0', null이면 오늘 날짜 반환) const normalizeDateValue = (value: string | null | undefined): string => { diff --git a/src/components/dev/generators/index.ts b/src/components/dev/generators/index.ts index 3c7ba787..62b9fa8d 100644 --- a/src/components/dev/generators/index.ts +++ b/src/components/dev/generators/index.ts @@ -2,6 +2,8 @@ * 샘플 데이터 생성 공통 유틸리티 */ +import { getLocalDateString } from '@/utils/date'; + // 랜덤 선택 export function randomPick(arr: T[]): T { return arr[Math.floor(Math.random() * arr.length)]; @@ -30,12 +32,12 @@ export function randomPhone(): string { export function dateAfterDays(days: number): string { const date = new Date(); date.setDate(date.getDate() + days); - return date.toISOString().split('T')[0]; + return getLocalDateString(date); } // 오늘 날짜 export function today(): string { - return new Date().toISOString().split('T')[0]; + return getLocalDateString(new Date()); } // 랜덤 층수 diff --git a/src/components/material/ReceivingManagement/InspectionCreate.tsx b/src/components/material/ReceivingManagement/InspectionCreate.tsx index 657a7cf1..350f2faa 100644 --- a/src/components/material/ReceivingManagement/InspectionCreate.tsx +++ b/src/components/material/ReceivingManagement/InspectionCreate.tsx @@ -13,6 +13,7 @@ import { useState, useCallback, useMemo, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { Calendar } from 'lucide-react'; +import { getTodayString } from '@/utils/date'; import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate'; import { materialInspectionCreateConfig } from './inspectionConfig'; import { ContentSkeleton } from '@/components/ui/skeleton'; @@ -66,10 +67,7 @@ export function InspectionCreate({ id }: Props) { const [selectedTargetId, setSelectedTargetId] = useState(id || ''); // 검사 정보 - const [inspectionDate, setInspectionDate] = useState(() => { - const today = new Date(); - return today.toISOString().split('T')[0]; - }); + const [inspectionDate, setInspectionDate] = useState(() => getTodayString()); const [inspector, setInspector] = useState(''); const [lotNo, setLotNo] = useState(() => generateLotNo()); diff --git a/src/components/material/ReceivingManagement/ReceivingReceiptContent.tsx b/src/components/material/ReceivingManagement/ReceivingReceiptContent.tsx index 2f2a04fa..05f04ddd 100644 --- a/src/components/material/ReceivingManagement/ReceivingReceiptContent.tsx +++ b/src/components/material/ReceivingManagement/ReceivingReceiptContent.tsx @@ -9,6 +9,7 @@ import type { ReceivingDetail } from './types'; import { DocumentHeader } from '@/components/document-system'; +import { getTodayString } from '@/utils/date'; interface ReceivingReceiptContentProps { data: ReceivingDetail; @@ -38,7 +39,7 @@ export function ReceivingReceiptContent({ data: detail }: ReceivingReceiptConten 입고번호 {detail.orderNo} 입고일자 - {detail.receivingDate || today.toISOString().split('T')[0]} + {detail.receivingDate || getTodayString()} 발주번호 {detail.orderNo} 입고LOT diff --git a/src/components/material/StockStatus/mockData.ts b/src/components/material/StockStatus/mockData.ts index 6ee9521c..85c6ef80 100644 --- a/src/components/material/StockStatus/mockData.ts +++ b/src/components/material/StockStatus/mockData.ts @@ -3,6 +3,7 @@ */ import type { StockItem, StockDetail, StockStats, FilterTab } from './types'; +import { getLocalDateString } from '@/utils/date'; // 재고 상태 결정 함수 function getStockStatus(stockQty: number, safetyStock: number): 'normal' | 'low' | 'out' { @@ -418,7 +419,7 @@ export function generateStockDetail(item: StockItem): StockDetail { const daysAgo = seededInt(lotSeed, 5, 60); const date = new Date('2025-12-23'); // 고정 날짜 사용 date.setDate(date.getDate() - daysAgo); - const dateStr = date.toISOString().split('T')[0]; + const dateStr = getLocalDateString(date); const lotDate = dateStr.replace(/-/g, '').slice(2); return { diff --git a/src/components/orders/documents/PurchaseOrderDocument.tsx b/src/components/orders/documents/PurchaseOrderDocument.tsx index 57c7549d..a288eb61 100644 --- a/src/components/orders/documents/PurchaseOrderDocument.tsx +++ b/src/components/orders/documents/PurchaseOrderDocument.tsx @@ -5,6 +5,7 @@ * - 스크린샷 형식 + 지출결의서 디자인 스타일 */ +import { getTodayString } from "@/utils/date"; import { OrderItem } from "../actions"; /** @@ -53,7 +54,7 @@ export function PurchaseOrderDocument({ expectedShipDate = "-", deliveryMethod = "상차", address = "-", - orderDate = new Date().toISOString().split("T")[0], + orderDate = getTodayString(), installationCount = 3, items = [], remarks, diff --git a/src/components/outbound/ShipmentManagement/ShipmentCreate.tsx b/src/components/outbound/ShipmentManagement/ShipmentCreate.tsx index e46343a2..9014b806 100644 --- a/src/components/outbound/ShipmentManagement/ShipmentCreate.tsx +++ b/src/components/outbound/ShipmentManagement/ShipmentCreate.tsx @@ -8,6 +8,7 @@ import { useState, useCallback, useEffect } from 'react'; import { useRouter } from 'next/navigation'; +import { getTodayString } from '@/utils/date'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; @@ -60,7 +61,7 @@ export function ShipmentCreate() { // 폼 상태 const [formData, setFormData] = useState({ lotNo: '', - scheduledDate: new Date().toISOString().split('T')[0], + scheduledDate: getTodayString(), priority: 'normal', deliveryMethod: 'pickup', logisticsCompany: '', diff --git a/src/components/pricing/PricingFormClient.tsx b/src/components/pricing/PricingFormClient.tsx index 9d9f4cf0..44d8956e 100644 --- a/src/components/pricing/PricingFormClient.tsx +++ b/src/components/pricing/PricingFormClient.tsx @@ -13,6 +13,7 @@ import { useState, useEffect, useCallback, useMemo } from 'react'; import { useRouter } from 'next/navigation'; +import { getTodayString } from '@/utils/date'; import { DollarSign, Package, @@ -98,7 +99,7 @@ export function PricingFormClient({ // 폼 상태 const [effectiveDate, setEffectiveDate] = useState( - initialData?.effectiveDate || new Date().toISOString().split('T')[0] + initialData?.effectiveDate || getTodayString() ); const [receiveDate, setReceiveDate] = useState(initialData?.receiveDate || ''); const [author, setAuthor] = useState(initialData?.author || ''); diff --git a/src/components/quality/InspectionManagement/mockData.ts b/src/components/quality/InspectionManagement/mockData.ts index 83d69fa1..90316f4e 100644 --- a/src/components/quality/InspectionManagement/mockData.ts +++ b/src/components/quality/InspectionManagement/mockData.ts @@ -3,6 +3,7 @@ import type { InspectionStats, InspectionItem, } from './types'; +import { getTodayString } from '@/utils/date'; // 검사 항목 템플릿 (조인트바 예시) export const inspectionItemsTemplate: InspectionItem[] = [ @@ -237,7 +238,7 @@ export const mockInspections: Inspection[] = [ // 통계 데이터 계산 export const calculateStats = (inspections: Inspection[]): InspectionStats => { - const today = new Date().toISOString().split('T')[0]; + const today = getTodayString(); const waitingCount = inspections.filter(i => i.status === '대기').length; const inProgressCount = inspections.filter(i => i.status === '진행중').length; diff --git a/src/components/quotes/QuoteRegistration.tsx b/src/components/quotes/QuoteRegistration.tsx index 7cf655d5..a23329ca 100644 --- a/src/components/quotes/QuoteRegistration.tsx +++ b/src/components/quotes/QuoteRegistration.tsx @@ -34,6 +34,7 @@ import { Loader2, } from "lucide-react"; import { toast } from "sonner"; +import { getTodayString } from "@/utils/date"; // 필드명 매핑 const FIELD_NAME_MAP: Record = { @@ -119,7 +120,7 @@ const createNewItem = (): QuoteItem => ({ // 초기 폼 데이터 export const INITIAL_QUOTE_FORM: QuoteFormData = { - registrationDate: new Date().toISOString().split("T")[0], + registrationDate: getTodayString(), writer: "드미트리", // TODO: 로그인 사용자 정보로 대체 clientId: "", clientName: "", diff --git a/src/components/quotes/QuoteRegistrationV2.tsx b/src/components/quotes/QuoteRegistrationV2.tsx index a81ea723..0fe54277 100644 --- a/src/components/quotes/QuoteRegistrationV2.tsx +++ b/src/components/quotes/QuoteRegistrationV2.tsx @@ -48,6 +48,7 @@ import { isNextRedirectError } from "@/lib/utils/redirect-error"; import { useDevFill } from "@/components/dev/useDevFill"; import type { Vendor } from "../accounting/VendorManagement"; import type { BomMaterial, CalculationResults } from "./types"; +import { getLocalDateString, getDateAfterDays } from "@/utils/date"; // ============================================================================= // 타입 정의 @@ -117,7 +118,7 @@ const createNewLocation = (): LocationItem => ({ // 초기 폼 데이터 const INITIAL_FORM_DATA: QuoteFormDataV2 = { - registrationDate: new Date().toISOString().split("T")[0], + registrationDate: getLocalDateString(new Date()), writer: "", // useAuth()에서 currentUser.name으로 설정됨 clientId: "", clientName: "", @@ -251,14 +252,14 @@ export function QuoteRegistrationV2({ } const testData: QuoteFormDataV2 = { - registrationDate: new Date().toISOString().split("T")[0], + registrationDate: getLocalDateString(new Date()), writer: writerName, clientId: clients[0]?.id?.toString() || "", clientName: clients[0]?.company_name || "테스트 거래처", siteName: "테스트 현장", manager: "홍길동", contact: "010-1234-5678", - dueDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().split("T")[0], + dueDate: getDateAfterDays(7), remarks: "[DevFill] 테스트 견적입니다.", status: "draft", locations: testLocations, diff --git a/src/components/quotes/types.ts b/src/components/quotes/types.ts index 49275dde..310029da 100644 --- a/src/components/quotes/types.ts +++ b/src/components/quotes/types.ts @@ -6,6 +6,8 @@ * - product_category: screen, steel */ +import { formatDateForInput } from "@/utils/date"; + // ===== 견적 상태 ===== export type QuoteStatus = | 'draft' // 작성중 @@ -33,29 +35,6 @@ export const QUOTE_STATUS_COLORS: Record = { converted: 'bg-indigo-100 text-indigo-800', }; -// ===== 날짜 형식 변환 헬퍼 ===== -/** - * API 날짜 문자열을 HTML date input용 YYYY-MM-DD 형식으로 변환 - * 지원 형식: ISO 8601, datetime string, date only - */ -function formatDateForInput(dateStr: string | null | undefined): string { - if (!dateStr) return ''; - - // 이미 YYYY-MM-DD 형식인 경우 - if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) { - return dateStr; - } - - // ISO 8601 또는 datetime 형식 (2025-01-06T00:00:00.000Z, 2025-01-06 00:00:00) - const date = new Date(dateStr); - if (isNaN(date.getTime())) { - return ''; // 유효하지 않은 날짜 - } - - // YYYY-MM-DD 형식으로 변환 - return date.toISOString().split('T')[0]; -} - // ===== 제품 카테고리 ===== export type ProductCategory = 'screen' | 'steel'; @@ -717,7 +696,12 @@ export function transformV2ToApi( ): Record { // 1. calculation_inputs 생성 (폼 복원용) - const calculationInputs: CalculationInputs = { + // bomResults 수집 (인자로 받은 것 또는 locations에 저장된 것) + const collectedBomResults = bomResults || data.locations + .map(loc => loc.bomResult) + .filter((br): br is BomCalculationResult => br !== undefined); + + const calculationInputs: CalculationInputs & { bomResults?: BomCalculationResult[] } = { items: data.locations.map(loc => ({ productCategory: 'screen', // TODO: 동적으로 결정 productName: loc.productName, @@ -732,6 +716,8 @@ export function transformV2ToApi( code: loc.code, quantity: loc.quantity, })), + // BOM 결과 저장 (조회 시 복원용) + bomResults: collectedBomResults.length > 0 ? collectedBomResults : undefined, }; // 2. items 생성 (BOM 결과 있으면 자재 상세, 없으면 완제품 기준) @@ -867,7 +853,15 @@ export function transformV2ToApi( * - 없으면 items에서 추출 시도 (레거시 호환) */ export function transformApiToV2(apiData: QuoteApiData): QuoteFormDataV2 { - const calcInputs = apiData.calculation_inputs?.items || []; + // 원본 API 데이터(calculation_inputs)와 변환된 데이터(calculationInputs) 모두 지원 + // getQuoteById가 transformApiToFrontend를 적용하면 calculationInputs(camelCase)가 됨 + const rawCalcInputs = apiData.calculation_inputs; + const transformedCalcInputs = (apiData as unknown as { calculationInputs?: CalculationInputs & { bomResults?: BomCalculationResult[] } }).calculationInputs; + const calculationInputs = rawCalcInputs || transformedCalcInputs; + + const calcInputs = calculationInputs?.items || []; + // BOM 결과 복원 (저장 시 calculation_inputs.bomResults에 저장됨) + const savedBomResults = (calculationInputs as { bomResults?: BomCalculationResult[] } | undefined)?.bomResults || []; // calculation_inputs에서 locations 복원 let locations: LocationItem[] = []; @@ -884,6 +878,9 @@ export function transformApiToV2(apiData: QuoteApiData): QuoteFormDataV2 { ); const qty = ci.quantity || 1; + // 해당 인덱스의 BOM 결과 복원 + const bomResult = savedBomResults[index]; + return { id: `loc-${index}`, floor: ci.floor || '', @@ -900,6 +897,7 @@ export function transformApiToV2(apiData: QuoteApiData): QuoteFormDataV2 { inspectionFee: ci.inspectionFee || 50000, unitPrice: totalPrice > 0 ? Math.round(totalPrice / qty) : undefined, totalPrice: totalPrice > 0 ? totalPrice : undefined, + bomResult: bomResult, // BOM 결과 복원 }; }); } @@ -910,43 +908,44 @@ export function transformApiToV2(apiData: QuoteApiData): QuoteFormDataV2 { return 'draft'; }; + // 변환된 Quote 데이터 타입 (camelCase) + const transformed = apiData as unknown as { + registrationDate?: string; + createdBy?: string; + clientId?: string; + clientName?: string; + siteName?: string; + managerName?: string; + managerContact?: string; + deliveryDate?: string; + description?: string; + }; + return { id: String(apiData.id), - registrationDate: formatDateForInput(apiData.registration_date), - writer: apiData.creator?.name || '', - clientId: apiData.client_id ? String(apiData.client_id) : '', - clientName: apiData.client?.name || apiData.client_name || '', - siteName: apiData.site_name || '', - manager: apiData.manager || apiData.manager_name || '', - contact: apiData.contact || apiData.manager_contact || '', - dueDate: formatDateForInput(apiData.completion_date || apiData.delivery_date), - remarks: apiData.remarks || apiData.description || '', + // raw API: registration_date, transformed: registrationDate + registrationDate: formatDateForInput(apiData.registration_date || transformed.registrationDate), + // raw API: creator?.name, transformed: createdBy + writer: apiData.creator?.name || transformed.createdBy || '', + // raw API: client_id, transformed: clientId + clientId: apiData.client_id ? String(apiData.client_id) : (transformed.clientId || ''), + // raw API: client?.name || client_name, transformed: clientName + clientName: apiData.client?.name || apiData.client_name || transformed.clientName || '', + // raw API: site_name, transformed: siteName + siteName: apiData.site_name || transformed.siteName || '', + // raw API: manager || manager_name, transformed: managerName + manager: apiData.manager || apiData.manager_name || transformed.managerName || '', + // raw API: contact || manager_contact, transformed: managerContact + contact: apiData.contact || apiData.manager_contact || transformed.managerContact || '', + // raw API: completion_date || delivery_date, transformed: deliveryDate + dueDate: formatDateForInput(apiData.completion_date || apiData.delivery_date || transformed.deliveryDate), + // raw API: remarks || description, transformed: description + remarks: apiData.remarks || apiData.description || transformed.description || '', status: mapStatus(apiData.status), locations: locations, }; } -/** - * 날짜 형식 변환 헬퍼 (V2용 - formatDateForInput과 동일) - * API 날짜 문자열을 HTML date input용 YYYY-MM-DD 형식으로 변환 - */ -function formatDateForInputV2(dateStr: string | null | undefined): string { - if (!dateStr) return ''; - - // 이미 YYYY-MM-DD 형식인 경우 - if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) { - return dateStr; - } - - // ISO 8601 또는 datetime 형식 - const date = new Date(dateStr); - if (isNaN(date.getTime())) { - return ''; - } - - return date.toISOString().split('T')[0]; -} - // ===== QuoteFormData → API 요청 데이터 변환 ===== export function transformFormDataToApi(formData: QuoteFormData): Record { // calculationResults가 있으면 BOM 자재 기반으로 items 생성 diff --git a/src/utils/date.ts b/src/utils/date.ts new file mode 100644 index 00000000..7c366025 --- /dev/null +++ b/src/utils/date.ts @@ -0,0 +1,59 @@ +/** + * 날짜 관련 유틸리티 함수 + */ + +/** + * 로컬 시간대 기준 YYYY-MM-DD 형식 반환 + * + * 주의: toISOString()은 UTC 기준이므로 한국 시간대(UTC+9)에서 + * 오전 9시 이전에 사용하면 하루 전 날짜가 반환됨 + * + * @example + * // 2025-01-26 08:30 KST + * new Date().toISOString().split('T')[0] // "2025-01-25" (잘못됨) + * getLocalDateString(new Date()) // "2025-01-26" (정확함) + */ +export function getLocalDateString(date: Date = new Date()): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; +} + +/** + * 오늘 날짜를 YYYY-MM-DD 형식으로 반환 (로컬 시간대 기준) + */ +export function getTodayString(): string { + return getLocalDateString(new Date()); +} + +/** + * N일 후 날짜를 YYYY-MM-DD 형식으로 반환 (로컬 시간대 기준) + */ +export function getDateAfterDays(days: number): string { + const date = new Date(); + date.setDate(date.getDate() + days); + return getLocalDateString(date); +} + +/** + * API 날짜 문자열을 HTML date input용 YYYY-MM-DD 형식으로 변환 + * 지원 형식: ISO 8601, datetime string, date only + */ +export function formatDateForInput(dateStr: string | null | undefined): string { + if (!dateStr) return ''; + + // 이미 YYYY-MM-DD 형식인 경우 + if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) { + return dateStr; + } + + // ISO 8601 또는 datetime 형식 (2025-01-06T00:00:00.000Z, 2025-01-06 00:00:00) + const date = new Date(dateStr); + if (isNaN(date.getTime())) { + return ''; // 유효하지 않은 날짜 + } + + // 로컬 시간대 기준 YYYY-MM-DD 형식으로 변환 + return getLocalDateString(date); +} \ No newline at end of file