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:
유병철
2026-02-25 22:33:17 +09:00
parent dc7e152311
commit e094c5ae49
15 changed files with 184 additions and 166 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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]);
// ===== 엑셀 컬럼 정의 =====

View File

@@ -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) => (

View File

@@ -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>
);

View File

@@ -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)}

View File

@@ -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"

View File

@@ -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>
{/* 트리 아이템 목록 */}

View File

@@ -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>
{/* 하위 부서 (재귀) */}

View File

@@ -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" />

View File

@@ -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>

View File

@@ -51,6 +51,7 @@ import {
} from './types';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
export function SalaryManagement() {
const { canExport } = usePermission();
// ===== 상태 관리 =====

View File

@@ -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" />

View File

@@ -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" />

View File

@@ -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]);
// ===== 테이블 컬럼 (탭별) =====