
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="project-container">
<div class="top-zone mb30">
<div class="search-zone" style="background-color: #dfe5f3;">
<div style="position: relative;">
<input type="text" class="form-control sm" style="border: 0;" placeholder="제목을 입력해주세요" v-model="keyword">
<button class="btn-ico sm ico-after ico-sch" @click="searchProjects(keyword)"></button>
</div>
</div>
<div class="layout center justify-end gap10">
<select v-model="selectedSort" name="" id="" class="form-select sm" style="border: 0; width: 200px;">
<option v-for="option in sortOptions" :key="option.value" :value="option.value">
{{ option.text }}
</option>
</select>
<button class="btn sm" style="background-color: #f4d01e;border: #f4d01e;"><img src="../../../resources/img/content/ico_folder.svg" alt="" style="vertical-align: middle;">폴더</button>
</div>
</div>
<div class="bottom-zone">
<!-- 새 카드 -->
<div class="card new-card" @click="goToModelingCreate()">
<div class="new-symbol">
<img src="../../../resources/img/content/ico_digital_asset.svg" alt="">
</div>
<div class="new-text">새로운<br>디지털 자산 만들기</div>
<div class="new-icon"></div>
</div>
<!-- v-for 카드 반복 -->
<div class="card" v-for="(project, index) in projects" :key="index">
<div class="thumbnail" @click="goToModelingUpdate(project)">
<img :src="project.img" alt="" />
</div>
<div class="card-footer">
<div class="w_100 layout center space-between">
<p class="title" @click="goToModelingUpdate(project)">{{ project.name }}</p>
<div class="layout center">
<button @click="toggleFavorite(index)">
<img
:src="project.isFavorite
? require('../../../resources/img/content/ico_favorites.svg')
: require('../../../resources/img/content/ico_unFavorites.svg')"
/>
</button>
<button @click="toggleMenu(index)">
<img src="../../../resources/img/component/common/ico_bread_dot.svg"
alt=""
style="transform: rotate(90deg); width: 20px; height: 20px;" />
</button>
</div>
</div>
<p class="date">{{ project.date }}</p>
</div>
<!-- 드롭다운 메뉴 표시 -->
<div v-if="openMenuIndex === index" class="dropdown-menu" :class="dropdownDirection">
<button><img src="../../../resources/img/content/ico_subShare.svg" alt=""> 공유</button>
<button @click="duplicateProject(project.id)"><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="activePopup === 'versions'" class="popup-overlay" @click.self="closePopup()">
<div class="popup">
<button class="close-btn" @click="closePopup()">X</button>
<h3>작업내역</h3>
<img src="../../../resources/img/content/ico_digital_asset.svg" alt="작업 이미지" />
<div class="versions">
<p>버전 목록</p>
<ul v-if="oldProjects && oldProjects.length > 0">
<li v-for="project in oldProjects" :key="project.id">
{{ project.date }}
</li>
</ul>
<ul v-if="!oldProjects">
<li>5월 3일 오후 2:50</li>
<li>5월 3일 오후 1:14</li>
<li>5월 1일 오전 11:50</li>
</ul>
</div>
</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, renameProjectProc } from '../../../resources/api/asset';
import dayjs from 'dayjs';
export default {
data() {
return {
projects: [
// { name: '프로젝트A', date: '2025.05.12', img: require('../../../resources/img/content/sample1.png'),isFavorite: false },
// { name: '프로젝트B', date: '2025.05.12', img: require('../../../resources/img/content/sample1.png'),isFavorite: true },
// { name: '프로젝트C', date: '2025.05.12', img: require('../../../resources/img/content/sample1.png'),isFavorite: false },
// { name: '프로젝트D', date: '2025.05.12', img: require('../../../resources/img/content/sample1.png'),isFavorite: true },
// { name: '프로젝트E', date: '2025.05.12', img: require('../../../resources/img/content/sample1.png'),isFavorite: false },
],
oldProjects:[], // 이전 버전 목록
members: [],
openMenuIndex: null,
dropdownDirection: 'right', // 또는 'left'
activePopup: null, // 실행 중인 팝업창(이전 버전, 이름 변경)
keyword: '', // 검색 키워드
selectedSort: 'LATEST', // 프로젝트 목록 정렬 기준 선택 (기본은 '최근 수정순')
sortOptions: [ // 프로젝트 목록 정렬 기준
{ value: 'LATEST', text: '최근 수정순' },
{ value: 'OLDEST', text: '오래된 순' },
{ value: 'NAME_ASC', text: '이름순' },
],
newProjectName: '', // 변경할 프로젝트 이름
selectedProject: null, // 선택된 프로젝트
};
},
methods: {
// 즐겨찾기 버튼 클릭
toggleFavorite(index) {
this.projects[index].isFavorite = !this.projects[index].isFavorite; // 즐겨찾기 설정/해제 변경
this.updateFavorite(this.projects[index].groupId, this.projects[index].isFavorite); // 즐겨찾기 설정 업데이트 -> 서버로 변경사항 보내기
},
toggleMenu(index) {
if (this.openMenuIndex === index) {
this.openMenuIndex = null;
return;
}
this.$nextTick(() => {
const card = this.$el.querySelectorAll('.card')[index + 1]; // 0번은 "새 카드"니까 +1
if (!card) return;
const rect = card.getBoundingClientRect();
const windowWidth = window.innerWidth;
// 카드가 오른쪽 200px 이내에 있으면 왼쪽으로 드롭다운
this.dropdownDirection = (windowWidth - rect.right < 200) ? 'left' : 'right';
this.openMenuIndex = index;
});
},
// 프로젝트 목록 가져오기(대표 프로젝트만 화면에서 조회)
loadMainProjects() {
findAllProjectsProc({
memberId: this.getMemId,
isMain: 'Y',
sortOption: this.selectedSort, // 정렬 기준
})
.then(response => {
const projectList = response.data.result.projects;
this.projects = projectList.map(project => ({
id: project.projectId,
groupId: project.projectGroupId,
isMain: project.isMain,
name: project.projectName,
date: project.createdAt,
img: require('../../../resources/img/content/sample1.png'), // 백엔드 - 썸네일 기능 추가 후 수정
isFavorite: project.isFavorite === 'Y'
}));
})
.catch(error => {console.error('자산 목록 조회 실패: ', error);})
},
// 프로젝트 이름으로 검색
searchProjects(keyword) {
findAllProjectsProc({
memberId: this.getMemId,
projectName: keyword,
isMain: 'Y'
})
.then(response => {
const projectList = response.data.result.projects;
this.projects = projectList.map(project => ({
id: project.projectId,
groupId: project.projectGroupId,
isMain: project.isMain,
name: project.projectName,
date: project.createdAt,
img: require('../../../resources/img/content/sample1.png'), // 백엔드 - 썸네일 기능 추가 후 수정
isFavorite: project.isFavorite,
}));
})
.catch(error => {console.error('프로젝트 검색 실패: ', error);})
},
// 이전 프로젝트 목록 가져오기
loadOldProjects(groupId) {
findAllProjectsProc({
memberId: this.getMemId,
projectGroupId: groupId,
isMain: 'N' // 이전 프로젝트만 목록에 뜨게 함.
})
.then(response => {
const projectList = response.data.result.projects;
this.oldProjects = projectList.map(project => ({
id: project.projectId,
name: project.projectName,
date: dayjs(project.createdAt).format('M월 D일 A h:mm'),
img: require('../../../resources/img/content/sample1.png'), // 백엔드 - 썸네일 기능 추가 후 수정
}));
})
.catch(error => {console.error('자산 목록 조회 실패: ', error);})
},
// 모델링 페이지로 이동(생성)
goToModelingCreate() {
this.$router.push('/modeling.page');
},
// 모델링 페이지로 이동(기존 프로젝트 수정 - 기존 정보 가져옴)
goToModelingUpdate(project) {
this.$router.push({ name: 'Modeling', query: {projectId: project.id}});
},
// 프로젝트 복제
duplicateProject(projectId) {
const data = {
memberId : this.getMemId
};
duplicateProjectProc(projectId, data)
.then((response) => {
this.loadMainProjects(); // 복제 후 목록 조회 새로고침
this.openMenuIndex = null; // 메뉴 토글 닫기
})
.catch((error) => {
console.error('복제 실패: ', error);
})
},
// 팝업창 열기
openPopup(popupType, project){
this.activePopup = popupType;
this.newProjectName = project.name;
this.selectedProject = project;
this.loadOldProjects(project.groupId);
},
// 팝업창 닫기
closePopup(){
console.log('팝업창 닫기: ');
this.activePopup = null;
},
// 프로젝트 삭제
deleteProject(projectGroupId) {
const data = {
memberId : this.getMemId
};
deleteProjectProc(projectGroupId, data)
.then((response) => {
this.loadMainProjects(); // 삭제 후 목록 조회 새로고침
this.openMenuIndex = null; // 메뉴 토글 닫기
})
.catch((error) => {
console.error('삭제 실패: ', error);
})
},
// 즐겨찾기 설정
updateFavorite(projectGroupId, isFavorite) {
const data = {
memberId : this.getMemId,
isFavorite : isFavorite ? 'Y' : 'N' // 'Y' : 즐겨찾기 설정, 'N' : 즐겨찾기 해제
}
updateProjectFavoriteProc(projectGroupId, data)
.then((response) => {
this.loadMainProjects(); // 즐겨찾기 설정 후 목록 새로고침
})
.catch((error) => {
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: {
// 정렬 기준 변경 감지하여 프로젝트 목록 새로고침
selectedSort() {
this.loadMainProjects();
}
},
computed: {
...mapGetters([
'getMemId',
'getMemNm',
'getMemLoginId'
])
},
components: {},
created() {},
mounted() {
console.log("로그인한 사용자:", this.getMemId);
this.loadMainProjects();
},
beforeUnmount() {},
};
</script>
<style scoped>
.popup-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.popup {
position: relative;
background: white;
padding: 24px;
border-radius: 8px;
width: 400px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.close-btn {
position: absolute;
top: 10px;
right: 10px;
background: none;
border: none;
font-size: 18px;
cursor: pointer;
}
.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>