Skip to content

Commit

Permalink
Merge pull request #17 from hw-coconote/feat/HC-158
Browse files Browse the repository at this point in the history
[Feat] HC-158  s3에 이미지 업로드 및 디폴트 이미지 설정 및 이미지 보기 추가
  • Loading branch information
sseho authored Oct 2, 2024
2 parents ef98d0d + f0d1b5b commit e2eda10
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 65 deletions.
Binary file added src/assets/file.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
203 changes: 139 additions & 64 deletions src/components/thread/ThreadComponent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@
</ul>

<div class="input-group">

<div class="image-group">
<div v-for="(file, index) in fileList" :key="index">
<button type="button" @click="deleteImage(index)">삭제</button>
<img :src="file.imageUrl" alt="image" style="height: 120px; width: 120px; object-fit: cover;">
<img :src="file.imageUrl" @error="e => e.target.src = require('@/assets/file.png')" style="height: 120px; width: 120px; object-fit: cover;">
<p class="custom-contents">{{file.name}}</p>
</div>
</div>
Expand All @@ -40,7 +41,7 @@
v-on:keypress.enter="sendMessage"
/>
<div class="input-group-append">
<button class="btn btn-primary" type="button" @click="sendMessage" :disabled="!message">보내기</button>
<button class="btn btn-primary" type="button" @click="sendMessage" :disabled="!message && fileList.length === 0">보내기</button>
</div>
</div>
</div>
Expand All @@ -49,7 +50,8 @@

<script>
import ThreadLineComponent from "@/components/thread/ThreadLineComponent.vue";
import axios from "axios";
// import axios from "axios";
import axios from '@/services/axios'
import SockJS from "sockjs-client";
// import Stomp from "stompjs";
import { Stomp } from "@stomp/stompjs";
Expand Down Expand Up @@ -82,6 +84,8 @@ export default {
isLastPage: false,
files: null,
fileList: [],
// uploadProgress: [], // 업로드 진행 상황
filesRes: null,
};
},
created() {
Expand Down Expand Up @@ -111,16 +115,108 @@ export default {
}
},
computed: {},
methods: {
async sendMessage() {
const authToken = localStorage.getItem('accessToken');
if(this.fileList.length>0) {
try{
const presignedUrls = await this.getPresignedURL();
// 각 파일에 대해 Presigned URL을 이용하여 S3에 업로드
const uploadedFileUrls = await Promise.all(this.fileList.map(file => this.uploadFileToS3(file.file, presignedUrls[file.name])));
// 파일 중 업로드가 실패한 파일이 있으면 필터링
const successfulUploads = uploadedFileUrls.filter(url => url !== null);
// 성공적으로 업로드된 파일만 메타데이터 저장
if (successfulUploads.length) {
await this.saveFileMetadata(successfulUploads);
} else {
alert('모든 파일 업로드에 실패했습니다.');
}
}catch(error){
console.error('Upload failed:', error);
alert('파일 업로드 중 오류가 발생했습니다.');
}
}
this.ws.send(
"/pub/chat/message",
{Authorization: authToken},
JSON.stringify({
type: "TALK",
channelId: this.roomId,
senderId: this.sender,
content: this.message,
workspaceId: this.workspaceId,
files: this.filesRes?.map(file => ({fileId:file.id, fileName: file.fileName, fileURL: file.fileUrl }))
})
);
this.message = "";
this.fileList = [];
this.uploadProgress = [];
this.filesRes = null;
},
async getPresignedURL(){
const reqFiles = this.fileList.map(file => ({fileName:file.name, fileSize:file.size}))
const response = await axios.post(
`${process.env.VUE_APP_API_BASE_URL}/files/presigned-urls`, reqFiles
);
return response.data.result;
},
async uploadFileToS3(file, presignedUrl) {
try {
const config = {
headers: {
'Content-Type': file.type, // 파일 타입 지정
},
// onUploadProgress: (progressEvent) => {
// const index = this.files.indexOf(file); // 인덱스 찾기
// this.uploadProgress[index] = Math.round((progressEvent.loaded * 100) / progressEvent.total); // 업로드 진행상황 업데이트
// },
};
await axios.put(presignedUrl, file, config)
// S3에 업로드된 파일의 URL에서 ? 앞부분만 반환 (쿼리 파라미터 제거)
return this.extractS3Url(presignedUrl);
} catch (error) {
console.error(`Error uploading ${file.name}:`, error);
return null; // 업로드 실패 시 null 반환
}
},
// Presigned URL에서 ? 이전의 S3 URL만 남김
extractS3Url(presignedUrl) {
return presignedUrl.split('?')[0]; // ? 기준으로 앞부분만 추출
},
async saveFileMetadata(uploadedFileUrls) {
const metadataDto = {
channelId: this.id, // 적절한 채널 ID로 수정하세요
fileType: 'THREAD', // 백엔드에서 필요한 Enum 값 (FileType.THREAD, FileType.CANVAS 등)
fileSaveListDto: uploadedFileUrls.map((url, index) => ({
fileName: this.fileList[index].name, // 원본 파일 이름
fileUrl: url, // 짧아진 S3 URL
})), // 파일 메타데이터 리스트
};
const response = await axios.post('http://localhost:8080/api/v1/files/metadata', metadataDto);
this.filesRes = response.data.result;
},
fileUpdate(){
this.files.forEach(file => {
this.fileList.push({
name: file.name,
size: file.size,
type: file.type,
file,
imageUrl: URL.createObjectURL(file)})
});
this.files = null;
},
async getMessageList() {
try {
Expand Down Expand Up @@ -187,33 +283,11 @@ export default {
}, 100);
}
},
async getPresignedURL(){
const reqFiles = this.fileList.map(file => ({fileName:file.name, fileSize:file.size}))
const response = await axios.post(
`${process.env.VUE_APP_API_BASE_URL}/files/presigned-urls`, reqFiles
);
console.log(response.data);
},
deleteImage(index){
this.fileList.splice(index, 1);
},
sendMessage() {
const authToken = localStorage.getItem('accessToken');
if(this.fileList.length>0) this.getPresignedURL();
this.ws.send(
"/pub/chat/message",
{Authorization: authToken},
JSON.stringify({
type: "TALK",
channelId: this.roomId,
senderId: this.sender,
content: this.message,
workspaceId: this.workspaceId,
})
);
this.message = "";
},
recvMessage(recv) {
this.messages.unshift({
id: recv.id,
Expand Down Expand Up @@ -263,42 +337,42 @@ export default {
);
},
getTime(createdAt) {
const createdTime = new Date(createdAt);
let hour = createdTime.getHours();
let minute = createdTime.getMinutes();
let ampm;
if(hour < 12) {
ampm = '오전'
} else {
ampm = '오후'
hour -= 12;
}
if(hour < 10) {
hour = '0'+hour;
}
const createdTime = new Date(createdAt);
let hour = createdTime.getHours();
let minute = createdTime.getMinutes();
let ampm;
if(hour < 12) {
ampm = '오전'
} else {
ampm = '오후'
hour -= 12;
}
if(hour < 10) {
hour = '0'+hour;
}
if(minute < 10) {
minute = '0'+minute;
}
if(minute < 10) {
minute = '0'+minute;
}
return ampm + ' ' + hour + ':' + minute;
},
isDifferentDay(d1, d2) {
const day1 = new Date(d1);
const day2 = new Date(d2);
return ampm + ' ' + hour + ':' + minute;
},
isDifferentDay(d1, d2) {
const day1 = new Date(d1);
const day2 = new Date(d2);
if(day1.getFullYear() == day2.getFullYear()
&& day1.getMonth() == day2.getMonth()
&& day1.getDay() == day2.getDay()) return false;
if(day1.getFullYear() == day2.getFullYear()
&& day1.getMonth() == day2.getMonth()
&& day1.getDay() == day2.getDay()) return false;
return true;
},
getDay(createdAt) {
const createdTime = new Date(createdAt);
return true;
},
getDay(createdAt) {
const createdTime = new Date(createdAt);
return `${createdTime.getFullYear()}${createdTime.getMonth() + 1}${createdTime.getDate()}`;
}
return `${createdTime.getFullYear()}${createdTime.getMonth() + 1}${createdTime.getDate()}`;
}
},
};
</script>
Expand Down Expand Up @@ -328,17 +402,18 @@ export default {
width: 120px;
max-height: 180px;
}
.custom-contents{
max-width: 120px; /* 제목의 최대 너비를 설정 */
overflow: hidden; /* 내용이 넘칠 경우 숨김 처리 */
text-overflow: ellipsis !important; /* 넘치는 텍스트에 '...' 추가 (이거 적용안됨 이후 수정필요)*/
white-space: nowrap; /* 텍스트 줄 바꿈 방지 */
}
.text-group {
display: flex;
flex-direction: row;
}
.form-control {
width: 100%;
}
.custom-contents{
max-width: 120px; /* 제목의 최대 너비를 설정 */
overflow: hidden; /* 내용이 넘칠 경우 숨김 처리 */
text-overflow: ellipsis !important; /* 넘치는 텍스트에 '...' 추가 (이거 적용안됨 이후 수정필요)*/
white-space: nowrap; /* 텍스트 줄 바꿈 방지 */
}
</style>
21 changes: 20 additions & 1 deletion src/components/thread/ThreadLineComponent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,14 @@
<div class="tag">tag</div>
</div>
<div class="content">{{content}}</div>
<div class="files">{{files}}</div>

<div class="image-group">
<div v-for="(file, index) in this.files" :key="index">
<img :src="file.fileURL" alt="image" @error="e => e.target.src = require('@/assets/file.png')" style="height: 120px; width: 120px; object-fit: cover;">
<p class="custom-contents">{{file.fileName}}</p>
</div>
</div>

<div class="comment">comment</div>
</div>
</div>
Expand Down Expand Up @@ -57,6 +64,18 @@
}
.files {
}
.image-group {
display: flex;
flex-direction: row;
width: 120px;
max-height: 180px;
}
.custom-contents{
max-width: 120px; /* 제목의 최대 너비를 설정 */
overflow: hidden; /* 내용이 넘칠 경우 숨김 처리 */
text-overflow: ellipsis !important; /* 넘치는 텍스트에 '...' 추가 (이거 적용안됨 이후 수정필요)*/
white-space: nowrap; /* 텍스트 줄 바꿈 방지 */
}
.comment {
Expand Down

0 comments on commit e2eda10

Please sign in to comment.