
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">
<div class="prompt-zone mb10">
<div class="tabs layout center">
<button
v-for="tab in tabs"
:key="tab"
:class="{ active: activeTab === tab }"
@click="activeTab = tab"
>
{{ tab }}
</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">
<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>
</div>
<div class="h_100" v-else-if="activeTab === 'Text Prompt'">
<textarea class="form-control sm" style="height: 100%;" placeholder="텍스트를 입력하세요."></textarea>
</div>
</div>
</div>
<div class="btn-zone gap10 mb30">
<button class="btn md ico-before ico-reset" @click="resetImage">초기화</button>
<button class="btn md ico-before ico-3d">3D 변환</button>
</div>
<div class="direction-zone mb30">
<ul>
<li v-for="btn in buttons" :key="btn.icon" class="button-list">
<button class="layout center gap10">
<span class="icon-wrap"><img :src="btn.icon" :alt="btn.label + ' 아이콘'" ></span>
<span class="button-font"> {{ btn.label }}</span>
</button>
</li>
</ul>
</div>
<div class="zoom-container" tabindex="0">
<div class="zoom-controls">
<button @click="decreaseZoom" aria-label="Zoom out">-</button>
<input
type="range"
min="50"
max="200"
step="5"
v-model="zoom"
@input="updateZoom"
aria-label="Zoom level"
/>
<button @click="increaseZoom" aria-label="Zoom in">+</button>
</div>
<div class="zoom-display">{{ zoom }}%</div>
</div>
</div>
<div class="right-zone stretch">
<div class="canvas-zone h_100">
<div class="top-zone layout center space-between">
<!-- 왼쪽 툴 -->
<ul class="toolbar left layout gap10">
<li
v-for="tool in subTolls.slice(0, 3)"
:key="tool.id"
>
<button
:title="tool.title"
@click=" tool.title === '저장' ? submit() : toggleTool(tool)">
<img :src="tool.icon" :alt="tool.title + ' 아이콘'" style="vertical-align: middle;"/>
</button>
</li>
</ul>
<!-- 오른쪽 툴 -->
<ul class="toolbar right layout gap10">
<li
v-for="tool in subTolls.slice(-5)"
:key="tool.id"
:class="{ active: activeToolId === tool.id && !activeSubToolId }"
style="position: relative;">
<button :title="tool.title" @click="toggleTool(tool)" >
<img :src="tool.icon" :alt="tool.title + ' 아이콘'" style="vertical-align: middle;"/>
</button>
<div v-if="activeModalTool?.id === tool.id" class="custom-modal">
<div class="modal-content">
<p class="modeling-title">{{ activeModalTool.title }}</p>
<div class="modeling-content">
<ul v-if="activeModalTool.title === '참여자'" class="mb10">
<li v-for="(member, index) in members" :key="member.id">
{{ member.name }} <span v-if="member.isOwner === 'Y'">(대표)</span>
</li>
</ul>
<div v-else-if="activeModalTool.title === '코멘트'" class="mb10">
<div class="input-group mb10">
<label for="">writer</label>
<input type="text" class="form-control sm" :value="getMemNm" disabled>
</div>
<div class="input-group mb10">
<label for="">summary</label>
<input type="text" class="form-control sm" v-model="localCommentData.summary">
</div>
<div class="input-group">
<label for="">comment</label>
<textarea class="form-control sm" v-model="localCommentData.comment"></textarea>
</div>
</div>
<div v-else-if="activeModalTool.title === '피드백'" class="mb10">
<div class="input-group mb10">
<label for="">writer</label>
<input type="text" class="form-control sm" :value="getMemNm" disabled>
</div>
<div class="input-group mb10">
<label for="">content</label>
<input type="text" class="form-control sm" v-model="feedback">
</div>
</div>
<div class="btn-group layout center justify-center gap10">
<button class="btn sm black" v-if="activeModalTool.title === '참여자'" @click="closeModal">닫기</button>
<button class="btn sm purple" v-if="activeModalTool.title === '코멘트'" @click="saveComment">저장</button>
<button class="btn sm black" v-if="activeModalTool.title === '코멘트'" @click="closeModal">취소</button>
<button class="btn sm purple" v-if="activeModalTool.title === '피드백'" @click="saveFeedback">저장</button>
<button class="btn sm black" v-if="activeModalTool.title === '피드백'" @click="closeModal">취소</button>
</div>
</div>
</div>
</div>
</li>
</ul>
</div>
<div class="bottom-zone"></div>
</div>
<div class="equipment-zone h_100">
<div class="button-collection h_100">
<ul>
<li
v-for="tool in tools"
:key="tool.id"
:data-tool-id="tool.id"
:class="{ active: activeToolId === tool.id }"
style="position: relative;">
<button
:title="tool.title"
@click="toggleTool(tool)"
>
<img :src="getToolIcon(tool)" :alt="tool.title + ' 아이콘'" />
<input
type="color"
:ref="'colorInput_' + tool.id"
v-model="selectedColor"
@input.stop
v-if="tool.type === 'palette' && openedToolId === tool.id" class="palette-picker"
/>
</button>
<!-- 자식 버튼이 있을 경우 -->
<ul
v-if="tool.children && tool.children.length && openedToolId === tool.id"
class="child-buttons"
>
<li
v-for="child in tool.children"
:key="child.id"
:class="{ active: activeSubToolId === child.id }"
@click.stop="selectSubTool(tool, child)"
>
<button :title="child.title">
{{ child.title }}
</button>
</li>
</ul>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import { saveProjectProc, findProjectProc, updateProjectProc, saveFeedbackProc } from '../../../resources/api/modeling';
export default {
data() {
return {
tabs: ['Image Prompt', 'Text Prompt', ],
activeTab: 'Image Prompt',
buttons: [
{ icon: require('../../../resources/img/content/ico_right.svg'), label: '오른쪽으로 90도 회전(R)' },
{ icon: require('../../../resources/img/content/ico_left.svg'), label: '왼쪽으로 90도 회전(L)' },
{ icon: require('../../../resources/img/content/ico_top_bottom.svg'), label: '상하 대칭' },
{ icon: require('../../../resources/img/content/ico_left_right.svg'), label: '좌우 대칭' }
],
zoom: 100,
tools: [
{
id: 1,
icon: {
normal: require('../../../resources/img/content/ico_move_w.svg'),
active: require('../../../resources/img/content/ico_move.svg'),
},
title: '이동도구',
children: []
},
{
id: 2,
icon: {
normal: require('../../../resources/img/content/ico_3dview_w.svg'),
active: require('../../../resources/img/content/ico_3dview.svg'),
},
title: '크기조절도구',
children: []
},
{
id: 3,
icon: {
normal: require('../../../resources/img/content/ico_copy_w.svg'),
active: require('../../../resources/img/content/ico_copy.svg'),
},
title: '복사도구',
children: []
},
{
id: 4,
icon: {
normal: require('../../../resources/img/content/ico_crop_w.svg'),
active: require('../../../resources/img/content/ico_crop.svg'),
},
title: '자르기 도구',
children: [
{
id: 11,
title: '선택 객체 자르기'
},
{
id: 12,
title: '사용자 지정 자르기'
}
]
},
{
id: 5,
icon: {
normal: require('../../../resources/img/content/ico_punching_w.svg'),
active: require('../../../resources/img/content/ico_punching.svg'),
},
title: '패스도구',
children: [
{
id: 51,
title: '원형 패스 도구'
},
{
id: 52,
title: '사각형 패스 도구'
}
]
},
{
id: 6,
icon: {
normal: require('../../../resources/img/content/ico_palette_w.svg'),
active: require('../../../resources/img/content/ico_palette.svg'),
},
title: '팔레트 도구',
type: 'palette',
children: []
},
{
id: 7,
icon: {
normal: require('../../../resources/img/content/ico_texture_w.svg'),
active: require('../../../resources/img/content/ico_texture.svg'),
},
title: '재질 도구',
children: [
{
id: 71,
title: '금속 재질 도구'
},
{
id: 72,
title: '투명 재질 도구'
},
{
id: 73,
title: '플라스틱 재질 도구'
},
{
id: 74,
title: '목재 재질 도구'
}
]
},
{
id: 8,
icon: {
normal: require('../../../resources/img/content/ico_measure_w.svg'),
active: require('../../../resources/img/content/ico_measure.svg'),
},
title: '길이 측정 도구',
children: []
},
{
id: 9,
icon: {
normal: require('../../../resources/img/content/ico_reset_w.svg'),
active: require('../../../resources/img/content/ico_resett.svg'),
},
title: '초기화',
children: []
},
],
subTolls:[
{
id: 100,
icon: require('../../../resources/img/content/ico_save_w.svg'),
title: '저장',
},
{
id: 200,
icon: require('../../../resources/img/content/ico_turnBack_w.svg'),
title: '뒤로 되돌리기',
},
{
id: 300,
icon: require('../../../resources/img/content/ico_trunFoward_w.svg'),
title: '앞으로 되돌리기',
},
{
id: 400,
icon: require('../../../resources/img/content/ico_user_w.svg'),
title: '참여자',
type: 'modal'
},
{
id: 500,
icon: require('../../../resources/img/content/ico_comment_w.svg'),
title: '코멘트',
type: 'modal'
},
{
id: 600,
icon: require('../../../resources/img/content/ico_share_w.svg'),
title: '공유',
},
{
id: 700,
icon: require('../../../resources/img/content/ico_comment_w.svg'),
title: '피드백',
type: 'modal'
},
{
id: 800,
icon: require('../../../resources/img/content/ico_close_w.svg'),
title: '닫기',
},
],
activeToolId: 1,
activeSubToolId: null,
openedToolId: null,
selectedColor: '#000000',
activeModalTool : false,
// 주석 입력 데이터
commentData: {
summary: '',
comment: ''
},
// 주석 입력 데이터 복사본
localCommentData: {
summary: '',
comment: ''
},
projectId: null, // 프로젝트 수정 시 사용
projectGroupId: null, // 프로젝트 피드백 작성 시 사용
isReset: null, // 이미지 초기화 시 사용
feedback: '', // 피드백 내용
};
},
methods: {
decreaseZoom() {
if (this.zoom > 50) {
this.zoom -= 5
}
},
increaseZoom() {
if (this.zoom < 200) {
this.zoom += 5
}
},
updateZoom(event) {
this.zoom = Number(event.target.value)
},
toggleTool(tool) {
if (tool.type === 'modal') {
this.openModal(tool);
return;
}
// 기존 도구(tool) 처리 로직
this.activeToolId = tool.id;
if (tool.children && tool.children.length) {
this.openedToolId = this.openedToolId === tool.id ? null : tool.id;
this.activeSubToolId = null;
} else {
this.activeSubToolId = null;
this.openedToolId = tool.id;
}
if (tool.type === 'palette') {
this.$nextTick(() => {
console.log('colorInput ref:', this.$refs.colorInput);
this.$refs.colorInput?.click();
});
}
},
selectSubTool(tool, child) {
this.activeToolId = tool.id;
this.activeSubToolId = child.id;
this.openedToolId = tool.id;
},
openModal(tool) {
this.activeModalTool = tool; // or tool.id
},
closeModal() {
this.activeModalTool = null;
},
// 프로젝트 모델링 저장
submit(){
if(this.projectId) {
this.updateProject(this.projectId); // 수정
} else {
this.createProject(); // 생성
}
},
// 새로운 프로젝트 저장
createProject() {
const formData = new FormData();
const fileInput = this.$refs.fileInput; // 업로드 파일 가져오기
if (!fileInput && this.isReset == true) {
alert("파일을 선택해주세요.");
return;
}
formData.append("imageFile", fileInput.files[0]);
formData.append("memberId", this.getMemId);
formData.append("summary", this.commentData.summary);
formData.append("comment", this.commentData.comment);
saveProjectProc(formData)
.then(response => {
this.$router.push('/asset.page'); // 저장 후 자산 페이지로 이동
})
.catch(error => {console.error('자산 생성 실패: ', error);})
},
// 기존 프로젝트 수정 후 저장
updateProject(projectId) {
const formData = new FormData();
const fileInput = this.$refs.fileInput; // 업로드 파일 가져오기(없으면 기존 이미지 유지: fileInput==null)
if (!fileInput && this.isReset == true) {
alert("파일을 선택해주세요.");
return;
}
if (fileInput && fileInput.files && fileInput.files.length > 0) {
formData.append("imageFile", fileInput.files[0]);
}
formData.append("memberId", this.getMemId);
formData.append("summary", this.commentData.summary);
formData.append("comment", this.commentData.comment);
updateProjectProc(projectId, formData)
.then(response => {
this.$router.push('/asset.page'); // 저장 후 자산 페이지로 이동
})
.catch(error => {console.error('자산 수정 실패: ', error);})
},
// 주석 저장
saveComment() {
this.commentData = {...this.localCommentData}; // 복사본 데이터를 옮겨놓고, 프로젝트 저장 시 서버로 보냄
this.closeModal();
},
// 기존 프로젝트 정보 가져오기(기존 프로젝트 수정 시)
loadProjectData(projectId) {
const data = {
memberId : this.getMemId
};
findProjectProc(projectId, data)
.then(response => {
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();
const memberList = projectInfo.projectMember;
this.members = memberList.map(member => ({
id: member.memberId,
name: member.memberName,
isOwner: member.isOwner
}));
})
},
// 이미지 초기화
resetImage(){
this.isReset = true;
},
// 피드백 저장
saveFeedback() {
if(this.projectGroupId == null) { // 생성된 프로젝트에 한하여 피드백 저장 가능
console.error('신규 프로젝트에는 피드백을 남길 수 없습니다.');
return;
}
const data = {
memberId : this.getMemId,
feedbackContent : this.feedback
};
saveFeedbackProc(this.projectGroupId, data)
.then(response => {
console.log('피드백 저장 완료');
})
.catch(error => {console.error('피드백 저장 실패');});
this.closeModal();
}
},
watch: {},
computed: {
getToolIcon() {
return (tool) => {
const isActive = this.activeToolId === tool.id;
if (tool.icon && typeof tool.icon === 'object') {
return isActive ? tool.icon.active : tool.icon.normal;
}
return tool.icon;
};
},
...mapGetters([
'getMemId',
'getMemNm',
'getMemLoginId'
])
},
components: {},
created() {},
mounted() {
// 프로젝트 아이디가 쿼리로 들어올 경우 -> 기존 데이터 불러오기
const projectId = this.$route.query.projectId;
if(projectId) {
this.loadProjectData(projectId);
}
},
beforeUnmount() {},
};
</script>