zioinfo-esn/frontend/src/pages/uiws/ScheduleCalendar.tsx
DESKTOP-TKLFCPR\ython d566edd9d6 feat(ux): 날짜 입력 캘린더 전환 - 일정등록 datetime-local (surgical, 캘린더 파일만)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 08:43:18 +09:00

128 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
)
}