fix(date): UTC 기반 날짜를 로컬 타임존으로 변경
- 공통 날짜 유틸리티 함수 추가 (src/utils/date.ts)
- getLocalDateString(): 로컬 타임존 YYYY-MM-DD 포맷
- getTodayString(): 오늘 날짜 반환
- getDateAfterDays(): N일 후 날짜 계산
- formatDateForInput(): API 응답 → input 포맷 변환
- toISOString().split('T')[0] 패턴을 공통 함수로 교체
- 견적: QuoteRegistration, QuoteRegistrationV2, types
- 건설: contract, site-briefings, estimates, bidding types
- 건설: IssueDetailForm, ConstructionDetailClient, ProjectEndDialog
- 자재: InspectionCreate, ReceivingReceiptContent, StockStatus/mockData
- 품질: InspectionManagement/mockData
- 기타: PricingFormClient, ShipmentCreate, PurchaseOrderDocument
- 기타: MainDashboard, attendance/actions, dev/generators
문제: toISOString()은 UTC 기준이라 한국(UTC+9)에서 오전 9시 이전에
전날 날짜가 표시되는 버그 발생
해결: 로컬 타임존 기반 날짜 포맷 함수로 통일
This commit is contained in:
@@ -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`,
|
||||
|
||||
@@ -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() {
|
||||
</div>
|
||||
|
||||
{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);
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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: '',
|
||||
});
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
* 샘플 데이터 생성 공통 유틸리티
|
||||
*/
|
||||
|
||||
import { getLocalDateString } from '@/utils/date';
|
||||
|
||||
// 랜덤 선택
|
||||
export function randomPick<T>(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());
|
||||
}
|
||||
|
||||
// 랜덤 층수
|
||||
|
||||
@@ -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<string>(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());
|
||||
|
||||
|
||||
@@ -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
|
||||
<span className="text-muted-foreground">입고번호</span>
|
||||
<span className="font-medium">{detail.orderNo}</span>
|
||||
<span className="text-muted-foreground">입고일자</span>
|
||||
<span>{detail.receivingDate || today.toISOString().split('T')[0]}</span>
|
||||
<span>{detail.receivingDate || getTodayString()}</span>
|
||||
<span className="text-muted-foreground">발주번호</span>
|
||||
<span>{detail.orderNo}</span>
|
||||
<span className="text-muted-foreground">입고LOT</span>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<ShipmentCreateFormData>({
|
||||
lotNo: '',
|
||||
scheduledDate: new Date().toISOString().split('T')[0],
|
||||
scheduledDate: getTodayString(),
|
||||
priority: 'normal',
|
||||
deliveryMethod: 'pickup',
|
||||
logisticsCompany: '',
|
||||
|
||||
@@ -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 || '');
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
Loader2,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { getTodayString } from "@/utils/date";
|
||||
|
||||
// 필드명 매핑
|
||||
const FIELD_NAME_MAP: Record<string, string> = {
|
||||
@@ -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: "",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<QuoteStatus, string> = {
|
||||
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<string, unknown> {
|
||||
|
||||
// 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<string, unknown> {
|
||||
// calculationResults가 있으면 BOM 자재 기반으로 items 생성
|
||||
|
||||
59
src/utils/date.ts
Normal file
59
src/utils/date.ts
Normal file
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user