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 ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <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> <DialogHeader>
<DialogTitle>{title}</DialogTitle> <DialogTitle>{title}</DialogTitle>
</DialogHeader> </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"> <div className="flex items-center gap-3">
<Label className="text-sm font-medium min-w-[80px]"></Label> <Label className="text-sm font-medium shrink-0 w-[90px]"></Label>
<Select <Select
value={formData.employeeId} value={formData.employeeId}
onValueChange={(value) => handleChange('employeeId', value)} onValueChange={(value) => handleChange('employeeId', value)}
> >
<SelectTrigger className="min-w-[200px] w-auto"> <SelectTrigger className="flex-1 min-w-0">
<SelectValue placeholder="선택" /> <SelectValue placeholder="선택" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -121,25 +121,25 @@ export function AttendanceInfoDialog({
</div> </div>
{/* 기준일 */} {/* 기준일 */}
<div className="flex items-center justify-between"> <div className="flex items-center gap-3">
<Label className="text-sm font-medium min-w-[80px]"></Label> <Label className="text-sm font-medium shrink-0 w-[90px]"></Label>
<DatePicker <DatePicker
value={formData.baseDate} value={formData.baseDate}
onChange={(date) => handleChange('baseDate', date)} onChange={(date) => handleChange('baseDate', date)}
className="w-[200px]" className="flex-1 min-w-0"
align="end" align="end"
/> />
</div> </div>
{/* 출근 시간 */} {/* 출근 시간 */}
<div className="flex items-center justify-between"> <div className="flex items-center gap-3">
<Label className="text-sm font-medium min-w-[80px]"> </Label> <Label className="text-sm font-medium shrink-0 w-[90px]"> </Label>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 flex-1 min-w-0">
<Select <Select
value={formData.checkInHour} value={formData.checkInHour}
onValueChange={(value) => handleChange('checkInHour', value)} onValueChange={(value) => handleChange('checkInHour', value)}
> >
<SelectTrigger className="min-w-[90px] w-auto"> <SelectTrigger className="flex-1 min-w-0">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -154,7 +154,7 @@ export function AttendanceInfoDialog({
value={formData.checkInMinute} value={formData.checkInMinute}
onValueChange={(value) => handleChange('checkInMinute', value)} onValueChange={(value) => handleChange('checkInMinute', value)}
> >
<SelectTrigger className="min-w-[90px] w-auto"> <SelectTrigger className="flex-1 min-w-0">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -169,14 +169,14 @@ export function AttendanceInfoDialog({
</div> </div>
{/* 퇴근 시간 */} {/* 퇴근 시간 */}
<div className="flex items-center justify-between"> <div className="flex items-center gap-3">
<Label className="text-sm font-medium min-w-[80px]"> </Label> <Label className="text-sm font-medium shrink-0 w-[90px]"> </Label>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 flex-1 min-w-0">
<Select <Select
value={formData.checkOutHour} value={formData.checkOutHour}
onValueChange={(value) => handleChange('checkOutHour', value)} onValueChange={(value) => handleChange('checkOutHour', value)}
> >
<SelectTrigger className="min-w-[90px] w-auto"> <SelectTrigger className="flex-1 min-w-0">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -191,7 +191,7 @@ export function AttendanceInfoDialog({
value={formData.checkOutMinute} value={formData.checkOutMinute}
onValueChange={(value) => handleChange('checkOutMinute', value)} onValueChange={(value) => handleChange('checkOutMinute', value)}
> >
<SelectTrigger className="min-w-[90px] w-auto"> <SelectTrigger className="flex-1 min-w-0">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -206,14 +206,14 @@ export function AttendanceInfoDialog({
</div> </div>
{/* 야간 연장 시간 */} {/* 야간 연장 시간 */}
<div className="flex items-center justify-between"> <div className="flex items-center gap-3">
<Label className="text-sm font-medium min-w-[80px]"> </Label> <Label className="text-sm font-medium shrink-0 w-[90px]"> </Label>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 flex-1 min-w-0">
<Select <Select
value={formData.nightOvertimeHours} value={formData.nightOvertimeHours}
onValueChange={(value) => handleChange('nightOvertimeHours', value)} onValueChange={(value) => handleChange('nightOvertimeHours', value)}
> >
<SelectTrigger className="min-w-[90px] w-auto"> <SelectTrigger className="flex-1 min-w-0">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -228,7 +228,7 @@ export function AttendanceInfoDialog({
value={formData.nightOvertimeMinutes} value={formData.nightOvertimeMinutes}
onValueChange={(value) => handleChange('nightOvertimeMinutes', value)} onValueChange={(value) => handleChange('nightOvertimeMinutes', value)}
> >
<SelectTrigger className="min-w-[90px] w-auto"> <SelectTrigger className="flex-1 min-w-0">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -243,14 +243,14 @@ export function AttendanceInfoDialog({
</div> </div>
{/* 주말 연장 시간 */} {/* 주말 연장 시간 */}
<div className="flex items-center justify-between"> <div className="flex items-center gap-3">
<Label className="text-sm font-medium min-w-[80px]"> </Label> <Label className="text-sm font-medium shrink-0 w-[90px]"> </Label>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 flex-1 min-w-0">
<Select <Select
value={formData.weekendOvertimeHours} value={formData.weekendOvertimeHours}
onValueChange={(value) => handleChange('weekendOvertimeHours', value)} onValueChange={(value) => handleChange('weekendOvertimeHours', value)}
> >
<SelectTrigger className="min-w-[90px] w-auto"> <SelectTrigger className="flex-1 min-w-0">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -265,7 +265,7 @@ export function AttendanceInfoDialog({
value={formData.weekendOvertimeMinutes} value={formData.weekendOvertimeMinutes}
onValueChange={(value) => handleChange('weekendOvertimeMinutes', value)} onValueChange={(value) => handleChange('weekendOvertimeMinutes', value)}
> >
<SelectTrigger className="min-w-[90px] w-auto"> <SelectTrigger className="flex-1 min-w-0">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>

View File

@@ -60,20 +60,20 @@ export function ReasonInfoDialog({
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-md"> <DialogContent className="max-w-md p-4 sm:p-6">
<DialogHeader> <DialogHeader>
<DialogTitle> </DialogTitle> <DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<div className="space-y-4 py-4"> <div className="space-y-3 py-2">
{/* 대상 선택 */} {/* 대상 선택 */}
<div className="flex items-center justify-between"> <div className="flex items-center gap-3">
<Label className="text-sm font-medium min-w-[80px]"></Label> <Label className="text-sm font-medium shrink-0 w-[60px]"></Label>
<Select <Select
value={formData.employeeId} value={formData.employeeId}
onValueChange={(value) => handleChange('employeeId', value)} onValueChange={(value) => handleChange('employeeId', value)}
> >
<SelectTrigger className="min-w-[200px] w-auto"> <SelectTrigger className="flex-1 min-w-0">
<SelectValue placeholder="선택" /> <SelectValue placeholder="선택" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -87,24 +87,24 @@ export function ReasonInfoDialog({
</div> </div>
{/* 기준일 */} {/* 기준일 */}
<div className="flex items-center justify-between"> <div className="flex items-center gap-3">
<Label className="text-sm font-medium min-w-[80px]"></Label> <Label className="text-sm font-medium shrink-0 w-[60px]"></Label>
<DatePicker <DatePicker
value={formData.baseDate} value={formData.baseDate}
onChange={(date) => handleChange('baseDate', date)} onChange={(date) => handleChange('baseDate', date)}
className="w-[200px]" className="flex-1 min-w-0"
align="end" align="end"
/> />
</div> </div>
{/* 유형 선택 */} {/* 유형 선택 */}
<div className="flex items-center justify-between"> <div className="flex items-center gap-3">
<Label className="text-sm font-medium min-w-[80px]"></Label> <Label className="text-sm font-medium shrink-0 w-[60px]"></Label>
<Select <Select
value={formData.reasonType} value={formData.reasonType}
onValueChange={(value) => handleChange('reasonType', value)} onValueChange={(value) => handleChange('reasonType', value)}
> >
<SelectTrigger className="min-w-[200px] w-auto"> <SelectTrigger className="flex-1 min-w-0">
<SelectValue placeholder="선택" /> <SelectValue placeholder="선택" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>

View File

@@ -335,7 +335,7 @@ export function AttendanceManagement() {
const handleSubmitReason = useCallback((data: ReasonFormData) => { 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]); }, [router]);
// ===== 엑셀 컬럼 정의 ===== // ===== 엑셀 컬럼 정의 =====

View File

@@ -237,7 +237,7 @@ export function CalendarManagement() {
{/* 테이블 */} {/* 테이블 */}
<div className="rounded-md border overflow-x-auto"> <div className="rounded-md border overflow-x-auto">
<Table className="table-fixed"> <Table className="min-w-[700px]">
<TableHeader> <TableHeader>
<TableRow> <TableRow>
{visibleColumns.map((col) => ( {visibleColumns.map((col) => (

View File

@@ -2,7 +2,7 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { useRouter, useSearchParams } from 'next/navigation'; 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 { Button } from '@/components/ui/button';
import { CardNumberInput } from '@/components/ui/card-number-input'; import { CardNumberInput } from '@/components/ui/card-number-input';
import { formatCardNumber } from '@/lib/formatters'; import { formatCardNumber } from '@/lib/formatters';
@@ -33,6 +33,7 @@ import {
deleteCard, deleteCard,
getActiveEmployees, getActiveEmployees,
} from './actions'; } from './actions';
import { useMenuStore } from '@/stores/menuStore';
function formatExpiryDate(value: string): string { function formatExpiryDate(value: string): string {
if (value && value.length === 4) { if (value && value.length === 4) {
@@ -102,6 +103,7 @@ interface CardDetailProps {
export function CardDetail({ card, mode: initialMode, isLoading }: CardDetailProps) { export function CardDetail({ card, mode: initialMode, isLoading }: CardDetailProps) {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const sidebarCollapsed = useMenuStore((state) => state.sidebarCollapsed);
const [mode, setMode] = useState(initialMode); const [mode, setMode] = useState(initialMode);
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [employees, setEmployees] = useState<Array<{ id: string; label: string }>>([]); const [employees, setEmployees] = useState<Array<{ id: string; label: string }>>([]);
@@ -234,14 +236,14 @@ export function CardDetail({ card, mode: initialMode, isLoading }: CardDetailPro
<PageLayout> <PageLayout>
<PageHeader title="카드 상세" description="카드 정보를 관리합니다" icon={CreditCard} /> <PageHeader title="카드 상세" description="카드 정보를 관리합니다" icon={CreditCard} />
<div className="space-y-6"> <div className="space-y-6 pb-20">
{/* 기본 정보 */} {/* 기본 정보 */}
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="text-base"> </CardTitle> <CardTitle className="text-base"> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <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> <div>
<dt className="text-sm font-medium text-muted-foreground"></dt> <dt className="text-sm font-medium text-muted-foreground"></dt>
<dd className="text-sm mt-1">{getCardCompanyLabel(card?.cardCompany || '')}</dd> <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> <CardTitle className="text-base"> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <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> <div>
<dt className="text-sm font-medium text-muted-foreground"></dt> <dt className="text-sm font-medium text-muted-foreground"></dt>
<dd className="text-sm mt-1">{card?.user?.departmentName || '-'}</dd> <dd className="text-sm mt-1">{card?.user?.departmentName || '-'}</dd>
@@ -346,15 +348,27 @@ export function CardDetail({ card, mode: initialMode, isLoading }: CardDetailPro
</CardContent> </CardContent>
</Card> </Card>
{/* 하단 버튼 */} </div>
<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"> {/* 하단 버튼 (sticky) */}
<Trash2 className="w-4 h-4 mr-2" /> <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>
<Button onClick={handleEdit}> <Button onClick={handleEdit} size="sm" className="md:size-default">
<Edit className="w-4 h-4 mr-2" /> <Edit className="h-4 w-4 md:mr-2" />
<span className="hidden md:inline"></span>
</Button> </Button>
</div> </div>
</div> </div>
@@ -387,7 +401,7 @@ export function CardDetail({ card, mode: initialMode, isLoading }: CardDetailPro
icon={CreditCard} icon={CreditCard}
/> />
<div className="space-y-6"> <div className="space-y-6 pb-20">
{/* 기본 정보 */} {/* 기본 정보 */}
<Card> <Card>
<CardHeader> <CardHeader>
@@ -395,7 +409,7 @@ export function CardDetail({ card, mode: initialMode, isLoading }: CardDetailPro
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{/* Row 1: 카드사 | 종류 | 카드번호 | 카드명 */} {/* 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 <FormField
label="카드사" label="카드사"
required required
@@ -435,7 +449,7 @@ export function CardDetail({ card, mode: initialMode, isLoading }: CardDetailPro
</div> </div>
{/* Row 2: 카드 별칭 | 유효기간 | CSV | 결제일 */} {/* 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 <FormField
label="카드 별칭" label="카드 별칭"
value={formData.alias} value={formData.alias}
@@ -471,7 +485,7 @@ export function CardDetail({ card, mode: initialMode, isLoading }: CardDetailPro
</div> </div>
{/* Row 3: 총 한도 | 사용 금액 | 잔여한도 | 상태 */} {/* 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 <FormField
label="총 한도" label="총 한도"
type="currency" type="currency"
@@ -518,7 +532,7 @@ export function CardDetail({ card, mode: initialMode, isLoading }: CardDetailPro
<CardTitle className="text-base"> </CardTitle> <CardTitle className="text-base"> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <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 <FormField
label="부서 / 사용자 / 직책" label="부서 / 사용자 / 직책"
type="select" type="select"
@@ -543,21 +557,22 @@ export function CardDetail({ card, mode: initialMode, isLoading }: CardDetailPro
</CardContent> </CardContent>
</Card> </Card>
{/* 하단 버튼 */} </div>
<div className="flex items-center justify-between">
<Button variant="outline" onClick={handleCancel}> {/* 하단 버튼 (sticky) */}
<X className="w-4 h-4 mr-2" /> <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">
</Button> <X className="h-4 w-4 md:mr-2" />
<Button onClick={handleSubmit} disabled={isSaving}> <span className="hidden md:inline"></span>
{isSaving ? ( </Button>
<Loader2 className="h-4 w-4 mr-2 animate-spin" /> <Button onClick={handleSubmit} disabled={isSaving} size="sm" className="md:size-default">
) : ( {isSaving ? (
<Save className="w-4 h-4 mr-2" /> <Loader2 className="h-4 w-4 md:mr-2 animate-spin" />
)} ) : (
{isCreateMode ? '등록' : '저장'} <Save className="h-4 w-4 md:mr-2" />
</Button> )}
</div> <span className="hidden md:inline">{isCreateMode ? '등록' : '저장'}</span>
</Button>
</div> </div>
</PageLayout> </PageLayout>
); );

View File

@@ -75,7 +75,7 @@ export function CardManagement() {
// ===== 핸들러 ===== // ===== 핸들러 =====
const handleRowClick = useCallback((item: CardType) => { const handleRowClick = useCallback((item: CardType) => {
router.push(`/ko/hr/card-management/${item.id}`); router.push(`/ko/hr/card-management/${item.id}?mode=view`);
}, [router]); }, [router]);
const handleCreate = useCallback(() => { const handleCreate = useCallback(() => {
@@ -263,6 +263,7 @@ export function CardManagement() {
{CARD_STATUS_LABELS[item.status]} {CARD_STATUS_LABELS[item.status]}
</Badge> </Badge>
} }
showCheckbox={false}
isSelected={false} isSelected={false}
onToggleSelection={() => {}} onToggleSelection={() => {}}
onClick={() => handleRowClick(item)} onClick={() => handleRowClick(item)}

View File

@@ -30,16 +30,13 @@ export function DepartmentToolbar({
</div> </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"> <span className="text-sm text-muted-foreground whitespace-nowrap">
{totalCount} {totalCount}
{selectedCount > 0 && (
<> / <span className="text-primary font-medium">{selectedCount} </span></>
)}
</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 && ( {selectedCount > 0 && (
<Button <Button
size="sm" size="sm"

View File

@@ -37,7 +37,6 @@ export function DepartmentTree({
/> />
<span className="font-medium text-sm"></span> <span className="font-medium text-sm"></span>
</div> </div>
<div className="w-24 text-right font-medium text-sm"></div>
</div> </div>
{/* 트리 아이템 목록 */} {/* 트리 아이템 목록 */}

View File

@@ -3,7 +3,7 @@
import { memo } from 'react'; import { memo } from 'react';
import { Checkbox } from '@/components/ui/checkbox'; import { Checkbox } from '@/components/ui/checkbox';
import { Button } from '@/components/ui/button'; 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'; import type { DepartmentTreeItemProps } from './types';
/** /**
@@ -33,15 +33,15 @@ export const DepartmentTreeItem = memo(function DepartmentTreeItem({
<> <>
{/* 현재 행 */} {/* 현재 행 */}
<div <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` }} 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 <Button
variant="ghost" variant="ghost"
size="sm" 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)} onClick={() => onToggleExpand(department.id)}
> >
{isExpanded ? ( {isExpanded ? (
@@ -56,42 +56,44 @@ export const DepartmentTreeItem = memo(function DepartmentTreeItem({
checked={isSelected} checked={isSelected}
onCheckedChange={() => onToggleSelect(department.id)} onCheckedChange={() => onToggleSelect(department.id)}
aria-label={`${department.name} 선택`} aria-label={`${department.name} 선택`}
className="shrink-0"
/> />
{/* 부서명 */} {/* 부서명 */}
<span className="truncate">{department.name}</span> <span className="break-words">{department.name}</span>
</div> </div>
{/* 작업 버튼 (호버 시 표시) */} {/* 작업 버튼 (선택 시 부서명 아래에 표시, 데스크톱: 호버 시에도 표시) */}
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity"> {isSelected && (
<Button <div className="flex items-center gap-1 mt-2 ml-10">
variant="ghost" <Button
size="sm" variant="ghost"
className="h-7 w-7 p-0" size="sm"
onClick={() => onAdd(department.id)} className="h-7 w-7 p-0"
title="하위 부서 추가" onClick={() => onAdd(department.id)}
> title="하위 부서 추가"
<Plus className="h-4 w-4" /> >
</Button> <Plus className="h-4 w-4" />
<Button </Button>
variant="ghost" <Button
size="sm" size="icon"
className="h-7 w-7 p-0" className="h-7 w-7"
onClick={() => onEdit(department)} onClick={() => onEdit(department)}
title="수정" title="수정"
> >
<Pencil className="h-4 w-4" /> <SquarePen className="h-4 w-4" />
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="h-7 w-7 p-0 text-destructive hover:text-destructive" className="h-7 w-7 p-0 text-destructive hover:text-destructive"
onClick={() => onDelete(department)} onClick={() => onDelete(department)}
title="삭제" title="삭제"
> >
<Trash2 className="h-4 w-4" /> <Trash2 className="h-4 w-4" />
</Button> </Button>
</div> </div>
)}
</div> </div>
{/* 하위 부서 (재귀) */} {/* 하위 부서 (재귀) */}

View File

@@ -569,7 +569,7 @@ export function EmployeeForm({
</CardHeader> </CardHeader>
<CardContent className="pt-6 space-y-4"> <CardContent className="pt-6 space-y-4">
{/* 프로필 사진 + 사원코드/성별 */} {/* 프로필 사진 + 사원코드/성별 */}
<div className="flex gap-6"> <div className="flex flex-col md:flex-row gap-6">
{/* 프로필 사진 영역 */} {/* 프로필 사진 영역 */}
{fieldSettings.showProfileImage && ( {fieldSettings.showProfileImage && (
<div className="space-y-2 flex-shrink-0"> <div className="space-y-2 flex-shrink-0">
@@ -783,47 +783,50 @@ export function EmployeeForm({
<div className="space-y-2"> <div className="space-y-2">
{formData.departmentPositions.map((dp) => ( {formData.departmentPositions.map((dp) => (
<div key={dp.id} className="flex items-center gap-2"> <div key={dp.id} className="flex items-center gap-2">
<Select <div className="flex-1 min-w-0 grid grid-cols-1 sm:grid-cols-2 gap-2">
value={dp.departmentId} <Select
onValueChange={(value) => handleDepartmentSelect(dp.id, value)} value={dp.departmentId}
disabled={isViewMode} onValueChange={(value) => handleDepartmentSelect(dp.id, value)}
> disabled={isViewMode}
<SelectTrigger className="flex-1" disabled={isViewMode}> >
<SelectValue placeholder="부서 선택"> <SelectTrigger disabled={isViewMode}>
{dp.departmentName || '부서 선택'} <SelectValue placeholder="부서 선택">
</SelectValue> {dp.departmentName || '부서 선택'}
</SelectTrigger> </SelectValue>
<SelectContent> </SelectTrigger>
{departments.map((dept) => ( <SelectContent>
<SelectItem key={dept.id} value={String(dept.id)}> {departments.map((dept) => (
<span style={{ fontFamily: 'monospace' }}> <SelectItem key={dept.id} value={String(dept.id)}>
{formatDepartmentName(dept.name, dept.depth)} <span style={{ fontFamily: 'monospace' }}>
</span> {formatDepartmentName(dept.name, dept.depth)}
</SelectItem> </span>
))} </SelectItem>
</SelectContent> ))}
</Select> </SelectContent>
<Select </Select>
value={dp.positionId} <Select
onValueChange={(value) => handlePositionSelect(dp.id, value)} value={dp.positionId}
disabled={isViewMode} onValueChange={(value) => handlePositionSelect(dp.id, value)}
> disabled={isViewMode}
<SelectTrigger className="flex-1" disabled={isViewMode}> >
<SelectValue placeholder="직책 선택"> <SelectTrigger disabled={isViewMode}>
{dp.positionName || '직책 선택'} <SelectValue placeholder="직책 선택">
</SelectValue> {dp.positionName || '직책 선택'}
</SelectTrigger> </SelectValue>
<SelectContent> </SelectTrigger>
{titles.map((title) => ( <SelectContent>
<SelectItem key={title.id} value={String(title.id)}>{title.name}</SelectItem> {titles.map((title) => (
))} <SelectItem key={title.id} value={String(title.id)}>{title.name}</SelectItem>
</SelectContent> ))}
</Select> </SelectContent>
</Select>
</div>
{!isViewMode && ( {!isViewMode && (
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
size="sm" size="sm"
className="shrink-0"
onClick={() => handleRemoveDepartmentPosition(dp.id)} onClick={() => handleRemoveDepartmentPosition(dp.id)}
> >
<Trash2 className="w-4 h-4 text-destructive" /> <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="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> <div>
<span className="text-sm text-muted-foreground block"> </span> <span className="text-xs sm:text-sm text-muted-foreground block"> </span>
<span className="text-lg font-semibold text-blue-600"> <span className="text-sm sm:text-lg font-semibold text-blue-600">
{formatCurrency(salaryDetail.baseSalary + calculateTotalAllowance())} {formatCurrency(salaryDetail.baseSalary + calculateTotalAllowance())}
</span> </span>
</div> </div>
<div> <div>
<span className="text-sm text-muted-foreground block"> </span> <span className="text-xs sm:text-sm text-muted-foreground block"> </span>
<span className="text-lg font-semibold text-red-600"> <span className="text-sm sm:text-lg font-semibold text-red-600">
-{formatCurrency(salaryDetail.totalDeduction)} -{formatCurrency(salaryDetail.totalDeduction)}
</span> </span>
</div> </div>
<div> <div>
<span className="text-sm text-muted-foreground block"></span> <span className="text-xs sm:text-sm text-muted-foreground block"></span>
<span className="text-xl font-bold text-primary"> <span className="text-base sm:text-xl font-bold text-primary">
{formatCurrency(calculateNetPayment())} {formatCurrency(calculateNetPayment())}
</span> </span>
</div> </div>

View File

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

View File

@@ -114,7 +114,7 @@ export function VacationGrantDialog({
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="사원을 선택하세요" /> <SelectValue placeholder="사원을 선택하세요" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent position="popper" className="max-h-[200px]">
{isLoadingEmployees ? ( {isLoadingEmployees ? (
<div className="flex items-center justify-center py-2"> <div className="flex items-center justify-center py-2">
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin" />

View File

@@ -123,7 +123,7 @@ export function VacationRequestDialog({
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="사원을 선택하세요" /> <SelectValue placeholder="사원을 선택하세요" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent position="popper" className="max-h-[200px]">
{isLoadingEmployees ? ( {isLoadingEmployees ? (
<div className="flex items-center justify-center py-2"> <div className="flex items-center justify-center py-2">
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin" />

View File

@@ -380,9 +380,9 @@ export function VacationManagement() {
// ===== 탭 옵션 (카드 아래에 표시됨) ===== // ===== 탭 옵션 (카드 아래에 표시됨) =====
const tabs: TabOption[] = useMemo(() => [ const tabs: TabOption[] = useMemo(() => [
{ value: 'usage', label: MAIN_TAB_LABELS.usage, count: usageData.length, color: 'blue' }, { value: 'usage', label: MAIN_TAB_LABELS.usage, mobileLabel: '사용', count: usageData.length, color: 'blue' },
{ value: 'grant', label: MAIN_TAB_LABELS.grant, count: grantData.length, color: 'green' }, { value: 'grant', label: MAIN_TAB_LABELS.grant, mobileLabel: '부여', count: grantData.length, color: 'green' },
{ value: 'request', label: MAIN_TAB_LABELS.request, count: requestData.length, color: 'purple' }, { value: 'request', label: MAIN_TAB_LABELS.request, mobileLabel: '신청', count: requestData.length, color: 'purple' },
], [usageData.length, grantData.length, requestData.length]); ], [usageData.length, grantData.length, requestData.length]);
// ===== 테이블 컬럼 (탭별) ===== // ===== 테이블 컬럼 (탭별) =====