
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
<template>
<div class="w_100 h_100 layout sterech gap30">
<div class="left-zone" style="background-color: #d5dce7;">
<div class="layout center space-between">
<img src="../../../resources/img/content/feedback.svg" alt="">
<div>
<p class="feedback-title">Feedback</p>
<select name="sort" id="sort" class="form-select sm" style="border: none;min-width: 140px;">
<option value="">최근순</option>
<option value="">최근순</option>
</select>
</div>
</div>
<div class="feedback-list">
<div v-for="feedback in feedbackList" :key="feedback.id" class="feedback-item" :class="{ unread: feedback.status === 'unread' }">
<div class="feedback-card">
<h4>
<img src="../../../resources/img/content/ico_confirmed.svg" alt="" v-if="feedback.status === 'confirmed'" style="vertical-align: middle;">
<img src="../../../resources/img/content/ico_unconfirmed.svg" alt="" v-else style="vertical-align: middle;">
<span>{{ feedback.project }}</span>
</h4>
<div class="feedback-message">
<p>{{ feedback.message }}</p>
</div>
<div class="meta">
<span><img src="../../../resources/img/content/ico_clock.svg" alt="" style="vertical-align: middle;"> {{ feedback.time }}</span> | <span><img src="../../../resources/img/content/ico_feeduser.svg" alt="" style="vertical-align: middle;"> {{ feedback.user }}</span>
</div>
</div>
<div class="btn-group" v-if="feedback.status === 'unread'">
<button class="btn sm purple" @click="markAsRead(feedback.id)">확인</button>
<!-- <button class="btn sm black" @click="replyTo(feedback)">회신</button> -->
<button class="btn sm black" @click="loadFeedbackChatList(feedback.chatRoomId, feedback)">회신</button>
</div>
<div class="btn-group" v-else>
<!-- <button class="btn sm primary" @click="replyTo(feedback)">확인됨</button> -->
<button class="btn sm primary" @click="loadFeedbackChatList(feedback.chatRoomId, feedback)">확인됨</button>
</div>
</div>
</div>
</div>
<div class="right-zone feedback-detail-zone">
<div class="summary">
<p><span>일주일 간 총 피드백 수 </span> <strong>{{ feedbackSummary.total }}건</strong></p>|
<p><span class="unread">미확인</span> <strong>{{ feedbackSummary.unread }}건</strong></p> |
<p><span class="read">확인</span> <strong>{{ feedbackSummary.read }}건</strong></p>
</div>
<div class="feedback-detail" v-if="selectedFeedback">
<div class="top-zone">
<h3 class="project-name">{{ selectedFeedback.project }}</h3><span style="font-weight: 700;"> feedback message</span>
</div>
<ul >
<li v-for="chat in feedbackChatList" :key="chat.id">
<div class="layout center space-between">
<strong><img src="../../../resources/img/content/ico_feeduser.svg" alt="" style="vertical-align: middle;"> {{ chat.user }}</strong>
<div class="right-content">
<p>{{ chat.content }}</p>
</div>
</div>
<p style="text-align: right;"> <span>{{ chat.date }} </span><span><img src="../../../resources/img/content/ico_clock.svg" alt="" style="vertical-align: middle;"> {{ chat.time }}</span></p>
</li>
</ul>
<div style="position: relative;">
<input type="text" class="form-control sm" v-model="replyText" placeholder="피드백 회신 내용을 작성하세요."></input>
<button class="send-btn" @click="submitReply"><img src="../../../resources/img/content/ico_send.svg" alt="" style="vertical-align: middle;"></button>
</div>
</div>
<div class="feedback-detail first-view" v-else>
<img src="../../../resources/img/content/unfeedback.svg" alt="">
<p>피드백이 선택되지 않았습니다.</p>
<p>피드백을 선택하면 상세내용을 확인할 수 있습니다.</p>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import { checkFeedbackProc, findAllChatMsgsProc, findAllFeedbacksProc } from '../../../resources/api/feedback';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import 'dayjs/locale/ko'; // 한국어 locale 불러오기
import SockJS from 'sockjs-client';
import { Client } from '@stomp/stompjs';
dayjs.extend(relativeTime);
dayjs.locale('ko');
export default {
data() {
return {
feedbackList: [
// {
// id: 1,
// project: '프로젝트 C',
// message: '“크기를 두 배 증가시켜 주세요..”',
// time: '5분 전',
// user: '김철수님',
// status: 'unread' // 'unread' | 'read' | 'confirmed'
// },
// {
// id: 2,
// project: '프로젝트 B',
// message: '“플라스틱 재질을 다음과 같이..”',
// time: '30분 전',
// user: '이영희님',
// status: 'unread'
// },
// {
// id: 3,
// project: '프로젝트 B',
// message: '“좀 더 밝은 색으로 바꿔주세요..”',
// time: '어제',
// user: '김철수님',
// status: 'confirmed'
// }
],
feedbackSummary: {
total: 3,
unread: 2,
read: 1
},
selectedFeedback: null, // 회신용
replyingFeedbackId: null, // 회신 폼 표시용
feedbackChatList: [
// {
// id: 1,
// name: '홍길동',
// content: '교육 내용이 이해하기 쉬웠습니다.',
// date: '2025-06-24',
// time: '14:30',
// status: '미확인'
// },
// {
// id: 2,
// name: '김영희',
// content: '실습 예제가 더 많았으면 좋겠습니다.',
// date: '2025-06-23',
// time: '10:15',
// status: '확인'
// },
// {
// id: 3,
// name: '박철수',
// content: '강사님 설명이 친절해서 좋았습니다.',
// date: '2025-06-22',
// time: '16:45',
// status: '회신완료'
// },
// {
// id: 4,
// name: '이민정',
// content: '온라인 자료 접근이 어려웠어요.',
// date: '2025-06-21',
// time: '09:05',
// status: '미확인'
// },
// {
// id: 5,
// name: '최현우',
// content: '교육 시간이 너무 짧은 것 같아요.',
// date: '2025-06-20',
// time: '13:20',
// status: '확인'
// }
],
replyText: '',
socket: null,
stompClient: null,
connectedRoomId: null,
};
},
methods: {
// async markAsRead(id) {
// // 서버에 상태값 전달
// try {
// await axios.post(`/api/feedback/${id}/checkFeedback.json`);
// const item = this.feedbackList.find(f => f.id === id);
// if (item) item.status = 'read';
// } catch (err) {
// console.error('확인 처리 실패:', err);
// }
// },
async submitReply() {
// if (!this.replyText.trim()) return;
// try {
// await axios.post(`/api/feedback/${this.replyingFeedbackId}/reply`, {
// reply: this.replyText
// });
// alert('회신이 전송되었습니다.');
// this.replyText = '';
// this.selectedFeedback = null;
// this.replyingFeedbackId = null;
// } catch (err) {
// console.error('회신 실패:', err);
// }
if (!this.replyText.trim() || !this.stompClient) return;
const msg = {
chatRoomId: this.selectedFeedback.chatRoomId,
senderId: this.getMemId,
msgContent: this.replyText,
};
this.stompClient.publish({
destination: "/pub/chat.sendMessage",
body: JSON.stringify(msg)
});
this.replyText = '';
},
replyTo(feedback) {
this.selectedFeedback = feedback;
this.replyingFeedbackId = feedback.id;
},
loadFeedbacks() {
findAllFeedbacksProc({
memberId: this.getMemId
})
.then(response => {
const feedbackList = response.data.result;
this.feedbackList = feedbackList.map(feedback => ({
id: feedback.feedbackId,
projectGroupId: feedback.projectGroupId,
project: feedback.projectName,
message: `"${feedback.feedbackContent}"`,
time: dayjs(feedback.createdAt).fromNow(),
user: feedback.memberName + '님',
chatRoomId: feedback.chatRoomId,
status: feedback.isChecked === 'Y' ? 'read' : 'unread' // 'unread' | 'read' | 'confirmed'
}));
})
.catch(error => {console.error('피드백 목록 조회 실패: ', error);})
},
markAsRead(feedbackId) {
const data = {
memberId : this.getMemId
};
checkFeedbackProc(feedbackId, data)
.then(response => {
this.loadFeedbacks();
console.log('피드백 확인 완료');
})
.catch(error => {
console.error('피드백 확인 실패: ', error);
})
},
loadFeedbackChatList(chatRoomId, feedback) {
const data = {
memberId : this.getMemId
};
findAllChatMsgsProc(chatRoomId, data)
.then(response => {
const chatList = response.data.result;
this.feedbackChatList = chatList.map(chat => ({
id: chat.chatMsgId,
user: chat.senderName,
content: chat.msgContent,
time: dayjs(chat.createdAt).fromNow()
}));
this.replyTo(feedback);
this.connectToChatRoom(feedback.chatRoomId);
})
.catch(error => {
console.error('채팅 조회 실패: ', error);
})
},
connectToChatRoom(roomId) {
if (this.stompClient && this.connectedRoomId === roomId) return;
const socket = new SockJS('http://localhost:10911/ws');
const client = new Client({
webSocketFactory: () => socket,
onConnect: () => {
client.subscribe(`/sub/chat.room.${roomId}`, (message) => {
const msg = JSON.parse(message.body);
this.feedbackChatList.push({
id: msg.chatMsgId,
user: msg.senderName,
content: msg.msgContent,
time: dayjs().fromNow()
});
});
// 연결 완료 시점에만 설정
this.stompClient = client;
this.connectedRoomId = roomId;
},
});
client.activate();
},
},
watch: {},
computed: {
...mapGetters([
'getMemId',
'getMemNm',
'getMemLoginId'
])
},
components: {},
created() {},
mounted() {
this.loadFeedbacks();
},
beforeUnmount() {},
};
</script>