zioinfo-esn/frontend/src/pages/uiws/MessageBox.tsx
DESKTOP-TKLFCPR\ython 707416f78c feat(uiws): UIWS 업무모듈(업무일지·일정·쪽지·통계)+2FA 이식
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 21:10:47 +09:00

119 lines
5.4 KiB
TypeScript

import { useEffect, useState } from 'react'
import { listReceived, listSent, receivedDetail, sentDetail, sendMessage } from '../../api/uiws'
import { PageHeader, SearchBar, Input, Button, DataGrid, Pagination, Modal, FormField, YnBadge, Spinner, type Column } from '../../components/uiws/ui'
type Tab = 'received' | 'sent'
export default function MessageBox() {
const [tab, setTab] = useState<Tab>('received')
const [rows, setRows] = useState<any[]>([])
const [page, setPage] = useState(0)
const [totalPages, setTotalPages] = useState(0)
const [keyword, setKeyword] = useState('')
const [loading, setLoading] = useState(false)
const [compose, setCompose] = useState(false)
const [detail, setDetail] = useState<any>(null)
const load = async () => {
setLoading(true)
try {
const fn = tab === 'received' ? listReceived : listSent
const res = await fn({ page, size: 20, titleKeyword: keyword || undefined })
const data = res.data.data
setRows(data.content ?? [])
setTotalPages(data.totalPages ?? 0)
} finally {
setLoading(false)
}
}
useEffect(() => { load() }, [tab, page])
const receivedCols: Column<any>[] = [
{ key: 'title', header: '제목' },
{ key: 'senderNm', header: '보낸사람', width: 130 },
{ key: 'sentAt', header: '받은시각', width: 170 },
{ key: 'readYn', header: '상태', width: 90, render: r => <YnBadge yn={r.readYn} /> },
]
const sentCols: Column<any>[] = [
{ key: 'title', header: '제목' },
{ key: 'receiverSummary', header: '받는사람', width: 160 },
{ key: 'open', header: '개봉', width: 100, align: 'center', render: r => `${r.openCount}/${r.totalCount}` },
{ key: 'sentAt', header: '보낸시각', width: 170 },
]
const open = async (r: any) => {
const res = tab === 'received' ? await receivedDetail(r.messageId) : await sentDetail(r.messageId)
setDetail({ ...res.data.data, _tab: tab })
if (tab === 'received') load() // 개봉처리 반영
}
return (
<div className="uiws-scope">
<PageHeader title="쪽지" subtitle="UIWS 이식 · 사내 쪽지 송수신"
actions={<Button onClick={() => setCompose(true)}>+ </Button>} />
<SearchBar>
<Button variant={tab === 'received' ? 'primary' : 'ghost'} onClick={() => { setTab('received'); setPage(0) }}></Button>
<Button variant={tab === 'sent' ? 'primary' : 'ghost'} onClick={() => { setTab('sent'); setPage(0) }}></Button>
<div style={{ flex: 1 }} />
<Input value={keyword} onChange={setKeyword} placeholder="제목 검색" style={{ minWidth: 200 }} />
<Button variant="ghost" onClick={() => { setPage(0); load() }}></Button>
</SearchBar>
{loading ? <Spinner /> : (
<>
<DataGrid columns={tab === 'received' ? receivedCols : sentCols} rows={rows} rowKey={r => r.messageId} onRowClick={open} empty="쪽지가 없습니다." />
<Pagination page={page} totalPages={totalPages} onChange={setPage} />
</>
)}
{compose && <Compose onClose={() => setCompose(false)} onSent={() => { setCompose(false); load() }} />}
{detail && <DetailModal detail={detail} onClose={() => setDetail(null)} />}
</div>
)
}
function Compose({ onClose, onSent }: { onClose: () => void; onSent: () => void }) {
const [receiverId, setReceiverId] = useState('')
const [title, setTitle] = useState('')
const [content, setContent] = useState('')
const [err, setErr] = useState('')
const send = async () => {
setErr('')
try {
await sendMessage({ title, content, receivers: [{ receiverId, rcvType: 'RECV' }] })
onSent()
} catch (e: any) { setErr(e?.response?.data?.message || '전송 실패') }
}
return (
<Modal title="쪽지 보내기" onClose={onClose}
footer={<><Button variant="ghost" onClick={onClose}></Button><Button onClick={send}></Button></>}>
<FormField label="받는사람 ID"><Input value={receiverId} onChange={setReceiverId} style={{ width: '100%' }} /></FormField>
<FormField label="제목"><Input value={title} onChange={setTitle} 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 DetailModal({ detail, onClose }: { detail: any; onClose: () => void }) {
return (
<Modal title={detail.title} onClose={onClose} footer={<Button variant="ghost" onClick={onClose}></Button>}>
<div style={{ fontSize: 13, color: 'var(--uiws-text-muted)', marginBottom: 10 }}>
{detail._tab === 'received' ? `보낸사람: ${detail.senderNm}` : `개봉 ${detail.openCount}/${detail.totalCount}`} · {detail.sentAt}
</div>
<div style={{ fontSize: 14, color: 'var(--uiws-text)', whiteSpace: 'pre-wrap' }}>{detail.content}</div>
{detail._tab === 'sent' && detail.receivers && (
<div style={{ marginTop: 14 }}>
<div style={{ fontSize: 12, color: 'var(--uiws-text-muted)', marginBottom: 6 }}> </div>
{detail.receivers.map((r: any, i: number) => (
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13, padding: '4px 0' }}>
<span>{r.receiverNm} ({r.rcvType})</span><YnBadge yn={r.readYn} />
</div>
))}
</div>
)}
</Modal>
)
}