feat(WEB): 입력 컴포넌트 공통화 및 UI 개선

- 숫자/통화/전화번호/사업자번호 등 특수 입력 컴포넌트 추가
- MobileCard 컴포넌트 통합 (ListMobileCard 제거)
- IntegratedListTemplateV2 페이지네이션 버그 수정 (NaN 이슈)
- IntegratedDetailTemplate 타이틀 중복 수정
- 문서 시스템 컴포넌트 추가
- 헤더 벨 아이콘 포커스 스타일 개선

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-01-21 20:56:17 +09:00
parent cfa72fe19b
commit 835c06ce94
190 changed files with 8575 additions and 2354 deletions

View File

@@ -28,7 +28,7 @@ import {
type FilterValues,
} from '@/components/templates/UniversalListPage';
import { DateRangeSelector } from '@/components/molecules/DateRangeSelector';
import { ListMobileCard, InfoField } from '@/components/organisms/ListMobileCard';
import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard';
import { AttendanceInfoDialog } from './AttendanceInfoDialog';
import { ReasonInfoDialog } from './ReasonInfoDialog';
import {

View File

@@ -6,7 +6,7 @@ import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { TableRow, TableCell } from '@/components/ui/table';
import { ListMobileCard, InfoField } from '@/components/organisms/ListMobileCard';
import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard';
import {
UniversalListPage,
type UniversalListConfig,

View File

@@ -56,9 +56,9 @@ export const cardConfig: DetailConfig<Card> = {
{
key: 'cardNumber',
label: '카드번호',
type: 'text',
type: 'cardNumber',
required: true,
placeholder: '1234-1234-1234-1234',
placeholder: '0000-0000-0000-0000',
helpText: '16자리 카드번호를 입력하세요',
},
{

View File

@@ -22,7 +22,7 @@ import {
type UniversalListConfig,
type TabOption,
} from '@/components/templates/UniversalListPage';
import { ListMobileCard, InfoField } from '@/components/organisms/ListMobileCard';
import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard';
import { toast } from 'sonner';
import type { Card } from './types';
import {

View File

@@ -12,6 +12,9 @@ import {
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { CurrencyInput } from '@/components/ui/currency-input';
import { PhoneInput } from '@/components/ui/phone-input';
import { PersonalNumberInput } from '@/components/ui/personal-number-input';
import {
Select,
SelectContent,
@@ -189,10 +192,10 @@ export function EmployeeDialog({
<div className="space-y-2">
<Label htmlFor="residentNumber"></Label>
<Input
<PersonalNumberInput
id="residentNumber"
value={formData.residentNumber}
onChange={(e) => handleChange('residentNumber', e.target.value)}
onChange={(value) => handleChange('residentNumber', value)}
disabled={isViewMode}
placeholder="000000-0000000"
/>
@@ -200,10 +203,10 @@ export function EmployeeDialog({
<div className="space-y-2">
<Label htmlFor="phone"></Label>
<Input
<PhoneInput
id="phone"
value={formData.phone}
onChange={(e) => handleChange('phone', e.target.value)}
onChange={(value) => handleChange('phone', value)}
disabled={isViewMode}
placeholder="010-0000-0000"
/>
@@ -223,13 +226,12 @@ export function EmployeeDialog({
<div className="space-y-2">
<Label htmlFor="salary"></Label>
<Input
<CurrencyInput
id="salary"
type="number"
value={formData.salary}
onChange={(e) => handleChange('salary', e.target.value)}
value={formData.salary ? Number(formData.salary) : undefined}
onChange={(value) => handleChange('salary', value?.toString() ?? '')}
disabled={isViewMode}
placeholder="연봉 (원)"
placeholder="연봉"
/>
</div>
</div>

View File

@@ -15,6 +15,9 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { CurrencyInput } from '@/components/ui/currency-input';
import { PhoneInput } from '@/components/ui/phone-input';
import { PersonalNumberInput } from '@/components/ui/personal-number-input';
import {
Select,
SelectContent,
@@ -403,10 +406,10 @@ export function EmployeeForm({
<div className="space-y-2">
<Label htmlFor="residentNumber"></Label>
<Input
<PersonalNumberInput
id="residentNumber"
value={formData.residentNumber}
onChange={(e) => handleChange('residentNumber', e.target.value)}
onChange={(value) => handleChange('residentNumber', value)}
placeholder="000000-0000000"
disabled={isViewMode}
/>
@@ -414,10 +417,10 @@ export function EmployeeForm({
<div className="space-y-2">
<Label htmlFor="phone"></Label>
<Input
<PhoneInput
id="phone"
value={formData.phone}
onChange={(e) => handleChange('phone', e.target.value)}
onChange={(value) => handleChange('phone', value)}
placeholder="010-0000-0000"
disabled={isViewMode}
/>
@@ -439,12 +442,11 @@ export function EmployeeForm({
<div className="space-y-2">
<Label htmlFor="salary"></Label>
<Input
<CurrencyInput
id="salary"
type="number"
value={formData.salary}
onChange={(e) => handleChange('salary', e.target.value)}
placeholder="연봉 (원)"
value={formData.salary ? Number(formData.salary) : undefined}
onChange={(value) => handleChange('salary', value?.toString() ?? '')}
placeholder="연봉"
disabled={isViewMode}
/>
</div>

View File

@@ -8,7 +8,7 @@ import type { DetailConfig } from '@/components/templates/IntegratedDetailTempla
* IntegratedDetailTemplate 마이그레이션 (2025-01-20)
*/
export const employeeCreateConfig: DetailConfig = {
title: '사원 등록',
title: '사원',
description: '새로운 사원을 등록합니다',
icon: Users,
basePath: '/hr/employee-management',
@@ -27,7 +27,7 @@ export const employeeCreateConfig: DetailConfig = {
*/
export const employeeEditConfig: DetailConfig = {
...employeeCreateConfig,
title: '사원 수정',
title: '사원',
description: '사원 정보를 수정합니다',
actions: {
...employeeCreateConfig.actions,

View File

@@ -27,7 +27,7 @@ import {
type FilterValues,
} from '@/components/templates/UniversalListPage';
import { DateRangeSelector } from '@/components/molecules/DateRangeSelector';
import { ListMobileCard, InfoField } from '@/components/organisms/ListMobileCard';
import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard';
import { FieldSettingsDialog } from './FieldSettingsDialog';
import { UserInviteDialog } from './UserInviteDialog';
import type {

View File

@@ -12,6 +12,7 @@ import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Separator } from '@/components/ui/separator';
import { CurrencyInput } from '@/components/ui/currency-input';
import {
Select,
SelectContent,
@@ -263,15 +264,11 @@ export function SalaryDetailDialog({
<div className="flex justify-between items-center">
<span className="text-muted-foreground"></span>
{isEditingAllowances ? (
<div className="flex items-center gap-1">
<Input
type="number"
value={editedAllowances.positionAllowance}
onChange={(e) => handleAllowanceChange('positionAllowance', e.target.value)}
className="w-28 h-7 text-right text-sm"
/>
<span className="text-xs"></span>
</div>
<CurrencyInput
value={editedAllowances.positionAllowance}
onChange={(value) => handleAllowanceChange('positionAllowance', String(value ?? 0))}
className="w-32 h-7 text-sm"
/>
) : (
<span>{formatCurrency(editedAllowances.positionAllowance)}</span>
)}
@@ -279,15 +276,11 @@ export function SalaryDetailDialog({
<div className="flex justify-between items-center">
<span className="text-muted-foreground"></span>
{isEditingAllowances ? (
<div className="flex items-center gap-1">
<Input
type="number"
value={editedAllowances.overtimeAllowance}
onChange={(e) => handleAllowanceChange('overtimeAllowance', e.target.value)}
className="w-28 h-7 text-right text-sm"
/>
<span className="text-xs"></span>
</div>
<CurrencyInput
value={editedAllowances.overtimeAllowance}
onChange={(value) => handleAllowanceChange('overtimeAllowance', String(value ?? 0))}
className="w-32 h-7 text-sm"
/>
) : (
<span>{formatCurrency(editedAllowances.overtimeAllowance)}</span>
)}
@@ -295,15 +288,11 @@ export function SalaryDetailDialog({
<div className="flex justify-between items-center">
<span className="text-muted-foreground"></span>
{isEditingAllowances ? (
<div className="flex items-center gap-1">
<Input
type="number"
value={editedAllowances.mealAllowance}
onChange={(e) => handleAllowanceChange('mealAllowance', e.target.value)}
className="w-28 h-7 text-right text-sm"
/>
<span className="text-xs"></span>
</div>
<CurrencyInput
value={editedAllowances.mealAllowance}
onChange={(value) => handleAllowanceChange('mealAllowance', String(value ?? 0))}
className="w-32 h-7 text-sm"
/>
) : (
<span>{formatCurrency(editedAllowances.mealAllowance)}</span>
)}
@@ -311,15 +300,11 @@ export function SalaryDetailDialog({
<div className="flex justify-between items-center">
<span className="text-muted-foreground"></span>
{isEditingAllowances ? (
<div className="flex items-center gap-1">
<Input
type="number"
value={editedAllowances.transportAllowance}
onChange={(e) => handleAllowanceChange('transportAllowance', e.target.value)}
className="w-28 h-7 text-right text-sm"
/>
<span className="text-xs"></span>
</div>
<CurrencyInput
value={editedAllowances.transportAllowance}
onChange={(value) => handleAllowanceChange('transportAllowance', String(value ?? 0))}
className="w-32 h-7 text-sm"
/>
) : (
<span>{formatCurrency(editedAllowances.transportAllowance)}</span>
)}
@@ -327,15 +312,11 @@ export function SalaryDetailDialog({
<div className="flex justify-between items-center">
<span className="text-muted-foreground"></span>
{isEditingAllowances ? (
<div className="flex items-center gap-1">
<Input
type="number"
value={editedAllowances.otherAllowance}
onChange={(e) => handleAllowanceChange('otherAllowance', e.target.value)}
className="w-28 h-7 text-right text-sm"
/>
<span className="text-xs"></span>
</div>
<CurrencyInput
value={editedAllowances.otherAllowance}
onChange={(value) => handleAllowanceChange('otherAllowance', String(value ?? 0))}
className="w-32 h-7 text-sm"
/>
) : (
<span>{formatCurrency(editedAllowances.otherAllowance)}</span>
)}

View File

@@ -26,7 +26,7 @@ import {
type FilterFieldConfig,
type FilterValues,
} from '@/components/templates/UniversalListPage';
import { ListMobileCard, InfoField } from '@/components/organisms/ListMobileCard';
import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard';
import { SalaryDetailDialog } from './SalaryDetailDialog';
import {
getSalaries,

View File

@@ -14,6 +14,7 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { QuantityInput } from '@/components/ui/quantity-input';
import {
Select,
SelectContent,
@@ -166,12 +167,11 @@ export function VacationGrantDialog({
{/* 부여 일수 */}
<div className="grid gap-2">
<Label htmlFor="grantDays"> </Label>
<Input
<QuantityInput
id="grantDays"
type="number"
min={1}
value={formData.grantDays}
onChange={(e) => setFormData(prev => ({ ...prev, grantDays: parseInt(e.target.value) || 1 }))}
onChange={(value) => setFormData(prev => ({ ...prev, grantDays: value ?? 1 }))}
/>
</div>

View File

@@ -12,6 +12,7 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { NumberInput } from '@/components/ui/number-input';
import {
Select,
SelectContent,
@@ -158,15 +159,15 @@ export function VacationRegisterDialog({
<Label htmlFor="days">
<span className="text-red-500">*</span>
</Label>
<Input
<NumberInput
id="days"
type="number"
min="0"
step="0.5"
value={formData.days || ''}
onChange={(e) => setFormData((prev: VacationFormData) => ({ ...prev, days: parseFloat(e.target.value) || 0 }))}
className={errors.days ? 'border-red-500' : ''}
min={0}
step={0.5}
value={formData.days}
onChange={(value) => setFormData((prev: VacationFormData) => ({ ...prev, days: value ?? 0 }))}
error={!!errors.days}
placeholder="일수를 입력하세요"
allowDecimal
/>
{errors.days && (
<p className="text-sm text-red-500">{errors.days}</p>

View File

@@ -48,7 +48,7 @@ import {
type FilterValues,
} from '@/components/templates/UniversalListPage';
import { DateRangeSelector } from '@/components/molecules/DateRangeSelector';
import { ListMobileCard, InfoField } from '@/components/organisms/ListMobileCard';
import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard';
import { VacationGrantDialog } from './VacationGrantDialog';
import { VacationRequestDialog } from './VacationRequestDialog';
import type {