- Integrated multi-language functionality across various user management components, including user list, roles list, and user authorization pages, enhancing accessibility for diverse users. - Updated UI elements to utilize translation keys, ensuring that all text is dynamically translated based on user preferences. - Improved error handling messages to be localized, providing a better user experience in case of issues. These changes significantly enhance the usability and internationalization of the user management features, making the application more inclusive.
307 lines
11 KiB
TypeScript
307 lines
11 KiB
TypeScript
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";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
} from "@/components/ui/dialog";
|
|
import { LoadingSpinner } from "@/components/common/LoadingSpinner";
|
|
import { validateBusinessNumber, formatBusinessNumber } from "@/lib/validation/businessNumber";
|
|
|
|
interface CompanyFormModalProps {
|
|
modalState: CompanyModalState;
|
|
isLoading: boolean;
|
|
error: string | null;
|
|
onClose: () => void;
|
|
onSave: () => Promise<boolean>;
|
|
onFormChange: (field: keyof CompanyFormData, value: string) => void;
|
|
onClearError: () => void;
|
|
t?: (key: string, params?: Record<string, string | number>) => string;
|
|
}
|
|
|
|
/**
|
|
* 회사 등록/수정 모달 컴포넌트
|
|
*/
|
|
export function CompanyFormModal({
|
|
modalState,
|
|
isLoading,
|
|
error,
|
|
onClose,
|
|
onSave,
|
|
onFormChange,
|
|
onClearError,
|
|
t,
|
|
}: CompanyFormModalProps) {
|
|
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;
|
|
});
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
const [businessNumberError, setBusinessNumberError] = useState<string>("");
|
|
|
|
// 모달이 열려있지 않으면 렌더링하지 않음
|
|
if (!modalState.isOpen) return null;
|
|
|
|
const { mode, formData, selectedCompany } = modalState;
|
|
const isEditMode = mode === "edit";
|
|
|
|
// 사업자등록번호 변경 처리
|
|
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("");
|
|
}
|
|
};
|
|
|
|
// 저장 처리
|
|
const handleSave = async () => {
|
|
// 입력값 검증 (필수 필드)
|
|
if (!formData.company_name.trim()) {
|
|
return;
|
|
}
|
|
if (!formData.business_registration_number.trim()) {
|
|
return;
|
|
}
|
|
|
|
// 사업자등록번호 최종 검증
|
|
const validation = validateBusinessNumber(formData.business_registration_number);
|
|
if (!validation.isValid) {
|
|
setBusinessNumberError(validation.message);
|
|
return;
|
|
}
|
|
|
|
setIsSaving(true);
|
|
onClearError();
|
|
setBusinessNumberError("");
|
|
|
|
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 (
|
|
<Dialog open={modalState.isOpen} onOpenChange={handleCancel}>
|
|
<DialogContent
|
|
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}
|
|
>
|
|
<DialogHeader>
|
|
<DialogTitle>{isEditMode ? _t("company.form.titleEdit") : _t("company.form.titleCreate")}</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4 py-4">
|
|
{/* 회사명 입력 (필수) */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="company_name">
|
|
{_t("company.form.companyName")} <span className="text-destructive">*</span>
|
|
</Label>
|
|
<Input
|
|
id="company_name"
|
|
value={formData.company_name}
|
|
onChange={(e) => onFormChange("company_name", e.target.value)}
|
|
placeholder={_t("company.form.companyNamePlaceholder")}
|
|
disabled={isLoading || isSaving}
|
|
className={error ? "border-destructive" : ""}
|
|
autoFocus
|
|
/>
|
|
</div>
|
|
|
|
{/* 사업자등록번호 입력 (필수) */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="business_registration_number">
|
|
{_t("company.form.businessNumber")} <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">{_t("company.form.businessNumberHint")}</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* 대표자명 입력 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="representative_name">{_t("company.form.representativeName")}</Label>
|
|
<Input
|
|
id="representative_name"
|
|
value={formData.representative_name || ""}
|
|
onChange={(e) => onFormChange("representative_name", e.target.value)}
|
|
placeholder={_t("company.form.representativeNamePlaceholder")}
|
|
disabled={isLoading || isSaving}
|
|
/>
|
|
</div>
|
|
|
|
{/* 대표 연락처 입력 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="representative_phone">{_t("company.form.representativePhone")}</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">{_t("company.form.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">{_t("company.form.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">{_t("company.form.address")}</Label>
|
|
<Input
|
|
id="address"
|
|
value={formData.address || ""}
|
|
onChange={(e) => onFormChange("address", e.target.value)}
|
|
placeholder={_t("company.form.addressPlaceholder")}
|
|
disabled={isLoading || isSaving}
|
|
/>
|
|
</div>
|
|
|
|
{/* 에러 메시지 */}
|
|
{error && (
|
|
<div className="rounded-md bg-destructive/10 p-3">
|
|
<p className="text-sm text-destructive">{error}</p>
|
|
</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">{_t("company.form.companyCodeLabel")}</span> {modalState.selectedCompany.company_code}
|
|
</p>
|
|
<p>
|
|
<span className="font-medium">{_t("company.form.writerLabel")}</span> {modalState.selectedCompany.writer}
|
|
</p>
|
|
<p>
|
|
<span className="font-medium">{_t("company.form.regdateLabel")}</span>{" "}
|
|
{new Date(modalState.selectedCompany.regdate).toLocaleDateString("ko-KR")}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={handleCancel} disabled={isLoading || isSaving}>
|
|
{_t("company.form.cancel")}
|
|
</Button>
|
|
<Button
|
|
onClick={handleSave}
|
|
disabled={
|
|
isLoading ||
|
|
isSaving ||
|
|
!formData.company_name.trim() ||
|
|
!formData.business_registration_number.trim() ||
|
|
!!businessNumberError
|
|
}
|
|
className="min-w-[80px]"
|
|
>
|
|
{(isLoading || isSaving) && <LoadingSpinner className="mr-2 h-4 w-4" />}
|
|
{isEditMode ? _t("company.form.update") : _t("company.form.save")}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|