2025-08-21 09:41:46 +09:00
|
|
|
import { useState } from "react";
|
|
|
|
|
import { CompanyModalState, CompanyFormData } from "@/types/company";
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
|
import { Label } from "@/components/ui/label";
|
2025-11-05 16:36:32 +09:00
|
|
|
import {
|
|
|
|
|
ResizableDialog,
|
|
|
|
|
ResizableDialogContent,
|
|
|
|
|
ResizableDialogHeader,
|
|
|
|
|
ResizableDialogTitle,
|
|
|
|
|
ResizableDialogDescription,
|
|
|
|
|
ResizableDialogFooter,
|
|
|
|
|
} from "@/components/ui/resizable-dialog";
|
2025-08-21 09:41:46 +09:00
|
|
|
import { LoadingSpinner } from "@/components/common/LoadingSpinner";
|
2025-11-03 14:31:21 +09:00
|
|
|
import { validateBusinessNumber, formatBusinessNumber } from "@/lib/validation/businessNumber";
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
|
|
|
interface CompanyFormModalProps {
|
|
|
|
|
modalState: CompanyModalState;
|
|
|
|
|
isLoading: boolean;
|
|
|
|
|
error: string | null;
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
onSave: () => Promise<boolean>;
|
|
|
|
|
onFormChange: (field: keyof CompanyFormData, value: string) => void;
|
|
|
|
|
onClearError: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 회사 등록/수정 모달 컴포넌트
|
|
|
|
|
*/
|
|
|
|
|
export function CompanyFormModal({
|
|
|
|
|
modalState,
|
|
|
|
|
isLoading,
|
|
|
|
|
error,
|
|
|
|
|
onClose,
|
|
|
|
|
onSave,
|
|
|
|
|
onFormChange,
|
|
|
|
|
onClearError,
|
|
|
|
|
}: CompanyFormModalProps) {
|
|
|
|
|
const [isSaving, setIsSaving] = useState(false);
|
2025-11-03 14:31:21 +09:00
|
|
|
const [businessNumberError, setBusinessNumberError] = useState<string>("");
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
|
|
|
// 모달이 열려있지 않으면 렌더링하지 않음
|
|
|
|
|
if (!modalState.isOpen) return null;
|
|
|
|
|
|
|
|
|
|
const { mode, formData, selectedCompany } = modalState;
|
|
|
|
|
const isEditMode = mode === "edit";
|
|
|
|
|
|
2025-11-03 14:31:21 +09:00
|
|
|
// 사업자등록번호 변경 처리
|
|
|
|
|
const handleBusinessNumberChange = (value: string) => {
|
|
|
|
|
// 자동 포맷팅
|
|
|
|
|
const formatted = formatBusinessNumber(value);
|
|
|
|
|
onFormChange("business_registration_number", formatted);
|
|
|
|
|
|
|
|
|
|
// 유효성 검사 (10자리가 다 입력되었을 때만)
|
|
|
|
|
const cleaned = formatted.replace(/-/g, "");
|
|
|
|
|
if (cleaned.length === 10) {
|
|
|
|
|
const validation = validateBusinessNumber(formatted);
|
|
|
|
|
setBusinessNumberError(validation.isValid ? "" : validation.message);
|
|
|
|
|
} else if (cleaned.length < 10 && businessNumberError) {
|
|
|
|
|
// 10자리 미만이면 에러 초기화
|
|
|
|
|
setBusinessNumberError("");
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-08-21 09:41:46 +09:00
|
|
|
// 저장 처리
|
|
|
|
|
const handleSave = async () => {
|
2025-11-03 14:31:21 +09:00
|
|
|
// 입력값 검증 (필수 필드)
|
2025-08-21 09:41:46 +09:00
|
|
|
if (!formData.company_name.trim()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-11-03 14:31:21 +09:00
|
|
|
if (!formData.business_registration_number.trim()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 사업자등록번호 최종 검증
|
|
|
|
|
const validation = validateBusinessNumber(formData.business_registration_number);
|
|
|
|
|
if (!validation.isValid) {
|
|
|
|
|
setBusinessNumberError(validation.message);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
|
|
|
setIsSaving(true);
|
|
|
|
|
onClearError();
|
2025-11-03 14:31:21 +09:00
|
|
|
setBusinessNumberError("");
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const success = await onSave();
|
|
|
|
|
if (success) {
|
|
|
|
|
// 성공 시 모달 닫기
|
|
|
|
|
onClose();
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
// 에러는 부모 컴포넌트에서 처리
|
|
|
|
|
} finally {
|
|
|
|
|
setIsSaving(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 취소 처리
|
|
|
|
|
const handleCancel = () => {
|
|
|
|
|
onClearError();
|
|
|
|
|
onClose();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Enter 키 처리
|
|
|
|
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
|
|
|
if (e.key === "Enter" && !isLoading && !isSaving) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
handleSave();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
2025-11-05 16:36:32 +09:00
|
|
|
<ResizableDialog open={modalState.isOpen} onOpenChange={handleCancel}>
|
|
|
|
|
<ResizableDialogContent
|
|
|
|
|
className="sm:max-w-[425px]"
|
|
|
|
|
onKeyDown={handleKeyDown}
|
|
|
|
|
defaultWidth={500}
|
|
|
|
|
defaultHeight={600}
|
|
|
|
|
minWidth={400}
|
|
|
|
|
minHeight={500}
|
|
|
|
|
maxWidth={700}
|
|
|
|
|
maxHeight={800}
|
|
|
|
|
modalId="company-form"
|
|
|
|
|
userId={modalState.companyCode}
|
|
|
|
|
>
|
|
|
|
|
<ResizableDialogHeader>
|
|
|
|
|
<ResizableDialogTitle>{isEditMode ? "회사 정보 수정" : "새 회사 등록"}</ResizableDialogTitle>
|
|
|
|
|
</ResizableDialogHeader>
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
|
|
|
<div className="space-y-4 py-4">
|
2025-11-03 14:31:21 +09:00
|
|
|
{/* 회사명 입력 (필수) */}
|
2025-08-21 09:41:46 +09:00
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="company_name">
|
|
|
|
|
회사명 <span className="text-destructive">*</span>
|
|
|
|
|
</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="company_name"
|
|
|
|
|
value={formData.company_name}
|
|
|
|
|
onChange={(e) => onFormChange("company_name", e.target.value)}
|
|
|
|
|
placeholder="회사명을 입력하세요"
|
|
|
|
|
disabled={isLoading || isSaving}
|
|
|
|
|
className={error ? "border-destructive" : ""}
|
|
|
|
|
autoFocus
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-11-03 14:31:21 +09:00
|
|
|
{/* 사업자등록번호 입력 (필수) */}
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="business_registration_number">
|
|
|
|
|
사업자등록번호 <span className="text-destructive">*</span>
|
|
|
|
|
</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="business_registration_number"
|
|
|
|
|
value={formData.business_registration_number || ""}
|
|
|
|
|
onChange={(e) => handleBusinessNumberChange(e.target.value)}
|
|
|
|
|
placeholder="000-00-00000"
|
|
|
|
|
disabled={isLoading || isSaving}
|
|
|
|
|
maxLength={12}
|
|
|
|
|
className={businessNumberError ? "border-destructive" : ""}
|
|
|
|
|
/>
|
|
|
|
|
{businessNumberError ? (
|
|
|
|
|
<p className="text-xs text-destructive">{businessNumberError}</p>
|
|
|
|
|
) : (
|
|
|
|
|
<p className="text-xs text-muted-foreground">10자리 숫자 (자동 하이픈 추가)</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 대표자명 입력 */}
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="representative_name">대표자명</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="representative_name"
|
|
|
|
|
value={formData.representative_name || ""}
|
|
|
|
|
onChange={(e) => onFormChange("representative_name", e.target.value)}
|
|
|
|
|
placeholder="대표자명을 입력하세요"
|
|
|
|
|
disabled={isLoading || isSaving}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 대표 연락처 입력 */}
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="representative_phone">대표 연락처</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="representative_phone"
|
|
|
|
|
value={formData.representative_phone || ""}
|
|
|
|
|
onChange={(e) => onFormChange("representative_phone", e.target.value)}
|
|
|
|
|
placeholder="010-0000-0000"
|
|
|
|
|
disabled={isLoading || isSaving}
|
|
|
|
|
type="tel"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 이메일 입력 */}
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="email">이메일</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="email"
|
|
|
|
|
value={formData.email || ""}
|
|
|
|
|
onChange={(e) => onFormChange("email", e.target.value)}
|
|
|
|
|
placeholder="company@example.com"
|
|
|
|
|
disabled={isLoading || isSaving}
|
|
|
|
|
type="email"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 웹사이트 입력 */}
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="website">웹사이트</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="website"
|
|
|
|
|
value={formData.website || ""}
|
|
|
|
|
onChange={(e) => onFormChange("website", e.target.value)}
|
|
|
|
|
placeholder="https://example.com"
|
|
|
|
|
disabled={isLoading || isSaving}
|
|
|
|
|
type="url"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 회사 주소 입력 */}
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="address">회사 주소</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="address"
|
|
|
|
|
value={formData.address || ""}
|
|
|
|
|
onChange={(e) => onFormChange("address", e.target.value)}
|
|
|
|
|
placeholder="서울특별시 강남구..."
|
|
|
|
|
disabled={isLoading || isSaving}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-08-21 09:41:46 +09:00
|
|
|
{/* 에러 메시지 */}
|
|
|
|
|
{error && (
|
2025-11-03 14:31:21 +09:00
|
|
|
<div className="rounded-md bg-destructive/10 p-3">
|
|
|
|
|
<p className="text-sm text-destructive">{error}</p>
|
2025-08-21 09:41:46 +09:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* 수정 모드일 때 추가 정보 표시 */}
|
|
|
|
|
{isEditMode && modalState.selectedCompany && (
|
|
|
|
|
<div className="bg-muted/50 rounded-md p-3">
|
|
|
|
|
<div className="space-y-1 text-sm">
|
|
|
|
|
<p>
|
|
|
|
|
<span className="font-medium">회사 코드:</span> {modalState.selectedCompany.company_code}
|
|
|
|
|
</p>
|
|
|
|
|
<p>
|
|
|
|
|
<span className="font-medium">등록자:</span> {modalState.selectedCompany.writer}
|
|
|
|
|
</p>
|
|
|
|
|
<p>
|
|
|
|
|
<span className="font-medium">등록일:</span>{" "}
|
|
|
|
|
{new Date(modalState.selectedCompany.regdate).toLocaleDateString("ko-KR")}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-11-05 16:36:32 +09:00
|
|
|
<ResizableDialogFooter>
|
2025-08-21 09:41:46 +09:00
|
|
|
<Button variant="outline" onClick={handleCancel} disabled={isLoading || isSaving}>
|
|
|
|
|
취소
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
onClick={handleSave}
|
2025-11-03 14:31:21 +09:00
|
|
|
disabled={
|
|
|
|
|
isLoading ||
|
|
|
|
|
isSaving ||
|
|
|
|
|
!formData.company_name.trim() ||
|
|
|
|
|
!formData.business_registration_number.trim() ||
|
|
|
|
|
!!businessNumberError
|
|
|
|
|
}
|
2025-08-21 09:41:46 +09:00
|
|
|
className="min-w-[80px]"
|
|
|
|
|
>
|
|
|
|
|
{(isLoading || isSaving) && <LoadingSpinner className="mr-2 h-4 w-4" />}
|
|
|
|
|
{isEditMode ? "수정" : "등록"}
|
|
|
|
|
</Button>
|
2025-11-05 16:36:32 +09:00
|
|
|
</ResizableDialogFooter>
|
|
|
|
|
</ResizableDialogContent>
|
|
|
|
|
</ResizableDialog>
|
2025-08-21 09:41:46 +09:00
|
|
|
);
|
|
|
|
|
}
|