summaryrefslogtreecommitdiff
path: root/lib/techsales-rfq/table/detail-table/vendor-communication-drawer.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-05-29 08:17:25 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-05-29 08:17:25 +0000
commit0257350f55c00735cadbd5b507ef5cc9cd3adb10 (patch)
tree367056ce31ebf6aa01e2648701b76baaaa6fb484 /lib/techsales-rfq/table/detail-table/vendor-communication-drawer.tsx
parente484964b1d78cedabbe182c789a8e4c9b53e29d3 (diff)
(김준회) 기술영업 조선 RFQ - 캐시 문제 대응, 코멘트 기능 UX 향상
Diffstat (limited to 'lib/techsales-rfq/table/detail-table/vendor-communication-drawer.tsx')
-rw-r--r--lib/techsales-rfq/table/detail-table/vendor-communication-drawer.tsx140
1 files changed, 119 insertions, 21 deletions
diff --git a/lib/techsales-rfq/table/detail-table/vendor-communication-drawer.tsx b/lib/techsales-rfq/table/detail-table/vendor-communication-drawer.tsx
index 958cc8d1..4172ccd7 100644
--- a/lib/techsales-rfq/table/detail-table/vendor-communication-drawer.tsx
+++ b/lib/techsales-rfq/table/detail-table/vendor-communication-drawer.tsx
@@ -15,7 +15,6 @@ import {
DrawerHeader,
DrawerTitle,
} from "@/components/ui/drawer"
-import { ScrollArea } from "@/components/ui/scroll-area"
import { Badge } from "@/components/ui/badge"
import { toast } from "sonner"
import {
@@ -143,6 +142,11 @@ export function VendorCommunicationDrawer({
const fileInputRef = useRef<HTMLInputElement>(null);
const messagesEndRef = useRef<HTMLDivElement>(null);
+ // 자동 새로고침 관련 상태
+ const [autoRefresh, setAutoRefresh] = useState(true);
+ const [lastMessageCount, setLastMessageCount] = useState(0);
+ const intervalRef = useRef<NodeJS.Timeout | null>(null);
+
// 첨부파일 관련 상태
const [previewDialogOpen, setPreviewDialogOpen] = useState(false);
const [selectedAttachment, setSelectedAttachment] = useState<Attachment | null>(null);
@@ -151,8 +155,20 @@ export function VendorCommunicationDrawer({
useEffect(() => {
if (open && selectedRfq && selectedVendor) {
loadComments();
+ // 자동 새로고침 시작
+ if (autoRefresh) {
+ startAutoRefresh();
+ }
+ } else {
+ // 드로어가 닫히면 자동 새로고침 중지
+ stopAutoRefresh();
}
- }, [open, selectedRfq, selectedVendor]);
+
+ // 컴포넌트 언마운트 시 정리
+ return () => {
+ stopAutoRefresh();
+ };
+ }, [open, selectedRfq, selectedVendor, autoRefresh]);
// 스크롤 최하단으로 이동
useEffect(() => {
@@ -160,25 +176,79 @@ export function VendorCommunicationDrawer({
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
}
}, [comments]);
+
+ // 자동 새로고침 시작
+ const startAutoRefresh = () => {
+ stopAutoRefresh(); // 기존 interval 정리
+ intervalRef.current = setInterval(() => {
+ if (open && selectedRfq && selectedVendor && !isSubmitting) {
+ loadComments(true); // 자동 새로고침임을 표시
+ }
+ }, 60000); // 60초마다 새로고침
+ };
+
+ // 자동 새로고침 중지
+ const stopAutoRefresh = () => {
+ if (intervalRef.current) {
+ clearInterval(intervalRef.current);
+ intervalRef.current = null;
+ }
+ };
+
+ // 자동 새로고침 토글
+ const toggleAutoRefresh = () => {
+ setAutoRefresh(prev => {
+ const newValue = !prev;
+ if (newValue && open) {
+ startAutoRefresh();
+ } else {
+ stopAutoRefresh();
+ }
+ return newValue;
+ });
+ };
- // 코멘트 로드 함수
- const loadComments = async () => {
+ // 코멘트 로드 함수 (자동 새로고침 여부 파라미터 추가)
+ const loadComments = async (isAutoRefresh = false) => {
if (!selectedRfq || !selectedVendor) return;
try {
- setIsLoading(true);
+ // 자동 새로고침일 때는 로딩 표시하지 않음
+ if (!isAutoRefresh) {
+ setIsLoading(true);
+ }
// Server Action을 사용하여 코멘트 데이터 가져오기
const commentsData = await fetchTechSalesVendorComments(selectedRfq.id, selectedVendor.vendorId || 0);
+
+ // 새 메시지가 있는지 확인 (자동 새로고침일 때만)
+ if (isAutoRefresh) {
+ const newMessageCount = commentsData.length;
+ if (newMessageCount > lastMessageCount && lastMessageCount > 0) {
+ // 새 메시지 알림 (선택사항)
+ toast.success(`새 메시지 ${newMessageCount - lastMessageCount}개가 도착했습니다`);
+ }
+ setLastMessageCount(newMessageCount);
+ } else {
+ setLastMessageCount(commentsData.length);
+ }
+
setComments(commentsData as Comment[]); // 구체적인 타입으로 캐스팅
// Server Action을 사용하여 읽지 않은 메시지를 읽음 상태로 변경
await markTechSalesMessagesAsRead(selectedRfq.id, selectedVendor.vendorId || 0);
} catch (error) {
console.error("코멘트 로드 오류:", error);
- toast.error("메시지를 불러오는 중 오류가 발생했습니다");
+ if (!isAutoRefresh) { // 자동 새로고침일 때는 에러 토스트 표시하지 않음
+ toast.error("메시지를 불러오는 중 오류가 발생했습니다");
+ }
} finally {
- setIsLoading(false);
+ // 항상 로딩 상태를 해제하되, 최소 200ms는 유지하여 깜빡거림 방지
+ if (!isAutoRefresh) {
+ setTimeout(() => {
+ setIsLoading(false);
+ }, 200);
+ }
}
};
@@ -323,8 +393,8 @@ export function VendorCommunicationDrawer({
return (
<Drawer open={open} onOpenChange={onOpenChange}>
- <DrawerContent className="max-h-[85vh]">
- <DrawerHeader className="border-b">
+ <DrawerContent className="max-h-[80vh] flex flex-col">
+ <DrawerHeader className="border-b flex-shrink-0">
<DrawerTitle className="flex items-center gap-2">
<Avatar className="h-8 w-8">
<AvatarFallback className="bg-primary/10">
@@ -341,10 +411,10 @@ export function VendorCommunicationDrawer({
</DrawerDescription>
</DrawerHeader>
- <div className="p-0 flex flex-col h-[60vh]">
+ <div className="flex flex-col flex-1 min-h-0">
{/* 메시지 목록 */}
- <ScrollArea className="flex-1 p-4">
- {isLoading ? (
+ <div className="flex-1 p-4 overflow-y-auto min-h-[300px]">
+ {isLoading && comments.length === 0 ? (
<div className="flex h-full items-center justify-center">
<p className="text-muted-foreground">메시지 로딩 중...</p>
</div>
@@ -356,7 +426,15 @@ export function VendorCommunicationDrawer({
</div>
</div>
) : (
- <div className="space-y-4">
+ <div className="space-y-4 relative">
+ {isLoading && (
+ <div className="absolute top-0 right-0 z-10 bg-background/80 backdrop-blur-sm rounded-md px-2 py-1">
+ <div className="flex items-center gap-2">
+ <div className="w-2 h-2 bg-primary rounded-full animate-pulse" />
+ <span className="text-xs text-muted-foreground">새로고침 중...</span>
+ </div>
+ </div>
+ )}
{comments.map(comment => (
<div
key={comment.id}
@@ -436,11 +514,11 @@ export function VendorCommunicationDrawer({
<div ref={messagesEndRef} />
</div>
)}
- </ScrollArea>
+ </div>
{/* 선택된 첨부파일 표시 */}
{attachments.length > 0 && (
- <div className="p-2 bg-muted mx-4 rounded-md mb-2">
+ <div className="p-2 bg-muted mx-4 rounded-md mb-2 flex-shrink-0">
<div className="text-xs font-medium mb-1">첨부파일</div>
<div className="flex flex-wrap gap-2">
{attachments.map((file, index) => (
@@ -466,7 +544,7 @@ export function VendorCommunicationDrawer({
)}
{/* 메시지 입력 영역 */}
- <div className="p-4 border-t">
+ <div className="p-4 border-t flex-shrink-0">
<div className="flex gap-2 items-end">
<div className="flex-1">
<Textarea
@@ -503,11 +581,31 @@ export function VendorCommunicationDrawer({
</div>
</div>
- <DrawerFooter className="border-t">
- <div className="flex justify-between">
- <Button variant="outline" onClick={() => loadComments()}>
- 새로고침
- </Button>
+ <DrawerFooter className="border-t flex-shrink-0">
+ <div className="flex justify-between items-center">
+ <div className="flex items-center gap-2">
+ <Button variant="outline" onClick={() => loadComments()}>
+ 새로고침
+ </Button>
+ <Button
+ variant={autoRefresh ? "default" : "outline"}
+ size="sm"
+ onClick={toggleAutoRefresh}
+ className="gap-2"
+ >
+ {autoRefresh ? (
+ <>
+ <div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
+ 자동 새로고침 ON
+ </>
+ ) : (
+ <>
+ <div className="w-2 h-2 bg-gray-400 rounded-full" />
+ 자동 새로고침 OFF
+ </>
+ )}
+ </Button>
+ </div>
<DrawerClose asChild>
<Button variant="outline">닫기</Button>
</DrawerClose>