Files
sam-react-prod/src/components/ui/business-number-input.tsx
유병철 afd7bda269 feat(WEB): 견적 시스템 개선, 엑셀 다운로드, PDF 생성 기능 추가
견적 시스템:
- QuoteRegistrationV2: 할인 모달, 거래명세서 모달, vatType 필드 추가
- DiscountModal: 할인율/할인금액 상호 계산 모달
- QuoteTransactionModal: 거래명세서 미리보기 모달
- LocationDetailPanel, LocationListPanel 개선

템플릿 기능:
- UniversalListPage: 엑셀 다운로드 기능 추가 (전체/선택 다운로드)
- DocumentViewer: PDF 생성 기능 개선

신규 API:
- /api/pdf/generate: Puppeteer 기반 PDF 생성 엔드포인트

UI 개선:
- 입력 컴포넌트 placeholder 스타일 개선 (opacity 50%)
- 각종 리스트 컴포넌트 정렬/필터링 개선

패키지 추가:
- html2canvas, jspdf, puppeteer, dom-to-image-more

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 19:49:03 +09:00

114 lines
4.0 KiB
TypeScript

"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import { formatBusinessNumber, parseBusinessNumber, validateBusinessNumber } from "@/lib/formatters";
import { CheckCircle, XCircle } from "lucide-react";
export interface BusinessNumberInputProps
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "onChange" | "value" | "type"> {
value: string;
onChange: (value: string) => void;
error?: boolean;
showValidation?: boolean;
}
/**
* BusinessNumberInput - 사업자번호 자동 포맷팅 입력 컴포넌트
*
* 특징:
* - 입력 시 자동 하이픈 삽입 (000-00-00000)
* - 숫자만 입력 허용 (최대 10자리)
* - onChange에는 숫자만 전달 (DB 저장용)
* - 선택적 유효성 검사 표시
*
* @example
* <BusinessNumberInput
* value={businessNumber}
* onChange={setBusinessNumber}
* showValidation
* />
*/
const BusinessNumberInput = React.forwardRef<HTMLInputElement, BusinessNumberInputProps>(
({ className, value, onChange, error, showValidation = false, ...props }, ref) => {
// 표시용 포맷된 값
const displayValue = React.useMemo(() => formatBusinessNumber(value || ""), [value]);
// 유효성 검사 결과
const isValid = React.useMemo(() => {
if (!showValidation || !value || value.length !== 10) return null;
return validateBusinessNumber(value);
}, [value, showValidation]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const inputValue = e.target.value;
// 숫자만 추출하여 전달
const numbersOnly = parseBusinessNumber(inputValue);
onChange(numbersOnly);
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
// 숫자, 백스페이스, 탭, 화살표, 복사/붙여넣기 허용
const allowedKeys = [
"Backspace", "Delete", "Tab", "Escape", "Enter",
"ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown",
"Home", "End"
];
if (allowedKeys.includes(e.key)) return;
// Ctrl/Cmd + A, C, V, X 허용
if ((e.ctrlKey || e.metaKey) && ["a", "c", "v", "x"].includes(e.key.toLowerCase())) {
return;
}
// 숫자가 아니면 차단
if (!/^\d$/.test(e.key)) {
e.preventDefault();
}
};
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
e.preventDefault();
const pastedText = e.clipboardData.getData("text");
const numbersOnly = parseBusinessNumber(pastedText);
onChange(numbersOnly);
};
return (
<div className="relative">
<input
type="text"
inputMode="numeric"
ref={ref}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground/50 selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base bg-input-background transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
error && "border-red-500 focus-visible:border-red-500 focus-visible:ring-red-500/20",
showValidation && "pr-10",
className
)}
value={displayValue}
onChange={handleChange}
onKeyDown={handleKeyDown}
onPaste={handlePaste}
{...props}
/>
{showValidation && isValid !== null && (
<div className="absolute right-3 top-1/2 -translate-y-1/2">
{isValid ? (
<CheckCircle className="h-4 w-4 text-green-500" />
) : (
<XCircle className="h-4 w-4 text-red-500" />
)}
</div>
)}
</div>
);
}
);
BusinessNumberInput.displayName = "BusinessNumberInput";
export { BusinessNumberInput };