refactor(WEB): /new 페이지를 ?mode=new 방식으로 통합

- 출퇴근 관리: 우림블루나인비즈니스센터 좌표 수정 (37.5572518, 126.864441)
- 입금/출금/매출/카드 등록: /new 폴더 삭제 및 ?mode=new 쿼리 파라미터 방식으로 통합
- 매출 등록 페이지 제목 "등록 등록" 중복 수정

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-01-23 10:20:12 +09:00
parent 1a0b1c4c48
commit e44b3cd6cc
11 changed files with 143 additions and 69 deletions

View File

@@ -1,7 +0,0 @@
'use client';
import DepositDetailClientV2 from '@/components/accounting/DepositManagement/DepositDetailClientV2';
export default function DepositNewPage() {
return <DepositDetailClientV2 initialMode="create" />;
}

View File

@@ -1,7 +1,9 @@
'use client';
import { useEffect, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { DepositManagement } from '@/components/accounting/DepositManagement';
import DepositDetailClientV2 from '@/components/accounting/DepositManagement/DepositDetailClientV2';
import { getDeposits } from '@/components/accounting/DepositManagement/actions';
const DEFAULT_PAGINATION = {
@@ -12,18 +14,27 @@ const DEFAULT_PAGINATION = {
};
export default function DepositsPage() {
const searchParams = useSearchParams();
const mode = searchParams.get('mode');
const [data, setData] = useState<Awaited<ReturnType<typeof getDeposits>>['data']>([]);
const [pagination, setPagination] = useState(DEFAULT_PAGINATION);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// mode=new일 때는 데이터 로드 불필요
if (mode === 'new') {
setIsLoading(false);
return;
}
getDeposits({ perPage: 100 })
.then(result => {
setData(result.data);
setPagination(result.pagination);
})
.finally(() => setIsLoading(false));
}, []);
}, [mode]);
if (isLoading) {
return (
@@ -33,6 +44,11 @@ export default function DepositsPage() {
);
}
// mode=new일 때 등록 화면 표시
if (mode === 'new') {
return <DepositDetailClientV2 initialMode="create" />;
}
return (
<DepositManagement
initialData={data}

View File

@@ -1,5 +0,0 @@
import { SalesDetail } from '@/components/accounting/SalesManagement/SalesDetail';
export default function NewSalesPage() {
return <SalesDetail mode="new" />;
}

View File

@@ -1,7 +1,9 @@
'use client';
import { useEffect, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { SalesManagement } from '@/components/accounting/SalesManagement';
import { SalesDetail } from '@/components/accounting/SalesManagement/SalesDetail';
import { getSales } from '@/components/accounting/SalesManagement/actions';
const DEFAULT_PAGINATION = {
@@ -12,18 +14,27 @@ const DEFAULT_PAGINATION = {
};
export default function SalesPage() {
const searchParams = useSearchParams();
const mode = searchParams.get('mode');
const [data, setData] = useState<Awaited<ReturnType<typeof getSales>>['data']>([]);
const [pagination, setPagination] = useState(DEFAULT_PAGINATION);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// mode=new일 때는 데이터 로드 불필요
if (mode === 'new') {
setIsLoading(false);
return;
}
getSales({ perPage: 100 })
.then(result => {
setData(result.data);
setPagination(result.pagination);
})
.finally(() => setIsLoading(false));
}, []);
}, [mode]);
if (isLoading) {
return (
@@ -33,6 +44,11 @@ export default function SalesPage() {
);
}
// mode=new일 때 등록 화면 표시
if (mode === 'new') {
return <SalesDetail mode="new" />;
}
return (
<SalesManagement
initialData={data}

View File

@@ -1,7 +0,0 @@
'use client';
import WithdrawalDetailClientV2 from '@/components/accounting/WithdrawalManagement/WithdrawalDetailClientV2';
export default function WithdrawalNewPage() {
return <WithdrawalDetailClientV2 initialMode="create" />;
}

View File

@@ -1,7 +1,9 @@
'use client';
import { useEffect, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { WithdrawalManagement } from '@/components/accounting/WithdrawalManagement';
import WithdrawalDetailClientV2 from '@/components/accounting/WithdrawalManagement/WithdrawalDetailClientV2';
import { getWithdrawals } from '@/components/accounting/WithdrawalManagement/actions';
const DEFAULT_PAGINATION = {
@@ -12,18 +14,27 @@ const DEFAULT_PAGINATION = {
};
export default function WithdrawalsPage() {
const searchParams = useSearchParams();
const mode = searchParams.get('mode');
const [data, setData] = useState<Awaited<ReturnType<typeof getWithdrawals>>['data']>([]);
const [pagination, setPagination] = useState(DEFAULT_PAGINATION);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// mode=new일 때는 데이터 로드 불필요
if (mode === 'new') {
setIsLoading(false);
return;
}
getWithdrawals({ perPage: 100 })
.then(result => {
setData(result.data);
setPagination(result.pagination);
})
.finally(() => setIsLoading(false));
}, []);
}, [mode]);
if (isLoading) {
return (
@@ -33,6 +44,11 @@ export default function WithdrawalsPage() {
);
}
// mode=new일 때 등록 화면 표시
if (mode === 'new') {
return <WithdrawalDetailClientV2 initialMode="create" />;
}
return (
<WithdrawalManagement
initialData={data}

View File

@@ -6,15 +6,15 @@ import { Button } from '@/components/ui/button';
import GoogleMap from '@/components/attendance/GoogleMap';
import AttendanceComplete from '@/components/attendance/AttendanceComplete';
import { checkIn, checkOut, getTodayAttendance } from '@/components/attendance/actions';
import { getAttendanceSetting } from '@/components/settings/AttendanceSettingsManagement/actions';
import { useRouter } from 'next/navigation';
// ========================================
// 하드코딩 설정값 (MVP - 추후 API로 대체)
// ========================================
const SITE_LOCATION = {
name: '본사',
lat: 37.557358,
lng: 126.864414,
// 기본값 (API 로드 실패 시 사용) - 우림블루나인비즈니스센터 (강서구 염창동 양천로 583)
// 좌표 출처: Google Maps (2025-01-23)
const DEFAULT_SITE_LOCATION = {
name: '우림블루나인비즈니스센터',
lat: 37.5572518,
lng: 126.864441,
radius: 100,
};
@@ -46,10 +46,49 @@ export default function AttendancePage() {
const [userLocation, setUserLocation] = useState<{ lat: number; lng: number } | null>(null);
const [isMobile, setIsMobile] = useState(false);
// 출퇴근 설정 (API에서 로드)
const [siteLocation, setSiteLocation] = useState(DEFAULT_SITE_LOCATION);
const [isSettingsLoaded, setIsSettingsLoaded] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
// 출퇴근 설정 로드
useEffect(() => {
if (!mounted) return;
const loadSettings = async () => {
try {
const result = await getAttendanceSetting();
console.log('[AttendancePage] API 응답:', result);
if (result.success && result.data) {
const radius = result.data.allowedRadius;
console.log('[AttendancePage] API 반경값:', radius);
// TODO: 주소/좌표 설정 UI 추가 후 아래 주석 해제
// 현재는 테스트를 위해 좌표는 하드코딩, 반경만 API 연동
const finalLocation = {
name: DEFAULT_SITE_LOCATION.name, // 하드코딩: 우림블루나인
lat: DEFAULT_SITE_LOCATION.lat, // 하드코딩: 37.5494
lng: DEFAULT_SITE_LOCATION.lng, // 하드코딩: 126.8747
radius: (radius != null && Number(radius) > 0) ? Number(radius) : DEFAULT_SITE_LOCATION.radius,
};
console.log('[AttendancePage] 최종 위치:', finalLocation);
setSiteLocation(finalLocation);
} else {
console.log('[AttendancePage] API 실패, 기본값 사용');
}
} catch (error) {
console.error('[AttendancePage] loadSettings error:', error);
} finally {
setIsSettingsLoaded(true);
}
};
loadSettings();
}, [mounted]);
useEffect(() => {
const checkScreenSize = () => {
setIsMobile(window.innerWidth < 768);
@@ -169,13 +208,13 @@ export default function AttendancePage() {
const handleConfirm = () => setViewMode('main');
const handleClose = () => router.back();
if (!mounted) {
if (!mounted || !isSettingsLoaded) {
return (
<div className="flex h-[calc(100vh-120px)] bg-background">
<div className="flex-1 flex items-center justify-center bg-muted">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-2"></div>
<p className="text-muted-foreground text-sm"> ...</p>
<p className="text-muted-foreground text-sm"> ...</p>
</div>
</div>
</div>
@@ -185,7 +224,7 @@ export default function AttendancePage() {
if (viewMode === 'check-in-complete') {
return (
<div className="h-[calc(100vh-120px)]">
<AttendanceComplete type="check-in" time={checkInTime} date={currentDate} location={SITE_LOCATION.name} onConfirm={handleConfirm} />
<AttendanceComplete type="check-in" time={checkInTime} date={currentDate} location={siteLocation.name} onConfirm={handleConfirm} />
</div>
);
}
@@ -193,7 +232,7 @@ export default function AttendancePage() {
if (viewMode === 'check-out-complete') {
return (
<div className="h-[calc(100vh-120px)]">
<AttendanceComplete type="check-out" time={checkOutTime} date={currentDate} location={SITE_LOCATION.name} onConfirm={handleConfirm} />
<AttendanceComplete type="check-out" time={checkOutTime} date={currentDate} location={siteLocation.name} onConfirm={handleConfirm} />
</div>
);
}
@@ -206,7 +245,7 @@ export default function AttendancePage() {
return (
<div className="flex flex-col h-[calc(100vh-120px)]">
<div className="flex-1 relative">
<GoogleMap siteLocation={SITE_LOCATION} onDistanceChange={handleDistanceChange} />
<GoogleMap siteLocation={siteLocation} onDistanceChange={handleDistanceChange} />
{distance !== null && (
<div className="absolute top-3 left-3 bg-white/90 backdrop-blur px-3 py-1.5 rounded-lg shadow-md">
<div className="flex items-center gap-1.5 text-sm">
@@ -263,7 +302,7 @@ export default function AttendancePage() {
</Button>
{!isInRange && distance !== null && (
<p className="text-center text-sm text-orange-500"> ({SITE_LOCATION.radius}m) .</p>
<p className="text-center text-sm text-orange-500"> ({siteLocation.radius}m) .</p>
)}
</div>
</div>
@@ -274,7 +313,7 @@ export default function AttendancePage() {
return (
<div className="flex h-[calc(100vh-120px)] bg-background rounded-xl overflow-hidden">
<div className="flex-1 relative">
<GoogleMap siteLocation={SITE_LOCATION} onDistanceChange={handleDistanceChange} />
<GoogleMap siteLocation={siteLocation} onDistanceChange={handleDistanceChange} />
{distance !== null && (
<div className="absolute top-4 left-4 bg-white/90 backdrop-blur px-4 py-2 rounded-xl shadow-lg">
<div className="flex items-center gap-2 text-sm">
@@ -332,7 +371,7 @@ export default function AttendancePage() {
</Button>
{!isInRange && distance !== null && (
<p className="text-center text-sm text-orange-500"> ({SITE_LOCATION.radius}m) .</p>
<p className="text-center text-sm text-orange-500"> ({siteLocation.radius}m) .</p>
)}
{attendanceStatus === 'checked-in' && (

View File

@@ -1,25 +0,0 @@
'use client';
/**
* 카드 등록 페이지 - IntegratedDetailTemplate 적용
*/
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
import { cardConfig } from '@/components/hr/CardManagement/cardConfig';
import { createCard } from '@/components/hr/CardManagement/actions';
import type { CardFormData } from '@/components/hr/CardManagement/types';
export default function NewCardPage() {
const handleSubmit = async (data: Record<string, unknown>) => {
const result = await createCard(data as CardFormData);
return { success: result.success, error: result.error };
};
return (
<IntegratedDetailTemplate
config={cardConfig}
mode="create"
onSubmit={handleSubmit}
/>
);
}

View File

@@ -1,7 +1,31 @@
'use client';
import { useSearchParams } from 'next/navigation';
import { CardManagement } from '@/components/hr/CardManagement';
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
import { cardConfig } from '@/components/hr/CardManagement/cardConfig';
import { createCard } from '@/components/hr/CardManagement/actions';
import type { CardFormData } from '@/components/hr/CardManagement/types';
export default function CardManagementPage() {
const searchParams = useSearchParams();
const mode = searchParams.get('mode');
// mode=new일 때 등록 화면 표시
if (mode === 'new') {
const handleSubmit = async (data: Record<string, unknown>) => {
const result = await createCard(data as CardFormData);
return { success: result.success, error: result.error };
};
return (
<IntegratedDetailTemplate
config={cardConfig}
mode="create"
onSubmit={handleSubmit}
/>
);
}
return <CardManagement />;
}

View File

@@ -557,7 +557,7 @@ export function SalesDetail({ mode, salesId }: SalesDetailProps) {
// ===== 동적 config =====
const dynamicConfig = {
...salesConfig,
title: isNewMode ? '매출 상세_직접 등록' : '매출 상세',
title: isNewMode ? '매출' : '매출 상세',
actions: {
...salesConfig.actions,
submitLabel: isNewMode ? '등록' : '저장',

View File

@@ -131,10 +131,14 @@ export default function GoogleMap({ siteLocation, onDistanceChange }: GoogleMapP
useEffect(() => {
if (!isLoaded || !mapRef.current || !window.google) return;
console.log('[GoogleMap] 지도 초기화 시작');
// 좌표 유효성 검사 - 우림블루나인비즈니스센터 기본값 (강서구 염창동)
const lat = typeof siteLocation.lat === 'number' && !isNaN(siteLocation.lat) ? siteLocation.lat : 37.5458;
const lng = typeof siteLocation.lng === 'number' && !isNaN(siteLocation.lng) ? siteLocation.lng : 126.8718;
console.log('[GoogleMap] 지도 초기화 시작, 좌표:', lat, lng);
const map = new window.google.maps.Map(mapRef.current, {
center: { lat: siteLocation.lat, lng: siteLocation.lng },
center: { lat, lng },
zoom: 17,
disableDefaultUI: true,
zoomControl: true,
@@ -145,11 +149,14 @@ export default function GoogleMap({ siteLocation, onDistanceChange }: GoogleMapP
mapInstanceRef.current = map;
// 100m 반경 원 그리기 (파란색)
// radius 유효성 검사
const radius = typeof siteLocation.radius === 'number' && siteLocation.radius > 0 ? siteLocation.radius : 100;
// 반경 원 그리기 (파란색)
const circle = new window.google.maps.Circle({
map: map,
center: { lat: siteLocation.lat, lng: siteLocation.lng },
radius: siteLocation.radius,
center: { lat, lng },
radius: radius,
strokeColor: '#3B82F6',
strokeOpacity: 0.8,
strokeWeight: 2,
@@ -162,7 +169,7 @@ export default function GoogleMap({ siteLocation, onDistanceChange }: GoogleMapP
// 현장 중심 마커 (파란색)
new window.google.maps.Marker({
map: map,
position: { lat: siteLocation.lat, lng: siteLocation.lng },
position: { lat, lng },
icon: {
path: window.google.maps.SymbolPath.CIRCLE,
scale: 8,