feat: [설정] 설정 관리 전반 UI 개선
- 계정관리 상세/폼 개선 (AccountDetail, AccountDetailForm) - 근태설정, 휴가정책 관리 개선 - 바로빌 연동 회원가입 모달 개선 - 알림설정, 결제이력, 권한관리 UI 개선 - 직급/직책 관리 UI 개선 (RankManagement, TitleManagement) - 구독관리, 근무스케줄 관리 개선 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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={
|
||||||
|
|||||||
@@ -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={
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user