128 lines
6.7 KiB
TypeScript
128 lines
6.7 KiB
TypeScript
import { useEffect, useState } from 'react'
|
||
import { scheduleCalendar, createSchedule, scheduleDetail, deleteSchedule } from '../../api/uiws'
|
||
import { PageHeader, Panel, Button, Modal, FormField, Input, Spinner } from '../../components/uiws/ui'
|
||
|
||
interface Sched { scheduleId: number; title: string; startDt: string; endDt: string; scheType: string; importanceCd?: string }
|
||
|
||
const WEEK = ['일', '월', '화', '수', '목', '금', '토']
|
||
|
||
export default function ScheduleCalendar() {
|
||
const [base, setBase] = useState(new Date())
|
||
const [type, setType] = useState<'PERSONAL' | 'DEPT'>('PERSONAL')
|
||
const [items, setItems] = useState<Sched[]>([])
|
||
const [loading, setLoading] = useState(false)
|
||
const [showForm, setShowForm] = useState<string | null>(null) // date string
|
||
const [detail, setDetail] = useState<any>(null)
|
||
|
||
const ym = `${base.getFullYear()}-${String(base.getMonth() + 1).padStart(2, '0')}`
|
||
|
||
const load = async () => {
|
||
setLoading(true)
|
||
try {
|
||
const res = await scheduleCalendar({ type, view: 'month', baseDate: `${ym}-01` })
|
||
setItems(res.data.data ?? [])
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
useEffect(() => { load() }, [ym, type])
|
||
|
||
const first = new Date(base.getFullYear(), base.getMonth(), 1)
|
||
const startPad = first.getDay()
|
||
const daysInMonth = new Date(base.getFullYear(), base.getMonth() + 1, 0).getDate()
|
||
const cells: (number | null)[] = [...Array(startPad).fill(null), ...Array.from({ length: daysInMonth }, (_, i) => i + 1)]
|
||
|
||
const byDay = (d: number) => {
|
||
const ds = `${ym}-${String(d).padStart(2, '0')}`
|
||
return items.filter(s => (s.startDt ?? '').slice(0, 10) <= ds && ds <= (s.endDt ?? '').slice(0, 10))
|
||
}
|
||
|
||
const move = (delta: number) => setBase(new Date(base.getFullYear(), base.getMonth() + delta, 1))
|
||
|
||
return (
|
||
<div className="uiws-scope">
|
||
<PageHeader title="일정" subtitle="UIWS 이식 · 개인/부서 일정 달력"
|
||
actions={<Button onClick={() => setShowForm(`${ym}-01`)}>+ 일정 등록</Button>} />
|
||
|
||
<Panel style={{ display: 'flex', gap: 10, alignItems: 'center', marginBottom: 16 }}>
|
||
<Button variant="ghost" onClick={() => move(-1)}>‹ 이전</Button>
|
||
<div style={{ fontWeight: 700, minWidth: 130, textAlign: 'center', color: 'var(--uiws-text)' }}>{ym}</div>
|
||
<Button variant="ghost" onClick={() => move(1)}>다음 ›</Button>
|
||
<div style={{ flex: 1 }} />
|
||
<Button variant={type === 'PERSONAL' ? 'primary' : 'ghost'} onClick={() => setType('PERSONAL')}>개인</Button>
|
||
<Button variant={type === 'DEPT' ? 'primary' : 'ghost'} onClick={() => setType('DEPT')}>부서</Button>
|
||
</Panel>
|
||
|
||
{loading ? <Spinner /> : (
|
||
<div style={{ background: 'var(--uiws-surface)', border: '1px solid var(--uiws-border)', borderRadius: 12, overflow: 'hidden' }}>
|
||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7,1fr)' }}>
|
||
{WEEK.map((w, i) => (
|
||
<div key={w} style={{ padding: '10px 0', textAlign: 'center', fontSize: 12, fontWeight: 600,
|
||
color: i === 0 ? 'var(--uiws-danger)' : 'var(--uiws-text-muted)', borderBottom: '1px solid var(--uiws-border)' }}>{w}</div>
|
||
))}
|
||
{cells.map((d, i) => (
|
||
<div key={i} onClick={() => d && setShowForm(`${ym}-${String(d).padStart(2, '0')}`)}
|
||
style={{ minHeight: 96, padding: 6, borderRight: '1px solid var(--uiws-border)', borderBottom: '1px solid var(--uiws-border)',
|
||
cursor: d ? 'pointer' : 'default' }}>
|
||
{d && <div style={{ fontSize: 12, color: 'var(--uiws-text-muted)', marginBottom: 4 }}>{d}</div>}
|
||
{d && byDay(d).slice(0, 3).map(s => (
|
||
<div key={s.scheduleId} onClick={e => { e.stopPropagation(); scheduleDetail(s.scheduleId).then(r => setDetail(r.data.data)) }}
|
||
style={{ fontSize: 11, padding: '2px 6px', borderRadius: 6, marginBottom: 3,
|
||
background: 'var(--uiws-primary-soft)', color: 'var(--uiws-primary)', overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>
|
||
{s.title}
|
||
</div>
|
||
))}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{showForm && <ScheduleForm date={showForm} type={type} onClose={() => setShowForm(null)} onSaved={() => { setShowForm(null); load() }} />}
|
||
{detail && <ScheduleDetailModal detail={detail} onClose={() => setDetail(null)} onDeleted={() => { setDetail(null); load() }} />}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// datetime-local(yyyy-MM-ddTHH:mm) → API 계약(yyyy-MM-ddTHH:mm:ss) 보정: 초 없으면 :00 부가
|
||
function toApiDt(v: string): string {
|
||
if (!v) return v
|
||
return v.length === 16 ? `${v}:00` : v
|
||
}
|
||
|
||
function ScheduleForm({ date, type, onClose, onSaved }: { date: string; type: string; onClose: () => void; onSaved: () => void }) {
|
||
const [title, setTitle] = useState('')
|
||
const [startDt, setStartDt] = useState(`${date}T09:00`)
|
||
const [endDt, setEndDt] = useState(`${date}T18:00`)
|
||
const [content, setContent] = useState('')
|
||
const [err, setErr] = useState('')
|
||
const save = async () => {
|
||
setErr('')
|
||
try {
|
||
await createSchedule({ scheType: type, title, startDt: toApiDt(startDt), endDt: toApiDt(endDt), content, attachmentIds: [] })
|
||
onSaved()
|
||
} catch (e: any) { setErr(e?.response?.data?.message || '저장 실패') }
|
||
}
|
||
return (
|
||
<Modal title="일정 등록" onClose={onClose}
|
||
footer={<><Button variant="ghost" onClick={onClose}>취소</Button><Button onClick={save}>저장</Button></>}>
|
||
<FormField label="제목"><Input value={title} onChange={setTitle} style={{ width: '100%' }} /></FormField>
|
||
<FormField label="시작"><Input type="datetime-local" value={startDt} onChange={setStartDt} style={{ width: '100%' }} /></FormField>
|
||
<FormField label="종료"><Input type="datetime-local" value={endDt} onChange={setEndDt} style={{ width: '100%' }} /></FormField>
|
||
<FormField label="내용"><Input value={content} onChange={setContent} style={{ width: '100%' }} /></FormField>
|
||
{err && <div style={{ color: 'var(--uiws-danger)', fontSize: 13 }}>{err}</div>}
|
||
</Modal>
|
||
)
|
||
}
|
||
|
||
function ScheduleDetailModal({ detail, onClose, onDeleted }: { detail: any; onClose: () => void; onDeleted: () => void }) {
|
||
const remove = async () => { await deleteSchedule(detail.scheduleId); onDeleted() }
|
||
return (
|
||
<Modal title={detail.title} onClose={onClose}
|
||
footer={<><Button variant="danger" onClick={remove}>삭제</Button><Button variant="ghost" onClick={onClose}>닫기</Button></>}>
|
||
<div style={{ fontSize: 13, color: 'var(--uiws-text-muted)', marginBottom: 8 }}>{detail.scheType} · {detail.startDt} ~ {detail.endDt}</div>
|
||
<div style={{ fontSize: 14, color: 'var(--uiws-text)' }}>{detail.content || '내용 없음'}</div>
|
||
</Modal>
|
||
)
|
||
}
|