회사 관리 - 등록 페이지 수정
This commit is contained in:
@@ -8,6 +8,7 @@ import config from "../config/environment";
|
||||
import { AdminService } from "../services/adminService";
|
||||
import { EncryptUtil } from "../utils/encryptUtil";
|
||||
import { FileSystemManager } from "../utils/fileSystemManager";
|
||||
import { validateBusinessNumber } from "../utils/businessNumberValidator";
|
||||
|
||||
/**
|
||||
* 관리자 메뉴 목록 조회
|
||||
@@ -609,9 +610,15 @@ export const getCompanyList = async (
|
||||
|
||||
// Raw Query로 회사 목록 조회
|
||||
const companies = await query<any>(
|
||||
`SELECT
|
||||
company_code,
|
||||
` SELECT
|
||||
company_code,
|
||||
company_name,
|
||||
business_registration_number,
|
||||
representative_name,
|
||||
representative_phone,
|
||||
email,
|
||||
website,
|
||||
address,
|
||||
status,
|
||||
writer,
|
||||
regdate
|
||||
@@ -1659,9 +1666,15 @@ export async function getCompanyListFromDB(
|
||||
|
||||
// Raw Query로 회사 목록 조회
|
||||
const companies = await query<any>(
|
||||
`SELECT
|
||||
company_code,
|
||||
` SELECT
|
||||
company_code,
|
||||
company_name,
|
||||
business_registration_number,
|
||||
representative_name,
|
||||
representative_phone,
|
||||
email,
|
||||
website,
|
||||
address,
|
||||
writer,
|
||||
regdate,
|
||||
status
|
||||
@@ -2440,6 +2453,25 @@ export const createCompany = async (
|
||||
[company_name.trim()]
|
||||
);
|
||||
|
||||
// 사업자등록번호 유효성 검증
|
||||
const businessNumberValidation = validateBusinessNumber(
|
||||
req.body.business_registration_number?.trim() || ""
|
||||
);
|
||||
if (!businessNumberValidation.isValid) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: businessNumberValidation.message,
|
||||
errorCode: "INVALID_BUSINESS_NUMBER",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Raw Query로 사업자등록번호 중복 체크
|
||||
const existingBusinessNumber = await queryOne<any>(
|
||||
`SELECT company_code FROM company_mng WHERE business_registration_number = $1`,
|
||||
[req.body.business_registration_number?.trim()]
|
||||
);
|
||||
|
||||
if (existingCompany) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
@@ -2449,6 +2481,15 @@ export const createCompany = async (
|
||||
return;
|
||||
}
|
||||
|
||||
if (existingBusinessNumber) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "이미 등록된 사업자등록번호입니다.",
|
||||
errorCode: "DUPLICATE_BUSINESS_NUMBER",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// PostgreSQL 클라이언트 생성 (복잡한 코드 생성 쿼리용)
|
||||
const client = new Client({
|
||||
connectionString:
|
||||
@@ -2474,11 +2515,17 @@ export const createCompany = async (
|
||||
const insertQuery = `
|
||||
INSERT INTO company_mng (
|
||||
company_code,
|
||||
company_name,
|
||||
company_name,
|
||||
business_registration_number,
|
||||
representative_name,
|
||||
representative_phone,
|
||||
email,
|
||||
website,
|
||||
address,
|
||||
writer,
|
||||
regdate,
|
||||
status
|
||||
) VALUES ($1, $2, $3, $4, $5)
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
@@ -2488,6 +2535,12 @@ export const createCompany = async (
|
||||
const insertValues = [
|
||||
companyCode,
|
||||
company_name.trim(),
|
||||
req.body.business_registration_number?.trim() || null,
|
||||
req.body.representative_name?.trim() || null,
|
||||
req.body.representative_phone?.trim() || null,
|
||||
req.body.email?.trim() || null,
|
||||
req.body.website?.trim() || null,
|
||||
req.body.address?.trim() || null,
|
||||
writer,
|
||||
new Date(),
|
||||
"active",
|
||||
@@ -2552,7 +2605,16 @@ export const updateCompany = async (
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const { companyCode } = req.params;
|
||||
const { company_name, status } = req.body;
|
||||
const {
|
||||
company_name,
|
||||
business_registration_number,
|
||||
representative_name,
|
||||
representative_phone,
|
||||
email,
|
||||
website,
|
||||
address,
|
||||
status,
|
||||
} = req.body;
|
||||
|
||||
logger.info("회사 정보 수정 요청", {
|
||||
companyCode,
|
||||
@@ -2586,13 +2648,61 @@ export const updateCompany = async (
|
||||
return;
|
||||
}
|
||||
|
||||
// 사업자등록번호 중복 체크 및 유효성 검증 (자기 자신 제외)
|
||||
if (business_registration_number && business_registration_number.trim()) {
|
||||
// 유효성 검증
|
||||
const businessNumberValidation = validateBusinessNumber(business_registration_number.trim());
|
||||
if (!businessNumberValidation.isValid) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: businessNumberValidation.message,
|
||||
errorCode: "INVALID_BUSINESS_NUMBER",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 중복 체크
|
||||
const duplicateBusinessNumber = await queryOne<any>(
|
||||
`SELECT company_code FROM company_mng
|
||||
WHERE business_registration_number = $1 AND company_code != $2`,
|
||||
[business_registration_number.trim(), companyCode]
|
||||
);
|
||||
|
||||
if (duplicateBusinessNumber) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "이미 등록된 사업자등록번호입니다.",
|
||||
errorCode: "DUPLICATE_BUSINESS_NUMBER",
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Raw Query로 회사 정보 수정
|
||||
const result = await query<any>(
|
||||
`UPDATE company_mng
|
||||
SET company_name = $1, status = $2
|
||||
WHERE company_code = $3
|
||||
SET
|
||||
company_name = $1,
|
||||
business_registration_number = $2,
|
||||
representative_name = $3,
|
||||
representative_phone = $4,
|
||||
email = $5,
|
||||
website = $6,
|
||||
address = $7,
|
||||
status = $8
|
||||
WHERE company_code = $9
|
||||
RETURNING *`,
|
||||
[company_name.trim(), status || "active", companyCode]
|
||||
[
|
||||
company_name.trim(),
|
||||
business_registration_number?.trim() || null,
|
||||
representative_name?.trim() || null,
|
||||
representative_phone?.trim() || null,
|
||||
email?.trim() || null,
|
||||
website?.trim() || null,
|
||||
address?.trim() || null,
|
||||
status || "active",
|
||||
companyCode,
|
||||
]
|
||||
);
|
||||
|
||||
if (result.length === 0) {
|
||||
|
||||
52
backend-node/src/utils/businessNumberValidator.ts
Normal file
52
backend-node/src/utils/businessNumberValidator.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* 사업자등록번호 유효성 검사 유틸리티 (백엔드)
|
||||
*/
|
||||
|
||||
/**
|
||||
* 사업자등록번호 포맷 검증
|
||||
*/
|
||||
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: "",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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