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="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"> <div className="flex items-center gap-2">
<Checkbox <Checkbox
id="email-consent" id="email-consent"
@@ -371,7 +371,7 @@ export function AccountInfoClient({
</div> </div>
{/* SMS 수신 동의 */} {/* 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"> <div className="flex items-center gap-2">
<Checkbox <Checkbox
id="sms-consent" id="sms-consent"

View File

@@ -21,6 +21,7 @@ import { useDeleteDialog } from '@/hooks/useDeleteDialog';
import { PageLayout } from '@/components/organisms/PageLayout'; import { PageLayout } from '@/components/organisms/PageLayout';
import { PageHeader } from '@/components/organisms/PageHeader'; import { PageHeader } from '@/components/organisms/PageHeader';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useMenuStore } from '@/stores/menuStore';
import type { Account, AccountFormData, AccountStatus } from './types'; import type { Account, AccountFormData, AccountStatus } from './types';
import { import {
BANK_OPTIONS, BANK_OPTIONS,
@@ -39,6 +40,7 @@ interface AccountDetailProps {
export function AccountDetail({ account, mode: initialMode }: AccountDetailProps) { export function AccountDetail({ account, mode: initialMode }: AccountDetailProps) {
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 deleteDialog = useDeleteDialog({ const deleteDialog = useDeleteDialog({
onDelete: async (id) => deleteBankAccount(Number(id)), onDelete: async (id) => deleteBankAccount(Number(id)),
@@ -185,22 +187,28 @@ export function AccountDetail({ account, mode: initialMode }: AccountDetailProps
</CardContent> </CardContent>
</Card> </Card>
{/* 버튼 영역 */} </div>
<div className="flex items-center justify-between">
<Button variant="outline" onClick={handleBack}> {/* 하단 액션 버튼 (sticky) */}
<ArrowLeft 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={() => 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> </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>
</div> </div>
@@ -330,17 +338,18 @@ export function AccountDetail({ account, mode: initialMode }: AccountDetailProps
</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}> <span className="hidden md:inline"></span>
<Save className="w-4 h-4 mr-2" /> </Button>
{isCreateMode ? '등록' : '저장'} <Button onClick={handleSubmit} size="sm" className="md:size-default">
</Button> <Save className="h-4 w-4 md:mr-2" />
</div> <span className="hidden md:inline">{isCreateMode ? '등록' : '저장'}</span>
</Button>
</div> </div>
</PageLayout> </PageLayout>
); );

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -120,7 +120,7 @@ export function LeavePolicyManagement() {
<CardContent className="p-6"> <CardContent className="p-6">
<h3 className="text-lg font-semibold mb-6"> </h3> <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"> <div className="space-y-2">
<Label></Label> <Label></Label>
@@ -194,7 +194,7 @@ export function LeavePolicyManagement() {
<CardContent className="p-6"> <CardContent className="p-6">
<h3 className="text-lg font-semibold mb-6"> </h3> <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"> <div className="space-y-2">
<Label> </Label> <Label> </Label>
@@ -269,7 +269,7 @@ export function LeavePolicyManagement() {
</div> </div>
{settings.carryOverEnabled && ( {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"> <div className="space-y-2">
<Label> </Label> <Label> </Label>

View File

@@ -133,7 +133,7 @@ export function ItemSettingsDialog({ isOpen, onClose, settings, onSave }: ItemSe
return ( return (
<Dialog open={isOpen} onOpenChange={handleOpenChange}> <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"> <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"> <div className="flex items-center justify-between">

View File

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

View File

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

View File

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

View File

@@ -533,9 +533,9 @@ export function PermissionDetailClient({ permissionId, isNew = false, mode = 'vi
{!isNew && ( {!isNew && (
<Card> <Card>
<CardContent className="p-6"> <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> <h3 className="text-lg font-semibold"> </h3>
<div className="flex gap-2"> <div className="flex flex-wrap gap-2">
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"

View File

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

View File

@@ -3,7 +3,7 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { PageLayout } from '@/components/organisms/PageLayout'; import { PageLayout } from '@/components/organisms/PageLayout';
import { PageHeader } from '@/components/organisms/PageHeader'; 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 { ReorderButtons } from '@/components/molecules';
import { ContentSkeleton } from '@/components/ui/skeleton'; import { ContentSkeleton } from '@/components/ui/skeleton';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@@ -270,33 +270,59 @@ export function RankManagement() {
onDragStart={(e) => handleDragStart(e, index)} onDragStart={(e) => handleDragStart(e, index)}
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
onDragOver={(e) => handleDragOver(e, index)} 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' : '' draggedItem === index ? 'opacity-50 bg-muted' : ''
}`} }`}
> >
{/* 드래그 핸들 (PC만) */} <div className="flex items-center gap-3">
<GripVertical className="h-5 w-5 text-muted-foreground flex-shrink-0 hidden md:block" /> {/* 드래그 핸들 (PC만) */}
<GripVertical className="h-5 w-5 text-muted-foreground flex-shrink-0 hidden md:block" />
{/* 순서 변경 버튼 */} {/* 순서 변경 버튼 */}
<ReorderButtons <ReorderButtons
onMoveUp={() => handleMoveItem(index, index - 1)} onMoveUp={() => handleMoveItem(index, index - 1)}
onMoveDown={() => handleMoveItem(index, index + 1)} onMoveDown={() => handleMoveItem(index, index + 1)}
isFirst={index === 0} isFirst={index === 0}
isLast={index === ranks.length - 1} isLast={index === ranks.length - 1}
disabled={isSubmitting} disabled={isSubmitting}
size="xs" size="xs"
/> />
{/* 순서 번호 */} {/* 순서 번호 */}
<span className="text-sm text-muted-foreground w-8"> <span className="text-sm text-muted-foreground w-8">
{index + 1} {index + 1}
</span> </span>
{/* 직급명 */} {/* 직급명 */}
<span className="flex-1 font-medium">{rank.name}</span> <span className="flex-1 min-w-0 font-medium">{rank.name}</span>
{/* 액션 버튼 */} {/* 액션 버튼 (PC) */}
<div className="flex gap-1"> <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 <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
@@ -304,7 +330,7 @@ export function RankManagement() {
className="h-8 w-8 p-0" className="h-8 w-8 p-0"
disabled={isSubmitting} disabled={isSubmitting}
> >
<Pencil className="h-4 w-4" /> <Edit className="h-4 w-4" />
<span className="sr-only"></span> <span className="sr-only"></span>
</Button> </Button>
<Button <Button

View File

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

View File

@@ -3,7 +3,7 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { PageLayout } from '@/components/organisms/PageLayout'; import { PageLayout } from '@/components/organisms/PageLayout';
import { PageHeader } from '@/components/organisms/PageHeader'; 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 { ReorderButtons } from '@/components/molecules';
import { ContentSkeleton } from '@/components/ui/skeleton'; import { ContentSkeleton } from '@/components/ui/skeleton';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@@ -270,33 +270,59 @@ export function TitleManagement() {
onDragStart={(e) => handleDragStart(e, index)} onDragStart={(e) => handleDragStart(e, index)}
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
onDragOver={(e) => handleDragOver(e, index)} 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' : '' draggedItem === index ? 'opacity-50 bg-muted' : ''
}`} }`}
> >
{/* 드래그 핸들 (PC만) */} <div className="flex items-center gap-3">
<GripVertical className="h-5 w-5 text-muted-foreground flex-shrink-0 hidden md:block" /> {/* 드래그 핸들 (PC만) */}
<GripVertical className="h-5 w-5 text-muted-foreground flex-shrink-0 hidden md:block" />
{/* 순서 변경 버튼 */} {/* 순서 변경 버튼 */}
<ReorderButtons <ReorderButtons
onMoveUp={() => handleMoveItem(index, index - 1)} onMoveUp={() => handleMoveItem(index, index - 1)}
onMoveDown={() => handleMoveItem(index, index + 1)} onMoveDown={() => handleMoveItem(index, index + 1)}
isFirst={index === 0} isFirst={index === 0}
isLast={index === titles.length - 1} isLast={index === titles.length - 1}
disabled={isSubmitting} disabled={isSubmitting}
size="xs" size="xs"
/> />
{/* 순서 번호 */} {/* 순서 번호 */}
<span className="text-sm text-muted-foreground w-8"> <span className="text-sm text-muted-foreground w-8">
{index + 1} {index + 1}
</span> </span>
{/* 직책명 */} {/* 직책명 */}
<span className="flex-1 font-medium">{title.name}</span> <span className="flex-1 min-w-0 font-medium">{title.name}</span>
{/* 액션 버튼 */} {/* 액션 버튼 (PC) */}
<div className="flex gap-1"> <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 <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
@@ -304,7 +330,7 @@ export function TitleManagement() {
className="h-8 w-8 p-0" className="h-8 w-8 p-0"
disabled={isSubmitting} disabled={isSubmitting}
> >
<Pencil className="h-4 w-4" /> <Edit className="h-4 w-4" />
<span className="sr-only"></span> <span className="sr-only"></span>
</Button> </Button>
<Button <Button

View File

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