Files
hjjeong 1fb438bdcb 공용 DateInput + DataGrid 헤더 가독성 + 구매요청서 수정모드/공급업체 옵션
- 공용 DateInput (YYYY-MM-DD 통일): text input + Popover Calendar,
  숫자 8자리 자동 - 삽입. CompactDateRange / 다이얼로그 입고요청일 적용.
- DataGrid 헤더 라벨 truncate + TableHead 패딩 축소(!px-1.5):
  좁은 컬럼에서 라벨 겹침/잘림 해소.
- 구매요청서관리 그리드 컬럼 너비 합리화 (총 ~300px 절감)로 품명까지
  화면 안에 표시.
- 구매요청서 수정모드: 선택 1건 시 [구매요청서수정] 분기 →
  getDetail 로 헤더/라인 채워 다이얼로그 오픈. 확정·품의서생성 가드.
- 공급업체 옵션을 client_mng 기반 listVendorOptions 로 신설
  (운영 supply_mng=0 / client_mng=8946, M-BOM vendor 매칭).
- 주문유형 CommCodeSelect groupId 0000005 → 0000167 (계약구분).
- 고객사 셀렉트 → CustomerSelect 공용 컴포넌트로 교체.
- 그리드 delivery_request_date 점 형식 → YYYY-MM-DD 정규화.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:40:28 +09:00

166 lines
5.4 KiB
TypeScript

"use client";
/**
* DateInput — YYYY-MM-DD 형식 통일 공용 날짜 입력 컴포넌트
*
* 브라우저 `<input type="date">` 는 OS/로케일에 따라 "연도. 월. 일." 등 다른 placeholder 를
* 보여주는 문제가 있어, text input + Popover Calendar 로 표시·저장을 YYYY-MM-DD 로 통일.
*
* - 직접 타이핑: YYYY-MM-DD (8자리 숫자 입력 시 자동으로 - 삽입)
* - 캘린더 아이콘 클릭 → Popover Calendar 에서 날짜 선택
* - 항상 onChange 에 'YYYY-MM-DD' 또는 빈 문자열 전달
*/
import React, { useMemo, useState, useEffect, useRef } from "react";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Calendar } from "@/components/ui/calendar";
import { Input } from "@/components/ui/input";
import { Calendar as CalendarIcon, X } from "lucide-react";
import { cn } from "@/lib/utils";
import { format, parse, isValid } from "date-fns";
const FMT = "yyyy-MM-dd";
export interface DateInputProps {
value: string; // 'YYYY-MM-DD' 또는 ''
onChange: (v: string) => void;
disabled?: boolean;
placeholder?: string;
className?: string;
/** sm = h-7 (CompactFilter 행), md = h-9 (다이얼로그 폼) */
size?: "sm" | "md";
/** 값이 있을 때 ✕ 노출 (기본 true). 필수 필드는 false. */
clearable?: boolean;
}
function toDate(v: string): Date | null {
if (!v) return null;
const d = parse(v, FMT, new Date());
return isValid(d) ? d : null;
}
function isCompleteDate(v: string): boolean {
if (!/^\d{4}-\d{2}-\d{2}$/.test(v)) return false;
const d = parse(v, FMT, new Date());
return isValid(d);
}
/** 사용자 타이핑을 8자리 숫자로 받아 YYYY-MM-DD 로 슬라이스 */
function autoFormat(raw: string): string {
const digits = raw.replace(/\D/g, "").slice(0, 8);
if (digits.length <= 4) return digits;
if (digits.length <= 6) return `${digits.slice(0, 4)}-${digits.slice(4)}`;
return `${digits.slice(0, 4)}-${digits.slice(4, 6)}-${digits.slice(6, 8)}`;
}
export function DateInput({
value,
onChange,
disabled,
placeholder = "YYYY-MM-DD",
className,
size = "md",
clearable = true,
}: DateInputProps) {
const [open, setOpen] = useState(false);
const [text, setText] = useState(value || "");
const inputRef = useRef<HTMLInputElement>(null);
// 외부 value 동기화
useEffect(() => {
setText(value || "");
}, [value]);
const selectedDate = useMemo(() => toDate(value), [value]);
const invalid = text.length > 0 && !isCompleteDate(text);
const showClear = clearable && !disabled && !!value;
const onTextChange = (raw: string) => {
const formatted = autoFormat(raw);
setText(formatted);
if (formatted === "") {
onChange("");
} else if (isCompleteDate(formatted)) {
onChange(formatted);
}
// 아직 미완성이면 onChange 호출 안 함 (마지막 유효값 유지)
};
const onBlur = () => {
// 미완성 텍스트는 마지막 유효값으로 복귀
if (text && !isCompleteDate(text)) setText(value || "");
};
const handleCalendarSelect = (d: Date | undefined) => {
if (!d) { onChange(""); setText(""); }
else { const v = format(d, FMT); onChange(v); setText(v); }
setOpen(false);
};
const handleClear = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
onChange("");
setText("");
inputRef.current?.focus();
};
const h = size === "sm" ? "h-7" : "h-9";
const textCls = size === "sm" ? "text-xs" : "text-sm";
return (
<div className={cn("relative inline-flex items-center", className)}>
<Input
ref={inputRef}
type="text"
inputMode="numeric"
placeholder={placeholder}
value={text}
disabled={disabled}
onChange={(e) => onTextChange(e.target.value)}
onBlur={onBlur}
className={cn(
h, textCls, "pr-16 w-full",
invalid && "border-destructive focus-visible:ring-destructive",
)}
aria-invalid={invalid || undefined}
/>
{showClear && (
<button
type="button"
tabIndex={-1}
aria-label="날짜 지우기"
onMouseDown={(e) => { e.preventDefault(); e.stopPropagation(); }}
onClick={handleClear}
className="absolute right-8 top-1/2 -translate-y-1/2 z-10 inline-flex h-5 w-5 items-center justify-center rounded text-muted-foreground hover:text-foreground hover:bg-muted"
>
<X className="h-3.5 w-3.5" />
</button>
)}
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button
type="button"
disabled={disabled}
aria-label="캘린더 열기"
className={cn(
"absolute right-1 top-1/2 -translate-y-1/2 z-10 inline-flex items-center justify-center rounded text-muted-foreground hover:text-foreground hover:bg-muted disabled:opacity-50 disabled:cursor-not-allowed",
size === "sm" ? "h-5 w-5" : "h-6 w-6",
)}
>
<CalendarIcon className="h-3.5 w-3.5" />
</button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="end">
<Calendar
mode="single"
selected={selectedDate || undefined}
onSelect={handleCalendarSelect}
initialFocus
/>
</PopoverContent>
</Popover>
</div>
);
}