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 {
|
2025-12-05 10:46:10 +09:00
|
|
|
Dialog,
|
|
|
|
|
DialogContent,
|
|
|
|
|
DialogHeader,
|
|
|
|
|
DialogTitle,
|
|
|
|
|
DialogDescription,
|
|
|
|
|
DialogFooter,
|
|
|
|
|
} from "@/components/ui/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;
|
2026-04-01 15:57:12 +09:00
|
|
|
t?: (key: string, params?: Record<string, string | number>) => string;
|
2025-08-21 09:41:46 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 회사 등록/수정 모달 컴포넌트
|
|
|
|
|
*/
|
|
|
|
|
export function CompanyFormModal({
|
|
|
|
|
modalState,
|
|
|
|
|
isLoading,
|
|
|
|
|
error,
|
|
|
|
|
onClose,
|
|
|
|
|
onSave,
|
|
|
|
|
onFormChange,
|
|
|
|
|
onClearError,
|
2026-04-01 15:57:12 +09:00
|
|
|
t,
|
2025-08-21 09:41:46 +09:00
|
|
|
}: CompanyFormModalProps) {
|
2026-04-01 15:57:12 +09:00
|
|
|
const _t = t || ((key: string) => {
|
|
|
|
|
const defaults: Record<string, string> = {
|
|
|
|
|
"company.form.titleCreate": "새 회사 등록",
|
|
|
|
|
"company.form.titleEdit": "회사 정보 수정",
|
|
|
|
|
"company.form.companyName": "회사명",
|
|
|
|
|
"company.form.companyNamePlaceholder": "회사명을 입력하세요",
|
|
|
|
|
"company.form.businessNumber": "사업자등록번호",
|
|
|
|
|
"company.form.businessNumberHint": "10자리 숫자 (자동 하이픈 추가)",
|
|
|
|
|
"company.form.representativeName": "대표자명",
|
|
|
|
|
"company.form.representativeNamePlaceholder": "대표자명을 입력하세요",
|
|
|
|
|
"company.form.representativePhone": "대표 연락처",
|
|
|
|
|
"company.form.email": "이메일",
|
|
|
|
|
"company.form.website": "웹사이트",
|
|
|
|
|
"company.form.address": "회사 주소",
|
|
|
|
|
"company.form.addressPlaceholder": "서울특별시 강남구...",
|
|
|
|
|
"company.form.companyCodeLabel": "회사 코드:",
|
|
|
|
|
"company.form.writerLabel": "등록자:",
|
|
|
|
|
"company.form.regdateLabel": "등록일:",
|
|
|
|
|
"company.form.cancel": "취소",
|
|
|
|
|
"company.form.save": "등록",
|
|
|
|
|
"company.form.update": "수정",
|
|
|
|
|
};
|
|
|
|
|
return defaults[key] || key;
|
|
|
|
|
});
|
2025-08-21 09:41:46 +09:00
|
|
|
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-12-05 10:46:10 +09:00
|
|
|
<Dialog open={modalState.isOpen} onOpenChange={handleCancel}>
|
|
|
|
|
<DialogContent
|
2025-11-05 16:36:32 +09:00
|
|
|
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}
|
|
|
|
|
>
|
2025-12-05 10:46:10 +09:00
|
|
|
<DialogHeader>
|
2026-04-01 15:57:12 +09:00
|
|
|
<DialogTitle>{isEditMode ? _t("company.form.titleEdit") : _t("company.form.titleCreate")}</DialogTitle>
|
2025-12-05 10:46:10 +09:00
|
|
|
</DialogHeader>
|
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">
|
2026-04-01 15:57:12 +09:00
|
|
|
{_t("company.form.companyName")} <span className="text-destructive">*</span>
|
2025-08-21 09:41:46 +09:00
|
|
|
</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="company_name"
|
|
|
|
|
value={formData.company_name}
|
|
|
|
|
onChange={(e) => onFormChange("company_name", e.target.value)}
|
2026-04-01 15:57:12 +09:00
|
|
|
placeholder={_t("company.form.companyNamePlaceholder")}
|
2025-08-21 09:41:46 +09:00
|
|
|
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">
|
2026-04-01 15:57:12 +09:00
|
|
|
{_t("company.form.businessNumber")} <span className="text-destructive">*</span>
|
2025-11-03 14:31:21 +09:00
|
|
|
</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>
|
|
|
|
|
) : (
|
2026-04-01 15:57:12 +09:00
|
|
|
<p className="text-xs text-muted-foreground">{_t("company.form.businessNumberHint")}</p>
|
2025-11-03 14:31:21 +09:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 대표자명 입력 */}
|
|
|
|
|
<div className="space-y-2">
|
2026-04-01 15:57:12 +09:00
|
|
|
<Label htmlFor="representative_name">{_t("company.form.representativeName")}</Label>
|
2025-11-03 14:31:21 +09:00
|
|
|
<Input
|
|
|
|
|
id="representative_name"
|
|
|
|
|
value={formData.representative_name || ""}
|
|
|
|
|
onChange={(e) => onFormChange("representative_name", e.target.value)}
|
2026-04-01 15:57:12 +09:00
|
|
|
placeholder={_t("company.form.representativeNamePlaceholder")}
|
2025-11-03 14:31:21 +09:00
|
|
|
disabled={isLoading || isSaving}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 대표 연락처 입력 */}
|
|
|
|
|
<div className="space-y-2">
|
2026-04-01 15:57:12 +09:00
|
|
|
<Label htmlFor="representative_phone">{_t("company.form.representativePhone")}</Label>
|
2025-11-03 14:31:21 +09:00
|
|
|
<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">
|
2026-04-01 15:57:12 +09:00
|
|
|
<Label htmlFor="email">{_t("company.form.email")}</Label>
|
2025-11-03 14:31:21 +09:00
|
|
|
<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">
|
2026-04-01 15:57:12 +09:00
|
|
|
<Label htmlFor="website">{_t("company.form.website")}</Label>
|
2025-11-03 14:31:21 +09:00
|
|
|
<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">
|
2026-04-01 15:57:12 +09:00
|
|
|
<Label htmlFor="address">{_t("company.form.address")}</Label>
|
2025-11-03 14:31:21 +09:00
|
|
|
<Input
|
|
|
|
|
id="address"
|
|
|
|
|
value={formData.address || ""}
|
|
|
|
|
onChange={(e) => onFormChange("address", e.target.value)}
|
2026-04-01 15:57:12 +09:00
|
|
|
placeholder={_t("company.form.addressPlaceholder")}
|
2025-11-03 14:31:21 +09:00
|
|
|
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>
|
2026-04-01 15:57:12 +09:00
|
|
|
<span className="font-medium">{_t("company.form.companyCodeLabel")}</span> {modalState.selectedCompany.company_code}
|
2025-08-21 09:41:46 +09:00
|
|
|
</p>
|
|
|
|
|
<p>
|
2026-04-01 15:57:12 +09:00
|
|
|
<span className="font-medium">{_t("company.form.writerLabel")}</span> {modalState.selectedCompany.writer}
|
2025-08-21 09:41:46 +09:00
|
|
|
</p>
|
|
|
|
|
<p>
|
2026-04-01 15:57:12 +09:00
|
|
|
<span className="font-medium">{_t("company.form.regdateLabel")}</span>{" "}
|
2025-08-21 09:41:46 +09:00
|
|
|
{new Date(modalState.selectedCompany.regdate).toLocaleDateString("ko-KR")}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-12-05 10:46:10 +09:00
|
|
|
<DialogFooter>
|
2025-08-21 09:41:46 +09:00
|
|
|
<Button variant="outline" onClick={handleCancel} disabled={isLoading || isSaving}>
|
2026-04-01 15:57:12 +09:00
|
|
|
{_t("company.form.cancel")}
|
2025-08-21 09:41:46 +09:00
|
|
|
</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" />}
|
2026-04-01 15:57:12 +09:00
|
|
|
{isEditMode ? _t("company.form.update") : _t("company.form.save")}
|
2025-08-21 09:41:46 +09:00
|
|
|
</Button>
|
2025-12-05 10:46:10 +09:00
|
|
|
</DialogFooter>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
2025-08-21 09:41:46 +09:00
|
|
|
);
|
|
|
|
|
}
|