feat: [HR] 인사관리 전반 UI 개선
- 근태관리 다이얼로그 개선 (AttendanceInfoDialog, ReasonInfoDialog) - 카드관리 상세 페이지 개선 (CardDetail) - 부서관리 트리 컴포넌트 개선 (DepartmentToolbar, DepartmentTreeItem) - 직원관리 폼 개선 (EmployeeForm) - 급여/휴가 관리 UI 개선 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -94,20 +94,20 @@ export function AttendanceInfoDialog({
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogContent className="max-w-md max-h-[78vh] !grid !grid-rows-[auto_1fr_auto] !overflow-hidden p-4 sm:p-6">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-3 py-2 overflow-y-auto min-h-0">
|
||||
{/* 대상 선택 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-medium min-w-[80px]">대상</Label>
|
||||
<div className="flex items-center gap-3">
|
||||
<Label className="text-sm font-medium shrink-0 w-[90px]">대상</Label>
|
||||
<Select
|
||||
value={formData.employeeId}
|
||||
onValueChange={(value) => handleChange('employeeId', value)}
|
||||
>
|
||||
<SelectTrigger className="min-w-[200px] w-auto">
|
||||
<SelectTrigger className="flex-1 min-w-0">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -121,25 +121,25 @@ export function AttendanceInfoDialog({
|
||||
</div>
|
||||
|
||||
{/* 기준일 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-medium min-w-[80px]">기준일</Label>
|
||||
<div className="flex items-center gap-3">
|
||||
<Label className="text-sm font-medium shrink-0 w-[90px]">기준일</Label>
|
||||
<DatePicker
|
||||
value={formData.baseDate}
|
||||
onChange={(date) => handleChange('baseDate', date)}
|
||||
className="w-[200px]"
|
||||
className="flex-1 min-w-0"
|
||||
align="end"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 출근 시간 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-medium min-w-[80px]">출근 시간</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<Label className="text-sm font-medium shrink-0 w-[90px]">출근 시간</Label>
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<Select
|
||||
value={formData.checkInHour}
|
||||
onValueChange={(value) => handleChange('checkInHour', value)}
|
||||
>
|
||||
<SelectTrigger className="min-w-[90px] w-auto">
|
||||
<SelectTrigger className="flex-1 min-w-0">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -154,7 +154,7 @@ export function AttendanceInfoDialog({
|
||||
value={formData.checkInMinute}
|
||||
onValueChange={(value) => handleChange('checkInMinute', value)}
|
||||
>
|
||||
<SelectTrigger className="min-w-[90px] w-auto">
|
||||
<SelectTrigger className="flex-1 min-w-0">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -169,14 +169,14 @@ export function AttendanceInfoDialog({
|
||||
</div>
|
||||
|
||||
{/* 퇴근 시간 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-medium min-w-[80px]">퇴근 시간</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<Label className="text-sm font-medium shrink-0 w-[90px]">퇴근 시간</Label>
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<Select
|
||||
value={formData.checkOutHour}
|
||||
onValueChange={(value) => handleChange('checkOutHour', value)}
|
||||
>
|
||||
<SelectTrigger className="min-w-[90px] w-auto">
|
||||
<SelectTrigger className="flex-1 min-w-0">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -191,7 +191,7 @@ export function AttendanceInfoDialog({
|
||||
value={formData.checkOutMinute}
|
||||
onValueChange={(value) => handleChange('checkOutMinute', value)}
|
||||
>
|
||||
<SelectTrigger className="min-w-[90px] w-auto">
|
||||
<SelectTrigger className="flex-1 min-w-0">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -206,14 +206,14 @@ export function AttendanceInfoDialog({
|
||||
</div>
|
||||
|
||||
{/* 야간 연장 시간 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-medium min-w-[80px]">야간 연장 시간</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<Label className="text-sm font-medium shrink-0 w-[90px]">야간 연장</Label>
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<Select
|
||||
value={formData.nightOvertimeHours}
|
||||
onValueChange={(value) => handleChange('nightOvertimeHours', value)}
|
||||
>
|
||||
<SelectTrigger className="min-w-[90px] w-auto">
|
||||
<SelectTrigger className="flex-1 min-w-0">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -228,7 +228,7 @@ export function AttendanceInfoDialog({
|
||||
value={formData.nightOvertimeMinutes}
|
||||
onValueChange={(value) => handleChange('nightOvertimeMinutes', value)}
|
||||
>
|
||||
<SelectTrigger className="min-w-[90px] w-auto">
|
||||
<SelectTrigger className="flex-1 min-w-0">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -243,14 +243,14 @@ export function AttendanceInfoDialog({
|
||||
</div>
|
||||
|
||||
{/* 주말 연장 시간 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-medium min-w-[80px]">주말 연장 시간</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<Label className="text-sm font-medium shrink-0 w-[90px]">주말 연장</Label>
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<Select
|
||||
value={formData.weekendOvertimeHours}
|
||||
onValueChange={(value) => handleChange('weekendOvertimeHours', value)}
|
||||
>
|
||||
<SelectTrigger className="min-w-[90px] w-auto">
|
||||
<SelectTrigger className="flex-1 min-w-0">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -265,7 +265,7 @@ export function AttendanceInfoDialog({
|
||||
value={formData.weekendOvertimeMinutes}
|
||||
onValueChange={(value) => handleChange('weekendOvertimeMinutes', value)}
|
||||
>
|
||||
<SelectTrigger className="min-w-[90px] w-auto">
|
||||
<SelectTrigger className="flex-1 min-w-0">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -60,20 +60,20 @@ export function ReasonInfoDialog({
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogContent className="max-w-md p-4 sm:p-6">
|
||||
<DialogHeader>
|
||||
<DialogTitle>사유 정보</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-3 py-2">
|
||||
{/* 대상 선택 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-medium min-w-[80px]">대상</Label>
|
||||
<div className="flex items-center gap-3">
|
||||
<Label className="text-sm font-medium shrink-0 w-[60px]">대상</Label>
|
||||
<Select
|
||||
value={formData.employeeId}
|
||||
onValueChange={(value) => handleChange('employeeId', value)}
|
||||
>
|
||||
<SelectTrigger className="min-w-[200px] w-auto">
|
||||
<SelectTrigger className="flex-1 min-w-0">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -87,24 +87,24 @@ export function ReasonInfoDialog({
|
||||
</div>
|
||||
|
||||
{/* 기준일 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-medium min-w-[80px]">기준일</Label>
|
||||
<div className="flex items-center gap-3">
|
||||
<Label className="text-sm font-medium shrink-0 w-[60px]">기준일</Label>
|
||||
<DatePicker
|
||||
value={formData.baseDate}
|
||||
onChange={(date) => handleChange('baseDate', date)}
|
||||
className="w-[200px]"
|
||||
className="flex-1 min-w-0"
|
||||
align="end"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 유형 선택 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-medium min-w-[80px]">유형</Label>
|
||||
<div className="flex items-center gap-3">
|
||||
<Label className="text-sm font-medium shrink-0 w-[60px]">유형</Label>
|
||||
<Select
|
||||
value={formData.reasonType}
|
||||
onValueChange={(value) => handleChange('reasonType', value)}
|
||||
>
|
||||
<SelectTrigger className="min-w-[200px] w-auto">
|
||||
<SelectTrigger className="flex-1 min-w-0">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -335,7 +335,7 @@ export function AttendanceManagement() {
|
||||
|
||||
const handleSubmitReason = useCallback((data: ReasonFormData) => {
|
||||
// 문서 작성 화면으로 이동
|
||||
router.push(`/ko/hr/documents/new?type=${data.reasonType}`);
|
||||
router.push(`/ko/hr/documents/new?mode=new&type=${data.reasonType}`);
|
||||
}, [router]);
|
||||
|
||||
// ===== 엑셀 컬럼 정의 =====
|
||||
|
||||
@@ -237,7 +237,7 @@ export function CalendarManagement() {
|
||||
|
||||
{/* 테이블 */}
|
||||
<div className="rounded-md border overflow-x-auto">
|
||||
<Table className="table-fixed">
|
||||
<Table className="min-w-[700px]">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{visibleColumns.map((col) => (
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { CreditCard, Save, Trash2, X, Edit, Loader2, ExternalLink } from 'lucide-react';
|
||||
import { CreditCard, Save, Trash2, X, Edit, Loader2, ExternalLink, ArrowLeft } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { CardNumberInput } from '@/components/ui/card-number-input';
|
||||
import { formatCardNumber } from '@/lib/formatters';
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
deleteCard,
|
||||
getActiveEmployees,
|
||||
} from './actions';
|
||||
import { useMenuStore } from '@/stores/menuStore';
|
||||
|
||||
function formatExpiryDate(value: string): string {
|
||||
if (value && value.length === 4) {
|
||||
@@ -102,6 +103,7 @@ interface CardDetailProps {
|
||||
export function CardDetail({ card, mode: initialMode, isLoading }: CardDetailProps) {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const sidebarCollapsed = useMenuStore((state) => state.sidebarCollapsed);
|
||||
const [mode, setMode] = useState(initialMode);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [employees, setEmployees] = useState<Array<{ id: string; label: string }>>([]);
|
||||
@@ -234,14 +236,14 @@ export function CardDetail({ card, mode: initialMode, isLoading }: CardDetailPro
|
||||
<PageLayout>
|
||||
<PageHeader title="카드 상세" description="카드 정보를 관리합니다" icon={CreditCard} />
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-6 pb-20">
|
||||
{/* 기본 정보 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">기본 정보</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<dl className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<dl className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-muted-foreground">카드사</dt>
|
||||
<dd className="text-sm mt-1">{getCardCompanyLabel(card?.cardCompany || '')}</dd>
|
||||
@@ -306,7 +308,7 @@ export function CardDetail({ card, mode: initialMode, isLoading }: CardDetailPro
|
||||
<CardTitle className="text-base">사용자 정보</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<dl className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<dl className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-muted-foreground">부서</dt>
|
||||
<dd className="text-sm mt-1">{card?.user?.departmentName || '-'}</dd>
|
||||
@@ -346,15 +348,27 @@ export function CardDetail({ card, mode: initialMode, isLoading }: CardDetailPro
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 하단 버튼 */}
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button variant="outline" onClick={() => deleteDialog.single.open(card!.id)} className="text-destructive hover:bg-destructive hover:text-destructive-foreground">
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
삭제
|
||||
</div>
|
||||
|
||||
{/* 하단 버튼 (sticky) */}
|
||||
<div className={`fixed bottom-4 left-4 right-4 px-4 py-3 bg-background/95 backdrop-blur rounded-xl border shadow-lg z-50 transition-all duration-300 md:bottom-6 md:px-6 md:right-[48px] ${sidebarCollapsed ? 'md:left-[156px]' : 'md:left-[316px]'} flex items-center justify-between`}>
|
||||
<Button variant="outline" onClick={handleBack} size="sm" className="md:size-default">
|
||||
<ArrowLeft className="h-4 w-4 md:mr-2" />
|
||||
<span className="hidden md:inline">목록으로</span>
|
||||
</Button>
|
||||
<div className="flex items-center gap-1 md:gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => deleteDialog.single.open(card!.id)}
|
||||
size="sm"
|
||||
className="text-destructive hover:bg-destructive hover:text-destructive-foreground md:size-default"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 md:mr-2" />
|
||||
<span className="hidden md:inline">삭제</span>
|
||||
</Button>
|
||||
<Button onClick={handleEdit}>
|
||||
<Edit className="w-4 h-4 mr-2" />
|
||||
수정
|
||||
<Button onClick={handleEdit} size="sm" className="md:size-default">
|
||||
<Edit className="h-4 w-4 md:mr-2" />
|
||||
<span className="hidden md:inline">수정</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -387,7 +401,7 @@ export function CardDetail({ card, mode: initialMode, isLoading }: CardDetailPro
|
||||
icon={CreditCard}
|
||||
/>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-6 pb-20">
|
||||
{/* 기본 정보 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -395,7 +409,7 @@ export function CardDetail({ card, mode: initialMode, isLoading }: CardDetailPro
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Row 1: 카드사 | 종류 | 카드번호 | 카드명 */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<FormField
|
||||
label="카드사"
|
||||
required
|
||||
@@ -435,7 +449,7 @@ export function CardDetail({ card, mode: initialMode, isLoading }: CardDetailPro
|
||||
</div>
|
||||
|
||||
{/* Row 2: 카드 별칭 | 유효기간 | CSV | 결제일 */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<FormField
|
||||
label="카드 별칭"
|
||||
value={formData.alias}
|
||||
@@ -471,7 +485,7 @@ export function CardDetail({ card, mode: initialMode, isLoading }: CardDetailPro
|
||||
</div>
|
||||
|
||||
{/* Row 3: 총 한도 | 사용 금액 | 잔여한도 | 상태 */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<FormField
|
||||
label="총 한도"
|
||||
type="currency"
|
||||
@@ -518,7 +532,7 @@ export function CardDetail({ card, mode: initialMode, isLoading }: CardDetailPro
|
||||
<CardTitle className="text-base">사용자 정보</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<FormField
|
||||
label="부서 / 사용자 / 직책"
|
||||
type="select"
|
||||
@@ -543,21 +557,22 @@ export function CardDetail({ card, mode: initialMode, isLoading }: CardDetailPro
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 하단 버튼 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Button variant="outline" onClick={handleCancel}>
|
||||
<X className="w-4 h-4 mr-2" />
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} disabled={isSaving}>
|
||||
{isSaving ? (
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
{isCreateMode ? '등록' : '저장'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 하단 버튼 (sticky) */}
|
||||
<div className={`fixed bottom-4 left-4 right-4 px-4 py-3 bg-background/95 backdrop-blur rounded-xl border shadow-lg z-50 transition-all duration-300 md:bottom-6 md:px-6 md:right-[48px] ${sidebarCollapsed ? 'md:left-[156px]' : 'md:left-[316px]'} flex items-center justify-between`}>
|
||||
<Button variant="outline" onClick={handleCancel} size="sm" className="md:size-default">
|
||||
<X className="h-4 w-4 md:mr-2" />
|
||||
<span className="hidden md:inline">취소</span>
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} disabled={isSaving} size="sm" className="md:size-default">
|
||||
{isSaving ? (
|
||||
<Loader2 className="h-4 w-4 md:mr-2 animate-spin" />
|
||||
) : (
|
||||
<Save className="h-4 w-4 md:mr-2" />
|
||||
)}
|
||||
<span className="hidden md:inline">{isCreateMode ? '등록' : '저장'}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
|
||||
@@ -75,7 +75,7 @@ export function CardManagement() {
|
||||
|
||||
// ===== 핸들러 =====
|
||||
const handleRowClick = useCallback((item: CardType) => {
|
||||
router.push(`/ko/hr/card-management/${item.id}`);
|
||||
router.push(`/ko/hr/card-management/${item.id}?mode=view`);
|
||||
}, [router]);
|
||||
|
||||
const handleCreate = useCallback(() => {
|
||||
@@ -263,6 +263,7 @@ export function CardManagement() {
|
||||
{CARD_STATUS_LABELS[item.status]}
|
||||
</Badge>
|
||||
}
|
||||
showCheckbox={false}
|
||||
isSelected={false}
|
||||
onToggleSelection={() => {}}
|
||||
onClick={() => handleRowClick(item)}
|
||||
|
||||
@@ -30,16 +30,13 @@ export function DepartmentToolbar({
|
||||
</div>
|
||||
|
||||
{/* 선택 카운트 + 버튼 */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2 sm:gap-3 flex-wrap">
|
||||
<span className="text-sm text-muted-foreground whitespace-nowrap">
|
||||
전체 {totalCount}건
|
||||
{selectedCount > 0 && (
|
||||
<> / <span className="text-primary font-medium">{selectedCount}개 선택</span></>
|
||||
)}
|
||||
</span>
|
||||
{selectedCount > 0 && (
|
||||
<>
|
||||
<span className="text-sm text-muted-foreground">/</span>
|
||||
<span className="text-sm text-primary font-medium whitespace-nowrap">{selectedCount}개 항목 선택됨</span>
|
||||
</>
|
||||
)}
|
||||
{selectedCount > 0 && (
|
||||
<Button
|
||||
size="sm"
|
||||
|
||||
@@ -37,7 +37,6 @@ export function DepartmentTree({
|
||||
/>
|
||||
<span className="font-medium text-sm">부서명</span>
|
||||
</div>
|
||||
<div className="w-24 text-right font-medium text-sm">작업</div>
|
||||
</div>
|
||||
|
||||
{/* 트리 아이템 목록 */}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { memo } from 'react';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ChevronRight, ChevronDown, Plus, Pencil, Trash2 } from 'lucide-react';
|
||||
import { ChevronRight, ChevronDown, Plus, SquarePen, Trash2 } from 'lucide-react';
|
||||
import type { DepartmentTreeItemProps } from './types';
|
||||
|
||||
/**
|
||||
@@ -33,15 +33,15 @@ export const DepartmentTreeItem = memo(function DepartmentTreeItem({
|
||||
<>
|
||||
{/* 현재 행 */}
|
||||
<div
|
||||
className="group flex items-center px-4 py-3 hover:bg-muted/50 transition-colors"
|
||||
className="group px-4 py-3 hover:bg-muted/50 transition-colors"
|
||||
style={{ paddingLeft: `${paddingLeft + 16}px` }}
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 펼침/접힘 버튼 */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={`h-6 w-6 p-0 ${!hasChildren ? 'invisible' : ''}`}
|
||||
className={`h-6 w-6 p-0 shrink-0 ${!hasChildren ? 'invisible' : ''}`}
|
||||
onClick={() => onToggleExpand(department.id)}
|
||||
>
|
||||
{isExpanded ? (
|
||||
@@ -56,42 +56,44 @@ export const DepartmentTreeItem = memo(function DepartmentTreeItem({
|
||||
checked={isSelected}
|
||||
onCheckedChange={() => onToggleSelect(department.id)}
|
||||
aria-label={`${department.name} 선택`}
|
||||
className="shrink-0"
|
||||
/>
|
||||
|
||||
{/* 부서명 */}
|
||||
<span className="truncate">{department.name}</span>
|
||||
<span className="break-words">{department.name}</span>
|
||||
</div>
|
||||
|
||||
{/* 작업 버튼 (호버 시 표시) */}
|
||||
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 w-7 p-0"
|
||||
onClick={() => onAdd(department.id)}
|
||||
title="하위 부서 추가"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 w-7 p-0"
|
||||
onClick={() => onEdit(department)}
|
||||
title="수정"
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 w-7 p-0 text-destructive hover:text-destructive"
|
||||
onClick={() => onDelete(department)}
|
||||
title="삭제"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{/* 작업 버튼 (선택 시 부서명 아래에 표시, 데스크톱: 호버 시에도 표시) */}
|
||||
{isSelected && (
|
||||
<div className="flex items-center gap-1 mt-2 ml-10">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 w-7 p-0"
|
||||
onClick={() => onAdd(department.id)}
|
||||
title="하위 부서 추가"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={() => onEdit(department)}
|
||||
title="수정"
|
||||
>
|
||||
<SquarePen className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 w-7 p-0 text-destructive hover:text-destructive"
|
||||
onClick={() => onDelete(department)}
|
||||
title="삭제"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 하위 부서 (재귀) */}
|
||||
|
||||
@@ -569,7 +569,7 @@ export function EmployeeForm({
|
||||
</CardHeader>
|
||||
<CardContent className="pt-6 space-y-4">
|
||||
{/* 프로필 사진 + 사원코드/성별 */}
|
||||
<div className="flex gap-6">
|
||||
<div className="flex flex-col md:flex-row gap-6">
|
||||
{/* 프로필 사진 영역 */}
|
||||
{fieldSettings.showProfileImage && (
|
||||
<div className="space-y-2 flex-shrink-0">
|
||||
@@ -783,47 +783,50 @@ export function EmployeeForm({
|
||||
<div className="space-y-2">
|
||||
{formData.departmentPositions.map((dp) => (
|
||||
<div key={dp.id} className="flex items-center gap-2">
|
||||
<Select
|
||||
value={dp.departmentId}
|
||||
onValueChange={(value) => handleDepartmentSelect(dp.id, value)}
|
||||
disabled={isViewMode}
|
||||
>
|
||||
<SelectTrigger className="flex-1" disabled={isViewMode}>
|
||||
<SelectValue placeholder="부서 선택">
|
||||
{dp.departmentName || '부서 선택'}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{departments.map((dept) => (
|
||||
<SelectItem key={dept.id} value={String(dept.id)}>
|
||||
<span style={{ fontFamily: 'monospace' }}>
|
||||
{formatDepartmentName(dept.name, dept.depth)}
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
value={dp.positionId}
|
||||
onValueChange={(value) => handlePositionSelect(dp.id, value)}
|
||||
disabled={isViewMode}
|
||||
>
|
||||
<SelectTrigger className="flex-1" disabled={isViewMode}>
|
||||
<SelectValue placeholder="직책 선택">
|
||||
{dp.positionName || '직책 선택'}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{titles.map((title) => (
|
||||
<SelectItem key={title.id} value={String(title.id)}>{title.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex-1 min-w-0 grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
<Select
|
||||
value={dp.departmentId}
|
||||
onValueChange={(value) => handleDepartmentSelect(dp.id, value)}
|
||||
disabled={isViewMode}
|
||||
>
|
||||
<SelectTrigger disabled={isViewMode}>
|
||||
<SelectValue placeholder="부서 선택">
|
||||
{dp.departmentName || '부서 선택'}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{departments.map((dept) => (
|
||||
<SelectItem key={dept.id} value={String(dept.id)}>
|
||||
<span style={{ fontFamily: 'monospace' }}>
|
||||
{formatDepartmentName(dept.name, dept.depth)}
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
value={dp.positionId}
|
||||
onValueChange={(value) => handlePositionSelect(dp.id, value)}
|
||||
disabled={isViewMode}
|
||||
>
|
||||
<SelectTrigger disabled={isViewMode}>
|
||||
<SelectValue placeholder="직책 선택">
|
||||
{dp.positionName || '직책 선택'}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{titles.map((title) => (
|
||||
<SelectItem key={title.id} value={String(title.id)}>{title.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{!isViewMode && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="shrink-0"
|
||||
onClick={() => handleRemoveDepartmentPosition(dp.id)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 text-destructive" />
|
||||
|
||||
@@ -373,22 +373,22 @@ export function SalaryDetailDialog({
|
||||
|
||||
{/* 지급 합계 */}
|
||||
<div className="bg-primary/5 border-2 border-primary/20 rounded-lg p-4">
|
||||
<div className="grid grid-cols-3 gap-4 text-center">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 sm:gap-4 text-center">
|
||||
<div>
|
||||
<span className="text-sm text-muted-foreground block">급여 총액</span>
|
||||
<span className="text-lg font-semibold text-blue-600">
|
||||
<span className="text-xs sm:text-sm text-muted-foreground block">급여 총액</span>
|
||||
<span className="text-sm sm:text-lg font-semibold text-blue-600">
|
||||
{formatCurrency(salaryDetail.baseSalary + calculateTotalAllowance())}원
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm text-muted-foreground block">공제 총액</span>
|
||||
<span className="text-lg font-semibold text-red-600">
|
||||
<span className="text-xs sm:text-sm text-muted-foreground block">공제 총액</span>
|
||||
<span className="text-sm sm:text-lg font-semibold text-red-600">
|
||||
-{formatCurrency(salaryDetail.totalDeduction)}원
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm text-muted-foreground block">실지급액</span>
|
||||
<span className="text-xl font-bold text-primary">
|
||||
<span className="text-xs sm:text-sm text-muted-foreground block">실지급액</span>
|
||||
<span className="text-base sm:text-xl font-bold text-primary">
|
||||
{formatCurrency(calculateNetPayment())}원
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -51,6 +51,7 @@ import {
|
||||
} from './types';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
|
||||
|
||||
export function SalaryManagement() {
|
||||
const { canExport } = usePermission();
|
||||
// ===== 상태 관리 =====
|
||||
|
||||
@@ -114,7 +114,7 @@ export function VacationGrantDialog({
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="사원을 선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectContent position="popper" className="max-h-[200px]">
|
||||
{isLoadingEmployees ? (
|
||||
<div className="flex items-center justify-center py-2">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
|
||||
@@ -123,7 +123,7 @@ export function VacationRequestDialog({
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="사원을 선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectContent position="popper" className="max-h-[200px]">
|
||||
{isLoadingEmployees ? (
|
||||
<div className="flex items-center justify-center py-2">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
|
||||
@@ -380,9 +380,9 @@ export function VacationManagement() {
|
||||
|
||||
// ===== 탭 옵션 (카드 아래에 표시됨) =====
|
||||
const tabs: TabOption[] = useMemo(() => [
|
||||
{ value: 'usage', label: MAIN_TAB_LABELS.usage, count: usageData.length, color: 'blue' },
|
||||
{ value: 'grant', label: MAIN_TAB_LABELS.grant, count: grantData.length, color: 'green' },
|
||||
{ value: 'request', label: MAIN_TAB_LABELS.request, count: requestData.length, color: 'purple' },
|
||||
{ value: 'usage', label: MAIN_TAB_LABELS.usage, mobileLabel: '사용', count: usageData.length, color: 'blue' },
|
||||
{ value: 'grant', label: MAIN_TAB_LABELS.grant, mobileLabel: '부여', count: grantData.length, color: 'green' },
|
||||
{ value: 'request', label: MAIN_TAB_LABELS.request, mobileLabel: '신청', count: requestData.length, color: 'purple' },
|
||||
], [usageData.length, grantData.length, requestData.length]);
|
||||
|
||||
// ===== 테이블 컬럼 (탭별) =====
|
||||
|
||||
Reference in New Issue
Block a user