From fd7fc754f48327dfa935c9317016b8b4b610e29b Mon Sep 17 00:00:00 2001 From: dohyeons Date: Mon, 3 Nov 2025 14:31:21 +0900 Subject: [PATCH 1/9] =?UTF-8?q?=ED=9A=8C=EC=82=AC=20=EA=B4=80=EB=A6=AC=20-?= =?UTF-8?q?=20=EB=93=B1=EB=A1=9D=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/controllers/adminController.ts | 130 ++++++++++++++++-- .../src/utils/businessNumberValidator.ts | 52 +++++++ .../components/admin/CompanyFormModal.tsx | 130 +++++++++++++++++- frontend/constants/company.ts | 6 + frontend/hooks/useCompanyManagement.ts | 20 +++ frontend/lib/validation/businessNumber.ts | 74 ++++++++++ frontend/types/company.ts | 14 +- 7 files changed, 410 insertions(+), 16 deletions(-) create mode 100644 backend-node/src/utils/businessNumberValidator.ts create mode 100644 frontend/lib/validation/businessNumber.ts diff --git a/backend-node/src/controllers/adminController.ts b/backend-node/src/controllers/adminController.ts index b1638403..f79aec69 100644 --- a/backend-node/src/controllers/adminController.ts +++ b/backend-node/src/controllers/adminController.ts @@ -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( - `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( - `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( + `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 => { 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( + `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( `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) { diff --git a/backend-node/src/utils/businessNumberValidator.ts b/backend-node/src/utils/businessNumberValidator.ts new file mode 100644 index 00000000..92385f28 --- /dev/null +++ b/backend-node/src/utils/businessNumberValidator.ts @@ -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: "", + }; +} + diff --git a/frontend/components/admin/CompanyFormModal.tsx b/frontend/components/admin/CompanyFormModal.tsx index 7b51ac6a..dd87140e 100644 --- a/frontend/components/admin/CompanyFormModal.tsx +++ b/frontend/components/admin/CompanyFormModal.tsx @@ -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(""); // 모달이 열려있지 않으면 렌더링하지 않음 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({
- {/* 회사명 입력 */} + {/* 회사명 입력 (필수) */}
+ {/* 사업자등록번호 입력 (필수) */} +
+ + handleBusinessNumberChange(e.target.value)} + placeholder="000-00-00000" + disabled={isLoading || isSaving} + maxLength={12} + className={businessNumberError ? "border-destructive" : ""} + /> + {businessNumberError ? ( +

{businessNumberError}

+ ) : ( +

10자리 숫자 (자동 하이픈 추가)

+ )} +
+ + {/* 대표자명 입력 */} +
+ + onFormChange("representative_name", e.target.value)} + placeholder="대표자명을 입력하세요" + disabled={isLoading || isSaving} + /> +
+ + {/* 대표 연락처 입력 */} +
+ + onFormChange("representative_phone", e.target.value)} + placeholder="010-0000-0000" + disabled={isLoading || isSaving} + type="tel" + /> +
+ + {/* 이메일 입력 */} +
+ + onFormChange("email", e.target.value)} + placeholder="company@example.com" + disabled={isLoading || isSaving} + type="email" + /> +
+ + {/* 웹사이트 입력 */} +
+ + onFormChange("website", e.target.value)} + placeholder="https://example.com" + disabled={isLoading || isSaving} + type="url" + /> +
+ + {/* 회사 주소 입력 */} +
+ + onFormChange("address", e.target.value)} + placeholder="서울특별시 강남구..." + disabled={isLoading || isSaving} + /> +
+ {/* 에러 메시지 */} {error && ( -
-

{error}

+
+

{error}

)} @@ -129,7 +243,13 @@ export function CompanyFormModal({