page.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  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 { NotePreview } from '@/types/notification';
  7. import { NoteInboxResponse } from '@/types/response/note/inbox';
  8. import type { RecipientItem } from '@/types/response/note/searchRecipient';
  9. import NoteComposeModal from '../_components/NoteComposeModal';
  10. import NoteDetailModal from '../_components/NoteDetailModal';
  11. export default function NoteInboxPage()
  12. {
  13. const [notes, setNotes] = useState<NotePreview[]>([]);
  14. const [total, setTotal] = useState(0);
  15. const [unreadCount, setUnreadCount] = useState(0);
  16. const [page, setPage] = useState(1);
  17. const [loading, setLoading] = useState(true);
  18. const [composeOpen, setComposeOpen] = useState(false);
  19. const [composeInitialReceiver, setComposeInitialReceiver] = useState<RecipientItem|null>(null);
  20. const [composeInitialTitle, setComposeInitialTitle] = useState<string|undefined>(undefined);
  21. const [detailNoteId, setDetailNoteId] = useState<number|null>(null);
  22. const loadNotes = useCallback(async () => {
  23. setLoading(true);
  24. try {
  25. const res = await fetchApi<NoteInboxResponse>(`/api/note/inbox?page=${page}&perPage=20`);
  26. if (res.data) {
  27. setNotes(res.data.list || []);
  28. setTotal(res.data.total || 0);
  29. setUnreadCount(res.data.unreadCount || 0);
  30. }
  31. } catch {}
  32. setLoading(false);
  33. }, [page]);
  34. useEffect(() => {
  35. loadNotes();
  36. }, [loadNotes]);
  37. const openNote = (note: NotePreview) => {
  38. setDetailNoteId(note.id);
  39. if (!note.isRead) {
  40. setNotes(prev => prev.map(n => n.id === note.id ? { ...n, isRead: true } : n));
  41. setUnreadCount(prev => Math.max(0, prev - 1));
  42. }
  43. };
  44. const handleOpenCompose = () => {
  45. setComposeInitialReceiver(null);
  46. setComposeInitialTitle(undefined);
  47. setComposeOpen(true);
  48. };
  49. const handleReply = (sender: RecipientItem, originalTitle: string) => {
  50. setDetailNoteId(null);
  51. setComposeInitialReceiver(sender);
  52. setComposeInitialTitle(`RE: ${originalTitle}`);
  53. setComposeOpen(true);
  54. };
  55. const handleSent = () => {
  56. loadNotes();
  57. };
  58. const handleDeleted = () => {
  59. loadNotes();
  60. };
  61. const formatDate = (dateStr: string) => {
  62. const d = new Date(dateStr);
  63. 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')}`;
  64. };
  65. return (
  66. <>
  67. <div className="note-page__header">
  68. <h1 className="note-page__title">받은 쪽지함</h1>
  69. <div className="note-page__actions">
  70. <span className="note-page__meta">안읽은 쪽지: {unreadCount}건</span>
  71. <button type="button" onClick={handleOpenCompose} className="note-page__compose-btn">
  72. 쪽지 보내기
  73. </button>
  74. </div>
  75. </div>
  76. {loading && <Loading type={2} />}
  77. {!loading && notes.length === 0 && (
  78. <div className="note-page__empty">쪽지가 없습니다</div>
  79. )}
  80. {!loading && notes.length > 0 && (
  81. <div className="note-page__list">
  82. {notes.map(note => {
  83. const counterparty = formatNoteCounterparty({
  84. isSystem: note.isSystem,
  85. name: note.senderName,
  86. sid: note.senderSID,
  87. channelName: note.senderChannelName,
  88. channelHandle: note.senderChannelHandle
  89. });
  90. return (
  91. <div
  92. key={note.id}
  93. onClick={() => openNote(note)}
  94. className={`note-page__item${note.isRead ? '' : ' note-page__item--unread'}`}
  95. >
  96. <div className="note-page__item-body">
  97. <div className="note-page__item-top">
  98. {note.isSystem && <span className="note-page__badge">시스템</span>}
  99. <span className={`note-page__item-title${note.isRead ? '' : ' note-page__item-title--unread'}`}>{note.title}</span>
  100. </div>
  101. <div className="note-page__item-sub">
  102. <span className="note-page__item-party">{counterparty.primary}</span>
  103. {counterparty.secondary && <span className="note-page__item-handle">{counterparty.secondary}</span>}
  104. <span className="note-page__item-time">· {formatDate(note.createdAt)}</span>
  105. </div>
  106. </div>
  107. {!note.isRead && <div className="note-page__item-dot" />}
  108. </div>
  109. );
  110. })}
  111. </div>
  112. )}
  113. {total > 20 && (
  114. <div className="note-page__pager">
  115. <button type="button" onClick={() => setPage(p => Math.max(1, p - 1))} disabled={page <= 1} className="note-page__pager-btn">이전</button>
  116. <span className="note-page__pager-info">{page} / {Math.ceil(total / 20)}</span>
  117. <button type="button" onClick={() => setPage(p => p + 1)} disabled={page >= Math.ceil(total / 20)} className="note-page__pager-btn">다음</button>
  118. </div>
  119. )}
  120. <NoteComposeModal
  121. open={composeOpen}
  122. onClose={() => setComposeOpen(false)}
  123. initialReceiver={composeInitialReceiver}
  124. initialTitle={composeInitialTitle}
  125. onSent={handleSent}
  126. />
  127. <NoteDetailModal
  128. noteId={detailNoteId}
  129. onClose={() => setDetailNoteId(null)}
  130. onReply={handleReply}
  131. onDeleted={handleDeleted}
  132. />
  133. </>
  134. );
  135. }