
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="content">
<div class="sub-title-area mb-30">
<h2>사진 기록물</h2>
<div class="breadcrumb-list">
<ul>
<li>
<img :src="homeicon" alt="Home Icon">
<p>기록물</p>
</li>
<li><img :src="righticon" alt=""></li>
<li>사진 기록물</li>
</ul>
</div>
</div>
<form class="insert-form mb-50" @submit.prevent>
<dl>
<dd>
<label for="sj" class="require">제목</label>
<div class="wfull"><input type="text" id="sj" placeholder="제목을 입력하세요." v-model="requestDTO.sj"></div>
</dd>
<div class="hr"></div>
<dd>
<label for="prdctnYear">생산연도</label>
<input type="text" id="prdctnYear" placeholder="생산연도를 입력하세요" v-model="requestDTO.prdctnYear" pattern="[0-9]{4}" maxlength="4" @input="onlyNumberInput">
</dd>
<div class="hr"></div>
<dd>
<label for="adres">주소</label>
<div class="wfull"><input type="text" id="adres" placeholder="주소를 입력하세요" v-model="requestDTO.adres"></div>
</dd>
<div class="hr"></div>
<dd>
<label for="text">내용</label>
<div class="wfull">
<EditorComponent v-model:contents="requestDTO.cn" v-model:plainContents="requestDTO.searchCn" />
</div>
</dd>
<div class="hr"></div>
<dd>
<label for="category" class="flex align-center">
<p>카테고리</p><button type="button" class="category-add" @click="fnToggleModal">추가하기</button>
</label>
<ul class="category">
<li v-for="(item, idx) of selectedCtgries" :key="idx">{{ item.ctgryNm }} <button type="button" class="cancel" @click="fnDelCtgry(item.ctgryId)"><b>✕</b></button></li>
</ul>
</dd>
<div class="hr"></div>
<dd>
<label for="file" class="require">파일</label>
<ul class="wfull">
<li class="flex align-center">
<p>파일첨부</p>
<div class="invalid-feedback"><img :src="erroricon" alt=""><span>첨부파일은 건당 최대 10GB를 초과할 수 없습니다.</span>
</div>
</li>
<li class="file-insert">
<input type="file" id="fileInput" class="file-input" multiple accept="image/jpeg,image/png,image/gif,image/jpg" @change="handleFileSelect">
<label for="fileInput" class="file-label mb-20" @dragover.prevent="handleDragOver" @dragleave.prevent="handleDragLeave" @drop.prevent="handleDrop" :class="{ 'drag-over': isDragging }">
<div class="flex-center align-center">
<img :src="fileicon" alt="">
<p>파일첨부하기</p>
</div>
<p>파일을 첨부하시려면 이 영역으로 파일을 끌고 오거나 클릭해주세요</p>
</label>
<p class="mb-10">파일목록</p>
<div id="fileNames" class="file-names">
<div v-if="requestDTO.files.length === 0 && multipartFiles.length === 0">선택된 파일이 없습니다.</div>
<!-- 기존 등록된 파일 목록 -->
<div v-for="(file, idx) of requestDTO.files" :key="idx" class="mb-5">
<div class="flex align-center" style="gap: 10px;">
<input type="radio" name="thumbAt" :id="'oldFile_' + file.fileOrdr" v-model="selectedThumb" :value="file.fileNm">
<label :for="'oldFile_' + file.fileOrdr">
<p>대표사진 지정</p>
</label>
</div>
<div class="flex-sp-bw file-wrap">
<div class="file-name">
<img src="/client/resources/images/icon/imgicon.png" alt="fileicon">
<p>{{ file.fileNm }}</p>
</div>
<button type="button" class="cancel" @click="fnDelFile('old', file.fileOrdr)"><b>✕</b></button>
</div>
</div>
<!-- 새로 추가된 파일 목록 -->
<div v-for="(file, idx) of multipartFiles" :key="idx" class="mb-5">
<div class="flex align-center" style="gap: 10px;">
<input type="radio" name="thumbAt" :id="'newFile_' + idx" v-model="selectedThumb" :value="file.name">
<label :for="'newFile_' + idx">
<p>대표사진 지정</p>
</label>
</div>
<div class="flex-sp-bw file-wrap">
<div class="file-name">
<img src="/client/resources/images/icon/imgicon.png" alt="fileicon">
<p>{{ file.name }}</p>
</div>
<button type="button" class="cancel" @click="fnDelFile('new', idx)"><b>✕</b></button>
</div>
</div>
</div>
</li>
</ul>
</dd>
</dl>
</form>
<div class="btn-group flex-center">
<button type="button" class="cancel" @click="fnMoveTo('list')">취소</button>
<button type="button" class="register" @click="submitForm">
<span v-if="$isEmpty(pageId)">등록</span>
<span v-else>수정</span>
</button>
</div>
</div>
<CategorySelectModal v-if="isModalOpen" :selectedCtgries="selectedCtgries" @toggleModal="fnToggleModal" @addCtgries="fnAddCtgries" />
<div v-if="loading" class="loading-overlay">
<div class="loading-spinner"></div>
<div>
<p>등록 중입니다</p>
<p>잠시만 기다려주세요</p>
</div>
</div>
</template>
<script>
import { DoubleLeftOutlined, LeftOutlined, RightOutlined, DoubleRightOutlined } from '@ant-design/icons-vue';
// COMPONENT
import EditorComponent from '../../../component/editor/EditorComponent.vue';
import CategorySelectModal from '../../../component/modal/CategorySelectModal.vue';
// API
import { findDcryProc, saveDcry, updateDcry } from '@/resources/api/dcry';
export default {
components: {
DoubleLeftOutlined,
LeftOutlined,
RightOutlined,
DoubleRightOutlined,
EditorComponent,
CategorySelectModal,
},
data() {
return {
// 아이콘 경로
homeicon: 'client/resources/images/icon/home.png',
erroricon: 'client/resources/images/icon/error.png',
righticon: 'client/resources/images/icon/right.png',
fileicon: 'client/resources/images/icon/file.png',
searchicon: 'client/resources/images/icon/search.png',
pageId: null,
isModalOpen: false,
isDragging: false,
fileNames: [],
// 등록/수정 요청 객체
requestDTO: {
dcryId: null,
sj: null,
cn: null,
searchCn: null,
adres: null,
prdctnYear: null,
ty: 'P',
fileId: null,
files: [],
ctgryIds: [],
},
multipartFiles: [],
selectedCtgries: [], // 카테고리 목록
selectedThumb: null,
loading: false, // 로딩 상태 추가
};
},
created() {
this.pageId = this.$route.query.id;
if (!this.$isEmpty(this.pageId)) {
this.fnFindDcry(); // 상세 조회
}
},
methods: {
// 상세 조회
async fnFindDcry() {
try {
const response = await findDcryProc(this.pageId);
if (response.data.data.dcry.ty !== 'P') {
alert('올바른 접근이 아닙니다.');
this.fnMoveTo('list'); // 목록으로 이동
}
this.copyToDcryReqDTO(response.data.data.dcry);
} catch (error) {
alert('조회중 오류가 발생했습니다.');
this.fnMoveTo('list'); // 목록으로 이동
if (error.response) {
alert(error.response.data.message);
}
console.error(error.message);
}
},
// dcry > requestDTO
copyToDcryReqDTO(dcry) {
const copyFields = Object.keys(this.requestDTO).filter(key => key !== 'dcryId' && key !== 'ty' && key !== 'files');
copyFields.forEach(field => {
this.requestDTO[field] = this.$isEmpty(dcry[field]) ? null : dcry[field];
});
this.requestDTO.ty = 'P'; // 사진기록물
this.requestDTO.files = dcry.files.length > 0 ? dcry.files : []; // 기존 첨부파일
this.multipartFiles = [];
this.selectedCtgries = dcry.ctgrys.length > 0 ? dcry.ctgrys : [];
// 썸네일
const thumbFile = this.requestDTO.files.find(f => f.thumbAt === 'Y');
if (thumbFile) {
this.selectedThumb = thumbFile.fileNm;
}
},
// 카테고리 모달 열기/닫기
fnToggleModal() {
this.isModalOpen = !this.isModalOpen;
},
// 카테고리 등록
fnAddCtgries(selectedCtgries) {
this.selectedCtgries = [...this.selectedCtgries, ...selectedCtgries];
this.fnToggleModal(); // 카테고리 모달 닫기
},
// 카테고리 삭제
fnDelCtgry(id) {
this.selectedCtgries = this.selectedCtgries.filter(item => item.ctgryId !== id);
},
// 드래그 앤 드롭 이벤트 핸들러
handleDragOver(event) {
this.isDragging = true;
},
handleDragLeave(event) {
this.isDragging = false;
},
handleDrop(event) {
this.isDragging = false;
const files = event.dataTransfer.files;
if (files.length > 0) {
this.processFiles(files);
}
},
handleFileSelect(event) {
const files = event.target.files;
if (files.length > 0) {
this.processFiles(files);
}
},
// 파일 업로드 처리 함수
processFiles(files) {
const allowedTypes = ['jpg', 'jpeg', 'png', 'gif']; // 이미지 파일만 허용
const maxSize = 10 * 1024 * 1024 * 1024; // 10GB
// 중복 파일 확인을 위한 Map (키: 파일명+확장자(소문자), 값: 원본 파일명)
const existingFilesMap = new Map();
// 기존 업로드된 파일 정보 추가
this.requestDTO.files.forEach(file => {
const fullName = file.fileNm.toLowerCase();
existingFilesMap.set(fullName, file.fileNm);
});
// 새로 추가된 파일 정보 추가
this.multipartFiles.forEach(file => {
existingFilesMap.set(file.name.toLowerCase(), file.name);
});
for (let file of files) {
const fileNameLower = file.name.toLowerCase();
const fileExtension = fileNameLower.split('.').pop();
// 파일명+확장자 중복 검증 (대소문자 무시하고 비교)
if (existingFilesMap.has(fileNameLower)) {
alert("파일 목록에 이미 동일한 파일 명을 가진 파일이 있습니다. 동일한 이름을 가진 파일은 업로드할 수 없습니다.");
continue;
}
// 파일 타입 검증
if (!allowedTypes.includes(fileExtension)) {
alert("이미지 파일(jpg, jpeg, png, gif)만 업로드 가능합니다.");
continue;
}
// 파일 크기 제한 검증
if (file.size > maxSize) {
alert(`${file.name} 파일이 10GB를 초과합니다.`);
continue;
}
this.multipartFiles.push(file);
existingFilesMap.set(fileNameLower, file.name);
// 최초 등록 시 썸네일로 설정
if (this.requestDTO.files.length < 1 && this.multipartFiles.length > 0) {
this.selectedThumb = this.multipartFiles[0].name;
}
}
},
// 파일 삭제
fnDelFile(type, separator) {
if (type === 'new') {
this.multipartFiles.splice(separator, 1);
} else if (type === 'old') {
this.requestDTO.files = this.requestDTO.files.filter(item => item.fileOrdr !== separator);
}
this.validateThumbnail();
},
// 썸네일 유효성 검증 및 설정
validateThumbnail() {
// 1. 현재 selectedThumb가 기존 파일(requestDTO.files)에 존재하는지 확인
if (this.requestDTO.files && this.requestDTO.files.length > 0) {
const existsInCurrentFiles = this.requestDTO.files.some(file => file.fileNm === this.selectedThumb);
// 기존 파일에 없다면 첫 번째 기존 파일을 썸네일로 설정
if (!existsInCurrentFiles) {
this.selectedThumb = this.requestDTO.files[0].fileNm;
}
return;
}
// 2. 여기까지 왔다면 기존 파일이 없는 상태
// 새 파일(multipartFiles)이 있는지 확인
if (this.multipartFiles && this.multipartFiles.length > 0) {
const existsInNewFiles = this.multipartFiles.some(file => file.name === this.selectedThumb);
// 새 파일에 없다면 첫 번째 새 파일을 썸네일로 설정
if (!existsInNewFiles) {
this.selectedThumb = this.multipartFiles[0].name;
}
} else {
this.selectedThumb = null;
}
},
// 등록
async submitForm() {
// 유효성 검사
if (this.$isEmpty(this.requestDTO.sj)) {
alert("제목을 입력해 주세요.");
return;
}
if (!this.$isEmpty(this.requestDTO.prdctnYear)) {
if (!/^\d+$/.test(this.requestDTO.prdctnYear)) {
alert("생산연도는 숫자만 입력 가능합니다.");
return;
}
if (this.requestDTO.prdctnYear.length !== 4) {
alert("생산연도는 4자리로 입력해주세요.");
return;
}
}
if (this.$isEmpty(this.selectedThumb)) {
alert("썸네일로 사용할 이미지를 선택해주세요.");
return;
}
let count = this.multipartFiles.length
if (!this.$isEmpty(this.pageId)) {
count += this.requestDTO.files.length
}
if (count == 0) {
alert("파일을 1개 이상 첨부해 주세요.");
return;
}
this.loading = true; // 로딩 시작
try {
const formData = new FormData();
// 텍스트 데이터 추가
formData.append('sj', this.requestDTO.sj);
formData.append('ty', this.requestDTO.ty);
// 게시물 아이디
if (!this.$isEmpty(this.pageId)) {
formData.append('dcryId', this.pageId);
}
if (!this.$isEmpty(this.requestDTO.prdctnYear)) {
formData.append('prdctnYear', this.requestDTO.prdctnYear);
}
if (!this.$isEmpty(this.requestDTO.adres)) {
formData.append('adres', this.requestDTO.adres);
}
if (!this.$isEmpty(this.requestDTO.cn)) {
formData.append('cn', this.requestDTO.cn);
if (!this.$isEmpty(this.requestDTO.searchCn)) {
formData.append('searchCn', this.requestDTO.searchCn);
}
}
// 파일 아이디
if (!this.$isEmpty(this.requestDTO.fileId)) {
formData.append('fileId', this.requestDTO.fileId);
}
// 카테고리 Ids 추가
if (this.selectedCtgries && this.selectedCtgries.length > 0) {
for (let ctgry of this.selectedCtgries) {
formData.append("ctgryIds", ctgry.ctgryId);
}
}
// 파일 추가
if (this.multipartFiles.length > 0) {
for (let file of this.multipartFiles) {
formData.append("multipartFiles", file);
}
}
// 기존파일 수정
if (!this.$isEmpty(this.pageId) && this.requestDTO.files.length > 0) {
for (let file of this.requestDTO.files) {
formData.append("files", file.fileOrdr);
}
}
// 썸네일 정보 추가
if (this.selectedThumb !== null) {
formData.append('selectedThumb', this.selectedThumb);
}
// API 통신
const response = this.$isEmpty(this.pageId) ? await saveDcry(formData) : await updateDcry(formData);
let id = response.data.data.dcryId;
alert(this.$isEmpty(this.pageId) ? "등록되었습니다." : "수정되었습니다.");
// 상세 페이지로 이동
this.fnMoveTo('view', id);
} catch (error) {
if (error.response) {
alert(error.response.data.message);
} else {
alert("에러가 발생했습니다.");
}
console.error(error.message);
} finally {
this.loading = false; // 로딩 종료
};
},
// 페이지 이동
fnMoveTo(type, id) {
const routes = {
'list': { name: 'PicHistorySearch' },
'view': { name: 'PicHistoryDetail', query: { id } },
'edit': { name: 'PicHistoryInsert', query: this.$isEmpty(id) ? {} : { id } },
};
if (routes[type]) {
this.$router.push(routes[type]);
} else {
alert("올바르지 않은 경로를 요청하여 목록으로 이동합니다.");
this.$router.push(routes['list']);
}
},
// 생산연도 입력 제한
onlyNumberInput() {
this.requestDTO.prdctnYear = this.requestDTO.prdctnYear.replace(/[^0-9]/g, '');
}
}
};
</script>
<style>
.file-label {
border: 2px dashed #ddd;
border-radius: 8px;
padding: 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
}
.file-label:hover {
border-color: #aaa;
background-color: #f9f9f9;
}
.file-label.drag-over {
border-color: #1890ff;
background-color: rgba(24, 144, 255, 0.1);
}
</style>