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:
2026-01-26 17:15:22 +09:00
parent 7be8caf3f7
commit f9dafbc02c
21 changed files with 161 additions and 82 deletions

View File

@@ -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: "",

View File

@@ -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,

View File

@@ -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 생성