feat(WEB): 공사관리 리스트 공통화 및 캘린더/포맷터 기능 개선
공사관리 리스트 공통화: - 입찰/계약/견적/인수인계/이슈/품목/노무/현장/파트너/단가/기성/현장브리핑/구조검토/유틸리티/작업자현황 리스트 공통 포맷터 적용 - 중복 포맷팅 로직 제거 (-530줄) 캘린더 기능 개선: - CEODashboard CalendarSection 기능 확장 - ScheduleCalendar DayCell/MonthView/WeekView 개선 - ui/calendar 컴포넌트 기능 추가 유틸리티 개선: - date.ts 날짜 유틸 함수 추가 - formatAmount.ts 금액 포맷 함수 추가 신규 추가: - useListHandlers 훅 추가 - src/constants/ 디렉토리 추가 - 포맷터 공통화 계획 문서 추가 - SAM ERP/MES 정체성 분석 문서 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
import { Plus, ExternalLink } from 'lucide-react';
|
||||
import { ScheduleCalendar } from '@/components/common/ScheduleCalendar';
|
||||
import type { ScheduleEvent } from '@/components/common/ScheduleCalendar/types';
|
||||
import { CALENDAR_EVENTS, type CalendarEvent } from '@/constants/calendarEvents';
|
||||
import type {
|
||||
CalendarScheduleItem,
|
||||
CalendarViewType,
|
||||
@@ -38,6 +39,8 @@ const SCHEDULE_TYPE_COLORS: Record<string, string> = {
|
||||
construction: 'purple',
|
||||
issue: 'red',
|
||||
other: 'gray',
|
||||
holiday: 'red',
|
||||
tax: 'orange',
|
||||
};
|
||||
|
||||
// 이슈 뱃지별 색상
|
||||
@@ -118,7 +121,22 @@ export function CalendarSection({
|
||||
return issuesWithDate;
|
||||
}, [issuesWithDate, taskFilter]);
|
||||
|
||||
// ScheduleCalendar용 이벤트 변환 (스케줄 + 이슈 통합)
|
||||
// 현재 연도의 공휴일/세금일정
|
||||
const staticEvents: ScheduleEvent[] = useMemo(() => {
|
||||
const year = currentDate.getFullYear();
|
||||
const events = CALENDAR_EVENTS[year] || [];
|
||||
|
||||
return events.map((event: CalendarEvent) => ({
|
||||
id: `${event.type}-${event.date}`,
|
||||
title: event.type === 'holiday' ? `🔴 ${event.name}` : `🟠 ${event.name}`,
|
||||
startDate: event.date,
|
||||
endDate: event.date,
|
||||
color: SCHEDULE_TYPE_COLORS[event.type] || 'gray',
|
||||
data: { ...event, _type: event.type as 'holiday' | 'tax' },
|
||||
}));
|
||||
}, [currentDate]);
|
||||
|
||||
// ScheduleCalendar용 이벤트 변환 (스케줄 + 이슈 + 공휴일/세금 통합)
|
||||
const calendarEvents: ScheduleEvent[] = useMemo(() => {
|
||||
const scheduleEvents = filteredSchedules.map((schedule) => ({
|
||||
id: schedule.id,
|
||||
@@ -139,12 +157,12 @@ export function CalendarSection({
|
||||
data: { ...issue, _type: 'issue' as const },
|
||||
}));
|
||||
|
||||
return [...scheduleEvents, ...issueEvents];
|
||||
}, [filteredSchedules, filteredIssues]);
|
||||
return [...staticEvents, ...scheduleEvents, ...issueEvents];
|
||||
}, [staticEvents, filteredSchedules, filteredIssues]);
|
||||
|
||||
// 선택된 날짜의 일정 + 이슈 목록
|
||||
// 선택된 날짜의 일정 + 이슈 + 공휴일/세금 목록
|
||||
const selectedDateItems = useMemo(() => {
|
||||
if (!selectedDate) return { schedules: [], issues: [] };
|
||||
if (!selectedDate) return { schedules: [], issues: [], staticEvents: [] };
|
||||
// 로컬 타임존 기준으로 날짜 문자열 생성 (UTC 변환 방지)
|
||||
const year = selectedDate.getFullYear();
|
||||
const month = String(selectedDate.getMonth() + 1).padStart(2, '0');
|
||||
@@ -157,11 +175,14 @@ export function CalendarSection({
|
||||
|
||||
const dateIssues = filteredIssues.filter((issue) => issue.date === dateStr);
|
||||
|
||||
return { schedules: dateSchedules, issues: dateIssues };
|
||||
}, [selectedDate, filteredSchedules, filteredIssues]);
|
||||
// 공휴일/세금일정
|
||||
const dateStaticEvents = staticEvents.filter((event) => event.startDate === dateStr);
|
||||
|
||||
return { schedules: dateSchedules, issues: dateIssues, staticEvents: dateStaticEvents };
|
||||
}, [selectedDate, filteredSchedules, filteredIssues, staticEvents]);
|
||||
|
||||
// 총 건수 계산
|
||||
const totalItemCount = selectedDateItems.schedules.length + selectedDateItems.issues.length;
|
||||
const totalItemCount = selectedDateItems.schedules.length + selectedDateItems.issues.length + selectedDateItems.staticEvents.length;
|
||||
|
||||
// 날짜 포맷 (기획서: "1월 6일 화요일")
|
||||
const formatSelectedDate = (date: Date) => {
|
||||
@@ -313,6 +334,30 @@ export function CalendarSection({
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 max-h-[calc(100vh-400px)] overflow-y-auto pr-1">
|
||||
{/* 공휴일/세금일정 목록 */}
|
||||
{selectedDateItems.staticEvents.map((event) => {
|
||||
const eventData = event.data as CalendarEvent & { _type: string };
|
||||
const isHoliday = eventData.type === 'holiday';
|
||||
return (
|
||||
<div
|
||||
key={event.id}
|
||||
className={`p-3 rounded-lg ${
|
||||
isHoliday
|
||||
? 'bg-red-50 border border-red-200'
|
||||
: 'bg-orange-50 border border-orange-200'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg">{isHoliday ? '🔴' : '🟠'}</span>
|
||||
<span className="font-medium">{eventData.name}</span>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
{isHoliday ? '공휴일' : '세금 신고 마감일'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* 일정 목록 */}
|
||||
{selectedDateItems.schedules.map((schedule) => (
|
||||
<div
|
||||
|
||||
@@ -34,11 +34,7 @@ import {
|
||||
biddingDetailToFormData,
|
||||
} from './types';
|
||||
import { updateBidding } from './actions';
|
||||
|
||||
// 금액 포맷팅
|
||||
function formatAmount(amount: number): string {
|
||||
return new Intl.NumberFormat('ko-KR').format(amount);
|
||||
}
|
||||
import { formatNumber } from '@/utils/formatAmount';
|
||||
|
||||
interface BiddingDetailFormProps {
|
||||
mode: 'view' | 'edit';
|
||||
@@ -271,7 +267,7 @@ export default function BiddingDetailForm({
|
||||
<div className="space-y-2">
|
||||
<Label>입찰금액</Label>
|
||||
<Input
|
||||
value={formatAmount(formData.biddingAmount)}
|
||||
value={formatNumber(formData.biddingAmount)}
|
||||
disabled
|
||||
className="bg-muted text-right font-medium"
|
||||
/>
|
||||
@@ -392,14 +388,14 @@ export default function BiddingDetailForm({
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>{item.name}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatAmount(item.amount)}원
|
||||
{formatNumber(item.amount)}원
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
<TableRow className="bg-muted/50 font-medium">
|
||||
<TableCell>합계</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatAmount(expenseTotal)}원
|
||||
{formatNumber(expenseTotal)}원
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
@@ -465,26 +461,26 @@ export default function BiddingDetailForm({
|
||||
<TableCell className="text-right bg-gray-50">{item.height?.toFixed(2) || '0.00'}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50">{item.weight?.toFixed(2) || '0.00'}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50">{item.area?.toFixed(2) || '0.00'}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50">{formatAmount(item.steelScreen || 0)}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50">{formatAmount(item.caulking || 0)}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50">{formatAmount(item.rail || 0)}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50">{formatAmount(item.bottom || 0)}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50">{formatAmount(item.boxReinforce || 0)}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50">{formatAmount(item.shaft || 0)}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50">{formatAmount(item.painting || 0)}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50">{formatAmount(item.motor || 0)}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50">{formatAmount(item.controller || 0)}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50">{formatAmount(item.widthConstruction || 0)}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50">{formatAmount(item.heightConstruction || 0)}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50 font-medium">{formatAmount(item.unitPrice || 0)}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50">{formatNumber(item.steelScreen || 0)}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50">{formatNumber(item.caulking || 0)}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50">{formatNumber(item.rail || 0)}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50">{formatNumber(item.bottom || 0)}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50">{formatNumber(item.boxReinforce || 0)}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50">{formatNumber(item.shaft || 0)}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50">{formatNumber(item.painting || 0)}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50">{formatNumber(item.motor || 0)}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50">{formatNumber(item.controller || 0)}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50">{formatNumber(item.widthConstruction || 0)}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50">{formatNumber(item.heightConstruction || 0)}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50 font-medium">{formatNumber(item.unitPrice || 0)}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50">{item.expenseRate || 0}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50">{formatAmount(item.expense || 0)}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50">{formatNumber(item.expense || 0)}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50">{item.quantity || 0}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50">{formatAmount(item.cost || 0)}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50">{formatAmount(item.costExecution || 0)}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50">{formatAmount(item.marginCost || 0)}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50 font-medium">{formatAmount(item.marginCostExecution || 0)}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50">{formatAmount(item.expenseExecution || 0)}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50">{formatNumber(item.cost || 0)}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50">{formatNumber(item.costExecution || 0)}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50">{formatNumber(item.marginCost || 0)}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50 font-medium">{formatNumber(item.marginCostExecution || 0)}</TableCell>
|
||||
<TableCell className="text-right bg-gray-50">{formatNumber(item.expenseExecution || 0)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{/* 합계 행 */}
|
||||
@@ -492,26 +488,26 @@ export default function BiddingDetailForm({
|
||||
<TableCell colSpan={4} className="text-center font-bold">합계</TableCell>
|
||||
<TableCell className="text-right">{estimateDetailTotals.weight.toFixed(2)}</TableCell>
|
||||
<TableCell className="text-right">{estimateDetailTotals.area.toFixed(2)}</TableCell>
|
||||
<TableCell className="text-right">{formatAmount(estimateDetailTotals.steelScreen)}</TableCell>
|
||||
<TableCell className="text-right">{formatAmount(estimateDetailTotals.caulking)}</TableCell>
|
||||
<TableCell className="text-right">{formatAmount(estimateDetailTotals.rail)}</TableCell>
|
||||
<TableCell className="text-right">{formatAmount(estimateDetailTotals.bottom)}</TableCell>
|
||||
<TableCell className="text-right">{formatAmount(estimateDetailTotals.boxReinforce)}</TableCell>
|
||||
<TableCell className="text-right">{formatAmount(estimateDetailTotals.shaft)}</TableCell>
|
||||
<TableCell className="text-right">{formatAmount(estimateDetailTotals.painting)}</TableCell>
|
||||
<TableCell className="text-right">{formatAmount(estimateDetailTotals.motor)}</TableCell>
|
||||
<TableCell className="text-right">{formatAmount(estimateDetailTotals.controller)}</TableCell>
|
||||
<TableCell className="text-right">{formatAmount(estimateDetailTotals.widthConstruction)}</TableCell>
|
||||
<TableCell className="text-right">{formatAmount(estimateDetailTotals.heightConstruction)}</TableCell>
|
||||
<TableCell className="text-right font-bold">{formatAmount(estimateDetailTotals.unitPrice)}</TableCell>
|
||||
<TableCell className="text-right">{formatNumber(estimateDetailTotals.steelScreen)}</TableCell>
|
||||
<TableCell className="text-right">{formatNumber(estimateDetailTotals.caulking)}</TableCell>
|
||||
<TableCell className="text-right">{formatNumber(estimateDetailTotals.rail)}</TableCell>
|
||||
<TableCell className="text-right">{formatNumber(estimateDetailTotals.bottom)}</TableCell>
|
||||
<TableCell className="text-right">{formatNumber(estimateDetailTotals.boxReinforce)}</TableCell>
|
||||
<TableCell className="text-right">{formatNumber(estimateDetailTotals.shaft)}</TableCell>
|
||||
<TableCell className="text-right">{formatNumber(estimateDetailTotals.painting)}</TableCell>
|
||||
<TableCell className="text-right">{formatNumber(estimateDetailTotals.motor)}</TableCell>
|
||||
<TableCell className="text-right">{formatNumber(estimateDetailTotals.controller)}</TableCell>
|
||||
<TableCell className="text-right">{formatNumber(estimateDetailTotals.widthConstruction)}</TableCell>
|
||||
<TableCell className="text-right">{formatNumber(estimateDetailTotals.heightConstruction)}</TableCell>
|
||||
<TableCell className="text-right font-bold">{formatNumber(estimateDetailTotals.unitPrice)}</TableCell>
|
||||
<TableCell className="text-right">-</TableCell>
|
||||
<TableCell className="text-right">{formatAmount(estimateDetailTotals.expense)}</TableCell>
|
||||
<TableCell className="text-right">{formatNumber(estimateDetailTotals.expense)}</TableCell>
|
||||
<TableCell className="text-right">{estimateDetailTotals.quantity}</TableCell>
|
||||
<TableCell className="text-right">{formatAmount(estimateDetailTotals.cost)}</TableCell>
|
||||
<TableCell className="text-right">{formatAmount(estimateDetailTotals.costExecution)}</TableCell>
|
||||
<TableCell className="text-right">{formatAmount(estimateDetailTotals.marginCost)}</TableCell>
|
||||
<TableCell className="text-right font-bold">{formatAmount(estimateDetailTotals.marginCostExecution)}</TableCell>
|
||||
<TableCell className="text-right">{formatAmount(estimateDetailTotals.expenseExecution)}</TableCell>
|
||||
<TableCell className="text-right">{formatNumber(estimateDetailTotals.cost)}</TableCell>
|
||||
<TableCell className="text-right">{formatNumber(estimateDetailTotals.costExecution)}</TableCell>
|
||||
<TableCell className="text-right">{formatNumber(estimateDetailTotals.marginCost)}</TableCell>
|
||||
<TableCell className="text-right font-bold">{formatNumber(estimateDetailTotals.marginCostExecution)}</TableCell>
|
||||
<TableCell className="text-right">{formatNumber(estimateDetailTotals.expenseExecution)}</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
* - 등록 버튼 없음 (견적완료 시 자동 등록)
|
||||
*/
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { FileText, Clock, Trophy, Pencil } from 'lucide-react';
|
||||
import { useListHandlers } from '@/hooks/useListHandlers';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { TableCell, TableRow } from '@/components/ui/table';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
@@ -33,6 +33,8 @@ import {
|
||||
BIDDING_STATUS_LABELS,
|
||||
} from './types';
|
||||
import { getBiddingList, getBiddingStats, deleteBidding, deleteBiddings } from './actions';
|
||||
import { formatNumber } from '@/utils/formatAmount';
|
||||
import { formatDate } from '@/utils/date';
|
||||
|
||||
// 테이블 컬럼 정의
|
||||
const tableColumns = [
|
||||
@@ -64,32 +66,14 @@ const MOCK_BIDDERS = [
|
||||
{ value: 'lee', label: '이영희' },
|
||||
];
|
||||
|
||||
// 금액 포맷팅
|
||||
function formatAmount(amount: number): string {
|
||||
return new Intl.NumberFormat('ko-KR').format(amount);
|
||||
}
|
||||
|
||||
// 날짜 포맷팅
|
||||
function formatDate(dateStr: string | null): string {
|
||||
if (!dateStr) return '-';
|
||||
const date = new Date(dateStr);
|
||||
return date
|
||||
.toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
})
|
||||
.replace(/\. /g, '-')
|
||||
.replace('.', '');
|
||||
}
|
||||
|
||||
interface BiddingListClientProps {
|
||||
initialData?: Bidding[];
|
||||
initialStats?: BiddingStats;
|
||||
}
|
||||
|
||||
export default function BiddingListClient({ initialData = [], initialStats }: BiddingListClientProps) {
|
||||
const router = useRouter();
|
||||
// ===== 공통 핸들러 Hook =====
|
||||
const { handleRowClick, handleEdit } = useListHandlers<Bidding>('construction/project/bidding');
|
||||
|
||||
// ===== 외부 상태 (UniversalListPage 외부에서 관리) =====
|
||||
// Stats 카드 클릭 필터용
|
||||
@@ -113,21 +97,6 @@ export default function BiddingListClient({ initialData = [], initialStats }: Bi
|
||||
}
|
||||
}, [initialStats]);
|
||||
|
||||
// ===== 핸들러 =====
|
||||
const handleRowClick = useCallback(
|
||||
(item: Bidding) => {
|
||||
router.push(`/ko/construction/project/bidding/${item.id}?mode=view`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(item: Bidding) => {
|
||||
router.push(`/ko/construction/project/bidding/${item.id}?mode=edit`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
// ===== UniversalListPage Config =====
|
||||
const config: UniversalListConfig<Bidding> = useMemo(
|
||||
() => ({
|
||||
@@ -363,7 +332,7 @@ export default function BiddingListClient({ initialData = [], initialStats }: Bi
|
||||
<TableCell>{item.projectName}</TableCell>
|
||||
<TableCell>{item.bidderName}</TableCell>
|
||||
<TableCell className="text-center">{item.totalCount}</TableCell>
|
||||
<TableCell className="text-right">{formatAmount(item.biddingAmount)}</TableCell>
|
||||
<TableCell className="text-right">{formatNumber(item.biddingAmount)}</TableCell>
|
||||
<TableCell className="text-center">{formatDate(item.bidDate)}</TableCell>
|
||||
<TableCell className="text-center">{formatDate(item.submissionDate)}</TableCell>
|
||||
<TableCell className="text-center">{formatDate(item.confirmDate)}</TableCell>
|
||||
@@ -411,7 +380,7 @@ export default function BiddingListClient({ initialData = [], initialStats }: Bi
|
||||
onClick={() => handleRowClick(item)}
|
||||
details={[
|
||||
{ label: '거래처', value: item.partnerName },
|
||||
{ label: '입찰금액', value: `${formatAmount(item.biddingAmount)}원` },
|
||||
{ label: '입찰금액', value: `${formatNumber(item.biddingAmount)}원` },
|
||||
{ label: '입찰일자', value: formatDate(item.biddingDate) },
|
||||
{ label: '총 개소', value: `${item.totalCount}` },
|
||||
]}
|
||||
|
||||
@@ -40,11 +40,7 @@ import {
|
||||
getEmptyElectronicApproval,
|
||||
} from '../common';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
|
||||
// 금액 포맷팅
|
||||
function formatAmount(amount: number): string {
|
||||
return new Intl.NumberFormat('ko-KR').format(amount);
|
||||
}
|
||||
import { formatNumber } from '@/utils/formatAmount';
|
||||
|
||||
interface ContractDetailFormProps {
|
||||
mode: 'view' | 'edit' | 'create';
|
||||
@@ -365,7 +361,7 @@ export default function ContractDetailForm({
|
||||
<Label>계약금액</Label>
|
||||
<Input
|
||||
type="text"
|
||||
value={formatAmount(formData.contractAmount)}
|
||||
value={formatNumber(formData.contractAmount)}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value.replace(/[^0-9]/g, '');
|
||||
handleFieldChange('contractAmount', parseInt(value) || 0);
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
* - filterConfig (multi: 거래처, 계약담당자, 공사PM / single: 상태, 정렬)
|
||||
*/
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { FileText, Clock, CheckCircle, Pencil, Trash2 } from 'lucide-react';
|
||||
import { useListHandlers } from '@/hooks/useListHandlers';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { TableCell, TableRow } from '@/components/ui/table';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
@@ -32,6 +32,8 @@ import {
|
||||
CONTRACT_STATUS_LABELS,
|
||||
} from './types';
|
||||
import { getContractList, getContractStats, deleteContract, deleteContracts } from './actions';
|
||||
import { formatNumber } from '@/utils/formatAmount';
|
||||
import { formatDate, formatDateRange } from '@/utils/date';
|
||||
|
||||
// 테이블 컬럼 정의
|
||||
const tableColumns = [
|
||||
@@ -67,40 +69,14 @@ const MOCK_CONSTRUCTION_PMS = [
|
||||
{ value: 'park', label: '박PM' },
|
||||
];
|
||||
|
||||
// 금액 포맷팅
|
||||
function formatAmount(amount: number): string {
|
||||
return new Intl.NumberFormat('ko-KR').format(amount);
|
||||
}
|
||||
|
||||
// 날짜 포맷팅
|
||||
function formatDate(dateStr: string | null): string {
|
||||
if (!dateStr) return '-';
|
||||
const date = new Date(dateStr);
|
||||
return date
|
||||
.toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
})
|
||||
.replace(/\. /g, '-')
|
||||
.replace('.', '');
|
||||
}
|
||||
|
||||
// 계약기간 포맷팅
|
||||
function formatPeriod(startDate: string | null, endDate: string | null): string {
|
||||
const start = formatDate(startDate);
|
||||
const end = formatDate(endDate);
|
||||
if (start === '-' && end === '-') return '-';
|
||||
return `${start} ~ ${end}`;
|
||||
}
|
||||
|
||||
interface ContractListClientProps {
|
||||
initialData?: Contract[];
|
||||
initialStats?: ContractStats;
|
||||
}
|
||||
|
||||
export default function ContractListClient({ initialData = [], initialStats }: ContractListClientProps) {
|
||||
const router = useRouter();
|
||||
// ===== 공통 핸들러 Hook =====
|
||||
const { handleRowClick, handleEdit } = useListHandlers<Contract>('construction/project/contract');
|
||||
|
||||
// ===== 외부 상태 =====
|
||||
const [activeStatTab, setActiveStatTab] = useState<'all' | 'pending' | 'completed'>('all');
|
||||
@@ -120,21 +96,6 @@ export default function ContractListClient({ initialData = [], initialStats }: C
|
||||
}
|
||||
}, [initialStats]);
|
||||
|
||||
// ===== 핸들러 =====
|
||||
const handleRowClick = useCallback(
|
||||
(item: Contract) => {
|
||||
router.push(`/ko/construction/project/contract/${item.id}?mode=view`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(item: Contract) => {
|
||||
router.push(`/ko/construction/project/contract/${item.id}?mode=edit`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
// ===== UniversalListPage Config =====
|
||||
const config: UniversalListConfig<Contract> = useMemo(
|
||||
() => ({
|
||||
@@ -376,9 +337,9 @@ export default function ContractListClient({ initialData = [], initialStats }: C
|
||||
<TableCell className="text-center">{item.contractManagerName}</TableCell>
|
||||
<TableCell className="text-center">{item.constructionPMName || '-'}</TableCell>
|
||||
<TableCell className="text-center">{item.totalLocations}</TableCell>
|
||||
<TableCell className="text-right">{formatAmount(item.contractAmount)}원</TableCell>
|
||||
<TableCell className="text-right">{formatNumber(item.contractAmount)}원</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{formatPeriod(item.contractStartDate, item.contractEndDate)}
|
||||
{formatDateRange(item.contractStartDate, item.contractEndDate)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<span className={CONTRACT_STATUS_STYLES[item.status]}>{CONTRACT_STATUS_LABELS[item.status]}</span>
|
||||
@@ -433,7 +394,7 @@ export default function ContractListClient({ initialData = [], initialStats }: C
|
||||
details={[
|
||||
{ label: '거래처', value: item.partnerName },
|
||||
{ label: '총 개소', value: `${item.totalLocations}개` },
|
||||
{ label: '계약금액', value: `${formatAmount(item.contractAmount)}원` },
|
||||
{ label: '계약금액', value: `${formatNumber(item.contractAmount)}원` },
|
||||
{ label: '계약담당자', value: item.contractManagerName },
|
||||
{ label: '공사PM', value: item.constructionPMName || '-' },
|
||||
]}
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
* - filterConfig (multi: 거래처, 견적자 / single: 상태, 정렬)
|
||||
*/
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { FileText, FileTextIcon, Clock, FileCheck, Pencil } from 'lucide-react';
|
||||
import { useListHandlers } from '@/hooks/useListHandlers';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { TableCell, TableRow } from '@/components/ui/table';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
} from './types';
|
||||
import { getEstimateList, getEstimateStats, deleteEstimate, deleteEstimates, getClientOptions, getUserOptions } from './actions';
|
||||
import type { ClientOption, UserOption } from './actions';
|
||||
import { formatNumber } from '@/utils/formatAmount';
|
||||
|
||||
// 테이블 컬럼 정의
|
||||
const tableColumns = [
|
||||
@@ -49,18 +50,14 @@ const tableColumns = [
|
||||
{ key: 'actions', label: '작업', className: 'w-[80px] text-center' },
|
||||
];
|
||||
|
||||
// 금액 포맷팅
|
||||
function formatAmount(amount: number): string {
|
||||
return new Intl.NumberFormat('ko-KR').format(amount);
|
||||
}
|
||||
|
||||
interface EstimateListClientProps {
|
||||
initialData?: Estimate[];
|
||||
initialStats?: EstimateStats;
|
||||
}
|
||||
|
||||
export default function EstimateListClient({ initialData = [], initialStats }: EstimateListClientProps) {
|
||||
const router = useRouter();
|
||||
// ===== 공통 핸들러 Hook =====
|
||||
const { handleRowClick, handleEdit } = useListHandlers<Estimate>('construction/project/bidding/estimates');
|
||||
|
||||
// ===== 외부 상태 (UniversalListPage 외부에서 관리) =====
|
||||
// Stats 카드 클릭 필터용
|
||||
@@ -103,21 +100,6 @@ export default function EstimateListClient({ initialData = [], initialStats }: E
|
||||
});
|
||||
}, []);
|
||||
|
||||
// ===== 핸들러 =====
|
||||
const handleRowClick = useCallback(
|
||||
(item: Estimate) => {
|
||||
router.push(`/ko/construction/project/bidding/estimates/${item.id}?mode=view`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(item: Estimate) => {
|
||||
router.push(`/ko/construction/project/bidding/estimates/${item.id}?mode=edit`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
// ===== UniversalListPage Config =====
|
||||
const config: UniversalListConfig<Estimate> = useMemo(
|
||||
() => ({
|
||||
@@ -346,7 +328,7 @@ export default function EstimateListClient({ initialData = [], initialStats }: E
|
||||
<TableCell>{item.projectName}</TableCell>
|
||||
<TableCell className="text-center">{item.estimatorName}</TableCell>
|
||||
<TableCell className="text-center">{item.itemCount}</TableCell>
|
||||
<TableCell className="text-right">{formatAmount(item.estimateAmount)}</TableCell>
|
||||
<TableCell className="text-right">{formatNumber(item.estimateAmount)}</TableCell>
|
||||
<TableCell className="text-center">{item.completedDate || '-'}</TableCell>
|
||||
<TableCell className="text-center">{item.bidDate || '-'}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
@@ -391,7 +373,7 @@ export default function EstimateListClient({ initialData = [], initialStats }: E
|
||||
details={[
|
||||
{ label: '거래처', value: item.partnerName },
|
||||
{ label: '견적자', value: item.estimatorName },
|
||||
{ label: '견적금액', value: `${formatAmount(item.estimateAmount)}원` },
|
||||
{ label: '견적금액', value: `${formatNumber(item.estimateAmount)}원` },
|
||||
{ label: '입찰일', value: item.bidDate || '-' },
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -12,11 +12,7 @@
|
||||
import type { EstimateDetailFormData } from '../types';
|
||||
import type { CompanyInfo } from '../actions';
|
||||
import { DocumentHeader } from '@/components/document-system';
|
||||
|
||||
// 금액 포맷팅
|
||||
function formatAmount(amount: number): string {
|
||||
return new Intl.NumberFormat('ko-KR').format(amount);
|
||||
}
|
||||
import { formatNumber } from '@/utils/formatAmount';
|
||||
|
||||
// 금액을 한글로 변환
|
||||
function amountToKorean(amount: number): string {
|
||||
@@ -169,9 +165,9 @@ export function EstimateDocumentContent({ data }: EstimateDocumentContentProps)
|
||||
<td className="border border-gray-400 px-3 py-2">{item.name}</td>
|
||||
<td className="border border-gray-400 px-3 py-2 text-center">{item.quantity}</td>
|
||||
<td className="border border-gray-400 px-3 py-2 text-center">{item.unit}</td>
|
||||
<td className="border border-gray-400 px-3 py-2 text-right">{formatAmount(item.materialCost)}</td>
|
||||
<td className="border border-gray-400 px-3 py-2 text-right">{formatAmount(item.laborCost)}</td>
|
||||
<td className="border border-gray-400 px-3 py-2 text-right font-medium">{formatAmount(item.totalCost)}</td>
|
||||
<td className="border border-gray-400 px-3 py-2 text-right">{formatNumber(item.materialCost)}</td>
|
||||
<td className="border border-gray-400 px-3 py-2 text-right">{formatNumber(item.laborCost)}</td>
|
||||
<td className="border border-gray-400 px-3 py-2 text-right font-medium">{formatNumber(item.totalCost)}</td>
|
||||
<td className="border border-gray-400 px-3 py-2">{item.remarks}</td>
|
||||
</tr>
|
||||
))
|
||||
@@ -180,13 +176,13 @@ export function EstimateDocumentContent({ data }: EstimateDocumentContentProps)
|
||||
<tr className="font-medium">
|
||||
<td className="border border-gray-400 px-3 py-2 text-center" colSpan={3}>합 계</td>
|
||||
<td className="border border-gray-400 px-3 py-2 text-right">
|
||||
{formatAmount(formData.summaryItems.reduce((sum, item) => sum + item.materialCost, 0))}
|
||||
{formatNumber(formData.summaryItems.reduce((sum, item) => sum + item.materialCost, 0))}
|
||||
</td>
|
||||
<td className="border border-gray-400 px-3 py-2 text-right">
|
||||
{formatAmount(formData.summaryItems.reduce((sum, item) => sum + item.laborCost, 0))}
|
||||
{formatNumber(formData.summaryItems.reduce((sum, item) => sum + item.laborCost, 0))}
|
||||
</td>
|
||||
<td className="border border-gray-400 px-3 py-2 text-right">
|
||||
₩{formatAmount(formData.summaryItems.reduce((sum, item) => sum + item.totalCost, 0))}
|
||||
₩{formatNumber(formData.summaryItems.reduce((sum, item) => sum + item.totalCost, 0))}
|
||||
</td>
|
||||
<td className="border border-gray-400 px-3 py-2"></td>
|
||||
</tr>
|
||||
@@ -242,16 +238,16 @@ export function EstimateDocumentContent({ data }: EstimateDocumentContentProps)
|
||||
<td className="border border-gray-400 px-2 py-1 text-center">{index + 1}</td>
|
||||
<td className="border border-gray-400 px-2 py-1">{item.name}</td>
|
||||
<td className="border border-gray-400 px-2 py-1">{item.material}</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-right">{formatAmount(item.width)}</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-right">{formatAmount(item.height)}</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-right">{formatNumber(item.width)}</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-right">{formatNumber(item.height)}</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center">{item.quantity}</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center">SET</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-right">{formatAmount(item.unitPrice)}</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-right">{formatAmount(item.materialCost)}</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-right">{formatAmount(item.laborCost)}</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-right">{formatAmount(item.laborCost * item.quantity)}</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-right">{formatAmount(item.totalPrice)}</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-right">{formatAmount(item.totalCost)}</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-right">{formatNumber(item.unitPrice)}</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-right">{formatNumber(item.materialCost)}</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-right">{formatNumber(item.laborCost)}</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-right">{formatNumber(item.laborCost * item.quantity)}</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-right">{formatNumber(item.totalPrice)}</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-right">{formatNumber(item.totalCost)}</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
@@ -264,15 +260,15 @@ export function EstimateDocumentContent({ data }: EstimateDocumentContentProps)
|
||||
<td className="border border-gray-400 px-2 py-1 text-center">SET</td>
|
||||
<td className="border border-gray-400 px-2 py-1"></td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-right">
|
||||
{formatAmount(formData.detailItems.reduce((sum, item) => sum + item.materialCost, 0))}
|
||||
{formatNumber(formData.detailItems.reduce((sum, item) => sum + item.materialCost, 0))}
|
||||
</td>
|
||||
<td className="border border-gray-400 px-2 py-1"></td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-right">
|
||||
{formatAmount(formData.detailItems.reduce((sum, item) => sum + item.laborCost * item.quantity, 0))}
|
||||
{formatNumber(formData.detailItems.reduce((sum, item) => sum + item.laborCost * item.quantity, 0))}
|
||||
</td>
|
||||
<td className="border border-gray-400 px-2 py-1"></td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-right">
|
||||
{formatAmount(formData.detailItems.reduce((sum, item) => sum + item.totalCost, 0))}
|
||||
{formatNumber(formData.detailItems.reduce((sum, item) => sum + item.totalCost, 0))}
|
||||
</td>
|
||||
</tr>
|
||||
{/* 비고 행 */}
|
||||
|
||||
@@ -1,4 +1,2 @@
|
||||
// 금액 포맷팅
|
||||
export function formatAmount(amount: number): string {
|
||||
return new Intl.NumberFormat('ko-KR').format(amount);
|
||||
}
|
||||
// 공통 유틸 re-export (backward compatibility)
|
||||
export { formatNumber as formatAmount } from '@/utils/formatAmount';
|
||||
|
||||
@@ -49,11 +49,7 @@ import {
|
||||
type ElectronicApproval,
|
||||
getEmptyElectronicApproval,
|
||||
} from '../common';
|
||||
|
||||
// 금액 포맷팅
|
||||
function formatAmount(amount: number): string {
|
||||
return new Intl.NumberFormat('ko-KR').format(amount);
|
||||
}
|
||||
import { formatNumber } from '@/utils/formatAmount';
|
||||
|
||||
interface HandoverReportDetailFormProps {
|
||||
mode: 'view' | 'edit';
|
||||
@@ -315,7 +311,7 @@ export default function HandoverReportDetailForm({
|
||||
<Label>계약금액 (공급가액)</Label>
|
||||
<Input
|
||||
type="text"
|
||||
value={formatAmount(formData.contractAmount)}
|
||||
value={formatNumber(formData.contractAmount)}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value.replace(/[^0-9]/g, '');
|
||||
handleFieldChange('contractAmount', parseInt(value) || 0);
|
||||
@@ -493,7 +489,7 @@ export default function HandoverReportDetailForm({
|
||||
<TableCell className="text-center">{item.no}</TableCell>
|
||||
<TableCell>{item.name}</TableCell>
|
||||
<TableCell>{item.product}</TableCell>
|
||||
<TableCell className="text-right">{formatAmount(item.quantity)}</TableCell>
|
||||
<TableCell className="text-right">{formatNumber(item.quantity)}</TableCell>
|
||||
<TableCell>
|
||||
{isEditMode ? (
|
||||
<Input
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
* - 등록 버튼 없음 (계약 종료 시 자동 등록)
|
||||
*/
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { FileText, Clock, CheckCircle, Pencil } from 'lucide-react';
|
||||
import { useListHandlers } from '@/hooks/useListHandlers';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { TableCell, TableRow } from '@/components/ui/table';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
@@ -33,6 +33,8 @@ import {
|
||||
HANDOVER_STATUS_STYLES,
|
||||
} from './types';
|
||||
import { getHandoverReportList, getHandoverReportStats } from './actions';
|
||||
import { formatNumber } from '@/utils/formatAmount';
|
||||
import { formatDateRange } from '@/utils/date';
|
||||
|
||||
// 테이블 컬럼 정의
|
||||
const tableColumns = [
|
||||
@@ -71,33 +73,6 @@ const MOCK_CONSTRUCTION_PMS = [
|
||||
{ value: 'park', label: '박PM' },
|
||||
];
|
||||
|
||||
// 금액 포맷팅
|
||||
function formatAmount(amount: number): string {
|
||||
return new Intl.NumberFormat('ko-KR').format(amount);
|
||||
}
|
||||
|
||||
// 날짜 포맷팅
|
||||
function formatDate(dateStr: string | null): string {
|
||||
if (!dateStr) return '-';
|
||||
const date = new Date(dateStr);
|
||||
return date
|
||||
.toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
})
|
||||
.replace(/\. /g, '-')
|
||||
.replace('.', '');
|
||||
}
|
||||
|
||||
// 계약기간 포맷팅
|
||||
function formatPeriod(startDate: string | null, endDate: string | null): string {
|
||||
const start = formatDate(startDate);
|
||||
const end = formatDate(endDate);
|
||||
if (start === '-' && end === '-') return '-';
|
||||
return `${start} ~ ${end}`;
|
||||
}
|
||||
|
||||
interface HandoverReportListClientProps {
|
||||
initialData?: HandoverReport[];
|
||||
initialStats?: HandoverReportStats;
|
||||
@@ -107,7 +82,10 @@ export default function HandoverReportListClient({
|
||||
initialData = [],
|
||||
initialStats,
|
||||
}: HandoverReportListClientProps) {
|
||||
const router = useRouter();
|
||||
// ===== 공통 핸들러 Hook =====
|
||||
const { handleRowClick, handleEdit } = useListHandlers<HandoverReport>(
|
||||
'construction/project/contract/handover-report'
|
||||
);
|
||||
|
||||
// ===== 외부 상태 (UniversalListPage 외부에서 관리) =====
|
||||
const [activeStatTab, setActiveStatTab] = useState<'all' | 'pending' | 'completed'>('all');
|
||||
@@ -127,21 +105,6 @@ export default function HandoverReportListClient({
|
||||
}
|
||||
}, [initialStats]);
|
||||
|
||||
// ===== 핸들러 =====
|
||||
const handleRowClick = useCallback(
|
||||
(report: HandoverReport) => {
|
||||
router.push(`/ko/construction/project/contract/handover-report/${report.id}?mode=view`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(report: HandoverReport) => {
|
||||
router.push(`/ko/construction/project/contract/handover-report/${report.id}?mode=edit`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
// ===== UniversalListPage Config =====
|
||||
const config: UniversalListConfig<HandoverReport> = useMemo(
|
||||
() => ({
|
||||
@@ -363,9 +326,9 @@ export default function HandoverReportListClient({
|
||||
<TableCell className="text-center">{item.contractManagerName}</TableCell>
|
||||
<TableCell className="text-center">{item.constructionPMName || '-'}</TableCell>
|
||||
<TableCell className="text-center">{item.totalSites}</TableCell>
|
||||
<TableCell className="text-right">{formatAmount(item.contractAmount)}</TableCell>
|
||||
<TableCell className="text-right">{formatNumber(item.contractAmount)}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{formatPeriod(item.contractStartDate, item.contractEndDate)}
|
||||
{formatDateRange(item.contractStartDate, item.contractEndDate)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<span className={HANDOVER_STATUS_STYLES[item.status]}>
|
||||
@@ -410,7 +373,7 @@ export default function HandoverReportListClient({
|
||||
onClick={() => handleRowClick(item)}
|
||||
details={[
|
||||
{ label: '거래처', value: item.partnerName },
|
||||
{ label: '계약금액', value: `${formatAmount(item.contractAmount)}원` },
|
||||
{ label: '계약금액', value: `${formatNumber(item.contractAmount)}원` },
|
||||
{ label: '계약담당자', value: item.contractManagerName },
|
||||
{ label: '총 개소', value: `${item.totalSites}개소` },
|
||||
]}
|
||||
|
||||
@@ -8,12 +8,7 @@ import {
|
||||
import { toast } from 'sonner';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import type { HandoverReportDetail } from '../types';
|
||||
|
||||
// 금액 포맷팅
|
||||
function formatAmount(amount: number | undefined | null): string {
|
||||
if (amount === undefined || amount === null) return '0';
|
||||
return new Intl.NumberFormat('ko-KR').format(amount);
|
||||
}
|
||||
import { formatNumber } from '@/utils/formatAmount';
|
||||
|
||||
// 날짜 포맷팅 (년월)
|
||||
function formatYearMonth(dateStr: string | null): string {
|
||||
@@ -110,7 +105,7 @@ export function HandoverReportDocumentModal({
|
||||
{/* 계약금액 (공급가액) */}
|
||||
<tr>
|
||||
<th className="border border-gray-300 px-4 py-3 bg-gray-50 text-left font-medium">계약금액 (공급가액)</th>
|
||||
<td className="border border-gray-300 px-4 py-3">₩ {formatAmount(report.contractAmount)}</td>
|
||||
<td className="border border-gray-300 px-4 py-3">₩ {formatNumber(report.contractAmount)}</td>
|
||||
</tr>
|
||||
|
||||
{/* 계약 ITEM - 기획서: 구분, 수량, 비고 3컬럼 */}
|
||||
@@ -137,7 +132,7 @@ export function HandoverReportDocumentModal({
|
||||
{item.name || '-'}
|
||||
</td>
|
||||
<td className={`px-4 py-2 text-center border-l border-gray-300 ${idx > 0 ? 'border-t' : ''}`}>
|
||||
{formatAmount(item.quantity)}
|
||||
{formatNumber(item.quantity)}
|
||||
</td>
|
||||
<td className={`px-4 py-2 border-l border-gray-300 ${idx > 0 ? 'border-t' : ''}`}>
|
||||
{item.remark || '-'}
|
||||
@@ -160,7 +155,7 @@ export function HandoverReportDocumentModal({
|
||||
<th className="px-4 py-2 bg-gray-50 text-left font-medium w-40">2차 배관 유무</th>
|
||||
<td className="px-4 py-2">
|
||||
{report.hasSecondaryPiping
|
||||
? `포함 (${formatAmount(report.secondaryPipingAmount)})`
|
||||
? `포함 (${formatNumber(report.secondaryPipingAmount)})`
|
||||
: '미포함'}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -168,7 +163,7 @@ export function HandoverReportDocumentModal({
|
||||
<th className="border-t border-gray-300 px-4 py-2 bg-gray-50 text-left font-medium">도장 & 코킹 유무</th>
|
||||
<td className="border-t border-gray-300 px-4 py-2">
|
||||
{report.hasCoating
|
||||
? `포함 (${formatAmount(report.coatingAmount)})`
|
||||
? `포함 (${formatNumber(report.coatingAmount)})`
|
||||
: '미포함'}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -182,9 +177,9 @@ export function HandoverReportDocumentModal({
|
||||
<th className="border border-gray-300 px-4 py-3 bg-gray-50 text-left font-medium align-middle">장비 외 실행금액</th>
|
||||
<td className="border border-gray-300 px-4 py-3">
|
||||
<div className="space-y-1">
|
||||
<div>운반비 : {formatAmount(report.externalEquipmentCost?.shippingCost)}</div>
|
||||
<div>양중장비 : {formatAmount(report.externalEquipmentCost?.highAltitudeWork)}</div>
|
||||
<div>공과금 : {formatAmount(report.externalEquipmentCost?.publicExpense)}</div>
|
||||
<div>운반비 : {formatNumber(report.externalEquipmentCost?.shippingCost)}</div>
|
||||
<div>양중장비 : {formatNumber(report.externalEquipmentCost?.highAltitudeWork)}</div>
|
||||
<div>공과금 : {formatNumber(report.externalEquipmentCost?.publicExpense)}</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
*/
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { AlertTriangle, Pencil, Inbox, Clock, CheckCircle, XCircle, Undo2 } from 'lucide-react';
|
||||
import { useListHandlers } from '@/hooks/useListHandlers';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { TableCell, TableRow } from '@/components/ui/table';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
getIssueStats,
|
||||
withdrawIssues,
|
||||
} from './actions';
|
||||
import { formatDate } from '@/utils/date';
|
||||
|
||||
// 테이블 컬럼 정의
|
||||
const tableColumns = [
|
||||
@@ -67,12 +68,6 @@ const tableColumns = [
|
||||
{ key: 'actions', label: '작업', className: 'w-[80px] text-center' },
|
||||
];
|
||||
|
||||
// 날짜 포맷
|
||||
function formatDate(dateStr: string | null): string {
|
||||
if (!dateStr) return '-';
|
||||
return dateStr.split('T')[0];
|
||||
}
|
||||
|
||||
interface IssueManagementListClientProps {
|
||||
initialData?: Issue[];
|
||||
initialStats?: IssueStats;
|
||||
@@ -82,7 +77,10 @@ export default function IssueManagementListClient({
|
||||
initialData = [],
|
||||
initialStats,
|
||||
}: IssueManagementListClientProps) {
|
||||
const router = useRouter();
|
||||
// ===== 공통 핸들러 Hook =====
|
||||
const { handleRowClick, handleEdit, router } = useListHandlers<Issue>(
|
||||
'construction/project/issue-management'
|
||||
);
|
||||
|
||||
// ===== 외부 상태 (UniversalListPage 외부에서 관리) =====
|
||||
const [activeStatTab, setActiveStatTab] = useState<'all' | 'received' | 'in_progress' | 'resolved' | 'unresolved'>('all');
|
||||
@@ -105,21 +103,7 @@ export default function IssueManagementListClient({
|
||||
}
|
||||
}, [initialStats]);
|
||||
|
||||
// ===== 핸들러 =====
|
||||
const handleRowClick = useCallback(
|
||||
(item: Issue) => {
|
||||
router.push(`/ko/construction/project/issue-management/${item.id}?mode=view`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(item: Issue) => {
|
||||
router.push(`/ko/construction/project/issue-management/${item.id}?mode=edit`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
// ===== 추가 핸들러 =====
|
||||
const handleCreate = useCallback(() => {
|
||||
router.push('/ko/construction/project/issue-management?mode=new');
|
||||
}, [router]);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { format, startOfYear, endOfYear } from 'date-fns';
|
||||
import { useListHandlers } from '@/hooks/useListHandlers';
|
||||
import { Package, Plus, Pencil, Trash2, PackageCheck } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { TableCell, TableRow } from '@/components/ui/table';
|
||||
@@ -46,7 +46,8 @@ export default function ItemManagementClient({
|
||||
initialData = [],
|
||||
initialStats,
|
||||
}: ItemManagementClientProps) {
|
||||
const router = useRouter();
|
||||
// ===== 공통 핸들러 Hook =====
|
||||
const { handleRowClick, router } = useListHandlers<Item>('construction/order/base-info/items');
|
||||
const today = new Date();
|
||||
|
||||
// 날짜 상태 (당해년도 기본값)
|
||||
@@ -208,13 +209,7 @@ export default function ItemManagementClient({
|
||||
}
|
||||
}, [selectedItems.size, paginatedData]);
|
||||
|
||||
const handleRowClick = useCallback(
|
||||
(item: Item) => {
|
||||
router.push(`/ko/construction/order/base-info/items/${item.id}?mode=view`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
// ===== 추가 핸들러 (handleRowClick은 Hook에서 제공) =====
|
||||
const handleCreate = useCallback(() => {
|
||||
router.push('/ko/construction/order/base-info/items?mode=new');
|
||||
}, [router]);
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
*/
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { format, startOfYear, endOfYear } from 'date-fns';
|
||||
import { useListHandlers } from '@/hooks/useListHandlers';
|
||||
import { Hammer, Pencil, Trash2, HardHat } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { TableCell, TableRow } from '@/components/ui/table';
|
||||
@@ -52,7 +52,10 @@ export default function LaborManagementClient({
|
||||
initialData = [],
|
||||
initialStats,
|
||||
}: LaborManagementClientProps) {
|
||||
const router = useRouter();
|
||||
// ===== 공통 핸들러 Hook =====
|
||||
const { handleRowClick, handleEdit, router } = useListHandlers<Labor>(
|
||||
'construction/order/base-info/labor'
|
||||
);
|
||||
const today = new Date();
|
||||
|
||||
// ===== 외부 상태 (UniversalListPage 외부에서 관리) =====
|
||||
@@ -96,21 +99,7 @@ export default function LaborManagementClient({
|
||||
return value.toFixed(2);
|
||||
}, []);
|
||||
|
||||
// ===== 핸들러 =====
|
||||
const handleRowClick = useCallback(
|
||||
(labor: Labor) => {
|
||||
router.push(`/ko/construction/order/base-info/labor/${labor.id}?mode=view`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(labor: Labor) => {
|
||||
router.push(`/ko/construction/order/base-info/labor/${labor.id}?mode=edit`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
// ===== 추가 핸들러 =====
|
||||
const handleCreate = useCallback(() => {
|
||||
router.push('/ko/construction/order/base-info/labor?mode=new');
|
||||
}, [router]);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { getTodayString } from '@/utils/date';
|
||||
import { getTodayString, formatDate } from '@/utils/date';
|
||||
import { Plus, Trash2, FileText, Upload, X } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -47,13 +47,6 @@ interface ConstructionDetailClientProps {
|
||||
mode: 'view' | 'edit';
|
||||
}
|
||||
|
||||
// 날짜 포맷팅
|
||||
function formatDate(dateStr: string | null): string {
|
||||
if (!dateStr) return '-';
|
||||
const date = new Date(dateStr);
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
export default function ConstructionDetailClient({ id, mode }: ConstructionDetailClientProps) {
|
||||
const router = useRouter();
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
*/
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { HardHat, Pencil, Clock, CheckCircle } from 'lucide-react';
|
||||
import { useListHandlers } from '@/hooks/useListHandlers';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { TableCell, TableRow } from '@/components/ui/table';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
@@ -75,7 +75,10 @@ export default function ConstructionManagementListClient({
|
||||
initialData = [],
|
||||
initialStats,
|
||||
}: ConstructionManagementListClientProps) {
|
||||
const router = useRouter();
|
||||
// ===== 공통 핸들러 Hook =====
|
||||
const { handleRowClick, handleEdit, router } = useListHandlers<ConstructionManagement>(
|
||||
'construction/project/construction-management'
|
||||
);
|
||||
|
||||
// ===== 외부 상태 (UniversalListPage 외부에서 관리) =====
|
||||
const [activeStatTab, setActiveStatTab] = useState<'all' | 'in_progress' | 'completed'>('all');
|
||||
@@ -159,21 +162,6 @@ export default function ConstructionManagementListClient({
|
||||
return dateStr.split('T')[0];
|
||||
}, []);
|
||||
|
||||
// ===== 핸들러 =====
|
||||
const handleRowClick = useCallback(
|
||||
(item: ConstructionManagement) => {
|
||||
router.push(`/ko/construction/project/construction-management/${item.id}?mode=view`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(item: ConstructionManagement) => {
|
||||
router.push(`/ko/construction/project/construction-management/${item.id}?mode=edit`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
// 달력 이벤트 핸들러
|
||||
const handleCalendarDateClick = useCallback((date: Date) => {
|
||||
if (selectedCalendarDate && isSameDay(selectedCalendarDate, date)) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect, Fragment } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { FolderKanban, ClipboardList, PlayCircle, CheckCircle2 } from 'lucide-react';
|
||||
import { useListHandlers } from '@/hooks/useListHandlers';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
@@ -108,7 +108,8 @@ interface ProjectListClientProps {
|
||||
}
|
||||
|
||||
export default function ProjectListClient({ initialData = [], initialStats }: ProjectListClientProps) {
|
||||
const router = useRouter();
|
||||
// ===== 공통 핸들러 Hook =====
|
||||
const { handleRowClick, router } = useListHandlers<Project>('construction/project/execution-management');
|
||||
|
||||
// 상태
|
||||
const [projects, setProjects] = useState<Project[]>(initialData);
|
||||
@@ -238,19 +239,6 @@ export default function ProjectListClient({ initialData = [], initialStats }: Pr
|
||||
}
|
||||
}, [selectedItems.size, paginatedData]);
|
||||
|
||||
const handleRowClick = useCallback(
|
||||
(project: Project) => {
|
||||
router.push(`/ko/construction/project/execution-management/${project.id}?mode=view`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const handleGanttProjectClick = useCallback(
|
||||
(project: Project) => {
|
||||
router.push(`/ko/construction/project/execution-management/${project.id}?mode=view`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
// 금액 포맷
|
||||
const formatAmount = (amount: number) => {
|
||||
@@ -400,7 +388,7 @@ export default function ProjectListClient({ initialData = [], initialStats }: Pr
|
||||
projects={chartProjects}
|
||||
viewMode={chartViewMode}
|
||||
currentDate={chartDate}
|
||||
onProjectClick={handleGanttProjectClick}
|
||||
onProjectClick={handleRowClick}
|
||||
onDateChange={setChartDate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
*/
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Package, Pencil, Trash2 } from 'lucide-react';
|
||||
import { useListHandlers } from '@/hooks/useListHandlers';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { TableCell, TableRow } from '@/components/ui/table';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
@@ -84,7 +84,10 @@ export default function OrderManagementListClient({
|
||||
initialData = [],
|
||||
initialStats,
|
||||
}: OrderManagementListClientProps) {
|
||||
const router = useRouter();
|
||||
// ===== 공통 핸들러 Hook =====
|
||||
const { handleRowClick, handleEdit, router } = useListHandlers<Order>(
|
||||
'construction/order/order-management'
|
||||
);
|
||||
|
||||
// ===== 외부 상태 (UniversalListPage 외부에서 관리) =====
|
||||
const [startDate, setStartDate] = useState<string>('');
|
||||
@@ -157,21 +160,7 @@ export default function OrderManagementListClient({
|
||||
return dateStr.split('T')[0];
|
||||
}, []);
|
||||
|
||||
// ===== 핸들러 =====
|
||||
const handleRowClick = useCallback(
|
||||
(order: Order) => {
|
||||
router.push(`/ko/construction/order/order-management/${order.id}?mode=view`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(order: Order) => {
|
||||
router.push(`/ko/construction/order/order-management/${order.id}?mode=edit`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
// ===== 추가 핸들러 =====
|
||||
const handleCreate = useCallback(() => {
|
||||
router.push('/ko/construction/order/order-management?mode=new');
|
||||
}, [router]);
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
*/
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Building2, AlertTriangle, Pencil, Trash2 } from 'lucide-react';
|
||||
import { useListHandlers } from '@/hooks/useListHandlers';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { TableCell, TableRow } from '@/components/ui/table';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
@@ -49,7 +49,10 @@ interface PartnerListClientProps {
|
||||
}
|
||||
|
||||
export default function PartnerListClient({ initialData = [], initialStats }: PartnerListClientProps) {
|
||||
const router = useRouter();
|
||||
// ===== 공통 핸들러 Hook =====
|
||||
const { handleRowClick, handleEdit, router } = useListHandlers<Partner>(
|
||||
'construction/project/bidding/partners'
|
||||
);
|
||||
|
||||
// ===== 외부 상태 =====
|
||||
const [stats, setStats] = useState<PartnerStats>(
|
||||
@@ -67,25 +70,11 @@ export default function PartnerListClient({ initialData = [], initialStats }: Pa
|
||||
}
|
||||
}, [initialStats]);
|
||||
|
||||
// ===== 핸들러 =====
|
||||
const handleRowClick = useCallback(
|
||||
(item: Partner) => {
|
||||
router.push(`/ko/construction/project/bidding/partners/${item.id}?mode=view`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
// ===== 추가 핸들러 =====
|
||||
const handleCreate = useCallback(() => {
|
||||
router.push('/ko/construction/project/bidding/partners?mode=new');
|
||||
}, [router]);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(item: Partner) => {
|
||||
router.push(`/ko/construction/project/bidding/partners/${item.id}?mode=edit`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
// ===== UniversalListPage Config =====
|
||||
const config: UniversalListConfig<Partner> = useMemo(
|
||||
() => ({
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
*/
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { DollarSign, Package, CheckCircle, AlertCircle, Pencil, Trash2 } from 'lucide-react';
|
||||
import { useListHandlers } from '@/hooks/useListHandlers';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { TableCell, TableRow, TableHead } from '@/components/ui/table';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
@@ -54,7 +54,10 @@ export default function PricingListClient({
|
||||
initialData = [],
|
||||
initialStats,
|
||||
}: PricingListClientProps) {
|
||||
const router = useRouter();
|
||||
// ===== 공통 핸들러 Hook =====
|
||||
const { handleRowClick, handleEdit, router } = useListHandlers<Pricing>(
|
||||
'construction/order/base-info/pricing'
|
||||
);
|
||||
|
||||
// ===== 외부 상태 (UniversalListPage 외부에서 관리) =====
|
||||
const [activeStatTab, setActiveStatTab] = useState<'all' | 'in_use' | 'not_registered'>('all');
|
||||
@@ -96,21 +99,7 @@ export default function PricingListClient({
|
||||
return num.toLocaleString('ko-KR');
|
||||
}, []);
|
||||
|
||||
// ===== 핸들러 =====
|
||||
const handleRowClick = useCallback(
|
||||
(pricing: Pricing) => {
|
||||
router.push(`/ko/construction/order/base-info/pricing/${pricing.id}?mode=view`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(pricing: Pricing) => {
|
||||
router.push(`/ko/construction/order/base-info/pricing/${pricing.id}?mode=edit`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
// ===== 추가 핸들러 =====
|
||||
const handleCreate = useCallback(() => {
|
||||
router.push('/ko/construction/order/base-info/pricing?mode=new');
|
||||
}, [router]);
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
* - 삭제 기능 없음 (조회/수정 전용)
|
||||
*/
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { FileText, Pencil } from 'lucide-react';
|
||||
import { useListHandlers } from '@/hooks/useListHandlers';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { TableCell, TableRow } from '@/components/ui/table';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
@@ -68,7 +68,10 @@ export default function ProgressBillingManagementListClient({
|
||||
initialData = [],
|
||||
initialStats,
|
||||
}: ProgressBillingManagementListClientProps) {
|
||||
const router = useRouter();
|
||||
// ===== 공통 핸들러 Hook =====
|
||||
const { handleRowClick, handleEdit } = useListHandlers<ProgressBilling>(
|
||||
'construction/billing/progress-billing-management'
|
||||
);
|
||||
|
||||
// ===== 외부 상태 (UniversalListPage 외부에서 관리) =====
|
||||
const [activeStatTab, setActiveStatTab] = useState<'all' | 'contractWaiting' | 'contractComplete'>('all');
|
||||
@@ -88,21 +91,6 @@ export default function ProgressBillingManagementListClient({
|
||||
}
|
||||
}, [initialStats]);
|
||||
|
||||
// ===== 핸들러 =====
|
||||
const handleRowClick = useCallback(
|
||||
(item: ProgressBilling) => {
|
||||
router.push(`/ko/construction/billing/progress-billing-management/${item.id}?mode=view`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(item: ProgressBilling) => {
|
||||
router.push(`/ko/construction/billing/progress-billing-management/${item.id}?mode=edit`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
// ===== UniversalListPage Config =====
|
||||
const config: UniversalListConfig<ProgressBilling> = useMemo(
|
||||
() => ({
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
*/
|
||||
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Calendar, CalendarDays, CalendarCheck, CalendarClock, Pencil, Trash2 } from 'lucide-react';
|
||||
import { useListHandlers } from '@/hooks/useListHandlers';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { TableCell, TableRow } from '@/components/ui/table';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
@@ -90,7 +90,10 @@ interface SiteBriefingListClientProps {
|
||||
}
|
||||
|
||||
export default function SiteBriefingListClient({ initialData = [] }: SiteBriefingListClientProps) {
|
||||
const router = useRouter();
|
||||
// ===== 공통 핸들러 Hook =====
|
||||
const { handleRowClick, handleEdit, router } = useListHandlers<SiteBriefing>(
|
||||
'construction/project/bidding/site-briefings'
|
||||
);
|
||||
|
||||
// Stats 탭 상태
|
||||
const [activeStatTab, setActiveStatTab] = useState<'all' | 'scheduled' | 'attended'>('all');
|
||||
@@ -99,21 +102,7 @@ export default function SiteBriefingListClient({ initialData = [] }: SiteBriefin
|
||||
const [endDate, setEndDate] = useState('');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
// ===== 핸들러 =====
|
||||
const handleRowClick = useCallback(
|
||||
(item: SiteBriefing) => {
|
||||
router.push(`/ko/construction/project/bidding/site-briefings/${item.id}?mode=view`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(item: SiteBriefing) => {
|
||||
router.push(`/ko/construction/project/bidding/site-briefings/${item.id}?mode=edit`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
// ===== 추가 핸들러 =====
|
||||
const handleCreate = useCallback(() => {
|
||||
router.push('/ko/construction/project/bidding/site-briefings?mode=new');
|
||||
}, [router]);
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
* - 삭제 기능 (deleteConfirmMessage로 AlertDialog 대체)
|
||||
*/
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { Building2, HardHat, AlertCircle, Pencil } from 'lucide-react';
|
||||
import { useListHandlers } from '@/hooks/useListHandlers';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { TableCell, TableRow } from '@/components/ui/table';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
@@ -61,7 +61,8 @@ export default function SiteManagementListClient({
|
||||
initialData = [],
|
||||
initialStats,
|
||||
}: SiteManagementListClientProps) {
|
||||
const router = useRouter();
|
||||
// ===== 공통 핸들러 Hook =====
|
||||
const { handleRowClick, handleEdit } = useListHandlers<Site>('construction/order/site-management');
|
||||
|
||||
// ===== 외부 상태 (UniversalListPage 외부에서 관리) =====
|
||||
const [activeStatTab, setActiveStatTab] = useState<'all' | 'construction' | 'unregistered'>('all');
|
||||
@@ -81,21 +82,6 @@ export default function SiteManagementListClient({
|
||||
}
|
||||
}, [initialStats]);
|
||||
|
||||
// ===== 핸들러 =====
|
||||
const handleRowClick = useCallback(
|
||||
(site: Site) => {
|
||||
router.push(`/ko/construction/order/site-management/${site.id}?mode=view`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(site: Site) => {
|
||||
router.push(`/ko/construction/order/site-management/${site.id}?mode=edit`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
// ===== UniversalListPage Config =====
|
||||
const config: UniversalListConfig<Site> = useMemo(
|
||||
() => ({
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
*/
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ClipboardCheck, Clock, CheckCircle, Pencil, Trash2 } from 'lucide-react';
|
||||
import { useListHandlers } from '@/hooks/useListHandlers';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { TableCell, TableRow } from '@/components/ui/table';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
deleteStructureReview,
|
||||
deleteStructureReviews,
|
||||
} from './actions';
|
||||
import { formatDate } from '@/utils/date';
|
||||
|
||||
// 테이블 컬럼 정의
|
||||
const tableColumns = [
|
||||
@@ -60,12 +61,6 @@ const MOCK_PARTNERS = [
|
||||
{ value: '3', label: '회사명C' },
|
||||
];
|
||||
|
||||
// 날짜 포맷
|
||||
function formatDate(dateStr: string | null): string {
|
||||
if (!dateStr) return '-';
|
||||
return dateStr.split('T')[0];
|
||||
}
|
||||
|
||||
interface StructureReviewListClientProps {
|
||||
initialData?: StructureReview[];
|
||||
initialStats?: StructureReviewStats;
|
||||
@@ -75,7 +70,10 @@ export default function StructureReviewListClient({
|
||||
initialData = [],
|
||||
initialStats,
|
||||
}: StructureReviewListClientProps) {
|
||||
const router = useRouter();
|
||||
// ===== 공통 핸들러 Hook =====
|
||||
const { handleRowClick, handleEdit, router } = useListHandlers<StructureReview>(
|
||||
'construction/order/structure-review'
|
||||
);
|
||||
|
||||
// ===== 외부 상태 (UniversalListPage 외부에서 관리) =====
|
||||
const [activeStatTab, setActiveStatTab] = useState<'all' | 'pending' | 'completed'>('all');
|
||||
@@ -95,21 +93,7 @@ export default function StructureReviewListClient({
|
||||
}
|
||||
}, [initialStats]);
|
||||
|
||||
// ===== 핸들러 =====
|
||||
const handleRowClick = useCallback(
|
||||
(item: StructureReview) => {
|
||||
router.push(`/ko/construction/order/structure-review/${item.id}?mode=view`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(item: StructureReview) => {
|
||||
router.push(`/ko/construction/order/structure-review/${item.id}?mode=edit`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
// ===== 추가 핸들러 =====
|
||||
const handleCreate = useCallback(() => {
|
||||
router.push('/ko/construction/order/structure-review?mode=new');
|
||||
}, [router]);
|
||||
|
||||
@@ -42,6 +42,8 @@ import {
|
||||
deleteUtility,
|
||||
deleteUtilities,
|
||||
} from './actions';
|
||||
import { formatNumber } from '@/utils/formatAmount';
|
||||
import { formatDate } from '@/utils/date';
|
||||
|
||||
// 테이블 컬럼 정의
|
||||
const tableColumns = [
|
||||
@@ -59,17 +61,6 @@ const tableColumns = [
|
||||
{ key: 'actions', label: '작업', className: 'w-[100px] text-center' },
|
||||
];
|
||||
|
||||
// 날짜 포맷
|
||||
function formatDate(dateStr: string | null): string {
|
||||
if (!dateStr) return '-';
|
||||
return dateStr.split('T')[0];
|
||||
}
|
||||
|
||||
// 금액 포맷
|
||||
function formatAmount(amount: number): string {
|
||||
return amount.toLocaleString('ko-KR') + '원';
|
||||
}
|
||||
|
||||
interface UtilityManagementListClientProps {
|
||||
initialData?: Utility[];
|
||||
initialStats?: UtilityStats;
|
||||
@@ -350,7 +341,7 @@ export default function UtilityManagementListClient({
|
||||
<TableCell>{item.constructionPM}</TableCell>
|
||||
<TableCell>{item.utilityType}</TableCell>
|
||||
<TableCell>{formatDate(item.scheduledDate)}</TableCell>
|
||||
<TableCell className="text-right">{formatAmount(item.amount)}</TableCell>
|
||||
<TableCell className="text-right">{formatNumber(item.amount)}원</TableCell>
|
||||
<TableCell>{item.workTeamLeader}</TableCell>
|
||||
<TableCell>{formatDate(item.constructionStartDate)}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
@@ -391,7 +382,7 @@ export default function UtilityManagementListClient({
|
||||
details={[
|
||||
{ label: '거래처', value: item.partnerName },
|
||||
{ label: '공사PM', value: item.constructionPM },
|
||||
{ label: '금액', value: formatAmount(item.amount) },
|
||||
{ label: '금액', value: `${formatNumber(item.amount)}원` },
|
||||
]}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
* - 등록/삭제 버튼 없음 (조회 전용)
|
||||
*/
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { Users, Eye, FileText, Clock, CheckCircle } from 'lucide-react';
|
||||
import { useListHandlers } from '@/hooks/useListHandlers';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { TableCell, TableRow } from '@/components/ui/table';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
@@ -77,7 +77,8 @@ export default function WorkerStatusListClient({
|
||||
initialData = [],
|
||||
initialStats,
|
||||
}: WorkerStatusListClientProps) {
|
||||
const router = useRouter();
|
||||
// ===== 공통 핸들러 Hook =====
|
||||
const { handleRowClick } = useListHandlers<WorkerStatus>('construction/project/worker-status');
|
||||
|
||||
// ===== 외부 상태 (UniversalListPage 외부에서 관리) =====
|
||||
const [activeStatTab, setActiveStatTab] = useState<'all' | 'all_contract' | 'pending' | 'completed'>('all');
|
||||
@@ -97,21 +98,6 @@ export default function WorkerStatusListClient({
|
||||
}
|
||||
}, [initialStats]);
|
||||
|
||||
// ===== 핸들러 =====
|
||||
const handleRowClick = useCallback(
|
||||
(item: WorkerStatus) => {
|
||||
router.push(`/ko/construction/project/worker-status/${item.id}?mode=view`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const handleViewDetail = useCallback(
|
||||
(item: WorkerStatus) => {
|
||||
router.push(`/ko/construction/project/worker-status/${item.id}?mode=view`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
// ===== UniversalListPage Config =====
|
||||
const config: UniversalListConfig<WorkerStatus> = useMemo(
|
||||
() => ({
|
||||
@@ -387,7 +373,7 @@ export default function WorkerStatusListClient({
|
||||
className="h-8 w-8"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleViewDetail(item);
|
||||
handleRowClick(item);
|
||||
}}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
@@ -422,7 +408,7 @@ export default function WorkerStatusListClient({
|
||||
/>
|
||||
),
|
||||
}),
|
||||
[startDate, endDate, activeStatTab, stats, handleRowClick, handleViewDetail, searchQuery]
|
||||
[startDate, endDate, activeStatTab, stats, handleRowClick, searchQuery]
|
||||
);
|
||||
|
||||
return <UniversalListPage config={config} initialData={initialData} onSearchChange={setSearchQuery} />;
|
||||
|
||||
@@ -10,7 +10,10 @@ interface DayCellProps {
|
||||
isCurrentMonth: boolean;
|
||||
isToday: boolean;
|
||||
isSelected: boolean;
|
||||
isWeekend: boolean;
|
||||
isSaturday: boolean;
|
||||
isSunday: boolean;
|
||||
isHoliday?: boolean;
|
||||
holidayName?: string | null;
|
||||
isPast: boolean;
|
||||
badge?: DayBadge;
|
||||
onClick: (date: Date) => void;
|
||||
@@ -28,18 +31,24 @@ export function DayCell({
|
||||
isCurrentMonth,
|
||||
isToday,
|
||||
isSelected,
|
||||
isWeekend,
|
||||
isSaturday,
|
||||
isSunday,
|
||||
isHoliday = false,
|
||||
holidayName,
|
||||
isPast,
|
||||
badge,
|
||||
onClick,
|
||||
}: DayCellProps) {
|
||||
const dayNumber = format(date, 'd');
|
||||
const badgeColor = badge?.color || 'red';
|
||||
const isRedDay = isSunday || isHoliday; // 일요일 또는 공휴일 → 빨간색
|
||||
const isBlueDay = isSaturday && !isHoliday; // 토요일(공휴일 아닌 경우) → 파란색
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onClick(date)}
|
||||
title={holidayName || undefined}
|
||||
className={cn(
|
||||
'relative w-full h-8 flex items-center justify-center',
|
||||
'text-sm font-medium transition-colors rounded-md',
|
||||
@@ -48,10 +57,14 @@ export function DayCell({
|
||||
isCurrentMonth ? 'text-foreground' : 'text-muted-foreground/40',
|
||||
// 지난 일자 - 더 명확한 회색 (현재 월에서만)
|
||||
isPast && isCurrentMonth && !isToday && !isSelected && 'text-gray-400',
|
||||
// 주말 색상 (지난 일자가 아닌 경우만)
|
||||
isWeekend && isCurrentMonth && !isPast && 'text-red-500',
|
||||
// 지난 주말 - 연한 색상
|
||||
isWeekend && isCurrentMonth && isPast && !isToday && !isSelected && 'text-red-300',
|
||||
// 일요일/공휴일 색상 (지난 일자가 아닌 경우만) → 빨간색
|
||||
isRedDay && isCurrentMonth && !isPast && 'text-red-500',
|
||||
// 토요일 색상 (지난 일자가 아닌 경우만) → 파란색
|
||||
isBlueDay && isCurrentMonth && !isPast && 'text-blue-500',
|
||||
// 지난 일요일/공휴일 - 연한 색상
|
||||
isRedDay && isCurrentMonth && isPast && !isToday && !isSelected && 'text-red-300',
|
||||
// 지난 토요일 - 연한 파란색
|
||||
isBlueDay && isCurrentMonth && isPast && !isToday && !isSelected && 'text-blue-300',
|
||||
// 오늘 - 굵은 글씨 (외곽선은 부모 셀에 적용) - 보라색
|
||||
isToday && !isSelected && 'font-bold text-purple-600',
|
||||
// 선택됨 - 배경색 하이라이트 - 보라색
|
||||
|
||||
@@ -19,7 +19,8 @@ import {
|
||||
getEventsForDate,
|
||||
getBadgeForDate,
|
||||
} from './utils';
|
||||
import { getDay } from 'date-fns';
|
||||
import { getDay, format } from 'date-fns';
|
||||
import { isHoliday, getHolidayName } from '@/constants/calendarEvents';
|
||||
|
||||
/**
|
||||
* 월간 뷰 컴포넌트
|
||||
@@ -65,9 +66,14 @@ export function MonthView({
|
||||
{/* 요일 헤더 */}
|
||||
<div className="grid grid-cols-7 mb-1">
|
||||
{weekdayHeaders.map((day, index) => {
|
||||
const isWeekend =
|
||||
(weekStartsOn === 0 && (index === 0 || index === 6)) ||
|
||||
(weekStartsOn === 1 && (index === 5 || index === 6));
|
||||
// 일요일: weekStartsOn=0이면 index 0, weekStartsOn=1이면 index 6
|
||||
const isSunday =
|
||||
(weekStartsOn === 0 && index === 0) ||
|
||||
(weekStartsOn === 1 && index === 6);
|
||||
// 토요일: weekStartsOn=0이면 index 6, weekStartsOn=1이면 index 5
|
||||
const isSaturday =
|
||||
(weekStartsOn === 0 && index === 6) ||
|
||||
(weekStartsOn === 1 && index === 5);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -75,7 +81,8 @@ export function MonthView({
|
||||
className={cn(
|
||||
'text-center text-xs font-semibold py-2',
|
||||
'text-muted-foreground',
|
||||
isWeekend && 'text-red-400'
|
||||
isSunday && 'text-red-400',
|
||||
isSaturday && 'text-blue-400'
|
||||
)}
|
||||
>
|
||||
{day}
|
||||
@@ -168,7 +175,11 @@ function WeekRow({
|
||||
{/* 날짜 셀들 */}
|
||||
{weekDays.map((date, colIndex) => {
|
||||
const dayOfWeek = getDay(date);
|
||||
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
|
||||
const isSaturday = dayOfWeek === 6;
|
||||
const isSunday = dayOfWeek === 0;
|
||||
const dateStr = format(date, 'yyyy-MM-dd');
|
||||
const isHolidayDate = isHoliday(dateStr);
|
||||
const holidayName = getHolidayName(dateStr);
|
||||
const badge = getBadgeForDate(badges, date);
|
||||
const hiddenCount = hiddenEventCounts.get(colIndex) || 0;
|
||||
const dayEvents = getEventsForDate(events, date);
|
||||
@@ -201,7 +212,10 @@ function WeekRow({
|
||||
isCurrentMonth={isCurrMonth}
|
||||
isToday={isToday}
|
||||
isSelected={isSelected}
|
||||
isWeekend={isWeekend}
|
||||
isSaturday={isSaturday}
|
||||
isSunday={isSunday}
|
||||
isHoliday={isHolidayDate}
|
||||
holidayName={holidayName}
|
||||
isPast={isPast}
|
||||
badge={badge}
|
||||
onClick={onDateClick}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from './utils';
|
||||
import { format, getDay } from 'date-fns';
|
||||
import { ko } from 'date-fns/locale';
|
||||
import { isHoliday, getHolidayName } from '@/constants/calendarEvents';
|
||||
|
||||
/**
|
||||
* 주간 뷰 컴포넌트
|
||||
@@ -81,7 +82,13 @@ export function WeekView({
|
||||
<div className="grid grid-cols-7 mb-1">
|
||||
{weekDays.map((date) => {
|
||||
const dayOfWeek = getDay(date);
|
||||
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
|
||||
const isSaturday = dayOfWeek === 6;
|
||||
const isSunday = dayOfWeek === 0;
|
||||
const dateStr = format(date, 'yyyy-MM-dd');
|
||||
const isHolidayDate = isHoliday(dateStr);
|
||||
const holidayName = getHolidayName(dateStr);
|
||||
const isRedDay = isSunday || isHolidayDate; // 일요일 또는 공휴일 → 빨간색
|
||||
const isBlueDay = isSaturday && !isHolidayDate; // 토요일(공휴일 아닌 경우) → 파란색
|
||||
const isToday = checkIsToday(date);
|
||||
|
||||
return (
|
||||
@@ -91,11 +98,13 @@ export function WeekView({
|
||||
'text-center py-2',
|
||||
'flex flex-col items-center'
|
||||
)}
|
||||
title={holidayName || undefined}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'text-xs font-semibold text-muted-foreground',
|
||||
isWeekend && 'text-red-400'
|
||||
isRedDay && 'text-red-400',
|
||||
isBlueDay && 'text-blue-400'
|
||||
)}
|
||||
>
|
||||
{format(date, 'E', { locale: ko })}
|
||||
@@ -103,7 +112,8 @@ export function WeekView({
|
||||
<span
|
||||
className={cn(
|
||||
'text-lg font-bold mt-0.5',
|
||||
isWeekend && 'text-red-500',
|
||||
isRedDay && 'text-red-500',
|
||||
isBlueDay && 'text-blue-500',
|
||||
isToday && 'bg-primary text-primary-foreground rounded-full w-8 h-8 flex items-center justify-center'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -3,14 +3,36 @@
|
||||
import * as React from "react";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { DayPicker } from "react-day-picker";
|
||||
import { format, getDay } from "date-fns";
|
||||
|
||||
import { cn } from "./utils";
|
||||
import { buttonVariants } from "./button";
|
||||
import { isHoliday } from "@/constants/calendarEvents";
|
||||
|
||||
// 토요일 체크 함수 (react-day-picker modifier용)
|
||||
const saturdayMatcher = (date: Date) => {
|
||||
const dateStr = format(date, "yyyy-MM-dd");
|
||||
// 토요일이면서 공휴일이 아닌 경우만
|
||||
return getDay(date) === 6 && !isHoliday(dateStr);
|
||||
};
|
||||
|
||||
// 일요일 체크 함수 (react-day-picker modifier용)
|
||||
const sundayMatcher = (date: Date) => {
|
||||
return getDay(date) === 0;
|
||||
};
|
||||
|
||||
// 공휴일 체크 함수 (react-day-picker modifier용)
|
||||
const holidayMatcher = (date: Date) => {
|
||||
const dateStr = format(date, "yyyy-MM-dd");
|
||||
return isHoliday(dateStr);
|
||||
};
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
classNames,
|
||||
showOutsideDays = true,
|
||||
modifiers,
|
||||
modifiersClassNames,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DayPicker>) {
|
||||
// Hydration 불일치 방지: 클라이언트에서만 렌더링
|
||||
@@ -27,8 +49,25 @@ function Calendar({
|
||||
);
|
||||
}
|
||||
|
||||
// 공휴일/토요일/일요일 modifiers 병합
|
||||
const mergedModifiers = {
|
||||
saturday: saturdayMatcher,
|
||||
sunday: sundayMatcher,
|
||||
holiday: holidayMatcher,
|
||||
...modifiers,
|
||||
};
|
||||
|
||||
const mergedModifiersClassNames = {
|
||||
saturday: "text-blue-500 font-semibold",
|
||||
sunday: "text-red-500 font-semibold",
|
||||
holiday: "text-red-500 font-semibold",
|
||||
...modifiersClassNames,
|
||||
};
|
||||
|
||||
return (
|
||||
<DayPicker
|
||||
modifiers={mergedModifiers}
|
||||
modifiersClassNames={mergedModifiersClassNames}
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn("p-0 w-full", className)}
|
||||
classNames={{
|
||||
|
||||
102
src/constants/calendarEvents.ts
Normal file
102
src/constants/calendarEvents.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* 달력 이벤트 상수 (공휴일, 세금 마감일)
|
||||
* - 매년 연말에 다음 연도 데이터 추가 필요
|
||||
* - 추후 API 연동 시 이 파일을 API 호출로 대체
|
||||
*/
|
||||
|
||||
export type CalendarEventType = 'holiday' | 'tax';
|
||||
|
||||
export interface CalendarEvent {
|
||||
date: string;
|
||||
name: string;
|
||||
type: CalendarEventType;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 2026년 공휴일
|
||||
// ============================================
|
||||
export const HOLIDAYS_2026: CalendarEvent[] = [
|
||||
{ date: '2026-01-01', name: '신정', type: 'holiday' },
|
||||
{ date: '2026-02-16', name: '설날 연휴', type: 'holiday' },
|
||||
{ date: '2026-02-17', name: '설날', type: 'holiday' },
|
||||
{ date: '2026-02-18', name: '설날 연휴', type: 'holiday' },
|
||||
{ date: '2026-03-01', name: '삼일절', type: 'holiday' },
|
||||
{ date: '2026-03-02', name: '대체공휴일(삼일절)', type: 'holiday' }, // 3/1이 일요일
|
||||
{ date: '2026-05-05', name: '어린이날', type: 'holiday' },
|
||||
{ date: '2026-05-24', name: '부처님오신날', type: 'holiday' },
|
||||
{ date: '2026-06-06', name: '현충일', type: 'holiday' },
|
||||
{ date: '2026-08-15', name: '광복절', type: 'holiday' },
|
||||
{ date: '2026-08-17', name: '대체공휴일(광복절)', type: 'holiday' }, // 8/15가 토요일
|
||||
{ date: '2026-09-24', name: '추석 연휴', type: 'holiday' },
|
||||
{ date: '2026-09-25', name: '추석', type: 'holiday' },
|
||||
{ date: '2026-09-26', name: '추석 연휴', type: 'holiday' },
|
||||
{ date: '2026-10-03', name: '개천절', type: 'holiday' },
|
||||
{ date: '2026-10-09', name: '한글날', type: 'holiday' },
|
||||
{ date: '2026-12-25', name: '성탄절', type: 'holiday' },
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// 2026년 주요 세금 마감일
|
||||
// ============================================
|
||||
export const TAX_DEADLINES_2026: CalendarEvent[] = [
|
||||
{ date: '2026-01-26', name: '부가세 2기 확정신고', type: 'tax' }, // 1/25가 일요일 → 1/26
|
||||
{ date: '2026-03-31', name: '법인세 신고', type: 'tax' },
|
||||
{ date: '2026-06-01', name: '종합소득세 신고', type: 'tax' }, // 5/31이 일요일 → 6/1
|
||||
{ date: '2026-07-27', name: '부가세 1기 확정신고', type: 'tax' }, // 7/25가 토요일 → 7/27
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// 연도별 통합 데이터
|
||||
// ============================================
|
||||
export const CALENDAR_EVENTS: Record<number, CalendarEvent[]> = {
|
||||
2026: [...HOLIDAYS_2026, ...TAX_DEADLINES_2026],
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// 유틸리티 함수
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 특정 날짜의 모든 이벤트 조회
|
||||
*/
|
||||
export function getEventsForDate(date: string): CalendarEvent[] {
|
||||
const year = parseInt(date.substring(0, 4), 10);
|
||||
const events = CALENDAR_EVENTS[year] || [];
|
||||
return events.filter((e) => e.date === date);
|
||||
}
|
||||
|
||||
/**
|
||||
* 공휴일 여부 확인
|
||||
*/
|
||||
export function isHoliday(date: string): boolean {
|
||||
const year = parseInt(date.substring(0, 4), 10);
|
||||
const events = CALENDAR_EVENTS[year] || [];
|
||||
return events.some((e) => e.date === date && e.type === 'holiday');
|
||||
}
|
||||
|
||||
/**
|
||||
* 세금 마감일 여부 확인
|
||||
*/
|
||||
export function isTaxDeadline(date: string): boolean {
|
||||
const year = parseInt(date.substring(0, 4), 10);
|
||||
const events = CALENDAR_EVENTS[year] || [];
|
||||
return events.some((e) => e.date === date && e.type === 'tax');
|
||||
}
|
||||
|
||||
/**
|
||||
* 공휴일명 조회
|
||||
*/
|
||||
export function getHolidayName(date: string): string | null {
|
||||
const events = getEventsForDate(date);
|
||||
const holiday = events.find((e) => e.type === 'holiday');
|
||||
return holiday?.name || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 세금 마감일명 조회
|
||||
*/
|
||||
export function getTaxDeadlineName(date: string): string | null {
|
||||
const events = getEventsForDate(date);
|
||||
const tax = events.find((e) => e.type === 'tax');
|
||||
return tax?.name || null;
|
||||
}
|
||||
40
src/hooks/useListHandlers.ts
Normal file
40
src/hooks/useListHandlers.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
/**
|
||||
* 리스트 페이지 공통 핸들러 Hook
|
||||
*
|
||||
* 리스트 페이지에서 반복되는 라우팅 핸들러를 공통화
|
||||
* - handleRowClick: 상세 보기 (mode=view)
|
||||
* - handleEdit: 수정 모드 (mode=edit)
|
||||
*
|
||||
* @example
|
||||
* const { handleRowClick, handleEdit } = useListHandlers<Contract>(
|
||||
* 'construction/project/contract'
|
||||
* );
|
||||
*
|
||||
* // 자동으로 /ko/ 접두사 추가
|
||||
* // handleRowClick(item) → /ko/construction/project/contract/{id}?mode=view
|
||||
* // handleEdit(item) → /ko/construction/project/contract/{id}?mode=edit
|
||||
*/
|
||||
export function useListHandlers<T extends { id: string }>(basePath: string) {
|
||||
const router = useRouter();
|
||||
|
||||
const handleRowClick = useCallback(
|
||||
(item: T) => {
|
||||
router.push(`/ko/${basePath}/${item.id}?mode=view`);
|
||||
},
|
||||
[router, basePath]
|
||||
);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(item: T) => {
|
||||
router.push(`/ko/${basePath}/${item.id}?mode=edit`);
|
||||
},
|
||||
[router, basePath]
|
||||
);
|
||||
|
||||
return { handleRowClick, handleEdit, router };
|
||||
}
|
||||
@@ -56,4 +56,29 @@ export function formatDateForInput(dateStr: string | null | undefined): string {
|
||||
|
||||
// 로컬 시간대 기준 YYYY-MM-DD 형식으로 변환
|
||||
return getLocalDateString(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜 표시용 포맷 (YYYY-MM-DD)
|
||||
* @example formatDate("2025-01-06T00:00:00.000Z") // "2025-01-06"
|
||||
* @example formatDate(null) // "-"
|
||||
*/
|
||||
export function formatDate(dateStr: string | null | undefined): string {
|
||||
if (!dateStr) return '-';
|
||||
// ISO string에서 날짜 부분만 추출, 또는 이미 YYYY-MM-DD면 그대로
|
||||
return dateStr.split('T')[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜 범위 포맷 ("시작 ~ 종료")
|
||||
* @example formatDateRange("2025-01-01", "2025-12-31") // "2025-01-01 ~ 2025-12-31"
|
||||
*/
|
||||
export function formatDateRange(
|
||||
startDate: string | null | undefined,
|
||||
endDate: string | null | undefined
|
||||
): string {
|
||||
const start = formatDate(startDate);
|
||||
const end = formatDate(endDate);
|
||||
if (start === '-' && end === '-') return '-';
|
||||
return `${start} ~ ${end}`;
|
||||
}
|
||||
@@ -5,6 +5,16 @@
|
||||
* 1만원 이상: "1,000만원"
|
||||
*/
|
||||
|
||||
/**
|
||||
* 단순 숫자 포맷 (천단위 콤마만, 단위 없음)
|
||||
* @example formatNumber(1234567) // "1,234,567"
|
||||
* @example formatNumber(null) // "-"
|
||||
*/
|
||||
export function formatNumber(value: number | null | undefined): string {
|
||||
if (value == null || isNaN(value)) return '-';
|
||||
return new Intl.NumberFormat('ko-KR').format(value);
|
||||
}
|
||||
|
||||
export function formatAmount(amount: number): string {
|
||||
// NaN, undefined, null 처리
|
||||
if (amount == null || isNaN(amount)) {
|
||||
|
||||
Reference in New Issue
Block a user