feat: Daum 우편번호 서비스 연동 및 악성채권 UI 개선
- useDaumPostcode 공통 훅 생성 (Daum Postcode API 연동) - 우편번호 찾기 기능 적용: 악성채권, 거래처, 직원, 회사정보, 주문등록 - 악성채권 페이지 토글 순서 변경 (라벨 → 토글) - 악성채권 토글 기능 수정 (매출/매입 → 등록/해제) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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)}
|
||||
{/* 거래처 유형 */}
|
||||
{/* 거래처 유형 - 읽기 전용 */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-medium text-gray-700">거래처 유형</Label>
|
||||
<Switch
|
||||
checked={formData.vendorType === 'both'}
|
||||
onCheckedChange={(checked) => handleChange('vendorType', checked ? 'both' : 'sales')}
|
||||
disabled={isViewMode}
|
||||
className="data-[state=checked]:bg-orange-500"
|
||||
/>
|
||||
</div>
|
||||
<Label className="text-sm font-medium text-gray-700">거래처 유형</Label>
|
||||
<Input
|
||||
value={VENDOR_TYPE_LABELS[formData.vendorType] || '매출매입'}
|
||||
disabled
|
||||
className="bg-gray-50"
|
||||
/>
|
||||
</div>
|
||||
{/* 악성채권 등록 */}
|
||||
{/* 악성채권 등록 토글 + 업태/업종 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium text-gray-700">악성채권 등록</Label>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-sm font-medium text-gray-700">악성채권 등록</Label>
|
||||
<Switch
|
||||
checked={formData.settingToggle}
|
||||
onCheckedChange={(checked) => handleChange('settingToggle', checked)}
|
||||
disabled={isViewMode}
|
||||
className="data-[state=checked]:bg-orange-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Input
|
||||
value={formData.businessType}
|
||||
@@ -406,7 +420,7 @@ export function BadDebtDetail({ mode, recordId }: BadDebtDetailProps) {
|
||||
disabled={isViewMode}
|
||||
className="w-[120px] bg-white"
|
||||
/>
|
||||
<Button variant="outline" disabled={isViewMode} className="shrink-0">
|
||||
<Button variant="outline" disabled={isViewMode} onClick={openPostcode} className="shrink-0">
|
||||
우편번호 찾기
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
<Button variant="outline" disabled={isViewMode} className="shrink-0">
|
||||
<Button variant="outline" disabled={isViewMode} onClick={openPostcode} className="shrink-0">
|
||||
우편번호 찾기
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -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<EmployeeFormData>(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<FieldSettings>(initialFieldSettings);
|
||||
@@ -367,7 +382,7 @@ export function EmployeeForm({
|
||||
<div className="space-y-2">
|
||||
<Label>주소</Label>
|
||||
<div className="flex gap-2">
|
||||
<Button type="button" variant="default" size="sm" className="bg-blue-500 hover:bg-blue-600">
|
||||
<Button type="button" variant="default" size="sm" onClick={openPostcode} className="bg-blue-500 hover:bg-blue-600">
|
||||
우편번호 찾기
|
||||
</Button>
|
||||
<Input
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useDaumPostcode } from "@/hooks/useDaumPostcode";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -183,6 +184,17 @@ export function OrderRegistration({
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [fieldErrors, setFieldErrors] = useState<FieldErrors>({});
|
||||
|
||||
// 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"
|
||||
/>
|
||||
<Button variant="outline" type="button">
|
||||
<Button variant="outline" type="button" onClick={openPostcode}>
|
||||
우편번호 찾기
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
129
src/hooks/useDaumPostcode.ts
Normal file
129
src/hooks/useDaumPostcode.ts
Normal file
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user