page.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. 'use client';
  2. import { useEffect, useState } from 'react';
  3. import { fetchApi } from '@/lib/utils/client';
  4. import { NotePreview } from '@/types/notification';
  5. import { NoteInboxResponse, NoteDetail } from '@/types/response/note/inbox';
  6. export default function NoteInboxPage() {
  7. const [notes, setNotes] = useState<NotePreview[]>([]);
  8. const [total, setTotal] = useState(0);
  9. const [unreadCount, setUnreadCount] = useState(0);
  10. const [page, setPage] = useState(1);
  11. const [loading, setLoading] = useState(true);
  12. const [selectedNote, setSelectedNote] = useState<NoteDetail|null>(null);
  13. useEffect(() => {
  14. loadNotes();
  15. }, [page]);
  16. const loadNotes = async () => {
  17. setLoading(true);
  18. try {
  19. const res = await fetchApi<NoteInboxResponse>(`/api/note/inbox?pageNum=${page}&perPage=20`);
  20. if (res.data) {
  21. setNotes(res.data.list || []);
  22. setTotal(res.data.total || 0);
  23. setUnreadCount(res.data.unreadCount || 0);
  24. }
  25. } catch {}
  26. setLoading(false);
  27. };
  28. const openNote = async (note: NotePreview) => {
  29. // TODO: 상세 조회 API 호출 + 읽음 처리
  30. setSelectedNote({
  31. id: note.id,
  32. title: note.title,
  33. content: '(쪽지 내용 로딩...)',
  34. senderName: note.senderName || '시스템',
  35. createdAt: note.createdAt
  36. });
  37. if (!note.isRead) {
  38. setNotes(prev => prev.map(n => n.id === note.id ? { ...n, isRead: true } : n));
  39. setUnreadCount(prev => Math.max(0, prev - 1));
  40. }
  41. };
  42. const formatDate = (dateStr: string) => {
  43. const d = new Date(dateStr);
  44. 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')}`;
  45. };
  46. return (
  47. <div className="container mx-auto max-w-2xl p-4">
  48. <div className="flex items-center justify-between mb-4">
  49. <h1 className="text-xl font-bold">받은 쪽지함</h1>
  50. <div className="flex gap-2">
  51. <span className="text-sm text-gray-500">안읽은 쪽지: {unreadCount}건</span>
  52. <a href="/note/send" className="text-sm text-blue-500 hover:underline">쪽지 보내기</a>
  53. </div>
  54. </div>
  55. {loading && <div className="text-center text-gray-500 py-10">로딩 중...</div>}
  56. {!loading && notes.length === 0 && (
  57. <div className="text-center text-gray-500 py-10">쪽지가 없습니다</div>
  58. )}
  59. <div className="flex flex-col gap-2">
  60. {notes.map(note => (
  61. <div key={note.id} onClick={() => openNote(note)} className={`flex items-center gap-3 p-3 rounded-lg cursor-pointer border transition-colors ${note.isRead ? 'bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700' : 'bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800'}`}>
  62. <div className="flex-1 overflow-hidden">
  63. <div className="flex items-center gap-2">
  64. {note.isSystem && <span className="text-xs bg-gray-200 dark:bg-gray-700 px-1.5 py-0.5 rounded">시스템</span>}
  65. <span className={`text-sm ${note.isRead ? 'font-normal' : 'font-semibold'}`}>{note.title}</span>
  66. </div>
  67. <div className="text-xs text-gray-500 mt-1">
  68. {note.senderName || '시스템'} · {formatDate(note.createdAt)}
  69. </div>
  70. </div>
  71. {!note.isRead && <div className="w-2 h-2 rounded-full bg-blue-500 flex-shrink-0" />}
  72. </div>
  73. ))}
  74. </div>
  75. {total > 20 && (
  76. <div className="flex justify-center gap-2 mt-4">
  77. <button type="button" onClick={() => setPage(p => Math.max(1, p - 1))} disabled={page <= 1} className="px-3 py-1 text-sm border rounded disabled:opacity-50">이전</button>
  78. <span className="px-3 py-1 text-sm">{page} / {Math.ceil(total / 20)}</span>
  79. <button type="button" onClick={() => setPage(p => p + 1)} disabled={page >= Math.ceil(total / 20)} className="px-3 py-1 text-sm border rounded disabled:opacity-50">다음</button>
  80. </div>
  81. )}
  82. {/* 쪽지 상세 모달 */}
  83. {selectedNote && (
  84. <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50" onClick={() => setSelectedNote(null)}>
  85. <div className="bg-white dark:bg-gray-900 rounded-xl w-full max-w-md mx-4 p-6" onClick={e => e.stopPropagation()}>
  86. <div className="flex items-center justify-between mb-3">
  87. <h3 className="font-bold text-lg">{selectedNote.title}</h3>
  88. <button type="button" onClick={() => setSelectedNote(null)} className="text-gray-400 hover:text-gray-600 text-xl">×</button>
  89. </div>
  90. <div className="text-xs text-gray-500 mb-3">{selectedNote.senderName} · {formatDate(selectedNote.createdAt)}</div>
  91. <div className="text-sm whitespace-pre-wrap">{selectedNote.content}</div>
  92. </div>
  93. </div>
  94. )}
  95. </div>
  96. );
  97. }