From 7ae36c482c72aff04115b2fdd882114c207b0e40 Mon Sep 17 00:00:00 2001 From: Whatdoyumin Date: Tue, 19 Aug 2025 07:39:23 +0900 Subject: [PATCH 01/47] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=95=8C=EB=9E=8C=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=9D=BC=EC=9A=B0=ED=8C=85=20=EA=B2=BD=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/alarm/AlarmDropdown.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/alarm/AlarmDropdown.vue b/src/components/alarm/AlarmDropdown.vue index ccd5a50..86ed8ab 100644 --- a/src/components/alarm/AlarmDropdown.vue +++ b/src/components/alarm/AlarmDropdown.vue @@ -187,7 +187,7 @@ const handleNotificationClick = async (notification) => { if (notification.type === 'CHAT' && notification.relatedId) { // 채팅 알림 - 채팅방으로 이동 - targetUrl = `/chat?room=${notification.relatedId}` + targetUrl = `/chat?roomId=${notification.relatedId}` } else if (notification.type.includes('CONTRACT') && notification.relatedId) { // 계약 관련 알림 - 계약 페이지로 이동 targetUrl = `/contract/${notification.relatedId}` From 8e4d21ce3bac90c1130f12ac7e396271cb7764af Mon Sep 17 00:00:00 2001 From: Whatdoyumin Date: Tue, 19 Aug 2025 07:40:11 +0900 Subject: [PATCH 02/47] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=BF=BC=EB=A6=AC=20?= =?UTF-8?q?=EB=9D=BC=EC=9A=B0=ED=8C=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/chat/chatList/ChatList.vue | 36 ++++++++++++++++++----- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/src/components/chat/chatList/ChatList.vue b/src/components/chat/chatList/ChatList.vue index df0654e..8c167de 100644 --- a/src/components/chat/chatList/ChatList.vue +++ b/src/components/chat/chatList/ChatList.vue @@ -10,7 +10,7 @@ " @click="changeTab('owner')" > - 임대인 + 내가 파는 매물 @@ -62,6 +62,7 @@ import { ref, computed, onMounted, onUnmounted, nextTick, watch, provide } from 'vue' import ChatItem from './ChatItem.vue' import { getOwnerChatRooms, getBuyerChatRooms } from '@/apis/chatApi' +import { useRoute, useRouter } from 'vue-router' const props = defineProps({ initialRoomId: { @@ -79,6 +80,8 @@ const loading = ref(false) const error = ref(null) const currentUserId = ref(null) const updateTrigger = ref(0) +const router = useRouter() +const route = useRoute() // 현재 선택된 채팅방 ID 추적 const currentChatRoomId = ref(null) @@ -172,6 +175,11 @@ function selectRoom(room) { emit('selectRoom', null) + router.push({ + path: route.path, + query: { ...route.query, roomId: undefined }, + }) + setTimeout(() => { handleLeaveChatRoom(room.chatRoomId) cleanupChatRoomSubscriptions(room.chatRoomId) @@ -195,11 +203,21 @@ function selectRoom(room) { currentChatRoomId.value = room.chatRoomId markRoomAsRead(room.chatRoomId) emit('selectRoom', room) + + router.push({ + path: route.path, + query: { ...route.query, roomId: room.chatRoomId }, + }) }, 100) } else { currentChatRoomId.value = room.chatRoomId markRoomAsRead(room.chatRoomId) emit('selectRoom', room) + + router.push({ + path: route.path, + query: { ...route.query, roomId: room.chatRoomId }, + }) } } @@ -494,20 +512,24 @@ async function selectInitialRoom() { // 모든 채팅방에서 initialRoomId와 일치하는 방 찾기 const findAndSelectRoom = () => { const allRooms = [...ownerRooms.value, ...buyerRooms.value] - const targetRoom = allRooms.find((room) => String(room.chatRoomId) === String(props.initialRoomId)) - + const targetRoom = allRooms.find( + (room) => String(room.chatRoomId) === String(props.initialRoomId), + ) + if (targetRoom) { console.log('초기 채팅방 찾음:', targetRoom) selectRoom(targetRoom) - + // 해당 채팅방이 있는 탭으로 자동 전환 - const isOwnerRoom = ownerRooms.value.some(room => String(room.chatRoomId) === String(props.initialRoomId)) + const isOwnerRoom = ownerRooms.value.some( + (room) => String(room.chatRoomId) === String(props.initialRoomId), + ) if (isOwnerRoom && selectedTab.value !== 'owner') { selectedTab.value = 'owner' } else if (!isOwnerRoom && selectedTab.value !== 'buyer') { selectedTab.value = 'buyer' } - + return true } return false From e3697ef730ee080787a421298a0386f5347d6fb6 Mon Sep 17 00:00:00 2001 From: Whatdoyumin Date: Tue, 19 Aug 2025 07:40:23 +0900 Subject: [PATCH 03/47] =?UTF-8?q?=E2=9C=A8=20feat:=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/chat/chatRoom/ChatRoom.vue | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/components/chat/chatRoom/ChatRoom.vue b/src/components/chat/chatRoom/ChatRoom.vue index e795aff..f0af9ab 100644 --- a/src/components/chat/chatRoom/ChatRoom.vue +++ b/src/components/chat/chatRoom/ChatRoom.vue @@ -91,9 +91,11 @@ -
- 계약 요청 수락하기 - 거절 +
+ 계약 요청 수락하기 + 계약 요청 거절하기
@@ -138,9 +140,11 @@
-
- 계약 요청 수락하기 - 거절 +
+ 계약 요청 수락하기 + 계약 요청 거절하기
From b577c2c7a68800723a0b47e0331722781b79a395 Mon Sep 17 00:00:00 2001 From: Whatdoyumin Date: Tue, 19 Aug 2025 07:45:19 +0900 Subject: [PATCH 04/47] =?UTF-8?q?=F0=9F=92=84=20style:=20=EB=86=92?= =?UTF-8?q?=EC=9D=B4=20=EB=B7=B0=ED=8F=AC=ED=8A=B8=20=EA=B8=B0=EC=A4=80?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/chat/layout/ChatLayout.vue | 6 +++--- src/pages/chat/ChatPage.vue | 11 ++++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/components/chat/layout/ChatLayout.vue b/src/components/chat/layout/ChatLayout.vue index dcc88ce..56797d9 100644 --- a/src/components/chat/layout/ChatLayout.vue +++ b/src/components/chat/layout/ChatLayout.vue @@ -1,13 +1,13 @@ From 12227027038443091003a19bb7de4ac9f8c960cc Mon Sep 17 00:00:00 2001 From: Whatdoyumin Date: Tue, 19 Aug 2025 07:46:16 +0900 Subject: [PATCH 07/47] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=9B=94=EC=84=B8=EC=9D=BC=20=EA=B2=BD?= =?UTF-8?q?=EC=9A=B0=20=ED=91=9C=EC=8B=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/homes/homedetails/ListingBasicInfo.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/homes/homedetails/ListingBasicInfo.vue b/src/components/homes/homedetails/ListingBasicInfo.vue index 922cb52..c868888 100644 --- a/src/components/homes/homedetails/ListingBasicInfo.vue +++ b/src/components/homes/homedetails/ListingBasicInfo.vue @@ -6,10 +6,11 @@ {{ listing.leaseType === '전세' ? '전세 ' + formatNumber(listing.depositPrice) + '원' - : '월세 ' + + : '보증금 ' + formatNumber(listing.depositPrice) + '원' + ' / ' + + '월세 ' + formatNumber(listing.monthlyRent) + '원' }} From 763b107eb54f1b12df6832b910b1c743c433b2cb Mon Sep 17 00:00:00 2001 From: Whatdoyumin Date: Tue, 19 Aug 2025 07:46:36 +0900 Subject: [PATCH 08/47] =?UTF-8?q?=E2=9C=A8=20feat:=20homeId=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=20API=20=EC=9A=94=EC=B2=AD=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/step1HomeInfo/Step1BasicInfo.vue | 45 ++++++++++++++++--- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/src/components/pre-contract/common/step1HomeInfo/Step1BasicInfo.vue b/src/components/pre-contract/common/step1HomeInfo/Step1BasicInfo.vue index d9fe2d1..153d6a1 100644 --- a/src/components/pre-contract/common/step1HomeInfo/Step1BasicInfo.vue +++ b/src/components/pre-contract/common/step1HomeInfo/Step1BasicInfo.vue @@ -22,15 +22,17 @@ From 3dd6beb3e561373e5ac4ab67c84d45c557a1cb8d Mon Sep 17 00:00:00 2001 From: Whatdoyumin Date: Tue, 19 Aug 2025 07:47:44 +0900 Subject: [PATCH 10/47] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/homes/HomeDetailsPage.vue | 32 ++++++++++------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/src/pages/homes/HomeDetailsPage.vue b/src/pages/homes/HomeDetailsPage.vue index 3c0cbec..ec38952 100644 --- a/src/pages/homes/HomeDetailsPage.vue +++ b/src/pages/homes/HomeDetailsPage.vue @@ -1,13 +1,17 @@ @@ -48,6 +53,7 @@ const route = useRoute() const router = useRouter() const id = Number(route.params.no) +const isLoading = ref(true) const listing = ref(null) const images = ref([]) const isFavorite = ref(false) @@ -56,47 +62,31 @@ const processedAddress = ref('') onMounted(async () => { try { const data = await fetchListingById(id) - console.log('✅ 매물 상세 API 응답:', data) - if (data) { listing.value = data images.value = data.imageUrls || [] - - if (data.addr1) { - processedAddress.value = data.addr1 - } else { - processedAddress.value = data.addr2 || '주소정보 없음' - } - - console.log('최종 가공된 주소:', processedAddress.value) + processedAddress.value = data.addr1 || data.addr2 || '주소정보 없음' } } catch (err) { console.error('매물 조회 실패:', err) + } finally { + isLoading.value = false } }) const isCreatingChat = ref(false) const goToChat = async () => { - // props 대신 id 변수를 직접 사용합니다. if (!id) { - console.log('채팅방을 만들 수 없습니다 - 매물 ID 없음') alert('매물 정보를 찾을 수 없습니다. 페이지를 새로고침 해주세요.') return } - isCreatingChat.value = true try { - console.log('Creating chat room with propertyId:', id) const response = await createChatRoom(id) - console.log('Chat room creation response:', response) - if (response && response.data) { - // 채팅방 생성 성공 시 해당 채팅방으로 이동 - console.log('Navigating to chat with roomId:', response.data) router.push(`/chat?roomId=${response.data}`) } else { - console.error('채팅방 생성 실패: 응답에 chatRoomId가 없습니다', response) alert('채팅방 생성에 실패했습니다. 다시 시도해주세요.') } } catch (error) { From 97b965da8077468b0fcf2928fa192116362cfa4f Mon Sep 17 00:00:00 2001 From: Whatdoyumin Date: Tue, 19 Aug 2025 07:49:24 +0900 Subject: [PATCH 11/47] =?UTF-8?q?=E2=99=BB=20refactor:=20=EA=B3=84?= =?UTF-8?q?=EC=95=BD=EC=84=9C=20=EC=B1=84=ED=8C=85=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/contract/chat/ContractChat.vue | 108 ++++++++++++------ .../contract/chat/ContractChatInput.vue | 27 +++-- .../modals/step3/FinalClauseSelectModal.vue | 9 +- src/composables/chat/useChatAiButtons.js | 4 +- 4 files changed, 101 insertions(+), 47 deletions(-) diff --git a/src/components/contract/chat/ContractChat.vue b/src/components/contract/chat/ContractChat.vue index b398273..c9d84c4 100644 --- a/src/components/contract/chat/ContractChat.vue +++ b/src/components/contract/chat/ContractChat.vue @@ -50,7 +50,7 @@
- + @@ -72,7 +74,12 @@
+ @@ -124,6 +131,8 @@ const store = useSpecialContractStore() const route = useRoute() const router = useRouter() +const stepFromUrl = computed(() => Number(route.query.step ?? props.currentStep ?? 3)) + const inflightSync = ref(false) let syncTimer = null @@ -161,7 +170,7 @@ const { } = useContractChat(actualContractChatId, currentUserId, contractData) // 5) AI 버튼 규칙 -const { stepNum, isAi, aiButtons } = useChatAiButtons(props.currentStep, () => isOwner.value) +const { stepNum, isAi, aiButtons } = useChatAiButtons(stepFromUrl, () => isOwner.value) // UI 상태 const isLoadingOverlayVisible = ref(false) @@ -329,6 +338,20 @@ const responseFinalConfirm = async (accepted) => { if (res?.success) store.bumpFinalContractVersion() } +const onOwnerEditRequest = () => { + if (amOwner.value) isLoadingOverlayVisible.value = true +} +const onOwnerEditFailed = () => { + if (amOwner.value) isLoadingOverlayVisible.value = false +} + +const RE_ROUND_DONE = /(\d+)\s*차\s*수정이\s*완료되었습니다!.*\s*협상\s*라운드가\s*시작됩니다\./ + +const RE_TENANT_ACCEPT_MOD = + /임차인이\s*특약\s*(\d+)\s*번\s*수정\s*요청을\s*수락했습니다\.\s*특약이\s*변경되었습니다\./ +const RE_TENANT_ACCEPT_DEL = + /임차인이\s*특약\s*(\d+)\s*번\s*삭제\s*요청을\s*수락했습니다\.\s*특약이\s*삭제되었습니다\./ + // 4단계 적법성 검토 const responseFinal = async (accepted) => { const id = String(actualContractChatId.value) @@ -378,47 +401,40 @@ const handleAiAction = (payload) => { // 전송 const sendMessageUi = async (content, callback) => { - console.log('📨 ContractChat: 메시지 전송 요청:', content) - if (!isInputReady.value) { const result = { success: false, error: '채팅방이 준비되지 않았습니다.' } - if (callback) callback(result) + callback?.(result) return result } try { - // useContractChat의 sendContractMessage 호출 - const result = sendContractMessage(content, 'TEXT') - - console.log('📤 ContractChat: 전송 결과:', result) - - // 🔧 전송 성공한 경우에만 화면에 메시지 추가 - if (result) { - hookMessages.value.push({ - id: Date.now(), - senderId: currentUserId.value, - receiverId: contractReceiverId.value, - content, - sendTime: new Date().toISOString(), - type: 'TEXT', - isRead: false, - }) - nextTick(forceScrollToBottom) - } else { - console.warn('메시지 전송 실패:', result?.error || '알 수 없는 오류') - } + // 서버 전송 (실패 시 throw) + await sendContractMessage(content, 'TEXT') + + // ✅ 낙관적 업데이트: 즉시 로컬에 메시지 추가 + hookMessages.value.push({ + id: Date.now(), // 임시 키 + _localId: (crypto?.randomUUID && crypto.randomUUID()) || `local-${Date.now()}`, + senderId: currentUserId.value, + receiverId: contractReceiverId.value, + content, + sendTime: new Date().toISOString(), + type: 'TEXT', + isRead: false, + }) - // 🔧 콜백으로 결과 전달 - if (callback) callback(result) - return result + nextTick(forceScrollToBottom) + + const ok = { success: true } + callback?.(ok) + return ok } catch (error) { - console.error('계약 메시지 전송 중 오류:', error) - const errorResult = { + const err = { success: false, - error: error.message || '메시지 전송 중 오류가 발생했습니다.', + error: error?.message || '메시지 전송 중 오류가 발생했습니다.', } - if (callback) callback(errorResult) - return errorResult + callback?.(err) + return err } } @@ -480,7 +496,7 @@ const mergedMessages = computed(() => { const map = new Map() a.forEach((m, i) => map.set(keyOf(m, i), m)) - b.forEach((m, i) => map.set(keyOf(m, i + 10000), m)) + b.forEach((m, i) => map.set(keyOf(m, i, 10000), m)) const arr = [...map.values()] arr.sort((x, y) => tsOf(x, 0) - tsOf(y, 0)) @@ -495,7 +511,7 @@ const isNewerThanApi = (live) => { return liveTs > apiTs } -// 단일 디바운스 + 중복 방지 +// 단일 디바운스 중복 방지 const scheduleSync = () => { const id = actualContractChatId.value if (!id) return @@ -605,6 +621,19 @@ watch( return } + // 1) 임차인이 거절한 경우: "임차인이 특약 대화를 더 요청했습니다." + if (amOwner.value && t.includes('임차인이 특약 대화를 더 요청했습니다.')) { + isLoadingOverlayVisible.value = false + } + // 2) 임차인이 수락 → AI(9998)가 라운드 시작 알림을 보냄 + if (amOwner.value && sid === '9998' && RE_ROUND_DONE.test(t)) { + isLoadingOverlayVisible.value = false + } + + if (RE_TENANT_ACCEPT_MOD.test(t) || RE_TENANT_ACCEPT_DEL.test(t)) { + store.bumpFinalContractVersion() + } + // --- 특약 수정 요청 허용 트리거 감지 --- if (t.includes('위 문제점들을 검토하시고 필요시 임대인께서 수정 요청을 해주세요')) { try { @@ -698,6 +727,13 @@ watch( }, { immediate: false }, ) + +watch( + () => store.currentRound, + () => { + isLoadingOverlayVisible.value = false + }, +) From 2107d891017b5887c5d03096ff01771af7d0f3ea Mon Sep 17 00:00:00 2001 From: Whatdoyumin Date: Tue, 19 Aug 2025 09:47:24 +0900 Subject: [PATCH 17/47] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=EC=9B=B9=20?= =?UTF-8?q?=EC=86=8C=EC=BC=93=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/websocket.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/apis/websocket.js b/src/apis/websocket.js index 843776c..b024937 100644 --- a/src/apis/websocket.js +++ b/src/apis/websocket.js @@ -7,7 +7,8 @@ class WebSocketService { this.stompClient = null this.isConnected = ref(false) this.isConnecting = ref(false) - this.messageHandlers = new Map() + this.messageHandlers = new Map() // topic -> subscription + this.handlersByTopic = new Map() // topic -> handler ✅ 추가 this.connectionHandlers = [] this.pendingSubscriptions = [] // 대기 중인 구독들 } @@ -135,7 +136,7 @@ class WebSocketService { } async sendChatMessage(chatRoomId, senderId, receiverId, content, type = 'TEXT', fileUrl = null) { - const success = this.sendMessage('/app/chat/send', { + const success = await this.sendMessage('/app/chat/send', { chatRoomId, senderId, receiverId, From dc666f171a18ad770f2436722f8239e6143f2fd4 Mon Sep 17 00:00:00 2001 From: Whatdoyumin Date: Tue, 19 Aug 2025 09:47:43 +0900 Subject: [PATCH 18/47] =?UTF-8?q?=E2=9C=A8=20feat:=20=EB=82=B4=EA=B0=80=20?= =?UTF-8?q?=EB=B3=B4=EB=82=B8=20=EB=B2=84=ED=8A=BC=20=EB=B9=84=ED=99=9C?= =?UTF-8?q?=EC=84=B1=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/chat/chatRoom/ChatRoom.vue | 40 +++++++++++++++++++---- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/src/components/chat/chatRoom/ChatRoom.vue b/src/components/chat/chatRoom/ChatRoom.vue index f0af9ab..2a96bcf 100644 --- a/src/components/chat/chatRoom/ChatRoom.vue +++ b/src/components/chat/chatRoom/ChatRoom.vue @@ -92,10 +92,23 @@
- 계약 요청 수락하기 + 계약 요청 수락하기 + + + - 계약 요청 거절하기 + 계약 요청 거절하기 +
@@ -141,10 +154,23 @@
- 계약 요청 수락하기 + 계약 요청 수락하기 + + + - 계약 요청 거절하기 + 계약 요청 거절하기 +
@@ -188,7 +214,7 @@
{{ formatMessageTime(message.sendTime) }} - 읽음 + 읽음
From d712614780d6eb99564d2531a388138a10d68fe5 Mon Sep 17 00:00:00 2001 From: MeongW Date: Tue, 19 Aug 2025 10:26:47 +0900 Subject: [PATCH 19/47] :bug: fix: solve conflict --- src/apis/websocket.js | 43 +++++++++++++++++----------------- src/hooks/chat/useChatRoom.js | 2 +- src/hooks/chat/useWebSocket.js | 6 ++--- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/apis/websocket.js b/src/apis/websocket.js index b024937..042a925 100644 --- a/src/apis/websocket.js +++ b/src/apis/websocket.js @@ -83,35 +83,36 @@ class WebSocketService { }) } - async sendMessage(destination, message, retryCount = 30) { + async sendMessage(destination, message, retryCount = 5) { console.log('sendMessage 호출:', { destination, message }) - console.log('STOMP 클라이언트 상태:', { + + // STOMP 클라이언트가 없으면 연결 시도 + if (!this.stompClient) { + console.log('STOMP 클라이언트가 없음, 연결 시도...') + await this.connect() + } + + // 연결 상태 확인 + const isReady = this.stompClient?.connected && this.isConnected.value + + console.log('STOMP 상태:', { hasClient: !!this.stompClient, - isConnected: this.stompClient?.connected, + connected: this.stompClient?.connected, internalConnected: this.isConnected.value, + isReady: isReady, }) - if (!this.stompClient || !this.stompClient.connected) { - try { - await this.connect() - } catch (e) { - console.error('STOMP 연결 실패:', e) + if (!isReady) { + if (retryCount > 0) { + console.warn(`STOMP 연결 대기 중... (남은 시도: ${retryCount})`) + await new Promise((resolve) => setTimeout(resolve, 1000)) + return this.sendMessage(destination, message, retryCount - 1) + } else { + console.error('STOMP 연결 실패 - 재시도 횟수 초과') return false } } - // 연결 준비될 때까지 폴링 (connected 플래그만 사용) - let attempts = retryCount - while ((!this.stompClient || !this.stompClient.connected) && attempts > 0) { - console.warn(`STOMP 연결 대기... (남은 시도: ${attempts})`) - await new Promise((r) => setTimeout(r, 200)) - attempts-- - } - if (!this.stompClient || !this.stompClient.connected) { - console.error('STOMP 연결 실패 - 재시도 횟수 초과') - return false - } - try { const payload = { ...message, @@ -168,7 +169,7 @@ class WebSocketService { } async sendContractChatMessage(contractChatId, senderId, receiverId, content, type = 'TEXT') { - const success = this.sendMessage('/app/contract/chat/send', { + const success = await this.sendMessage('/app/contract/chat/send', { contractChatId, senderId, receiverId, diff --git a/src/hooks/chat/useChatRoom.js b/src/hooks/chat/useChatRoom.js index 4c720f7..31050fc 100644 --- a/src/hooks/chat/useChatRoom.js +++ b/src/hooks/chat/useChatRoom.js @@ -81,7 +81,7 @@ export function useChatRoom(chatRoomId, currentUserId, roomData) { } // 메시지 전송 - const sendMessage = (content, type = 'TEXT', fileUrl = null) => { + const sendMessage = async (content, type = 'TEXT', fileUrl = null) => { // 필수 조건 체크 - undefined와 null 모두 체크 if (!chatRoomId.value) { console.error('채팅방 ID가 없습니다:', chatRoomId.value) diff --git a/src/hooks/chat/useWebSocket.js b/src/hooks/chat/useWebSocket.js index b840da7..66df34b 100644 --- a/src/hooks/chat/useWebSocket.js +++ b/src/hooks/chat/useWebSocket.js @@ -21,16 +21,16 @@ export function useWebSocket() { connectionStatus.value = 'disconnected' } - const sendMessage = (destination, message) => { + const sendMessage = async (destination, message) => { return websocketService.sendMessage(destination, message) } - const sendChatMessage = (chatRoomId, senderId, receiverId, content, type = 'TEXT') => { + const sendChatMessage = async (chatRoomId, senderId, receiverId, content, type = 'TEXT') => { return websocketService.sendChatMessage(chatRoomId, senderId, receiverId, content, type) } // 🔧 계약 채팅 메시지 전송 메서드 추가 - const sendContractChatMessage = ( + const sendContractChatMessage = async ( contractChatId, senderId, receiverId, From 685b3bcb9ac2f889329d17b39c87dca420dcf1df Mon Sep 17 00:00:00 2001 From: MeongW Date: Tue, 19 Aug 2025 11:33:02 +0900 Subject: [PATCH 20/47] =?UTF-8?q?:bug:=20fix:=20=EB=8B=A4=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=ED=95=B4=EB=B2=84=EB=A6=B4=EA=B1=B0=EC=95=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/contractChatApi.js | 8 +++++-- src/apis/websocket.js | 42 ++++++++++--------------------------- 2 files changed, 17 insertions(+), 33 deletions(-) diff --git a/src/apis/contractChatApi.js b/src/apis/contractChatApi.js index 56aa28b..0fed33c 100644 --- a/src/apis/contractChatApi.js +++ b/src/apis/contractChatApi.js @@ -461,10 +461,14 @@ export const updateSignatureStatus = async (contractChatId, signatureData) => { export const getExportStatus = async (contractChatId) => { try { const response = await api.get(`/api/contract/${contractChatId}/export/status`) - return response.data.data + if (response.data && response.data.success) { + return response.data.data + } else { + return null + } } catch (error) { console.error('내보내기 상태 조회 실패:', error) - throw error + return null } } diff --git a/src/apis/websocket.js b/src/apis/websocket.js index 042a925..3227d0e 100644 --- a/src/apis/websocket.js +++ b/src/apis/websocket.js @@ -7,8 +7,7 @@ class WebSocketService { this.stompClient = null this.isConnected = ref(false) this.isConnecting = ref(false) - this.messageHandlers = new Map() // topic -> subscription - this.handlersByTopic = new Map() // topic -> handler ✅ 추가 + this.messageHandlers = new Map() this.connectionHandlers = [] this.pendingSubscriptions = [] // 대기 중인 구독들 } @@ -42,7 +41,7 @@ class WebSocketService { this.stompClient = new Client({ webSocketFactory: () => socket, reconnectDelay: 2000, // 재연결 지연 시간 단축 - debug: (str) => console.log('[STOMP DEBUG]', str), + // debug: (str) => console.log('[STOMP DEBUG]', str), onConnect: (frame) => { console.log('STOMP 연결 성공:', frame) this.isConnected.value = true @@ -84,27 +83,16 @@ class WebSocketService { } async sendMessage(destination, message, retryCount = 5) { - console.log('sendMessage 호출:', { destination, message }) - // STOMP 클라이언트가 없으면 연결 시도 if (!this.stompClient) { - console.log('STOMP 클라이언트가 없음, 연결 시도...') await this.connect() } // 연결 상태 확인 const isReady = this.stompClient?.connected && this.isConnected.value - console.log('STOMP 상태:', { - hasClient: !!this.stompClient, - connected: this.stompClient?.connected, - internalConnected: this.isConnected.value, - isReady: isReady, - }) - if (!isReady) { if (retryCount > 0) { - console.warn(`STOMP 연결 대기 중... (남은 시도: ${retryCount})`) await new Promise((resolve) => setTimeout(resolve, 1000)) return this.sendMessage(destination, message, retryCount - 1) } else { @@ -114,21 +102,14 @@ class WebSocketService { } try { - const payload = { - ...message, - } - - console.log('전송할 페이로드:', payload) this.stompClient.publish({ destination, - body: JSON.stringify(payload), + body: JSON.stringify(message), }) - console.log('메시지 전송 성공') return true } catch (error) { console.error('메시지 전송 실패:', error) if (retryCount > 0) { - console.log(`메시지 전송 재시도... (남은 시도: ${retryCount})`) await new Promise((resolve) => setTimeout(resolve, 200)) return this.sendMessage(destination, message, retryCount - 1) } @@ -201,16 +182,16 @@ class WebSocketService { } // 계약서 내보내기 관련 메서드 - sendContractExportSignature(contractChatId, signatureData) { - return this.sendMessage(`/app/contract/${contractChatId}/export/signature`, signatureData) + async sendContractExportSignature(contractChatId, signatureData) { + return await this.sendMessage(`/app/contract/${contractChatId}/export/signature`, signatureData) } - sendContractExportPassword(contractChatId, passwordData) { - return this.sendMessage(`/app/contract/${contractChatId}/export/password`, passwordData) + async sendContractExportPassword(contractChatId, passwordData) { + return await this.sendMessage(`/app/contract/${contractChatId}/export/password`, passwordData) } - getContractExportStatus(contractChatId) { - return this.sendMessage(`/app/contract/${contractChatId}/export/status`, {}) + async getContractExportStatus(contractChatId) { + return await this.sendMessage(`/app/contract/${contractChatId}/export/status`, {}) } onMessage(topic, handler) { @@ -250,8 +231,7 @@ class WebSocketService { const subscription = this.stompClient.subscribe(topic, (message) => { try { const data = JSON.parse(message.body) - const result = handler(data) - console.log('핸들러 호출 완료! 결과:', result) + handler(data) } catch (e) { console.error('파싱 실패:', e) console.error('Raw body:', message.body) @@ -297,4 +277,4 @@ class WebSocketService { } export const websocketService = new WebSocketService() -export default websocketService +export default websocketService \ No newline at end of file From efe799fa3673277bb70f7d4a512439fb176e50f8 Mon Sep 17 00:00:00 2001 From: Whatdoyumin Date: Tue, 19 Aug 2025 13:09:46 +0900 Subject: [PATCH 21/47] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=20=EC=9E=84?= =?UTF-8?q?=EB=8C=80=EC=9D=B8=20=EB=A1=9C=EB=94=A9=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/contract/chat/ContractChat.vue | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/components/contract/chat/ContractChat.vue b/src/components/contract/chat/ContractChat.vue index 7358e68..cb2b033 100644 --- a/src/components/contract/chat/ContractChat.vue +++ b/src/components/contract/chat/ContractChat.vue @@ -363,6 +363,9 @@ const RE_TENANT_ACCEPT_MOD = const RE_TENANT_ACCEPT_DEL = /임차인이\s*특약\s*(\d+)\s*번\s*삭제\s*요청을\s*수락했습니다\.\s*특약이\s*삭제되었습니다\./ +const RE_MORE_REQUEST = /(임차인|임대인).?이?\s*특약\s*대화를\s*더\s*요청했습니다\.?/ +const RE_START_CLAUSE_TALK = /(\d+)\s*번\s*특약에\s*대한\s*대화를\s*시작합니다!?/ + // 4단계 적법성 검토 const responseFinal = async (accepted) => { const id = String(actualContractChatId.value) @@ -625,10 +628,16 @@ watch( (m) => { if (!m) return const sid = String(m.senderId) - if (sid !== '9998' && sid !== '9999') return - const t = normalizeText(m.content) + // ✅ 발신자와 무관하게 로딩 해제 트리거 우선 처리 + if (RE_MORE_REQUEST.test(t) || RE_START_CLAUSE_TALK.test(t)) { + isLoadingOverlayVisible.value = false + } + + // ⬇️ 아래부터는 AI 메시지에만 적용되는 기존 단계 전환/동기화 로직 유지 + if (sid !== '9998' && sid !== '9999') return + // --- 1단계 감지 --- if (t.includes(`사전 조사를 토대로`)) { const next = { ...route.query, step: '1' } @@ -659,10 +668,6 @@ watch( return } - // 1) 임차인이 거절한 경우: "임차인이 특약 대화를 더 요청했습니다." - if (amOwner.value && t.includes('임차인이 특약 대화를 더 요청했습니다.')) { - isLoadingOverlayVisible.value = false - } // 2) 임차인이 수락 → AI(9998)가 라운드 시작 알림을 보냄 if (amOwner.value && sid === '9998' && RE_ROUND_DONE.test(t)) { isLoadingOverlayVisible.value = false From 22fc56ec513bf91e6818385db1814153c8d565c9 Mon Sep 17 00:00:00 2001 From: MeongW Date: Tue, 19 Aug 2025 13:36:49 +0900 Subject: [PATCH 22/47] =?UTF-8?q?:bug:=20fix:=20=EC=A3=84=EC=86=A1?= =?UTF-8?q?=ED=95=9C=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/contract/ContractCompletePage.vue | 142 ++++++++++++++++---- 1 file changed, 119 insertions(+), 23 deletions(-) diff --git a/src/pages/contract/ContractCompletePage.vue b/src/pages/contract/ContractCompletePage.vue index 602545e..1139448 100644 --- a/src/pages/contract/ContractCompletePage.vue +++ b/src/pages/contract/ContractCompletePage.vue @@ -419,23 +419,46 @@ watch( // WebSocket 메시지 핸들러 const handleExportStatusUpdate = (data) => { - console.log('WebSocket 메시지 수신:', data) + console.log('=== WebSocket 상태 업데이트 수신 ===') + console.log('받은 데이터:', JSON.stringify(data, null, 2)) + console.log('현재 userRole:', userRole.value) // 이전 상태 저장 const prevStatus = exportStatus.value ? { ...exportStatus.value } : null + console.log('이전 상태:', prevStatus) // 새로운 상태 업데이트 exportStatus.value = data - // 상대방이 방금 서명한 경우 - UI 상태만 업데이트 (alert 제거) + // 상태 변경 감지 if (prevStatus && data) { const wasOwnerSigned = prevStatus.ownerSignatureCompleted - const isBuyerSigned = prevStatus.buyerSignatureCompleted + const wasBuyerSigned = prevStatus.buyerSignatureCompleted const nowOwnerSigned = data.ownerSignatureCompleted const nowBuyerSigned = data.buyerSignatureCompleted - // 상태 변경이 있으면 자동으로 UI가 업데이트됨 - // alert 없이 진행 상태만 표시 + // 상대방이 방금 서명한 경우 + if (!wasOwnerSigned && nowOwnerSigned && userRole.value !== 'owner') { + console.log('임대인이 서명을 완료했습니다') + } + if (!wasBuyerSigned && nowBuyerSigned && userRole.value === 'owner') { + console.log('임차인이 서명을 완료했습니다') + } + + // 양쪽 모두 서명 완료된 경우 + if (nowOwnerSigned && nowBuyerSigned && (!wasOwnerSigned || !wasBuyerSigned)) { + console.log('양쪽 서명 완료! 최종 계약서 생성 시작...') + isLoading.value = true + + // 최종 PDF가 이미 있으면 바로 완료 화면으로 + if (data.finalPdfUrl) { + loadFinalPdf(data.finalPdfUrl).then(() => { + currentStep.value = 'complete' + isLoading.value = false + isWaitingForOther.value = false + }) + } + } } } @@ -467,20 +490,15 @@ const setupWebSocket = async () => { `/topic/contract/${contractId.value}/export/status`, handleExportStatusUpdate, ) - console.log('WebSocket 구독 완료:', `/topic/contract/${contractId.value}/export/status`) // 계약 완료 알림 구독 websocketService.onMessage( `/topic/contract/${contractId.value}/completion`, handleContractCompletion, ) - console.log('WebSocket 완료 알림 구독:', `/topic/contract/${contractId.value}/completion`) - // 초기 상태 요청 (즉시 전송) - console.log('초기 상태 요청 즉시 전송') - console.log('WebSocket 연결 상태:', websocketService.getConnectionStatus()) - const result = websocketService.getContractExportStatus(contractId.value) - console.log('초기 상태 요청 결과:', result) + // 초기 상태 요청 + await websocketService.getContractExportStatus(contractId.value) } catch (error) { console.error('WebSocket 연결 실패:', error) isConnected.value = false @@ -702,7 +720,9 @@ const proceedToPassword = async () => { isLoading.value = true try { // 서버에 서명 저장 + console.log('서명 서버 저장 시작...') await saveSignatureToServer() + console.log('서명 서버 저장 완료') // WebSocket 연결 확인 if (!websocketService.getConnectionStatus()) { @@ -735,25 +755,59 @@ const proceedToPassword = async () => { console.log('WebSocket 연결 상태:', websocketService.getConnectionStatus()) // WebSocket 서비스 메서드 사용 (await 추가) + console.log('서명 전송 시작, contractId:', contractId.value) + console.log('서명 메시지:', JSON.stringify(signatureMessage, null, 2)) + + // WebSocket 전송 시도 const sendResult = await websocketService.sendContractExportSignature( contractId.value, signatureMessage, ) - console.log('메시지 전송 결과:', sendResult) + console.log('WebSocket 서명 전송 결과:', sendResult) + // WebSocket 실패 시 또는 백업으로 HTTP API 사용 if (!sendResult) { console.warn('WebSocket 메시지 전송 실패. HTTP API 사용...') - try { - // HTTP API를 통한 서명 상태 업데이트 - const httpResult = await updateSignatureStatus(contractId.value, signatureMessage) - console.log('HTTP API 서명 상태 업데이트 결과:', httpResult) - - // 상태 폴링 시작 - startStatusPolling() - } catch (error) { - console.error('HTTP API 서명 상태 업데이트 실패:', error) + } + + // 항상 HTTP API로도 전송 (백업) + try { + console.log('HTTP API로 서명 상태 업데이트 시도...') + const httpResult = await updateSignatureStatus(contractId.value, signatureMessage) + console.log('HTTP API 서명 상태 업데이트 결과:', httpResult) + + // HTTP API 성공 시 본인 서명 상태를 즉시 업데이트 + if (httpResult && httpResult.success) { + if (userRole.value === 'owner') { + exportStatus.value = { + ...exportStatus.value, + ownerSignatureCompleted: true, + } + } else { + exportStatus.value = { + ...exportStatus.value, + buyerSignatureCompleted: true, + } + } } + } catch (error) { + console.error('HTTP API 서명 상태 업데이트 실패:', error) + alert('서명 전송에 실패했습니다. 다시 시도해주세요.') + return // 실패 시 대기 화면으로 가지 않음 } + + // 서명 완료 후 즉시 상태 폴링 시작 + console.log('상태 폴링 시작...') + startStatusPolling() + + // WebSocket이 실패한 경우에도 HTTP API로 상태 조회 + setTimeout(async () => { + const latestStatus = await getExportStatus(contractId.value) + if (latestStatus) { + console.log('HTTP로 가져온 최신 상태:', latestStatus) + exportStatus.value = latestStatus + } + }, 1000) // 대기 화면으로 이동 (백엔드에서 자동으로 최종 계약서 생성) console.log('서명 완료. 최종 계약서 생성 대기 중...') @@ -931,24 +985,66 @@ let pollingInterval = null const startStatusPolling = () => { console.log('상태 폴링 시작') + + // 즉시 한 번 실행 + getExportStatus(contractId.value).then(status => { + if (status) { + console.log('초기 폴링 상태:', status) + exportStatus.value = status + } + }).catch(err => { + console.error('초기 상태 조회 실패:', err) + }) pollingInterval = setInterval(async () => { try { const status = await getExportStatus(contractId.value) console.log('폴링 상태 확인:', status) + + // null 체크 + if (!status) { + console.warn('상태 조회 결과가 null입니다') + return + } // 상태 업데이트 exportStatus.value = status + + // 양쪽 서명 완료 확인 + if (status.ownerSignatureCompleted && status.buyerSignatureCompleted) { + console.log('양쪽 서명 완료 확인!') + + // 최종 PDF가 생성되었으면 + if (status.finalPdfUrl) { + console.log('최종 계약서 생성 완료!') + stopStatusPolling() + isWaitingForOther.value = false + isLoading.value = false + + // 완료 화면으로 이동 + loadFinalPdf(status.finalPdfUrl).then(() => { + currentStep.value = 'complete' + alert('계약서 서명이 완료되어 최종 계약서가 생성되었습니다!') + }) + } else { + // PDF 생성 중 + console.log('최종 계약서 생성 중...') + isLoading.value = true + } + } - // 완료 상태 확인 + // 완료 상태 확인 (별도 체크) if ((status.isCompleted || status.completed) && status.finalPdfUrl) { console.log('폴링: 최종 계약서 생성 완료!') stopStatusPolling() + isWaitingForOther.value = false + isLoading.value = false // 완료 화면으로 이동 if (currentStep.value === 'waiting') { loadFinalPdf(status.finalPdfUrl).then(() => { currentStep.value = 'complete' + alert('계약서 서명이 완료되어 최종 계약서가 생성되었습니다!') }) } } From 86cda14066e04016af8b98a0d0586d56dff14404 Mon Sep 17 00:00:00 2001 From: Whatdoyumin Date: Tue, 19 Aug 2025 15:43:01 +0900 Subject: [PATCH 23/47] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=B5=9C=EC=A2=85=20?= =?UTF-8?q?=EA=B3=84=EC=95=BD=EC=84=9C=20=EB=8B=A4=EC=9A=B4=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=201=ED=9A=8C=20=EC=A0=9C=ED=95=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../complete/ContractDownloadPanel.vue | 23 +++++++++++++++---- .../contract/complete/DownloadItem.vue | 10 +++++++- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/components/contract/complete/ContractDownloadPanel.vue b/src/components/contract/complete/ContractDownloadPanel.vue index 6001fa5..5bf7c3f 100644 --- a/src/components/contract/complete/ContractDownloadPanel.vue +++ b/src/components/contract/complete/ContractDownloadPanel.vue @@ -7,7 +7,8 @@ icon="pdf" title="PDF 다운로드" description="계약서를 PDF로 저장" - @click="handleDownload" + :disabled="isDownloading" + @activate="handleDownload" /> -
+
- +
-

중요한 일정

+

중요한 일정

  • 전입 신고 + 확정일자
  • 임대차 신고 + 실거래 신고
  • @@ -28,7 +28,7 @@
-
+

전입신고 + 확정일자

-

주민센터 방문 (이사 당일 처리 권장)

-

+

주민센터 방문 (이사 당일 처리 권장)

+

대항력 + 우선변제권 확보

@@ -45,7 +45,7 @@
-
+

임대차 신고제

-

+

계약일로부터 30일 이내 미신고 시 과태료 100만 원 -

+

등기변동 알림 서비스

-

+

KB 스타뱅킹 앱에서 등록 · 사기 예방을 위한 필수 서비스

From 8a5b4f4db05a388e6499a9297401438138febd86 Mon Sep 17 00:00:00 2001 From: Whatdoyumin Date: Tue, 19 Aug 2025 15:43:46 +0900 Subject: [PATCH 25/47] =?UTF-8?q?=E2=9C=A8=20feat:=20username=EA=B3=BC=20?= =?UTF-8?q?=EC=9D=B8=EC=82=AC=EB=A7=90=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/layouts/menu/AuthSection.vue | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/layouts/menu/AuthSection.vue b/src/components/layouts/menu/AuthSection.vue index 372ce82..92fe705 100644 --- a/src/components/layouts/menu/AuthSection.vue +++ b/src/components/layouts/menu/AuthSection.vue @@ -2,7 +2,13 @@
diff --git a/src/components/pre-contract/owner/step5/Step5UploadTerms.vue b/src/components/pre-contract/owner/step5/Step5UploadTerms.vue index 76f329a..fc6a9d3 100644 --- a/src/components/pre-contract/owner/step5/Step5UploadTerms.vue +++ b/src/components/pre-contract/owner/step5/Step5UploadTerms.vue @@ -3,7 +3,7 @@

계약서 업로드

-

미리 작성된 계약서를 업로드하여 기존 특약을 추가하세요.

+

미리 작성된 계약서가 있다면 업로드하여 기존 특약을 추가하세요.

From be9b6a4d124fbfd978ff92b1abcfac51dd369529 Mon Sep 17 00:00:00 2001 From: Whatdoyumin Date: Tue, 19 Aug 2025 18:35:57 +0900 Subject: [PATCH 32/47] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=88=98=EB=9D=BD/?= =?UTF-8?q?=EA=B1=B0=EC=A0=88=20=EC=8B=9C=20alert=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/contract/chat/ContractChat.vue | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/components/contract/chat/ContractChat.vue b/src/components/contract/chat/ContractChat.vue index cb2b033..c9945ac 100644 --- a/src/components/contract/chat/ContractChat.vue +++ b/src/components/contract/chat/ContractChat.vue @@ -250,6 +250,21 @@ const respondGoToStep2 = async (accepted) => { console.warn('postGoToStep2 실패:', res?.message) return } + + if (accepted) { + alert( + amOwner.value + ? '임대인이 2단계 진행을 수락했습니다.' + : '임차인이 2단계 진행을 수락했습니다.', + ) + } else { + alert( + amOwner.value + ? '임대인이 2단계 진행을 거절했습니다.' + : '임차인이 2단계 진행을 거절했습니다.', + ) + } + await loadMessages(id) nextTick(forceScrollToBottom) } catch (e) { From abbd48cd2460988e265c9c0a1b3a267fc1efa53c Mon Sep 17 00:00:00 2001 From: Whatdoyumin Date: Tue, 19 Aug 2025 18:36:05 +0900 Subject: [PATCH 33/47] =?UTF-8?q?=E2=9C=A8=20feat:=20=EA=B8=B0=EB=B3=B8=20?= =?UTF-8?q?=ED=8A=B9=EC=95=BD=20=EA=B2=80=ED=86=A0=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/chat/aiUiRegistry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/chat/aiUiRegistry.js b/src/config/chat/aiUiRegistry.js index 57835d3..806886d 100644 --- a/src/config/chat/aiUiRegistry.js +++ b/src/config/chat/aiUiRegistry.js @@ -103,7 +103,7 @@ export const buttonsByStep = { 2: {}, 3: { [AI_SENDER.PLAIN]: [], - [AI_SENDER.BUTTON]: [{ label: '특약 검토', action: 'step3.openTermsReview' }], + [AI_SENDER.BUTTON]: [], }, 4: {}, } From 6de88e913b89b1491393de182c1fa929ff3c64cb Mon Sep 17 00:00:00 2001 From: MeongW Date: Tue, 19 Aug 2025 18:40:55 +0900 Subject: [PATCH 34/47] =?UTF-8?q?:bug:=20fix:=20UI=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/contractApi.js | 12 ++ src/apis/mypage.js | 4 +- src/components/alarm/AlarmDropdown.vue | 4 +- src/components/common/PropertyImage.vue | 52 ++++++- src/components/common/PropertyItem.vue | 27 +++- .../mypage/contracts/ContractsCard.vue | 61 ++++++-- src/components/risk-check/PropertyCard.vue | 2 +- .../risk-check/RiskCheckHistoryModal.vue | 6 +- .../confirm/BuildingRegistryForm.vue | 16 +- .../risk-check/result/DetailedAnalysis.vue | 52 +++++-- src/pages/mypage/MyPageContracts.vue | 140 +++++++++++++++--- src/pages/mypage/MyPageHome.vue | 15 +- src/pages/mypage/MyPageProperties.vue | 71 ++++++--- src/pages/risk-check/RiskCheckResult.vue | 117 +++------------ src/stores/mypage.js | 9 +- src/stores/useContractTermStore.js | 2 +- 16 files changed, 382 insertions(+), 208 deletions(-) diff --git a/src/apis/contractApi.js b/src/apis/contractApi.js index 88ef596..3e55e53 100644 --- a/src/apis/contractApi.js +++ b/src/apis/contractApi.js @@ -1,9 +1,21 @@ import api from './index' const CONTRACT_BASE_URL = '/api/contract' +const CONTRACT_CHAT_BASE_URL = '/api/chat/contract' // 전체 계약서 조회 (오른쪽) export const contractApi = { + // 계약 채팅방 URL로 이동 + moveContractChat: async (chatRoomId) => { + try { + const response = await api.get(`${CONTRACT_CHAT_BASE_URL}/${chatRoomId}/moveContractChat`) + return response.data + } catch (error) { + console.error('계약 채팅방 이동 실패: ', error) + throw error + } + }, + getContractBasic: async (contractChatId) => { try { const response = await api.post(`${CONTRACT_BASE_URL}/${contractChatId}/getContract`) diff --git a/src/apis/mypage.js b/src/apis/mypage.js index 4af8b70..677279b 100644 --- a/src/apis/mypage.js +++ b/src/apis/mypage.js @@ -157,9 +157,9 @@ export const mypageAPI = { }, // 매물 삭제 - deleteProperty: async (propertyId) => { + deleteProperty: async (homeId) => { try { - const response = await api.delete(`/api/mypage/properties/${propertyId}`) + const response = await api.delete(`/api/homes/${homeId}`) return response.data } catch (error) { console.error('매물 삭제 실패:', error) diff --git a/src/components/alarm/AlarmDropdown.vue b/src/components/alarm/AlarmDropdown.vue index 86ed8ab..13d7ab6 100644 --- a/src/components/alarm/AlarmDropdown.vue +++ b/src/components/alarm/AlarmDropdown.vue @@ -189,8 +189,8 @@ const handleNotificationClick = async (notification) => { // 채팅 알림 - 채팅방으로 이동 targetUrl = `/chat?roomId=${notification.relatedId}` } else if (notification.type.includes('CONTRACT') && notification.relatedId) { - // 계약 관련 알림 - 계약 페이지로 이동 - targetUrl = `/contract/${notification.relatedId}` + // 계약 관련 알림 - 계약 채팅방으로 이동 + targetUrl = `/contract-chat/${notification.relatedId}` } else if (notification.type === 'SYSTEM') { // 시스템 알림 - 알림 목록 페이지로 이동 targetUrl = '/notifications' diff --git a/src/components/common/PropertyImage.vue b/src/components/common/PropertyImage.vue index c258a43..541a79a 100644 --- a/src/components/common/PropertyImage.vue +++ b/src/components/common/PropertyImage.vue @@ -4,7 +4,7 @@ import PropertyImagePlaceholder from './PropertyImagePlaceholder.vue' const props = defineProps({ src: { - type: String, + type: [String, Array], default: '' }, alt: { @@ -40,12 +40,47 @@ const handleImageLoad = () => { // 이미지가 유효하지 않은지 확인 const isInvalidImage = computed(() => { - return !props.src || - props.src === '/property-placeholder.jpg' || - props.src.includes('example.com') || + const srcToCheck = processedSrc.value + + // S3 URL이나 unsplash 이미지는 유효한 것으로 처리 + if (srcToCheck && (srcToCheck.includes('.s3.') || + srcToCheck.includes('amazonaws.com') || + srcToCheck.includes('unsplash.com'))) { + return imageError.value // 에러가 발생한 경우에만 invalid로 처리 + } + + return !srcToCheck || + srcToCheck === '/property-placeholder.jpg' || + srcToCheck.includes('example.com') || imageError.value }) +// S3 URL인지 확인하고 처리 +const processedSrc = computed(() => { + if (!props.src) return '' + + // 배열인 경우 첫 번째 이미지 사용 + let srcUrl = props.src + if (Array.isArray(props.src)) { + if (props.src.length === 0) return '' + srcUrl = props.src[0] + } + + if (!srcUrl || typeof srcUrl !== 'string') return '' + + // S3 URL 패턴 확인 + if (srcUrl.includes('.s3.') || srcUrl.includes('amazonaws.com')) { + // 이미 전체 URL인 경우 그대로 반환 + if (srcUrl.startsWith('http://') || srcUrl.startsWith('https://')) { + return srcUrl + } + // 프로토콜이 없는 경우 https 추가 + return `https://${srcUrl}` + } + + return srcUrl +}) + const sizeClasses = computed(() => { switch (props.size) { case 'small': @@ -93,7 +128,7 @@ const imageClasses = computed(() => {
diff --git a/src/components/common/PropertyItem.vue b/src/components/common/PropertyItem.vue index ad8c4fe..21cff31 100644 --- a/src/components/common/PropertyItem.vue +++ b/src/components/common/PropertyItem.vue @@ -1,5 +1,5 @@ diff --git a/src/components/chat/chatList/ChatList.vue b/src/components/chat/chatList/ChatList.vue index 8c167de..99a446c 100644 --- a/src/components/chat/chatList/ChatList.vue +++ b/src/components/chat/chatList/ChatList.vue @@ -1,28 +1,37 @@