UI Unification: Refactor MonthlyWorkPage to Notepad design system
This commit is contained in:
@@ -1,14 +1,20 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
Calendar,
|
CalendarDays,
|
||||||
Save,
|
Save,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Loader2,
|
Loader2,
|
||||||
|
Calendar,
|
||||||
|
CheckCircle2,
|
||||||
|
XCircle,
|
||||||
|
FileText,
|
||||||
|
Clock
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { comms } from '@/communication';
|
import { comms } from '@/communication';
|
||||||
import { HolidayItem } from '@/types';
|
import { HolidayItem } from '@/types';
|
||||||
|
import { clsx } from 'clsx';
|
||||||
|
|
||||||
interface DayInfo extends HolidayItem {
|
interface DayInfo extends HolidayItem {
|
||||||
dayOfWeek: number;
|
dayOfWeek: number;
|
||||||
@@ -104,144 +110,200 @@ export function MonthlyWorkPage() {
|
|||||||
const freeDays = holidays.filter(h => h.free).length;
|
const freeDays = holidays.filter(h => h.free).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6 animate-fade-in pb-4 h-full">
|
||||||
{/* 헤더 */}
|
{/* 월별근무표 메인 카드 */}
|
||||||
<div className="glass-effect rounded-2xl p-6">
|
<div className="glass-effect rounded-3xl overflow-hidden shadow-2xl border border-white/10 flex flex-col h-full max-h-[calc(100vh-140px)]">
|
||||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
<div className="px-6 py-4 border-b border-white/10 flex flex-col xl:flex-row items-center justify-between gap-4 bg-white/[0.02] shrink-0">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="p-3 bg-primary-500/20 rounded-xl">
|
<div className="p-2 bg-primary-500/20 rounded-lg">
|
||||||
<Calendar className="w-6 h-6 text-primary-400" />
|
<CalendarDays className="w-5 h-5 text-primary-400" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-white">월별근무표</h1>
|
<h3 className="text-lg font-bold text-white tracking-tight">월별근무표</h3>
|
||||||
<p className="text-white/60 text-sm">휴일 및 근무일 관리</p>
|
<p className="text-white/30 text-[10px] uppercase font-bold tracking-widest mt-0.5">
|
||||||
|
Monthly Company Schedule
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex flex-wrap items-center justify-center gap-3">
|
||||||
{/* 월 선택 */}
|
{/* 월 선택 컨트롤 */}
|
||||||
<div className="flex items-center space-x-2 bg-white/10 rounded-lg px-3 py-2">
|
<div className="flex items-center gap-2 bg-white/5 border border-white/10 rounded-xl px-2 py-1 h-[40px]">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleMonthChange(-1)}
|
onClick={() => handleMonthChange(-1)}
|
||||||
className="p-1 hover:bg-white/10 rounded transition-colors"
|
className="p-1.5 hover:bg-white/10 rounded-lg text-white/50 hover:text-white transition-all"
|
||||||
>
|
>
|
||||||
<ChevronLeft className="w-5 h-5 text-white" />
|
<ChevronLeft className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<input
|
|
||||||
type="month"
|
<div className="relative">
|
||||||
value={month}
|
<Calendar className="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-primary-400" />
|
||||||
onChange={(e) => setMonth(e.target.value)}
|
<input
|
||||||
className="bg-transparent text-white text-center w-32 focus:outline-none"
|
type="month"
|
||||||
/>
|
value={month}
|
||||||
|
onChange={(e) => setMonth(e.target.value)}
|
||||||
|
className="bg-transparent border-none text-xs font-bold text-white pl-7 pr-2 focus:outline-none focus:ring-0 w-28 h-full cursor-pointer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => handleMonthChange(1)}
|
onClick={() => handleMonthChange(1)}
|
||||||
className="p-1 hover:bg-white/10 rounded transition-colors"
|
className="p-1.5 hover:bg-white/10 rounded-lg text-white/50 hover:text-white transition-all"
|
||||||
>
|
>
|
||||||
<ChevronRight className="w-5 h-5 text-white" />
|
<ChevronRight className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 버튼들 */}
|
{/* 빠른 통계 */}
|
||||||
<button
|
<div className="flex items-center gap-1.5 px-3 h-[40px] bg-white/5 border border-white/10 rounded-xl">
|
||||||
onClick={loadData}
|
<div className="flex items-center gap-1.5">
|
||||||
disabled={loading}
|
<div className="w-1.5 h-1.5 rounded-full bg-primary-400" />
|
||||||
className="flex items-center space-x-2 px-4 py-2 bg-white/10 hover:bg-white/20 rounded-lg text-white transition-colors disabled:opacity-50"
|
<span className="text-[10px] text-white/30 uppercase font-bold">Duty</span>
|
||||||
>
|
<span className="text-xs font-bold text-white ml-0.5">{workDays}</span>
|
||||||
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
|
</div>
|
||||||
<span className="hidden sm:inline">새로고침</span>
|
<div className="w-px h-3 bg-white/10 mx-1" />
|
||||||
</button>
|
<div className="flex items-center gap-1.5">
|
||||||
|
<div className="w-1.5 h-1.5 rounded-full bg-red-400" />
|
||||||
|
<span className="text-[10px] text-white/30 uppercase font-bold">Free</span>
|
||||||
|
<span className="text-xs font-bold text-white ml-0.5">{freeDays}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<div className="flex items-center gap-2">
|
||||||
onClick={handleSave}
|
{/* 새로고침 */}
|
||||||
disabled={saving || !hasChanges}
|
<button
|
||||||
className="flex items-center space-x-2 px-4 py-2 bg-primary-500 hover:bg-primary-600 rounded-lg text-white transition-colors disabled:opacity-50"
|
onClick={loadData}
|
||||||
>
|
disabled={loading}
|
||||||
{saving ? (
|
className="p-2 bg-white/5 hover:bg-white/10 border border-white/10 rounded-xl text-white/70 hover:text-white transition-all disabled:opacity-50 h-[40px] w-[40px] flex items-center justify-center"
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
title="새로고침"
|
||||||
) : (
|
>
|
||||||
<Save className="w-4 h-4" />
|
<RefreshCw className={clsx("w-4 h-4", loading && "animate-spin")} />
|
||||||
)}
|
</button>
|
||||||
<span>저장</span>
|
|
||||||
</button>
|
{/* 저장 버튼 */}
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={saving || !hasChanges}
|
||||||
|
className={clsx(
|
||||||
|
"flex items-center gap-2 px-4 py-2 rounded-xl font-bold text-xs transition-all active:scale-95 h-[40px]",
|
||||||
|
hasChanges
|
||||||
|
? "bg-primary-500 hover:bg-primary-600 text-white shadow-lg shadow-primary-500/20 border border-white/20"
|
||||||
|
: "bg-white/5 text-white/20 border border-white/5 cursor-not-allowed"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{saving ? (
|
||||||
|
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Save className="w-3.5 h-3.5" />
|
||||||
|
)}
|
||||||
|
<span>표준 저장</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 통계 */}
|
{/* 리스트 헤더 */}
|
||||||
<div className="mt-4 flex items-center space-x-6 text-sm">
|
<div className="bg-white/5 px-6 py-3 border-b border-white/5 flex items-center text-[10px] font-bold text-white/30 uppercase tracking-widest shrink-0">
|
||||||
<span className="text-white/70">
|
<div className="w-32 px-4">날짜 (Date)</div>
|
||||||
근무일: <span className="text-white font-semibold">{workDays}일</span>
|
<div className="w-20 px-4 text-center">요일</div>
|
||||||
</span>
|
<div className="w-24 px-4 text-center">휴일 지정</div>
|
||||||
<span className="text-white/70">
|
<div className="flex-1 px-4">업무 및 특이사항 (Memo)</div>
|
||||||
휴일: <span className="text-danger-400 font-semibold">{freeDays}일</span>
|
|
||||||
</span>
|
|
||||||
<span className="text-white/70">
|
|
||||||
총: <span className="text-white font-semibold">{holidays.length}일</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 테이블 */}
|
<div className="flex-1 overflow-y-auto custom-scrollbar divide-y divide-white/5">
|
||||||
<div className="glass-effect rounded-2xl overflow-hidden">
|
{loading ? (
|
||||||
{loading ? (
|
<div className="py-20 text-center">
|
||||||
<div className="flex items-center justify-center py-20">
|
<RefreshCw className="w-10 h-10 mx-auto mb-4 animate-spin text-primary-500/50" />
|
||||||
<Loader2 className="w-8 h-8 text-white animate-spin" />
|
<p className="text-white/50 font-medium text-sm">근무표 데이터를 불러오는 중...</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : holidays.length === 0 ? (
|
||||||
<div className="overflow-x-auto">
|
<div className="py-32 text-center">
|
||||||
<table className="w-full">
|
<Calendar className="w-16 h-16 mx-auto text-white/10 mb-4" />
|
||||||
<thead className="bg-white/10">
|
<p className="text-white/30 text-base font-bold">근무표가 생성되지 않았습니다</p>
|
||||||
<tr>
|
<p className="text-white/10 text-[10px] mt-2 uppercase tracking-[0.2em]">No schedule data found</p>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase w-32">날짜</th>
|
</div>
|
||||||
<th className="px-4 py-3 text-center text-xs font-medium text-white/70 uppercase w-20">요일</th>
|
) : (
|
||||||
<th className="px-4 py-3 text-center text-xs font-medium text-white/70 uppercase w-24">휴일</th>
|
holidays.map((day) => (
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase">메모</th>
|
<div
|
||||||
</tr>
|
key={day.idx}
|
||||||
</thead>
|
className={clsx(
|
||||||
<tbody className="divide-y divide-white/5">
|
"px-6 py-2.5 hover:bg-white/[0.03] transition-all group flex items-center",
|
||||||
{holidays.map((day) => (
|
day.dayOfWeek === 0 && "bg-red-500/[0.03]",
|
||||||
<tr
|
day.dayOfWeek === 6 && "bg-blue-500/[0.03]"
|
||||||
key={day.idx}
|
)}
|
||||||
className={`hover:bg-white/5 transition-colors ${
|
>
|
||||||
day.dayOfWeek === 0 ? 'bg-danger-500/10' :
|
{/* 날짜 */}
|
||||||
day.dayOfWeek === 6 ? 'bg-primary-500/10' : ''
|
<div className="w-32 px-4">
|
||||||
}`}
|
<div className="flex items-center gap-2">
|
||||||
>
|
<Clock className="w-3 h-3 text-white/20" />
|
||||||
<td className="px-4 py-3 text-white text-sm">
|
<span className="text-sm font-mono tracking-tight text-white/70">
|
||||||
{day.pdate}
|
{day.pdate}
|
||||||
</td>
|
</span>
|
||||||
<td className={`px-4 py-3 text-center text-sm font-medium ${
|
</div>
|
||||||
day.dayOfWeek === 0 ? 'text-danger-400' :
|
</div>
|
||||||
day.dayOfWeek === 6 ? 'text-primary-400' : 'text-white/70'
|
|
||||||
}`}>
|
{/* 요일 */}
|
||||||
{day.dayName}
|
<div className="w-20 px-4 text-center">
|
||||||
</td>
|
<span className={clsx(
|
||||||
<td className="px-4 py-3 text-center">
|
"text-xs font-bold",
|
||||||
<button
|
day.dayOfWeek === 0 ? "text-red-400" :
|
||||||
onClick={() => handleToggleFree(day.idx)}
|
day.dayOfWeek === 6 ? "text-blue-400" : "text-white/30"
|
||||||
className={`w-8 h-8 rounded-lg transition-colors ${
|
)}>
|
||||||
day.free
|
{day.dayName}
|
||||||
? 'bg-danger-500/20 text-danger-400 hover:bg-danger-500/30'
|
</span>
|
||||||
: 'bg-white/10 text-white/40 hover:bg-white/20'
|
</div>
|
||||||
}`}
|
|
||||||
>
|
{/* 휴일지정 */}
|
||||||
{day.free ? 'O' : '-'}
|
<div className="w-24 px-4 flex justify-center">
|
||||||
</button>
|
<button
|
||||||
</td>
|
onClick={() => handleToggleFree(day.idx)}
|
||||||
<td className="px-4 py-3">
|
className={clsx(
|
||||||
<input
|
"flex items-center justify-center w-8 h-8 rounded-xl border transition-all active:scale-90",
|
||||||
type="text"
|
day.free
|
||||||
value={day.memo || ''}
|
? "bg-red-500/10 border-red-500/30 text-red-400 shadow-lg shadow-red-500/10"
|
||||||
onChange={(e) => handleMemoChange(day.idx, e.target.value)}
|
: "bg-white/5 border-white/5 text-white/10 hover:border-white/10 hover:text-white/30"
|
||||||
placeholder="메모 입력..."
|
)}
|
||||||
className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-1.5 text-white text-sm focus:outline-none focus:border-primary-500"
|
title={day.free ? "휴일 해제" : "휴일 지정"}
|
||||||
/>
|
>
|
||||||
</td>
|
{day.free ? <CheckCircle2 className="w-4 h-4" /> : <XCircle className="w-4 h-4" />}
|
||||||
</tr>
|
</button>
|
||||||
))}
|
</div>
|
||||||
</tbody>
|
|
||||||
</table>
|
{/* 메모 */}
|
||||||
|
<div className="flex-1 px-4">
|
||||||
|
<div className="relative group/input">
|
||||||
|
<FileText className="absolute left-3 top-1/2 -translate-y-1/2 w-3 h-3 text-white/10 group-focus-within/input:text-primary-500 transition-colors" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={day.memo || ''}
|
||||||
|
onChange={(e) => handleMemoChange(day.idx, e.target.value)}
|
||||||
|
placeholder="메모를 입력하세요..."
|
||||||
|
className="w-full bg-white/5 border border-white/5 rounded-xl pl-9 pr-4 py-1.5 text-xs text-white placeholder:text-white/5 focus:outline-none focus:bg-white/10 focus:border-primary-500/30 transition-all font-medium"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 푸터 */}
|
||||||
|
<div className="px-6 py-2 flex items-center justify-between bg-white/[0.02] border-t border-white/5 shrink-0">
|
||||||
|
<div className="text-white/20 text-[9px] font-bold uppercase tracking-[0.2em] py-2">
|
||||||
|
Company Schedule Hub <span className="text-white/5 mx-2">/</span>
|
||||||
|
Month <span className="text-primary-400/50 font-mono tracking-normal">{month}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="text-white/20 text-[9px] flex items-center gap-4">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<div className="w-1 h-1 rounded-full bg-red-400" />
|
||||||
|
<span>Sunday (휴일)</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<div className="w-1 h-1 rounded-full bg-blue-400" />
|
||||||
|
<span>Saturday (토요일)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user