Files
sam-react-prod/src/components/settings/LeavePolicyManagement/index.tsx
유병철 19237be4aa refactor: UniversalListPage externalIsLoading 지원 및 스켈레톤 개선
- UniversalListPage에 externalIsLoading prop 추가
- CardTransactionDetailClient DevFill 자동입력 기능 추가
- 여러 컴포넌트 로딩 상태 처리 개선
- skeleton 컴포넌트 확장

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 20:54:16 +09:00

318 lines
11 KiB
TypeScript

'use client';
import { useState, useEffect, useCallback } from 'react';
import { PageLayout } from '@/components/organisms/PageLayout';
import { PageHeader } from '@/components/organisms/PageHeader';
import { CalendarDays, Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { ContentSkeleton } from '@/components/ui/skeleton';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { QuantityInput } from '@/components/ui/quantity-input';
import { Card, CardContent } from '@/components/ui/card';
import { Switch } from '@/components/ui/switch';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { toast } from 'sonner';
import { getLeavePolicy, updateLeavePolicy } from './actions';
import {
type LeavePolicySettings,
type LeaveStandardType,
DEFAULT_LEAVE_POLICY,
MONTH_OPTIONS,
DAY_OPTIONS,
} from './types';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
export function LeavePolicyManagement() {
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
const [settings, setSettings] = useState<LeavePolicySettings>(DEFAULT_LEAVE_POLICY);
// 데이터 로드
const loadData = useCallback(async () => {
setIsLoading(true);
try {
const result = await getLeavePolicy();
if (result.success && result.data) {
setSettings(result.data);
}
} catch (error) {
console.error('Failed to load leave policy:', error);
toast.error('휴가 정책을 불러오는데 실패했습니다.');
} finally {
setIsLoading(false);
}
}, []);
// 초기 로드
useEffect(() => {
loadData();
}, [loadData]);
// 저장
const handleSave = async () => {
setIsSaving(true);
try {
const result = await updateLeavePolicy(settings);
if (result.success) {
toast.success('휴가 정책이 저장되었습니다.');
if (result.data) {
setSettings(result.data);
}
} else {
toast.error(result.error || '저장에 실패했습니다.');
}
} catch (error) {
console.error('Failed to save leave policy:', error);
toast.error('저장 중 오류가 발생했습니다.');
} finally {
setIsSaving(false);
}
};
// 필드 업데이트
const updateField = <K extends keyof LeavePolicySettings>(
field: K,
value: LeavePolicySettings[K]
) => {
setSettings(prev => ({ ...prev, [field]: value }));
};
if (isLoading) {
return (
<PageLayout>
<PageHeader
title="휴가관리"
description="휴가 정책을 관리합니다"
icon={CalendarDays}
/>
<ContentSkeleton type="form" rows={6} />
</PageLayout>
);
}
return (
<PageLayout>
{/* 헤더 */}
<PageHeader
title="휴가관리"
description="휴가 정책을 관리합니다"
icon={CalendarDays}
/>
{/* 저장 버튼 */}
<div className="flex justify-end mb-4">
<Button onClick={handleSave} disabled={isSaving}>
{isSaving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
</Button>
</div>
<div className="space-y-6">
{/* 기본 정보 카드 */}
<Card>
<CardContent className="p-6">
<h3 className="text-lg font-semibold mb-6"> </h3>
<div className="grid grid-cols-2 gap-6">
{/* 기준 셀렉트 */}
<div className="space-y-2">
<Label></Label>
<Select
value={settings.standardType}
onValueChange={(v: LeaveStandardType) => updateField('standardType', v)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="fiscal"></SelectItem>
<SelectItem value="hire"></SelectItem>
</SelectContent>
</Select>
</div>
{/* 기준일 셀렉트 (월/일) */}
<div className="space-y-2">
<Label></Label>
<div className="flex items-center gap-2">
<Select
value={settings.fiscalStartMonth.toString()}
onValueChange={(v) => updateField('fiscalStartMonth', parseInt(v))}
disabled={settings.standardType === 'hire'}
>
<SelectTrigger className="w-24">
<SelectValue />
</SelectTrigger>
<SelectContent>
{MONTH_OPTIONS.map((opt) => (
<SelectItem key={opt.value} value={opt.value.toString()}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={settings.fiscalStartDay.toString()}
onValueChange={(v) => updateField('fiscalStartDay', parseInt(v))}
disabled={settings.standardType === 'hire'}
>
<SelectTrigger className="w-24">
<SelectValue />
</SelectTrigger>
<SelectContent>
{DAY_OPTIONS.map((opt) => (
<SelectItem key={opt.value} value={opt.value.toString()}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
{/* 안내 문구 */}
<div className="mt-6 text-sm text-muted-foreground space-y-1">
<p>! .</p>
<ul className="list-disc list-inside ml-2 space-y-1">
<li> 기준: 사원의 .</li>
<li> 기준: 회사의 .</li>
</ul>
</div>
</CardContent>
</Card>
{/* 연차 설정 카드 */}
<Card>
<CardContent className="p-6">
<h3 className="text-lg font-semibold mb-6"> </h3>
<div className="grid grid-cols-3 gap-6">
{/* 기본 연차 일수 */}
<div className="space-y-2">
<Label> </Label>
<div className="flex items-center gap-2">
<QuantityInput
min={0}
max={100}
value={settings.defaultAnnualLeave}
onChange={(value) => updateField('defaultAnnualLeave', value ?? 0)}
className="w-20"
/>
<span className="text-sm text-muted-foreground"></span>
</div>
</div>
{/* 근속년수당 추가 연차 */}
<div className="space-y-2">
<Label> </Label>
<div className="flex items-center gap-2">
<QuantityInput
min={0}
max={10}
value={settings.additionalLeavePerYear}
onChange={(value) => updateField('additionalLeavePerYear', value ?? 0)}
className="w-20"
/>
<span className="text-sm text-muted-foreground"></span>
</div>
</div>
{/* 최대 연차 일수 */}
<div className="space-y-2">
<Label> </Label>
<div className="flex items-center gap-2">
<QuantityInput
min={0}
max={100}
value={settings.maxAnnualLeave}
onChange={(value) => updateField('maxAnnualLeave', value ?? 0)}
className="w-20"
/>
<span className="text-sm text-muted-foreground"></span>
</div>
</div>
</div>
{/* 안내 문구 */}
<div className="mt-6 text-sm text-muted-foreground">
<p>! , .</p>
</div>
</CardContent>
</Card>
{/* 이월 설정 카드 */}
<Card>
<CardContent className="p-6">
<h3 className="text-lg font-semibold mb-6"> </h3>
<div className="space-y-6">
{/* 이월 허용 여부 */}
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label> </Label>
<p className="text-sm text-muted-foreground">
.
</p>
</div>
<Switch
checked={settings.carryOverEnabled}
onCheckedChange={(checked) => updateField('carryOverEnabled', checked)}
/>
</div>
{settings.carryOverEnabled && (
<div className="grid grid-cols-2 gap-6 pt-4 border-t">
{/* 최대 이월 일수 */}
<div className="space-y-2">
<Label> </Label>
<div className="flex items-center gap-2">
<QuantityInput
min={0}
max={100}
value={settings.carryOverMaxDays}
onChange={(value) => updateField('carryOverMaxDays', value ?? 0)}
className="w-20"
/>
<span className="text-sm text-muted-foreground"></span>
</div>
</div>
{/* 이월 연차 소멸 기간 */}
<div className="space-y-2">
<Label> </Label>
<div className="flex items-center gap-2">
<QuantityInput
min={0}
max={24}
value={settings.carryOverExpiryMonths}
onChange={(value) => updateField('carryOverExpiryMonths', value ?? 0)}
className="w-20"
/>
<span className="text-sm text-muted-foreground"></span>
</div>
</div>
</div>
)}
{/* 안내 문구 */}
{settings.carryOverEnabled && (
<div className="text-sm text-muted-foreground">
<p>! .</p>
</div>
)}
</div>
</CardContent>
</Card>
</div>
</PageLayout>
);
}