
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
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" ref="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);
// 모든 새 파일이 삭제된 경우 input 요소 초기화
if (this.multipartFiles.length === 0) {
this.$refs.fileInput.value = '';
}
} 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() {
// 공백제거
this.requestDTO.sj = this.$processTitle(this.requestDTO.sj);
this.requestDTO.adres = this.$processTitle(this.requestDTO.adres);
// 유효성 검사
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]) {
if (!this.$isEmpty(this.pageId) && type === 'list') {
this.$router.push({ name: 'PicHistoryDetail', query: { id: this.pageId } });
}
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>