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:
@@ -1,7 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import DepositDetailClientV2 from '@/components/accounting/DepositManagement/DepositDetailClientV2';
|
||||
|
||||
export default function DepositNewPage() {
|
||||
return <DepositDetailClientV2 initialMode="create" />;
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { SalesDetail } from '@/components/accounting/SalesManagement/SalesDetail';
|
||||
|
||||
export default function NewSalesPage() {
|
||||
return <SalesDetail mode="new" />;
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import WithdrawalDetailClientV2 from '@/components/accounting/WithdrawalManagement/WithdrawalDetailClientV2';
|
||||
|
||||
export default function WithdrawalNewPage() {
|
||||
return <WithdrawalDetailClientV2 initialMode="create" />;
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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' && (
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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 />;
|
||||
}
|
||||
|
||||
@@ -557,7 +557,7 @@ export function SalesDetail({ mode, salesId }: SalesDetailProps) {
|
||||
// ===== 동적 config =====
|
||||
const dynamicConfig = {
|
||||
...salesConfig,
|
||||
title: isNewMode ? '매출 상세_직접 등록' : '매출 상세',
|
||||
title: isNewMode ? '매출' : '매출 상세',
|
||||
actions: {
|
||||
...salesConfig.actions,
|
||||
submitLabel: isNewMode ? '등록' : '저장',
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user