feat: [설정] 설정 관리 전반 UI 개선

- 계정관리 상세/폼 개선 (AccountDetail, AccountDetailForm)
- 근태설정, 휴가정책 관리 개선
- 바로빌 연동 회원가입 모달 개선
- 알림설정, 결제이력, 권한관리 UI 개선
- 직급/직책 관리 UI 개선 (RankManagement, TitleManagement)
- 구독관리, 근무스케줄 관리 개선

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-02-25 22:33:28 +09:00
parent e094c5ae49
commit 23135ff01a
18 changed files with 267 additions and 214 deletions

View File

@@ -353,7 +353,7 @@ export function AccountInfoClient({
<div className="space-y-3 pl-4">
{/* 이메일 수신 동의 */}
<div className="flex items-center justify-between">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-1">
<div className="flex items-center gap-2">
<Checkbox
id="email-consent"
@@ -371,7 +371,7 @@ export function AccountInfoClient({
</div>
{/* SMS 수신 동의 */}
<div className="flex items-center justify-between">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-1">
<div className="flex items-center gap-2">
<Checkbox
id="sms-consent"

View File

@@ -21,6 +21,7 @@ import { useDeleteDialog } from '@/hooks/useDeleteDialog';
import { PageLayout } from '@/components/organisms/PageLayout';
import { PageHeader } from '@/components/organisms/PageHeader';
import { toast } from 'sonner';
import { useMenuStore } from '@/stores/menuStore';
import type { Account, AccountFormData, AccountStatus } from './types';
import {
BANK_OPTIONS,
@@ -39,6 +40,7 @@ interface AccountDetailProps {
export function AccountDetail({ account, mode: initialMode }: AccountDetailProps) {
const router = useRouter();
const searchParams = useSearchParams();
const sidebarCollapsed = useMenuStore((state) => state.sidebarCollapsed);
const [mode, setMode] = useState(initialMode);
const deleteDialog = useDeleteDialog({
onDelete: async (id) => deleteBankAccount(Number(id)),
@@ -185,22 +187,28 @@ export function AccountDetail({ account, mode: initialMode }: AccountDetailProps
</CardContent>
</Card>
{/* 버튼 영역 */}
<div className="flex items-center justify-between">
<Button variant="outline" onClick={handleBack}>
<ArrowLeft 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={() => account?.id && deleteDialog.single.open(String(account.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} size="sm" className="md:size-default">
<Edit className="h-4 w-4 md:mr-2" />
<span className="hidden md:inline"></span>
</Button>
<div className="flex items-center gap-2">
<Button variant="outline" onClick={() => account?.id && deleteDialog.single.open(String(account.id))} className="text-destructive hover:bg-destructive hover:text-destructive-foreground">
<Trash2 className="w-4 h-4 mr-2" />
</Button>
<Button onClick={handleEdit}>
<Edit className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
</div>
@@ -330,17 +338,18 @@ export function AccountDetail({ account, mode: initialMode }: AccountDetailProps
</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}>
<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} size="sm" className="md:size-default">
<Save className="h-4 w-4 md:mr-2" />
<span className="hidden md:inline">{isCreateMode ? '등록' : '저장'}</span>
</Button>
</div>
</PageLayout>
);

View File

@@ -12,7 +12,7 @@
import { useState, useCallback, useMemo } from 'react';
import { useRouter } from 'next/navigation';
import { Landmark, Save, Trash2, ArrowLeft } from 'lucide-react';
import { Landmark, Save, Trash2, ArrowLeft, Edit } from 'lucide-react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@@ -20,6 +20,7 @@ import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog';
import { PageLayout } from '@/components/organisms/PageLayout';
import { PageHeader } from '@/components/organisms/PageHeader';
import { FormField } from '@/components/molecules/FormField';
import { useMenuStore } from '@/stores/menuStore';
import type { Account, AccountCategory, AccountFormData } from './types';
import {
ACCOUNT_CATEGORY_OPTIONS,
@@ -98,6 +99,7 @@ export function AccountDetailForm({
isLoading,
}: AccountDetailFormProps) {
const router = useRouter();
const sidebarCollapsed = useMenuStore((state) => state.sidebarCollapsed);
const [mode, setMode] = useState(initialMode);
const [formData, setFormData] = useState<AccountFormData>(() => getInitialFormData(initialData));
const [isSaving, setIsSaving] = useState(false);
@@ -216,7 +218,7 @@ export function AccountDetailForm({
icon={Landmark}
/>
<div className="space-y-6">
<div className="space-y-6 pb-20">
{/* ===== 기본 정보 ===== */}
<Card>
<CardHeader>
@@ -321,26 +323,30 @@ export function AccountDetailForm({
<InsuranceAccountSection formData={formData} onChange={handleChange} disabled={disabled} />
)}
{/* ===== 하단 버튼 ===== */}
<div className="flex items-center justify-between">
<Button variant="outline" onClick={handleBack}>
<ArrowLeft className="w-4 h-4 mr-2" />
{/* ===== 하단 버튼 (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-2">
<div className="flex items-center gap-1 md:gap-2">
{isViewMode ? (
<>
{onDelete && (
<Button
variant="outline"
onClick={() => setShowDeleteDialog(true)}
className="text-destructive hover:bg-destructive hover:text-destructive-foreground"
size="sm"
className="text-destructive hover:bg-destructive hover:text-destructive-foreground md:size-default"
>
<Trash2 className="w-4 h-4 mr-2" />
<Trash2 className="h-4 w-4 md:mr-2" />
<span className="hidden md:inline"></span>
</Button>
)}
<Button onClick={handleEdit}></Button>
<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>
</>
) : (
<>
@@ -348,15 +354,16 @@ export function AccountDetailForm({
<Button
variant="outline"
onClick={() => setShowDeleteDialog(true)}
className="text-destructive hover:bg-destructive hover:text-destructive-foreground"
size="sm"
className="text-destructive hover:bg-destructive hover:text-destructive-foreground md:size-default"
>
<Trash2 className="w-4 h-4 mr-2" />
<Trash2 className="h-4 w-4 md:mr-2" />
<span className="hidden md:inline"></span>
</Button>
)}
<Button onClick={handleSubmit} disabled={isSaving}>
<Save className="w-4 h-4 mr-2" />
{isCreateMode ? '등록' : '저장'}
<Button onClick={handleSubmit} disabled={isSaving} size="sm" className="md:size-default">
<Save className="h-4 w-4 md:mr-2" />
<span className="hidden md:inline">{isCreateMode ? '등록' : '저장'}</span>
</Button>
</>
)}

View File

@@ -344,6 +344,7 @@ export function AccountManagement() {
{ACCOUNT_STATUS_LABELS[item.status]}
</Badge>
}
showCheckbox={false}
isSelected={false}
onToggleSelection={() => {}}
onClick={() => handleRowClick(item)}

View File

@@ -181,12 +181,10 @@ export function AttendanceSettingsManagement() {
<CardHeader>
<CardTitle className="text-lg">GPS </CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<CardContent className="space-y-6">
{/* GPS 출퇴근 사용 + 연동 부서 */}
<div className="flex items-center gap-6">
<div className="flex items-center gap-2 min-w-[200px]">
<span className="text-sm font-medium text-muted-foreground">GPS </span>
</div>
<div className="grid grid-cols-1 md:grid-cols-[200px_1fr_1fr] gap-4 items-start">
<span className="text-sm font-medium text-muted-foreground pt-0.5">GPS </span>
<label className="flex items-center gap-2 cursor-pointer">
<Checkbox
checked={settings.gpsEnabled}
@@ -194,7 +192,7 @@ export function AttendanceSettingsManagement() {
/>
<span className="text-sm">GPS </span>
</label>
<div className="flex items-center gap-2">
<div className="space-y-1">
<span className="text-sm font-medium text-muted-foreground"> </span>
<MultiSelectCombobox
options={departmentOptions}
@@ -204,22 +202,20 @@ export function AttendanceSettingsManagement() {
searchPlaceholder="부서 검색..."
emptyText="검색 결과가 없습니다."
disabled={!settings.gpsEnabled}
className="w-[200px]"
className="w-full"
/>
</div>
</div>
{/* 출퇴근 허용 반경 */}
<div className="flex items-center gap-6">
<div className="flex items-center gap-2 min-w-[200px]">
<span className="text-sm font-medium text-muted-foreground"> </span>
</div>
<div className="grid grid-cols-1 md:grid-cols-[200px_1fr] gap-4 items-start">
<span className="text-sm font-medium text-muted-foreground pt-0.5"> </span>
<Select
value={String(settings.allowedRadius)}
onValueChange={handleRadiusChange}
disabled={!settings.gpsEnabled}
>
<SelectTrigger className="min-w-[120px] w-auto">
<SelectTrigger className="w-full md:w-[200px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -240,10 +236,8 @@ export function AttendanceSettingsManagement() {
<CardTitle className="text-lg"> </CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center gap-6">
<div className="flex items-center gap-2 min-w-[200px]">
<span className="text-sm font-medium text-muted-foreground"> </span>
</div>
<div className="grid grid-cols-1 md:grid-cols-[200px_1fr_1fr] gap-4 items-start">
<span className="text-sm font-medium text-muted-foreground pt-0.5"> </span>
<label className="flex items-center gap-2 cursor-pointer">
<Checkbox
checked={settings.autoEnabled}
@@ -251,7 +245,7 @@ export function AttendanceSettingsManagement() {
/>
<span className="text-sm"> </span>
</label>
<div className="flex items-center gap-2">
<div className="space-y-1">
<span className="text-sm font-medium text-muted-foreground"> </span>
<MultiSelectCombobox
options={departmentOptions}
@@ -261,7 +255,7 @@ export function AttendanceSettingsManagement() {
searchPlaceholder="부서 검색..."
emptyText="검색 결과가 없습니다."
disabled={!settings.autoEnabled}
className="w-[200px]"
className="w-full"
/>
</div>
</div>

View File

@@ -96,7 +96,7 @@ export function SignupModal({ open, onOpenChange, onSuccess }: SignupModalProps)
<div className="space-y-4 py-2">
{/* 사업자등록번호 + 상호명 */}
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<FormField
type="businessNumber"
label="사업자등록번호"
@@ -115,7 +115,7 @@ export function SignupModal({ open, onOpenChange, onSuccess }: SignupModalProps)
</div>
{/* 대표자명 + 업태 + 업종 */}
<div className="grid grid-cols-3 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<FormField
label="대표자명"
required
@@ -146,7 +146,7 @@ export function SignupModal({ open, onOpenChange, onSuccess }: SignupModalProps)
/>
{/* 바로빌 아이디 + 비밀번호 */}
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<FormField
label="바로빌 아이디"
required
@@ -164,7 +164,7 @@ export function SignupModal({ open, onOpenChange, onSuccess }: SignupModalProps)
</div>
{/* 담당자명 + 담당자 연락처 */}
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<FormField
label="담당자명"
value={formData.managerName}

View File

@@ -3,7 +3,7 @@
import { useState, useEffect, useCallback } from 'react';
import { PageLayout } from '@/components/organisms/PageLayout';
import { PageHeader } from '@/components/organisms/PageHeader';
import { Link2, Loader2, Pencil, Check } from 'lucide-react';
import { Link2, Loader2, Edit, Check } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
@@ -103,7 +103,7 @@ export function BarobillIntegration() {
className="w-full"
onClick={() => setLoginOpen(true)}
>
<Pencil className="h-4 w-4 mr-2" />
<Edit className="h-4 w-4 mr-2" />
</Button>
</>

View File

@@ -120,7 +120,7 @@ export function LeavePolicyManagement() {
<CardContent className="p-6">
<h3 className="text-lg font-semibold mb-6"> </h3>
<div className="grid grid-cols-2 gap-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* 기준 셀렉트 */}
<div className="space-y-2">
<Label></Label>
@@ -194,7 +194,7 @@ export function LeavePolicyManagement() {
<CardContent className="p-6">
<h3 className="text-lg font-semibold mb-6"> </h3>
<div className="grid grid-cols-3 gap-6">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6">
{/* 기본 연차 일수 */}
<div className="space-y-2">
<Label> </Label>
@@ -269,7 +269,7 @@ export function LeavePolicyManagement() {
</div>
{settings.carryOverEnabled && (
<div className="grid grid-cols-2 gap-6 pt-4 border-t">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 pt-4 border-t">
{/* 최대 이월 일수 */}
<div className="space-y-2">
<Label> </Label>

View File

@@ -133,7 +133,7 @@ export function ItemSettingsDialog({ isOpen, onClose, settings, onSave }: ItemSe
return (
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
<DialogContent className="!w-[400px] !max-w-[400px] max-h-[80vh] overflow-y-auto p-0 bg-white border-gray-200">
<DialogContent className="w-[calc(100vw-2rem)] sm:!w-[400px] sm:!max-w-[400px] max-h-[80vh] overflow-y-auto p-0 bg-white border-gray-200">
{/* 헤더 */}
<DialogHeader className="sticky top-0 bg-white z-10 px-4 py-3 border-b border-gray-200">
<div className="flex items-center justify-between">

View File

@@ -68,41 +68,43 @@ function NotificationItemRow({ label, item, onChange, disabled }: NotificationIt
</div>
{/* 알림 소리 선택 */}
<div className="flex items-center gap-2 pl-2">
<span className="text-sm text-muted-foreground min-w-[80px]"> </span>
<Select
value={item.soundType}
onValueChange={(value: SoundType) =>
onChange({ ...item, soundType: value })
}
disabled={isDisabled}
>
<SelectTrigger className="min-w-[140px] w-auto h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
{SOUND_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
type="button"
variant="outline"
size="icon"
className="h-8 w-8"
onClick={() => playPreviewSound(item.soundType)}
disabled={isDisabled}
>
<Play className="h-3 w-3" />
</Button>
<div className="space-y-1 pl-2">
<span className="text-xs text-muted-foreground"> </span>
<div className="flex items-center gap-2">
<Select
value={item.soundType}
onValueChange={(value: SoundType) =>
onChange({ ...item, soundType: value })
}
disabled={isDisabled}
>
<SelectTrigger className="flex-1 sm:w-[140px] sm:flex-none h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
{SOUND_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
type="button"
variant="outline"
size="icon"
className="h-8 w-8 shrink-0"
onClick={() => playPreviewSound(item.soundType)}
disabled={isDisabled}
>
<Play className="h-3 w-3" />
</Button>
</div>
</div>
{/* 추가 알림 선택 */}
<div className="flex items-center gap-2 pl-2">
<span className="text-sm text-muted-foreground min-w-[80px]"> </span>
<div className="space-y-1 pl-2">
<span className="text-xs text-muted-foreground"> </span>
<label className="flex items-center gap-2 cursor-pointer">
<Checkbox
checked={item.email}
@@ -131,7 +133,7 @@ function NotificationSection({ title, enabled, onEnabledChange, children }: Noti
return (
<Card>
<div className="flex items-center justify-between px-6 pt-6 pb-3">
<div className="flex items-center justify-between px-4 sm:px-6 pt-4 sm:pt-6 pb-3">
<CardTitle className="text-base font-medium">{title}</CardTitle>
<Switch
checked={enabled}
@@ -140,8 +142,8 @@ function NotificationSection({ title, enabled, onEnabledChange, children }: Noti
}}
/>
</div>
<CardContent className="pt-0">
<div className="pl-4">
<CardContent className="pt-0 px-4 sm:px-6">
<div className="pl-0 sm:pl-4">
{children}
</div>
</CardContent>

View File

@@ -184,6 +184,7 @@ export function PaymentHistoryClient({
key={item.id}
id={item.id}
title={`${item.subscriptionName} - ${item.paymentDate}`}
showCheckbox={false}
isSelected={false}
onToggleSelection={() => {}}
infoGrid={

View File

@@ -153,6 +153,7 @@ export function PaymentHistoryManagement({
<ListMobileCard
id={item.id}
title={`${item.subscriptionName} - ${item.paymentDate}`}
showCheckbox={false}
isSelected={false}
onToggleSelection={() => {}}
infoGrid={

View File

@@ -533,9 +533,9 @@ export function PermissionDetailClient({ permissionId, isNew = false, mode = 'vi
{!isNew && (
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between mb-4">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-4">
<h3 className="text-lg font-semibold"> </h3>
<div className="flex gap-2">
<div className="flex flex-wrap gap-2">
<Button
variant="outline"
size="sm"

View File

@@ -7,6 +7,7 @@ import {
Shield,
Plus,
Pencil,
Edit,
Trash2,
Settings,
Eye,
@@ -349,30 +350,21 @@ export function PermissionManagement() {
isSelected={isSelected}
onToggleSelection={onToggle}
infoGrid={
<div className="grid grid-cols-2 gap-3">
<div className="grid grid-cols-1 gap-3">
<InfoField label="설명" value={item.description || '-'} />
<InfoField label="등록일" value={formatDate(item.created_at)} />
</div>
}
actions={
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => handleViewDetail(item)}
>
<Settings className="h-4 w-4 mr-2" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleEdit(item)}
>
<Pencil className="h-4 w-4" />
</Button>
</div>
<Button
variant="outline"
size="sm"
className="w-full"
onClick={() => handleViewDetail(item)}
>
<Settings className="h-4 w-4 mr-2" />
</Button>
}
/>
);

View File

@@ -3,7 +3,7 @@
import { useState, useEffect, useCallback } from 'react';
import { PageLayout } from '@/components/organisms/PageLayout';
import { PageHeader } from '@/components/organisms/PageHeader';
import { Award, Plus, GripVertical, Pencil, Trash2, Loader2 } from 'lucide-react';
import { Award, Plus, GripVertical, Edit, Trash2, Loader2 } from 'lucide-react';
import { ReorderButtons } from '@/components/molecules';
import { ContentSkeleton } from '@/components/ui/skeleton';
import { Button } from '@/components/ui/button';
@@ -270,33 +270,59 @@ export function RankManagement() {
onDragStart={(e) => handleDragStart(e, index)}
onDragEnd={handleDragEnd}
onDragOver={(e) => handleDragOver(e, index)}
className={`flex items-center gap-3 px-4 py-3 hover:bg-muted/50 transition-colors cursor-move ${
className={`px-4 py-3 hover:bg-muted/50 transition-colors cursor-move ${
draggedItem === index ? 'opacity-50 bg-muted' : ''
}`}
>
{/* 드래그 핸들 (PC만) */}
<GripVertical className="h-5 w-5 text-muted-foreground flex-shrink-0 hidden md:block" />
<div className="flex items-center gap-3">
{/* 드래그 핸들 (PC만) */}
<GripVertical className="h-5 w-5 text-muted-foreground flex-shrink-0 hidden md:block" />
{/* 순서 변경 버튼 */}
<ReorderButtons
onMoveUp={() => handleMoveItem(index, index - 1)}
onMoveDown={() => handleMoveItem(index, index + 1)}
isFirst={index === 0}
isLast={index === ranks.length - 1}
disabled={isSubmitting}
size="xs"
/>
{/* 순서 변경 버튼 */}
<ReorderButtons
onMoveUp={() => handleMoveItem(index, index - 1)}
onMoveDown={() => handleMoveItem(index, index + 1)}
isFirst={index === 0}
isLast={index === ranks.length - 1}
disabled={isSubmitting}
size="xs"
/>
{/* 순서 번호 */}
<span className="text-sm text-muted-foreground w-8">
{index + 1}
</span>
{/* 순서 번호 */}
<span className="text-sm text-muted-foreground w-8">
{index + 1}
</span>
{/* 직급명 */}
<span className="flex-1 font-medium">{rank.name}</span>
{/* 직급명 */}
<span className="flex-1 min-w-0 font-medium">{rank.name}</span>
{/* 액션 버튼 */}
<div className="flex gap-1">
{/* 액션 버튼 (PC) */}
<div className="hidden sm:flex gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => handleEdit(rank)}
className="h-8 w-8 p-0"
disabled={isSubmitting}
>
<Edit className="h-4 w-4" />
<span className="sr-only"></span>
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => deleteDialog.single.open(String(rank.id))}
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
disabled={deleteDialog.isPending}
>
<Trash2 className="h-4 w-4" />
<span className="sr-only"></span>
</Button>
</div>
</div>
{/* 액션 버튼 (모바일) */}
<div className="flex sm:hidden gap-1 justify-end mt-2">
<Button
variant="ghost"
size="sm"
@@ -304,7 +330,7 @@ export function RankManagement() {
className="h-8 w-8 p-0"
disabled={isSubmitting}
>
<Pencil className="h-4 w-4" />
<Edit className="h-4 w-4" />
<span className="sr-only"></span>
</Button>
<Button

View File

@@ -172,44 +172,38 @@ export function SubscriptionManagement({ initialData }: SubscriptionManagementPr
</h3>
{/* 사용량 정보 */}
<div className="space-y-6">
<div className="space-y-5">
{/* 사용자 수 */}
<div className="flex items-center gap-4">
<div className="w-24 text-sm text-muted-foreground flex-shrink-0">
</div>
<div className="flex-1">
<Progress value={subscription.userLimit ? (subscription.userCount / subscription.userLimit) * 100 : 30} className="h-2" />
</div>
<div className="text-sm text-blue-600 min-w-[100px] text-right">
{subscription.userCount} / {subscription.userLimit ? `${subscription.userLimit}` : '무제한'}
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground"> </span>
<span className="text-sm text-blue-600">
{subscription.userCount} / {subscription.userLimit ? `${subscription.userLimit}` : '무제한'}
</span>
</div>
<Progress value={subscription.userLimit ? (subscription.userCount / subscription.userLimit) * 100 : 30} className="h-2" />
</div>
{/* 저장 공간 */}
<div className="flex items-center gap-4">
<div className="w-24 text-sm text-muted-foreground flex-shrink-0">
</div>
<div className="flex-1">
<Progress value={storageProgress} className="h-2" />
</div>
<div className="text-sm text-blue-600 min-w-[120px] text-right">
{subscription.storageUsedFormatted} / {subscription.storageLimitFormatted}
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground"> </span>
<span className="text-sm text-blue-600">
{subscription.storageUsedFormatted} / {subscription.storageLimitFormatted}
</span>
</div>
<Progress value={storageProgress} className="h-2" />
</div>
{/* AI API 호출 */}
<div className="flex items-center gap-4">
<div className="w-24 text-sm text-muted-foreground flex-shrink-0">
AI API
</div>
<div className="flex-1">
<Progress value={apiProgress} className="h-2" />
</div>
<div className="text-sm text-blue-600 min-w-[100px] text-right">
{formatNumber(apiCallsUsed)} / {formatNumber(apiCallsLimit)}
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">AI API </span>
<span className="text-sm text-blue-600">
{formatNumber(apiCallsUsed)} / {formatNumber(apiCallsLimit)}
</span>
</div>
<Progress value={apiProgress} className="h-2" />
</div>
</div>
</CardContent>

View File

@@ -3,7 +3,7 @@
import { useState, useEffect, useCallback } from 'react';
import { PageLayout } from '@/components/organisms/PageLayout';
import { PageHeader } from '@/components/organisms/PageHeader';
import { Briefcase, Plus, GripVertical, Pencil, Trash2, Loader2 } from 'lucide-react';
import { Briefcase, Plus, GripVertical, Edit, Trash2, Loader2 } from 'lucide-react';
import { ReorderButtons } from '@/components/molecules';
import { ContentSkeleton } from '@/components/ui/skeleton';
import { Button } from '@/components/ui/button';
@@ -270,33 +270,59 @@ export function TitleManagement() {
onDragStart={(e) => handleDragStart(e, index)}
onDragEnd={handleDragEnd}
onDragOver={(e) => handleDragOver(e, index)}
className={`flex items-center gap-3 px-4 py-3 hover:bg-muted/50 transition-colors cursor-move ${
className={`px-4 py-3 hover:bg-muted/50 transition-colors cursor-move ${
draggedItem === index ? 'opacity-50 bg-muted' : ''
}`}
>
{/* 드래그 핸들 (PC만) */}
<GripVertical className="h-5 w-5 text-muted-foreground flex-shrink-0 hidden md:block" />
<div className="flex items-center gap-3">
{/* 드래그 핸들 (PC만) */}
<GripVertical className="h-5 w-5 text-muted-foreground flex-shrink-0 hidden md:block" />
{/* 순서 변경 버튼 */}
<ReorderButtons
onMoveUp={() => handleMoveItem(index, index - 1)}
onMoveDown={() => handleMoveItem(index, index + 1)}
isFirst={index === 0}
isLast={index === titles.length - 1}
disabled={isSubmitting}
size="xs"
/>
{/* 순서 변경 버튼 */}
<ReorderButtons
onMoveUp={() => handleMoveItem(index, index - 1)}
onMoveDown={() => handleMoveItem(index, index + 1)}
isFirst={index === 0}
isLast={index === titles.length - 1}
disabled={isSubmitting}
size="xs"
/>
{/* 순서 번호 */}
<span className="text-sm text-muted-foreground w-8">
{index + 1}
</span>
{/* 순서 번호 */}
<span className="text-sm text-muted-foreground w-8">
{index + 1}
</span>
{/* 직책명 */}
<span className="flex-1 font-medium">{title.name}</span>
{/* 직책명 */}
<span className="flex-1 min-w-0 font-medium">{title.name}</span>
{/* 액션 버튼 */}
<div className="flex gap-1">
{/* 액션 버튼 (PC) */}
<div className="hidden sm:flex gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => handleEdit(title)}
className="h-8 w-8 p-0"
disabled={isSubmitting}
>
<Edit className="h-4 w-4" />
<span className="sr-only"></span>
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => deleteDialog.single.open(String(title.id))}
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
disabled={deleteDialog.isPending}
>
<Trash2 className="h-4 w-4" />
<span className="sr-only"></span>
</Button>
</div>
</div>
{/* 액션 버튼 (모바일) */}
<div className="flex sm:hidden gap-1 justify-end mt-2">
<Button
variant="ghost"
size="sm"
@@ -304,7 +330,7 @@ export function TitleManagement() {
className="h-8 w-8 p-0"
disabled={isSubmitting}
>
<Pencil className="h-4 w-4" />
<Edit className="h-4 w-4" />
<span className="sr-only"></span>
</Button>
<Button

View File

@@ -183,7 +183,7 @@ export function WorkScheduleManagement() {
onValueChange={(value: EmploymentType) => handleEmploymentTypeChange(value)}
disabled={isLoading}
>
<SelectTrigger className="w-64">
<SelectTrigger className="w-full md:w-64">
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -229,13 +229,13 @@ export function WorkScheduleManagement() {
<CardTitle className="text-lg">1 </CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-6">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
<div className="space-y-2">
<Label> </Label>
<TimePicker
value={settings.workStartTime}
onChange={(value) => setSettings(prev => ({ ...prev, workStartTime: value }))}
className="w-40"
className="w-full sm:w-40"
minuteStep={1}
/>
</div>
@@ -244,7 +244,7 @@ export function WorkScheduleManagement() {
<TimePicker
value={settings.workEndTime}
onChange={(value) => setSettings(prev => ({ ...prev, workEndTime: value }))}
className="w-40"
className="w-full sm:w-40"
minuteStep={1}
/>
</div>
@@ -258,7 +258,7 @@ export function WorkScheduleManagement() {
<CardTitle className="text-lg"> </CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-6">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="weekly-hours"> </Label>
<div className="flex items-center gap-2">
@@ -270,9 +270,9 @@ export function WorkScheduleManagement() {
onChange={(value) =>
setSettings(prev => ({ ...prev, weeklyWorkHours: value ?? 0 }))
}
className="w-24"
className="flex-1 sm:w-24 sm:flex-none"
/>
<span className="text-sm text-muted-foreground"></span>
<span className="text-sm text-muted-foreground shrink-0"></span>
</div>
</div>
<div className="space-y-2">
@@ -286,9 +286,9 @@ export function WorkScheduleManagement() {
onChange={(value) =>
setSettings(prev => ({ ...prev, weeklyOvertimeHours: value ?? 0 }))
}
className="w-24"
className="flex-1 sm:w-24 sm:flex-none"
/>
<span className="text-sm text-muted-foreground"></span>
<span className="text-sm text-muted-foreground shrink-0"></span>
</div>
</div>
</div>
@@ -301,13 +301,13 @@ export function WorkScheduleManagement() {
<CardTitle className="text-lg">1 </CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-6">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
<div className="space-y-2">
<Label> </Label>
<TimePicker
value={settings.breakStartTime}
onChange={(value) => setSettings(prev => ({ ...prev, breakStartTime: value }))}
className="w-40"
className="w-full sm:w-40"
minuteStep={1}
/>
</div>
@@ -316,7 +316,7 @@ export function WorkScheduleManagement() {
<TimePicker
value={settings.breakEndTime}
onChange={(value) => setSettings(prev => ({ ...prev, breakEndTime: value }))}
className="w-40"
className="w-full sm:w-40"
minuteStep={1}
/>
</div>