회사 관리 - 등록 페이지 수정
This commit is contained in:
@@ -5,6 +5,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||
import { LoadingSpinner } from "@/components/common/LoadingSpinner";
|
||||
import { validateBusinessNumber, formatBusinessNumber } from "@/lib/validation/businessNumber";
|
||||
|
||||
interface CompanyFormModalProps {
|
||||
modalState: CompanyModalState;
|
||||
@@ -29,6 +30,7 @@ export function CompanyFormModal({
|
||||
onClearError,
|
||||
}: CompanyFormModalProps) {
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [businessNumberError, setBusinessNumberError] = useState<string>("");
|
||||
|
||||
// 모달이 열려있지 않으면 렌더링하지 않음
|
||||
if (!modalState.isOpen) return null;
|
||||
@@ -36,15 +38,43 @@ export function CompanyFormModal({
|
||||
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();
|
||||
@@ -81,7 +111,7 @@ export function CompanyFormModal({
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
{/* 회사명 입력 */}
|
||||
{/* 회사명 입력 (필수) */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="company_name">
|
||||
회사명 <span className="text-destructive">*</span>
|
||||
@@ -97,10 +127,94 @@ export function CompanyFormModal({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 사업자등록번호 입력 (필수) */}
|
||||
<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>
|
||||
|
||||
{/* 에러 메시지 */}
|
||||
{error && (
|
||||
<div className="bg-destructive/10 rounded-md p-3">
|
||||
<p className="text-destructive text-sm">{error}</p>
|
||||
<div className="rounded-md bg-destructive/10 p-3">
|
||||
<p className="text-sm text-destructive">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -129,7 +243,13 @@ export function CompanyFormModal({
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={isLoading || isSaving || !formData.company_name.trim()}
|
||||
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" />}
|
||||
|
||||
@@ -74,6 +74,12 @@ export const MOCK_COMPANIES: Company[] = [
|
||||
// 새 회사 등록 시 기본값
|
||||
export const DEFAULT_COMPANY_FORM_DATA = {
|
||||
company_name: "",
|
||||
business_registration_number: "",
|
||||
representative_name: "",
|
||||
representative_phone: "",
|
||||
email: "",
|
||||
website: "",
|
||||
address: "",
|
||||
};
|
||||
|
||||
// 페이징 관련 상수
|
||||
|
||||
@@ -144,6 +144,12 @@ export const useCompanyManagement = () => {
|
||||
selectedCompany: company,
|
||||
formData: {
|
||||
company_name: company.company_name,
|
||||
business_registration_number: company.business_registration_number || "",
|
||||
representative_name: company.representative_name || "",
|
||||
representative_phone: company.representative_phone || "",
|
||||
email: company.email || "",
|
||||
website: company.website || "",
|
||||
address: company.address || "",
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
@@ -175,6 +181,10 @@ export const useCompanyManagement = () => {
|
||||
setError("회사명을 입력해주세요.");
|
||||
return false;
|
||||
}
|
||||
if (!modalState.formData.business_registration_number.trim()) {
|
||||
setError("사업자등록번호를 입력해주세요.");
|
||||
return false;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
@@ -199,6 +209,10 @@ export const useCompanyManagement = () => {
|
||||
setError("올바른 데이터를 입력해주세요.");
|
||||
return false;
|
||||
}
|
||||
if (!modalState.formData.business_registration_number.trim()) {
|
||||
setError("사업자등록번호를 입력해주세요.");
|
||||
return false;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
@@ -206,6 +220,12 @@ export const useCompanyManagement = () => {
|
||||
try {
|
||||
await companyAPI.update(modalState.selectedCompany.company_code, {
|
||||
company_name: modalState.formData.company_name,
|
||||
business_registration_number: modalState.formData.business_registration_number,
|
||||
representative_name: modalState.formData.representative_name,
|
||||
representative_phone: modalState.formData.representative_phone,
|
||||
email: modalState.formData.email,
|
||||
website: modalState.formData.website,
|
||||
address: modalState.formData.address,
|
||||
status: modalState.selectedCompany.status,
|
||||
});
|
||||
closeModal();
|
||||
|
||||
74
frontend/lib/validation/businessNumber.ts
Normal file
74
frontend/lib/validation/businessNumber.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 사업자등록번호 유효성 검사 유틸리티
|
||||
*/
|
||||
|
||||
/**
|
||||
* 사업자등록번호 포맷 검증 (000-00-00000 형식)
|
||||
*/
|
||||
export function validateBusinessNumberFormat(value: string): boolean {
|
||||
if (!value || value.trim() === "") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 하이픈 제거
|
||||
const cleaned = value.replace(/-/g, "");
|
||||
|
||||
// 숫자 10자리인지 확인
|
||||
if (!/^\d{10}$/.test(cleaned)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 사업자등록번호 종합 검증 (포맷만 검사)
|
||||
* 실제 국세청 검증은 백엔드에서 API 호출로 처리하는 것을 권장
|
||||
*/
|
||||
export function validateBusinessNumber(value: string): {
|
||||
isValid: boolean;
|
||||
message: string;
|
||||
} {
|
||||
if (!value || value.trim() === "") {
|
||||
return {
|
||||
isValid: false,
|
||||
message: "사업자등록번호를 입력해주세요.",
|
||||
};
|
||||
}
|
||||
|
||||
if (!validateBusinessNumberFormat(value)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: "사업자등록번호는 10자리 숫자여야 합니다.",
|
||||
};
|
||||
}
|
||||
|
||||
// 포맷만 검증하고 통과
|
||||
return {
|
||||
isValid: true,
|
||||
message: "",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 사업자등록번호 포맷팅 (자동 하이픈 추가)
|
||||
*/
|
||||
export function formatBusinessNumber(value: string): string {
|
||||
if (!value) return "";
|
||||
|
||||
// 숫자만 추출
|
||||
const cleaned = value.replace(/\D/g, "");
|
||||
|
||||
// 최대 10자리까지만
|
||||
const limited = cleaned.slice(0, 10);
|
||||
|
||||
// 하이픈 추가 (000-00-00000)
|
||||
if (limited.length <= 3) {
|
||||
return limited;
|
||||
} else if (limited.length <= 5) {
|
||||
return `${limited.slice(0, 3)}-${limited.slice(3)}`;
|
||||
} else {
|
||||
return `${limited.slice(0, 3)}-${limited.slice(3, 5)}-${limited.slice(5)}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
export interface Company {
|
||||
company_code: string; // 회사 코드 (varchar 32) - PK
|
||||
company_name: string; // 회사명 (varchar 64)
|
||||
business_registration_number?: string; // 사업자등록번호 (varchar 20)
|
||||
representative_name?: string; // 대표자명 (varchar 100)
|
||||
representative_phone?: string; // 대표 연락처 (varchar 20)
|
||||
email?: string; // 이메일 (varchar 255)
|
||||
website?: string; // 웹사이트 (varchar 500)
|
||||
address?: string; // 회사 주소 (text)
|
||||
writer: string; // 등록자 (varchar 32)
|
||||
regdate: string; // 등록일시 (timestamp -> ISO string)
|
||||
status: string; // 상태 (varchar 32)
|
||||
@@ -20,7 +26,13 @@ export interface Company {
|
||||
|
||||
// 회사 등록/수정 폼 데이터
|
||||
export interface CompanyFormData {
|
||||
company_name: string; // 등록 시에는 회사명만 필요
|
||||
company_name: string; // 회사명 (필수)
|
||||
business_registration_number: string; // 사업자등록번호 (필수)
|
||||
representative_name?: string; // 대표자명
|
||||
representative_phone?: string; // 대표 연락처
|
||||
email?: string; // 이메일
|
||||
website?: string; // 웹사이트
|
||||
address?: string; // 회사 주소
|
||||
}
|
||||
|
||||
// 회사 검색 필터
|
||||
|
||||
Reference in New Issue
Block a user