박현정 박현정 3 days ago
250807 박현정 모델링 이미지 썸네일 / 프로젝트 이름 변경 기능 추가
@4143292d63f3164dc9d97ca84c7baba53024a4ea
client/resources/api/asset.js
--- client/resources/api/asset.js
+++ client/resources/api/asset.js
@@ -18,4 +18,9 @@
 // 자산(프로젝트) 즐겨찾기 설정 기능
 export const updateProjectFavoriteProc = (projectGroupId, data) => {
     return apiClient.post(`/project/${projectGroupId}/updateProjectFavorite.json`, data);
-}
(파일 끝에 줄바꿈 문자 없음)
+}
+
+// 자산(프로젝트) 이름 변경 기능
+export const renameProjectProc = (projectId, data) => {
+    return apiClient.post(`/project/${projectId}/renameProject.json`, data);
+}
client/views/pages/subPage/Asset.vue
--- client/views/pages/subPage/Asset.vue
+++ client/views/pages/subPage/Asset.vue
@@ -54,19 +54,18 @@
           <!-- 드롭다운 메뉴 표시 -->
         <div v-if="openMenuIndex === index" class="dropdown-menu"   :class="dropdownDirection">
             <button><img src="../../../resources/img/content/ico_subShare.svg" alt=""> 공유</button>
-            <button><img src="../../../resources/img/content/ico_subCopy.svg" alt="" @click="duplicateProject(project.projectId)"> 복사</button>
-            <button><img src="../../../resources/img/content/ico_trashcan.svg" alt="" @click="deleteProject(project.projectGroupId)"> 삭제</button>
-            <button><img src="../../../resources/img/content/ico_version.svg" alt=""> 버전기록</button>
-            <!-- @click="changeProjectName -->
-            <button><img src="" alt="" > 이름 변경</button> 
+            <button @click="duplicateProject(project.projectId)"><img src="../../../resources/img/content/ico_subCopy.svg" alt=""> 복사</button>
+            <button @click="deleteProject(project.projectGroupId)"><img src="../../../resources/img/content/ico_trashcan.svg" alt=""> 삭제</button>
+            <button @click="openPopup('versions', project)"><img src="../../../resources/img/content/ico_version.svg" alt=""> 버전기록</button>
+            <button @click="openPopup('rename', project)"><img src="" alt=""> 이름 변경</button> 
         </div>
       </div>
     </div>
     <div class="old-projects">
-      <!-- 팝업 창 -->
-      <div v-if="showPopup" class="popup-overlay" @click.self="showPopup = false">
+      <!-- 이전 버전 팝업 창 -->
+      <div v-if="activePopup === 'versions'" class="popup-overlay" @click.self="closePopup()">
         <div class="popup">
-          <button class="close-btn" @click="showPopup = false">X</button>
+          <button class="close-btn" @click="closePopup()">X</button>
           <!-- 여기에 내용 넣기 -->
           <h3>작업내역</h3>
           <img src="../../../resources/img/content/ico_digital_asset.svg" alt="작업 이미지" />
@@ -81,12 +80,28 @@
         </div>
       </div>
     </div>
+
+    <div class="change-project-name">
+      <!-- 이름 변경 팝업 창 -->
+      <div v-if="activePopup === 'rename'" class="popup-overlay" @click.self="closePopup()">
+        <div class="popup">
+          <h3>이름 변경</h3>
+          <div class="input-wrapper">
+            <input type="text" class="form-control sm" style="border: 0;" v-model="newProjectName" @keyup.enter="changeProjectName(selectedProject.id)">
+          </div>
+          <div class="button-wrapper">
+            <button @click="closePopup()">취소</button>
+            <button @click="changeProjectName(selectedProject.id)">저장</button>
+          </div>
+        </div>
+      </div>
+    </div>
   </div>
 </template>
 
 <script>
 import { mapGetters } from 'vuex';
-import { deleteProjectProc, findAllProjectsProc, duplicateProjectProc, updateProjectFavoriteProc } from '../../../resources/api/asset';
+import { deleteProjectProc, findAllProjectsProc, duplicateProjectProc, updateProjectFavoriteProc, renameProjectProc } from '../../../resources/api/asset';
 
 export default {
     data() {
@@ -101,14 +116,16 @@
             members: [],
             openMenuIndex: null,
             dropdownDirection: 'right', // 또는 'left'
-            // showPopup: false,
+            activePopup: null, // 실행 중인 팝업창(이전 버전, 이름 변경)
             keyword: '', // 검색 키워드
             selectedSort: 'LATEST', // 프로젝트 목록 정렬 기준 선택 (기본은 '최근 수정순')
             sortOptions: [ // 프로젝트 목록 정렬 기준
               { value: 'LATEST', text: '최근 수정순' },
               { value: 'OLDEST', text: '오래된 순' },
               { value: 'NAME_ASC', text: '이름순' },
-            ]
+            ],
+            newProjectName: '', // 변경할 프로젝트 이름
+            selectedProject: null, // 선택된 프로젝트
 
         };
     },
@@ -221,10 +238,17 @@
               console.error('복제 실패: ', error);
             })
         },
-        // showPopupFunc(){
-        //   console.log("팝업창 열기");
-        //   this.showPopup = true;
-        // },
+        // 팝업창 열기
+        openPopup(popupType, project){
+          this.activePopup = popupType;
+          this.newProjectName = project.name;
+          this.selectedProject = project;
+        },
+        // 팝업창 닫기
+        closePopup(){
+          console.log('팝업창 닫기: ');
+          this.activePopup = null;
+        },
         // 프로젝트 삭제
         deleteProject(projectGroupId) {
           const data = {
@@ -253,6 +277,23 @@
               console.error('즐겨찾기 설정 실패: ', error);
             })
         },
+        // 프로젝트 이름 변경
+        changeProjectName(projectId) {
+
+          const data = {
+            memberId : this.getMemId,
+            projectName : this.newProjectName
+          }
+          renameProjectProc(projectId, data)
+            .then((response) => {
+              console.log('이름 변경');
+              this.closePopup();
+              this.loadMainProjects(); // 이름 변경 후 목록 새로고침
+            })
+            .catch((error) => {
+              console.error('프로젝트 이름 변경 실패: '+ error);
+            })
+        }
         
     },
     watch: {
@@ -279,7 +320,7 @@
 
 </script>
 
-<!-- <style scoped>
+<style scoped>
 .popup-overlay {
   position: fixed;
   top: 0;
@@ -311,4 +352,18 @@
   font-size: 18px;
   cursor: pointer;
 }
-</style> -->
(파일 끝에 줄바꿈 문자 없음)
+
+.button-wrapper {
+  display: flex;
+  justify-content: flex-end;
+  gap: 8px;
+}
+
+.button-wrapper button {
+  padding: 8px 16px;
+  border: 1px solid #ccc;
+  border-radius: 4px;
+  cursor: pointer;
+  background-color: #f0f0f0;
+}
+</style>
(파일 끝에 줄바꿈 문자 없음)
client/views/pages/subPage/FeedBack.vue
--- client/views/pages/subPage/FeedBack.vue
+++ client/views/pages/subPage/FeedBack.vue
@@ -202,7 +202,7 @@
             // } catch (err) {
             // console.error('회신 실패:', err);
             // }
-            if (!this.replyText.trim() || !this.stompClient) return;
+            if (!this.replyText.trim() || !this.stompClient) return; // 빈 메시지 또는 stomp 연결 안돼있으면 전송 종료
 
             const msg = {
                 chatRoomId: this.selectedFeedback.chatRoomId,
@@ -210,9 +210,9 @@
                 msgContent: this.replyText,
             };
 
-            this.stompClient.publish({
+            this.stompClient.publish({ // 메시지 발행(클라이언트 -> 서버)
                 destination: "/pub/chat.sendMessage",
-                body: JSON.stringify(msg)
+                body: JSON.stringify(msg) // json 문자열로 메시지 전송
             });
             this.replyText = '';
         },
@@ -285,13 +285,16 @@
         connectToChatRoom(roomId) {
             if (this.stompClient && this.connectedRoomId === roomId) return;
 
-            const socket = new SockJS('http://localhost:10911/ws');
+            const socket = new SockJS('http://localhost:10911/ws'); // 소켓 연결
 
+            // stomp 설정
             const client = new Client({
                 webSocketFactory: () => socket,
                 onConnect: () => {
-                client.subscribe(`/sub/chat.room.${roomId}`, (message) => {
-                    const msg = JSON.parse(message.body);
+                client.subscribe(`/sub/chat.room.${roomId}`, (message) => { // 메시지 구독(서버 -> 클라이언트)
+                    
+                    const msg = JSON.parse(message.body); // 받은 메시지 json 형태로 파싱
+                    
                     this.feedbackChatList.push({
                     id: msg.chatMsgId,
                     user: msg.senderName,
client/views/pages/subPage/Modeling.vue
--- client/views/pages/subPage/Modeling.vue
+++ client/views/pages/subPage/Modeling.vue
@@ -13,9 +13,10 @@
                     </button>
                 </div>
                 <div class="tab-content">
-                    <div class="layout center justify-center h_100" style="background-color: #ffffff; border-radius: 2rem;"  v-if="activeTab === 'Image Prompt'">
-                        <div class="layout center center" style="flex-direction: column;">
-                            <input type="file" ref="fileInput" name="" id="file" class="sr-only">
+                    <div class="layout center justify-center h_100" :style="imagePromptStyle" v-if="activeTab === 'Image Prompt'"> 
+                        <!-- style="background-color: #ffffff; border-radius: 2rem;" -->
+                        <div v-if="!imageUrl" class="layout center center" style="flex-direction: column;">
+                            <input type="file" ref="fileInput" @change="handleFileChange" accept="image/*" name="" id="file" class="sr-only">
                             <span class="file-icon" id="file"><img src="../../../resources/img/content/ico_file.svg" alt=""></span>
                             <label for="file" style="text-align: center;">파일을 업로드하세요.</label>
                         </div>
@@ -377,6 +378,7 @@
             projectGroupId: null, // 프로젝트 피드백 작성 시 사용
             isReset: null, // 이미지 초기화 시 사용
             feedback: '', // 피드백 내용
+            imageUrl: '', // 이미지 썸네일
         };
     },
     methods: {
@@ -440,7 +442,7 @@
         createProject() {
             const formData = new FormData();
             const fileInput = this.$refs.fileInput; // 업로드 파일 가져오기
-            if (!fileInput && this.isReset == true) {
+            if (this.isReset == true) {
                 alert("파일을 선택해주세요.");
                 return;
             }
@@ -459,7 +461,7 @@
         updateProject(projectId) {
             const formData = new FormData();
             const fileInput = this.$refs.fileInput; // 업로드 파일 가져오기(없으면 기존 이미지 유지: fileInput==null)
-            if (!fileInput && this.isReset == true) {
+            if (this.isReset == true) {
                 alert("파일을 선택해주세요.");
                 return;
             }
@@ -491,9 +493,12 @@
                     const projectInfo = response.data.result;
                     this.projectId = projectId;
                     this.projectGroupId = projectInfo.project.projectGroupId;
+
                     this.localCommentData.summary =  projectInfo.projectComment.summary;
                     this.localCommentData.comment =  projectInfo.projectComment.comment;
                     this.saveComment();
+
+                    this.imageUrl = 'http://localhost:10911' + projectInfo.projectImage.fileUrl;
 
                     const memberList = projectInfo.projectMember; 
                     this.members = memberList.map(member => ({
@@ -507,6 +512,10 @@
         // 이미지 초기화
         resetImage(){
             this.isReset = true;
+            if (this.imageUrl) {
+                URL.revokeObjectURL(this.imageUrl); // 메모리 해제
+            }
+            this.imageUrl = '';
         },
         // 피드백 저장
         saveFeedback() {
@@ -524,7 +533,19 @@
                 })
                 .catch(error => {console.error('피드백 저장 실패');});
             this.closeModal();
-        }
+        },
+        // 이미지 썸네일 url 생성
+        handleFileChange(event){
+            
+            const file = event.target.files[0];
+            if(file && file.type.startsWith('image/')){
+                // 기존 썸네일 URL이 있다면 메모리 해제
+                if (this.imageUrl) {
+                    URL.revokeObjectURL(this.imageUrl);
+                }
+                this.imageUrl = URL.createObjectURL(file);
+            }
+        },
     }, 
     watch: {},
     computed: {
@@ -537,6 +558,22 @@
             return tool.icon;
             };
         },
+        imagePromptStyle() {
+            const style = {
+                'background-color': '#ffffff',
+                'border-radius': '2rem',
+                'cursor': 'pointer'
+            };
+
+            if (this.imageUrl) {
+                style['background-image'] = `url(${this.imageUrl})`;
+                style['background-size'] = 'contain';
+                style['background-position'] = 'center';
+                style['background-repeat'] = 'no-repeat';
+                style['cursor'] = 'default';
+            }
+            return style;
+        },
         ...mapGetters([
             'getMemId',
             'getMemNm',
Add a comment
List