From 41ef0bdd866a4c522aa6bc813906232e0e79ba09 Mon Sep 17 00:00:00 2001 From: byeongcheolryu Date: Wed, 24 Dec 2025 17:46:23 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Daum=20=EC=9A=B0=ED=8E=B8=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=97=B0=EB=8F=99=20?= =?UTF-8?q?=EB=B0=8F=20=EC=95=85=EC=84=B1=EC=B1=84=EA=B6=8C=20UI=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useDaumPostcode 공통 훅 생성 (Daum Postcode API 연동) - 우편번호 찾기 기능 적용: 악성채권, 거래처, 직원, 회사정보, 주문등록 - 악성채권 페이지 토글 순서 변경 (라벨 → 토글) - 악성채권 토글 기능 수정 (매출/매입 → 등록/해제) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../BadDebtCollection/BadDebtDetail.tsx | 40 ++++-- .../VendorManagement/VendorDetail.tsx | 14 +- .../hr/EmployeeManagement/EmployeeForm.tsx | 17 ++- src/components/orders/OrderRegistration.tsx | 14 +- .../settings/CompanyInfoManagement/index.tsx | 15 +- src/hooks/useAuthGuard.ts | 17 ++- src/hooks/useDaumPostcode.ts | 129 ++++++++++++++++++ 7 files changed, 221 insertions(+), 25 deletions(-) create mode 100644 src/hooks/useDaumPostcode.ts diff --git a/src/components/accounting/BadDebtCollection/BadDebtDetail.tsx b/src/components/accounting/BadDebtCollection/BadDebtDetail.tsx index fcd134d4..bd0933e5 100644 --- a/src/components/accounting/BadDebtCollection/BadDebtDetail.tsx +++ b/src/components/accounting/BadDebtCollection/BadDebtDetail.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState, useCallback, useMemo } from 'react'; +import { useDaumPostcode } from '@/hooks/useDaumPostcode'; import { useRouter } from 'next/navigation'; import { format } from 'date-fns'; import { AlertTriangle, Plus, X, FileText, Receipt, CreditCard, Upload, Download, Trash2 } from 'lucide-react'; @@ -141,6 +142,17 @@ export function BadDebtDetail({ mode, recordId }: BadDebtDetailProps) { const initialData = recordId ? getMockRecord(recordId) : getEmptyRecord(); const [formData, setFormData] = useState(initialData); + // Daum 우편번호 서비스 + const { openPostcode } = useDaumPostcode({ + onComplete: (result) => { + setFormData(prev => ({ + ...prev, + zipCode: result.zonecode, + address1: result.address, + })); + }, + }); + // 다이얼로그 상태 const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [showSaveDialog, setShowSaveDialog] = useState(false); @@ -349,26 +361,28 @@ export function BadDebtDetail({ mode, recordId }: BadDebtDetailProps) { {renderField('거래처 코드', 'vendorCode', formData.vendorCode, { disabled: true })} {renderField('거래처명', 'vendorName', formData.vendorName, { required: true })} {renderField('대표자명', 'representativeName', formData.representativeName)} - {/* 거래처 유형 */} + {/* 거래처 유형 - 읽기 전용 */}
-
- - handleChange('vendorType', checked ? 'both' : 'sales')} - disabled={isViewMode} - className="data-[state=checked]:bg-orange-500" - /> -
+
- {/* 악성채권 등록 */} + {/* 악성채권 등록 토글 + 업태/업종 */}
- +
+
+ + handleChange('settingToggle', checked)} + disabled={isViewMode} + className="data-[state=checked]:bg-orange-500" + /> +
+
-
diff --git a/src/components/accounting/VendorManagement/VendorDetail.tsx b/src/components/accounting/VendorManagement/VendorDetail.tsx index 3887c6e2..a5cd0fbc 100644 --- a/src/components/accounting/VendorManagement/VendorDetail.tsx +++ b/src/components/accounting/VendorManagement/VendorDetail.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState, useCallback, useMemo } from 'react'; +import { useDaumPostcode } from '@/hooks/useDaumPostcode'; import { useRouter } from 'next/navigation'; import { Building2, Trash2, Plus, X } from 'lucide-react'; import { Alert, AlertDescription } from '@/components/ui/alert'; @@ -147,6 +148,17 @@ export function VendorDetail({ mode, vendorId }: VendorDetailProps) { const initialData = vendorId ? getMockVendor(vendorId) : getEmptyVendor(); const [formData, setFormData] = useState(initialData); + // Daum 우편번호 서비스 + const { openPostcode } = useDaumPostcode({ + onComplete: (result) => { + setFormData(prev => ({ + ...prev, + zipCode: result.zonecode, + address1: result.address, + })); + }, + }); + // 다이얼로그 상태 const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [showSaveDialog, setShowSaveDialog] = useState(false); @@ -421,7 +433,7 @@ export function VendorDetail({ mode, vendorId }: VendorDetailProps) { disabled={isViewMode} className="w-[120px] bg-white" /> -
diff --git a/src/components/hr/EmployeeManagement/EmployeeForm.tsx b/src/components/hr/EmployeeManagement/EmployeeForm.tsx index f8a555ef..a70fad6d 100644 --- a/src/components/hr/EmployeeManagement/EmployeeForm.tsx +++ b/src/components/hr/EmployeeManagement/EmployeeForm.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState, useEffect } from 'react'; +import { useDaumPostcode } from '@/hooks/useDaumPostcode'; import { useRouter } from 'next/navigation'; import { PageLayout } from '@/components/organisms/PageLayout'; import { PageHeader } from '@/components/organisms/PageHeader'; @@ -77,6 +78,20 @@ export function EmployeeForm({ const router = useRouter(); const [formData, setFormData] = useState(initialFormData); + // Daum 우편번호 서비스 + const { openPostcode } = useDaumPostcode({ + onComplete: (result) => { + setFormData(prev => ({ + ...prev, + address: { + ...prev.address, + zipCode: result.zonecode, + address1: result.address, + }, + })); + }, + }); + // 항목 설정 상태 const [showFieldSettings, setShowFieldSettings] = useState(false); const [fieldSettings, setFieldSettings] = useState(initialFieldSettings); @@ -367,7 +382,7 @@ export function EmployeeForm({
- ({}); + // Daum 우편번호 서비스 + const { openPostcode } = useDaumPostcode({ + onComplete: (result) => { + setForm((prev) => ({ + ...prev, + zipCode: result.zonecode, + address: result.address, + })); + }, + }); + // 금액 계산 useEffect(() => { const subtotal = form.items.reduce((sum, item) => sum + item.amount, 0); @@ -692,7 +704,7 @@ export function OrderRegistration({ } className="w-32" /> -
diff --git a/src/components/settings/CompanyInfoManagement/index.tsx b/src/components/settings/CompanyInfoManagement/index.tsx index 48cc400a..fa6953ca 100644 --- a/src/components/settings/CompanyInfoManagement/index.tsx +++ b/src/components/settings/CompanyInfoManagement/index.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState, useRef, useCallback } from 'react'; +import { useDaumPostcode } from '@/hooks/useDaumPostcode'; import { Building2, Plus, Save, Upload, X, Search } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -113,9 +114,19 @@ export function CompanyInfoManagement() { } }; + // Daum 우편번호 서비스 + const { openPostcode } = useDaumPostcode({ + onComplete: (result) => { + setFormData(prev => ({ + ...prev, + zipCode: result.zonecode, + address: result.address, + })); + }, + }); + const handleAddressSearch = () => { - // TODO: 다음 주소 API 연동 - console.log('주소 검색'); + openPostcode(); }; const handleSave = async () => { diff --git a/src/hooks/useAuthGuard.ts b/src/hooks/useAuthGuard.ts index 1ed859a6..55159316 100644 --- a/src/hooks/useAuthGuard.ts +++ b/src/hooks/useAuthGuard.ts @@ -1,5 +1,5 @@ import { useEffect } from 'react'; -import { useRouter } from 'next/navigation'; +import { callLogoutAPI } from '@/lib/auth/logout'; /** * Auth Guard Hook @@ -18,8 +18,6 @@ import { useRouter } from 'next/navigation'; * ``` */ export function useAuthGuard() { - const router = useRouter(); - useEffect(() => { console.log('🔄 useAuthGuard: Starting auth check...'); @@ -36,15 +34,20 @@ export function useAuthGuard() { console.log('📥 Response status:', response.status); if (!response.ok) { - // 인증 실패 시 로그인 페이지로 이동 - console.log('⚠️ 인증 실패: 로그인 페이지로 이동'); - router.replace('/login'); + // 인증 실패 시: + // 1. 서버 로그아웃 API 호출하여 HttpOnly 쿠키 삭제 + // 2. 쿠키 삭제 후 로그인 페이지로 이동 + // (쿠키가 남아있으면 미들웨어가 대시보드로 리다이렉트하는 버그 방지) + console.log('⚠️ 인증 실패: 쿠키 삭제 후 로그인 페이지로 이동'); + await callLogoutAPI(); + window.location.href = '/login'; } else { console.log('✅ 인증 성공'); } } catch (error) { console.error('❌ 인증 확인 오류:', error); - router.replace('/login'); + await callLogoutAPI(); + window.location.href = '/login'; } }; diff --git a/src/hooks/useDaumPostcode.ts b/src/hooks/useDaumPostcode.ts new file mode 100644 index 00000000..9428ec15 --- /dev/null +++ b/src/hooks/useDaumPostcode.ts @@ -0,0 +1,129 @@ +'use client'; + +import { useCallback, useEffect, useState } from 'react'; + +// Daum Postcode 타입 정의 +interface DaumPostcodeData { + zonecode: string; // 우편번호 + address: string; // 기본 주소 + addressEnglish: string; // 영문 주소 + addressType: 'R' | 'J'; // R: 도로명, J: 지번 + userSelectedType: 'R' | 'J'; + roadAddress: string; // 도로명 주소 + roadAddressEnglish: string; + jibunAddress: string; // 지번 주소 + jibunAddressEnglish: string; + buildingCode: string; + buildingName: string; // 건물명 + apartment: 'Y' | 'N'; // 아파트 여부 + sido: string; // 시/도 + sigungu: string; // 시/군/구 + sigunguCode: string; + bname: string; // 법정동/법정리 + bname1: string; + bname2: string; + hname: string; // 행정동 + query: string; // 검색어 +} + +interface DaumPostcode { + open: () => void; +} + +interface DaumPostcodeConstructor { + new (options: { + oncomplete: (data: DaumPostcodeData) => void; + onclose?: () => void; + width?: string | number; + height?: string | number; + }): DaumPostcode; +} + +interface DaumNamespace { + Postcode: DaumPostcodeConstructor; +} + +declare global { + interface Window { + daum?: DaumNamespace; + } +} + +export interface PostcodeResult { + zonecode: string; // 우편번호 + address: string; // 기본 주소 (도로명) + buildingName: string; // 건물명 + jibunAddress: string; // 지번 주소 +} + +interface UseDaumPostcodeOptions { + onComplete?: (result: PostcodeResult) => void; +} + +export function useDaumPostcode(options?: UseDaumPostcodeOptions) { + const [isScriptLoaded, setIsScriptLoaded] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + // 스크립트 로드 + useEffect(() => { + // 이미 로드된 경우 + if (window.daum?.Postcode) { + setIsScriptLoaded(true); + return; + } + + // 이미 스크립트가 추가된 경우 + const existingScript = document.querySelector( + 'script[src*="postcode.v2.js"]' + ); + if (existingScript) { + existingScript.addEventListener('load', () => setIsScriptLoaded(true)); + return; + } + + // 스크립트 추가 + const script = document.createElement('script'); + script.src = + '//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js'; + script.async = true; + script.onload = () => setIsScriptLoaded(true); + document.head.appendChild(script); + + return () => { + // cleanup: 스크립트는 유지 (다른 컴포넌트에서 사용할 수 있음) + }; + }, []); + + // 우편번호 검색 팝업 열기 + const openPostcode = useCallback(() => { + if (!isScriptLoaded || !window.daum?.Postcode) { + console.warn('Daum Postcode script not loaded yet'); + return; + } + + setIsLoading(true); + + new window.daum.Postcode({ + oncomplete: (data: DaumPostcodeData) => { + const result: PostcodeResult = { + zonecode: data.zonecode, + address: data.roadAddress || data.jibunAddress, + buildingName: data.buildingName, + jibunAddress: data.jibunAddress, + }; + + options?.onComplete?.(result); + setIsLoading(false); + }, + onclose: () => { + setIsLoading(false); + }, + }).open(); + }, [isScriptLoaded, options]); + + return { + openPostcode, + isScriptLoaded, + isLoading, + }; +} \ No newline at end of file