page.tsx 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. 'use client';
  2. import { useCallback, useEffect, useState } from 'react';
  3. import { fetchApi } from '@/lib/utils/client';
  4. import { formatNoteCounterparty } from '@/lib/utils/note';
  5. import Loading from '@/app/component/Loading';
  6. import { NoteOutboxResponse, SentNotePreview } from '@/types/response/note/outbox';
  7. import type { RecipientItem } from '@/types/response/note/searchRecipient';
  8. import NoteComposeModal from '../_components/NoteComposeModal';
  9. import NoteDetailModal from '../_components/NoteDetailModal';
  10. export default function NoteOutboxPage()
  11. {
  12. const [notes, setNotes] = useState<SentNotePreview[]>([]);
  13. const [total, setTotal] = useState(0);
  14. const [page, setPage] = useState(1);
  15. const [loading, setLoading] = useState(true);
  16. const [composeOpen, setComposeOpen] = useState(false);
  17. const [composeInitialReceiver, setComposeInitialReceiver] = useState<RecipientItem|null>(null);
  18. const [composeInitialTitle, setComposeInitialTitle] = useState<string|undefined>(undefined);
  19. const [detailNoteId, setDetailNoteId] = useState<number|null>(null);
  20. const loadNotes = useCallback(async () => {
  21. setLoading(true);
  22. try {
  23. const res = await fetchApi<NoteOutboxResponse>(`/api/note/outbox?page=${page}&perPage=20`);
  24. if (res.data) {
  25. setNotes(res.data.list || []);
  26. setTotal(res.data.total || 0);
  27. }
  28. } catch {}
  29. setLoading(false);
  30. }, [page]);
  31. useEffect(() => {
  32. loadNotes();
  33. }, [loadNotes]);
  34. const openNote = (note: SentNotePreview) => {
  35. setDetailNoteId(note.id);
  36. };
  37. const handleOpenCompose = () => {
  38. setComposeInitialReceiver(null);
  39. setComposeInitialTitle(undefined);
  40. setComposeOpen(true);
  41. };
  42. const handleSent = () => {
  43. loadNotes();
  44. };
  45. const handleDeleted = () => {
  46. loadNotes();
  47. };
  48. const readCount = notes.filter(n => n.isRead).length;
  49. const formatDate = (dateStr: string) => {
  50. const d = new Date(dateStr);
  51. return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')} ${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
  52. };
  53. return (
  54. <>
  55. <div className="note-page__header">
  56. <h1 className="note-page__title">보낸 쪽지함</h1>
  57. <div className="note-page__actions">
  58. <span className="note-page__meta">읽음 {readCount} / {notes.length}건</span>
  59. <button type="button" onClick={handleOpenCompose} className="note-page__compose-btn">
  60. 쪽지 보내기
  61. </button>
  62. </div>
  63. </div>
  64. {loading && <Loading type={2} />}
  65. {!loading && notes.length === 0 && (
  66. <div className="note-page__empty">보낸 쪽지가 없습니다</div>
  67. )}
  68. {!loading && notes.length > 0 && (
  69. <div className="note-page__list">
  70. {notes.map(note => {
  71. const counterparty = formatNoteCounterparty({
  72. name: note.receiverName,
  73. sid: note.receiverSID,
  74. channelName: note.receiverChannelName,
  75. channelHandle: note.receiverChannelHandle
  76. });
  77. return (
  78. <div
  79. key={note.id}
  80. onClick={() => openNote(note)}
  81. className="note-page__item"
  82. >
  83. <div className="note-page__item-body">
  84. <div className="note-page__item-top">
  85. <span className={`note-page__badge${note.isRead ? ' note-page__badge--read' : ''}`}>
  86. {note.isRead ? '읽음' : '안읽음'}
  87. </span>
  88. <span className="note-page__item-title">{note.title}</span>
  89. </div>
  90. <div className="note-page__item-sub">
  91. <span className="note-page__item-party">{counterparty.primary}</span>
  92. {counterparty.secondary && <span className="note-page__item-handle">{counterparty.secondary}</span>}
  93. <span className="note-page__item-time">· {formatDate(note.createdAt)}</span>
  94. </div>
  95. </div>
  96. </div>
  97. );
  98. })}
  99. </div>
  100. )}
  101. {total > 20 && (
  102. <div className="note-page__pager">
  103. <button type="button" onClick={() => setPage(p => Math.max(1, p - 1))} disabled={page <= 1} className="note-page__pager-btn">이전</button>
  104. <span className="note-page__pager-info">{page} / {Math.ceil(total / 20)}</span>
  105. <button type="button" onClick={() => setPage(p => p + 1)} disabled={page >= Math.ceil(total / 20)} className="note-page__pager-btn">다음</button>
  106. </div>
  107. )}
  108. <NoteComposeModal
  109. open={composeOpen}
  110. onClose={() => setComposeOpen(false)}
  111. initialReceiver={composeInitialReceiver}
  112. initialTitle={composeInitialTitle}
  113. onSent={handleSent}
  114. />
  115. <NoteDetailModal
  116. noteId={detailNoteId}
  117. onClose={() => setDetailNoteId(null)}
  118. onDeleted={handleDeleted}
  119. />
  120. </>
  121. );
  122. }