diff --git a/src/main/kotlin/com/wafflestudio/csereal/common/entity/MainImageAttachable.kt b/src/main/kotlin/com/wafflestudio/csereal/common/entity/MainImageAttachable.kt index 6c23ea29..904b24b0 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/common/entity/MainImageAttachable.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/common/entity/MainImageAttachable.kt @@ -3,5 +3,5 @@ package com.wafflestudio.csereal.common.entity import com.wafflestudio.csereal.core.resource.mainImage.database.MainImageEntity interface MainImageAttachable { - val mainImage: MainImageEntity? + var mainImage: MainImageEntity? } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/resource/attachment/database/AttachmentEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/resource/attachment/database/AttachmentEntity.kt index 9e5d590c..6f219271 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/resource/attachment/database/AttachmentEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/resource/attachment/database/AttachmentEntity.kt @@ -13,12 +13,15 @@ import com.wafflestudio.csereal.core.seminar.database.SeminarEntity import jakarta.persistence.* @Entity(name = "attachment") +@Table(uniqueConstraints = [ UniqueConstraint(columnNames = ["filename", "directory"])]) class AttachmentEntity( var isDeleted: Boolean? = false, @Column(unique = true) val filename: String, + val directory: String? = null, + val attachmentsOrder: Int, val size: Long, @@ -57,4 +60,8 @@ class AttachmentEntity( @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "council_file_id") var councilFile: CouncilFileEntity? = null -) : BaseTimeEntity() +) : BaseTimeEntity() { + fun filePath(): String { + return if (directory.isNullOrBlank()) return filename else "$directory/$filename" + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/resource/attachment/service/AttachmentService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/resource/attachment/service/AttachmentService.kt index b51d0d37..4574c777 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/resource/attachment/service/AttachmentService.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/resource/attachment/service/AttachmentService.kt @@ -1,6 +1,7 @@ package com.wafflestudio.csereal.core.resource.attachment.service import com.wafflestudio.csereal.common.CserealException +import com.wafflestudio.csereal.core.resource.directory.Directory import com.wafflestudio.csereal.common.entity.AttachmentAttachable import com.wafflestudio.csereal.common.properties.EndpointProperties import com.wafflestudio.csereal.core.about.database.AboutEntity @@ -21,6 +22,7 @@ import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import org.springframework.web.multipart.MultipartFile +import java.lang.invoke.WrongMethodTypeException import java.nio.file.Files import java.nio.file.Paths @@ -53,22 +55,25 @@ class AttachmentServiceImpl( private val eventPublisher: ApplicationEventPublisher ) : AttachmentService { override fun uploadAttachmentInLabEntity(labEntity: LabEntity, requestAttachment: MultipartFile): AttachmentDto { - Files.createDirectories(Paths.get(path)) + val directory = "attachment/" + Directory.LAB.toString().lowercase() + val uploadDir = Paths.get(path, directory) + Files.createDirectories(uploadDir) val timeMillis = System.currentTimeMillis() val filename = "${timeMillis}_${requestAttachment.originalFilename}" - val totalFilename = path + filename - val saveFile = Paths.get(totalFilename) + val saveFile = Paths.get(path, directory, filename) requestAttachment.transferTo(saveFile) val attachment = AttachmentEntity( filename = filename, + directory = directory, attachmentsOrder = 1, size = requestAttachment.size ) labEntity.pdf = attachment + attachment.lab = labEntity attachmentRepository.save(attachment) return AttachmentDto( @@ -83,7 +88,9 @@ class AttachmentServiceImpl( contentEntityType: AttachmentAttachable, requestAttachments: List ): List { - Files.createDirectories(Paths.get(path)) + val directory = attachmentDirectoryOf(contentEntityType) + val uploadDir = Paths.get(path, directory) + Files.createDirectories(uploadDir) val attachmentsList = mutableListOf() @@ -91,12 +98,12 @@ class AttachmentServiceImpl( val timeMillis = System.currentTimeMillis() val filename = "${timeMillis}_${requestAttachment.originalFilename}" - val totalFilename = path + filename - val saveFile = Paths.get(totalFilename) + val saveFile = uploadDir.resolve(filename) requestAttachment.transferTo(saveFile) val attachment = AttachmentEntity( filename = filename, + directory = directory, attachmentsOrder = index + 1, size = requestAttachment.size ) @@ -124,7 +131,7 @@ class AttachmentServiceImpl( attachmentDto = AttachmentResponse( id = attachment.id, name = attachment.filename.substringAfter("_"), - url = "${endpointProperties.backend}/v1/file/${attachment.filename}", + url = "${endpointProperties.backend}/v1/file/${attachment.filePath()}", bytes = attachment.size ) } @@ -142,7 +149,7 @@ class AttachmentServiceImpl( val attachmentDto = AttachmentResponse( id = attachment.id, name = attachment.filename.substringAfter("_"), - url = "${endpointProperties.backend}/v1/file/${attachment.filename}", + url = "${endpointProperties.backend}/v1/file/${attachment.filePath()}", bytes = attachment.size ) list.add(attachmentDto) @@ -170,7 +177,7 @@ class AttachmentServiceImpl( @Transactional override fun deleteAttachment(attachment: AttachmentEntity) { - val fileDirectory = path + attachment.filename + val fileDirectory = path + attachment.filePath() attachmentRepository.delete(attachment) eventPublisher.publishEvent(FileDeleteEvent(fileDirectory)) } @@ -217,6 +224,22 @@ class AttachmentServiceImpl( contentEntity.attachments.add(attachment) attachment.councilFile = contentEntity } + + else -> { + throw WrongMethodTypeException("파일을 엔티티에 연결할 수 없습니다") + } + } + } + + private fun attachmentDirectoryOf(contentEntityType: AttachmentAttachable): String { + return "attachment/" + when (contentEntityType) { + is NewsEntity -> Directory.NEWS.toString().lowercase() + is NoticeEntity -> Directory.NOTICE.toString().lowercase() + is SeminarEntity -> Directory.SEMINAR.toString().lowercase() + is AboutEntity -> Directory.ABOUT.toString().lowercase() + is AcademicsEntity -> Directory.ACADEMICS.toString().lowercase() + is CouncilFileEntity -> Directory.COUNCIL.toString().lowercase() + else -> "" } } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/resource/common/api/FileController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/resource/common/api/FileController.kt index 9318f028..1b2c331c 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/resource/common/api/FileController.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/resource/common/api/FileController.kt @@ -26,13 +26,12 @@ class FileController( private val uploadPath: String, private val endpointProperties: EndpointProperties ) { - - @GetMapping("/{filename:.+}") + @GetMapping("/{*filepath}") fun serveFile( - @PathVariable filename: String, + @PathVariable filepath: String, request: HttpServletRequest ): ResponseEntity { - val file = Paths.get(uploadPath, filename) + val file = Paths.get(uploadPath, filepath) val resource = UrlResource(file.toUri()) if (resource.exists() || resource.isReadable) { @@ -41,6 +40,7 @@ class FileController( headers.contentType = MediaType.parseMediaType(contentType ?: "application/octet-stream") + val filename = filepath.substringAfterLast("/") val originalFilename = filename.substringAfter("_") val encodedFilename = URLEncoder.encode(originalFilename, UTF_8.toString()).replace("+", "%20") @@ -69,7 +69,7 @@ class FileController( val saveFile = Paths.get(totalFilename) file.transferTo(saveFile) - val imageUrl = "${endpointProperties.backend}/v1/file/$filename" + val imageUrl = "/v1/file/$filename" results.add( UploadFileInfo( @@ -93,9 +93,11 @@ class FileController( } @PreAuthorize("hasRole('STAFF')") - @DeleteMapping("/{filename:.+}") - fun deleteFile(@PathVariable filename: String): ResponseEntity { - val file = Paths.get(uploadPath, filename) + @DeleteMapping("/{*filepath}") + fun deleteFile( + @PathVariable filepath: String + ): ResponseEntity { + val file = Paths.get(uploadPath, filepath) if (Files.exists(file)) { Files.delete(file) diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/resource/directory/Directory.kt b/src/main/kotlin/com/wafflestudio/csereal/core/resource/directory/Directory.kt new file mode 100644 index 00000000..7fa14929 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/resource/directory/Directory.kt @@ -0,0 +1,15 @@ +package com.wafflestudio.csereal.core.resource.directory + +enum class Directory { + LAB, // for attachment + NEWS, // for attachment, mainImage + NOTICE, // for attachment + SEMINAR, // for attachment, mainImage + ABOUT, // for attachment, mainImage + ACADEMICS, // for attachment + COUNCIL, // for attachment, mainImage + PROFESSOR, // for mainImage + STAFF, // for mainImage + RESEARCH, // for mainImage + RECRUIT // for mainImage +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/resource/mainImage/database/MainImageEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/resource/mainImage/database/MainImageEntity.kt index 41bfa7b7..e8f11620 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/resource/mainImage/database/MainImageEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/resource/mainImage/database/MainImageEntity.kt @@ -4,13 +4,20 @@ import com.wafflestudio.csereal.common.entity.BaseTimeEntity import jakarta.persistence.* @Entity(name = "mainImage") +@Table(uniqueConstraints = [ UniqueConstraint(columnNames = ["filename", "directory"])]) class MainImageEntity( var isDeleted: Boolean? = false, @Column(unique = true) val filename: String, + val directory: String? = null, + val imagesOrder: Int, val size: Long -) : BaseTimeEntity() +) : BaseTimeEntity() { + fun filePath(): String { + return if (directory.isNullOrBlank()) return filename else "$directory/$filename" + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/resource/mainImage/service/MainImageService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/resource/mainImage/service/MainImageService.kt index d84cbded..0824ed08 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/resource/mainImage/service/MainImageService.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/resource/mainImage/service/MainImageService.kt @@ -11,6 +11,7 @@ import com.wafflestudio.csereal.core.news.database.NewsEntity import com.wafflestudio.csereal.core.recruit.database.RecruitEntity import com.wafflestudio.csereal.core.research.database.ResearchEntity import com.wafflestudio.csereal.core.resource.common.event.FileDeleteEvent +import com.wafflestudio.csereal.core.resource.directory.Directory import com.wafflestudio.csereal.core.resource.mainImage.database.MainImageRepository import com.wafflestudio.csereal.core.resource.mainImage.database.MainImageEntity import com.wafflestudio.csereal.core.resource.mainImage.dto.MainImageDto @@ -21,7 +22,6 @@ import org.springframework.transaction.annotation.Transactional import org.springframework.web.multipart.MultipartFile import org.apache.commons.io.FilenameUtils import org.springframework.context.ApplicationEventPublisher -import java.lang.invoke.WrongMethodTypeException import java.nio.file.Files import java.nio.file.Paths @@ -50,7 +50,9 @@ class MainImageServiceImpl( contentEntityType: MainImageAttachable, requestImage: MultipartFile ): MainImageDto { - Files.createDirectories(Paths.get(path)) + val directory = mainImageDirectoryOf(contentEntityType) + val uploadDir = Paths.get(path, directory) + Files.createDirectories(uploadDir) val extension = FilenameUtils.getExtension(requestImage.originalFilename) @@ -61,17 +63,17 @@ class MainImageServiceImpl( val timeMillis = System.currentTimeMillis() val filename = "${timeMillis}_${requestImage.originalFilename}" - val totalFilename = path + filename - val saveFile = Paths.get(totalFilename) + val saveFile = uploadDir.resolve(filename) requestImage.transferTo(saveFile) val mainImage = MainImageEntity( filename = filename, + directory = directory, imagesOrder = 1, size = requestImage.size ) - connectMainImageToEntity(contentEntityType, mainImage) + contentEntityType.mainImage = mainImage mainImageRepository.save(mainImage) return MainImageDto( @@ -83,9 +85,9 @@ class MainImageServiceImpl( // TODO: `MainImageEntity`의 메서드로 refactoring하기. @Transactional - override fun createImageURL(mainImage: MainImageEntity?): String? { - return if (mainImage != null) { - "${endpointProperties.backend}/v1/file/${mainImage.filename}" + override fun createImageURL(image: MainImageEntity?): String? { + return if (image != null) { + "${endpointProperties.backend}/v1/file/${image.filePath()}" } else { null } @@ -93,49 +95,22 @@ class MainImageServiceImpl( @Transactional override fun removeImage(image: MainImageEntity) { - val fileDirectory = path + image.filename + val fileDirectory = path + image.filePath() mainImageRepository.delete(image) eventPublisher.publishEvent(FileDeleteEvent(fileDirectory)) } - // TODO: 각 entity의 interface로 refactoring하기. - private fun connectMainImageToEntity(contentEntity: MainImageAttachable, mainImage: MainImageEntity) { - when (contentEntity) { - is NewsEntity -> { - contentEntity.mainImage = mainImage - } - - is SeminarEntity -> { - contentEntity.mainImage = mainImage - } - - is AboutEntity -> { - contentEntity.mainImage = mainImage - } - - is ProfessorEntity -> { - contentEntity.mainImage = mainImage - } - - is StaffEntity -> { - contentEntity.mainImage = mainImage - } - - is ResearchEntity -> { - contentEntity.mainImage = mainImage - } - - is RecruitEntity -> { - contentEntity.mainImage = mainImage - } - - is CouncilEntity -> { - contentEntity.mainImage = mainImage - } - - else -> { - throw WrongMethodTypeException("해당하는 엔티티가 없습니다") - } + private fun mainImageDirectoryOf(contentEntityType: MainImageAttachable): String { + return "mainImage/" + when (contentEntityType) { + is NewsEntity -> Directory.NEWS.toString().lowercase() + is SeminarEntity -> Directory.SEMINAR.toString().lowercase() + is AboutEntity -> Directory.ABOUT.toString().lowercase() + is ProfessorEntity -> Directory.PROFESSOR.toString().lowercase() + is StaffEntity -> Directory.STAFF.toString().lowercase() + is ResearchEntity -> Directory.RESEARCH.toString().lowercase() + is RecruitEntity -> Directory.RECRUIT.toString().lowercase() + is CouncilEntity -> Directory.COUNCIL.toString().lowercase() + else -> "" } } } diff --git a/src/main/resources/db/migration/V12__add_directory_to_attachment_and_mainimage.sql b/src/main/resources/db/migration/V12__add_directory_to_attachment_and_mainimage.sql new file mode 100644 index 00000000..2f878595 --- /dev/null +++ b/src/main/resources/db/migration/V12__add_directory_to_attachment_and_mainimage.sql @@ -0,0 +1,9 @@ +ALTER TABLE attachment +ADD COLUMN directory VARCHAR(255) NULL, +DROP CONSTRAINT UQ_attachment_filename, +ADD CONSTRAINT UQ_attachment_directory_filename UNIQUE (directory, filename); + +ALTER TABLE main_image +ADD COLUMN directory VARCHAR(255) NULL, +DROP CONSTRAINT UQ_main_image_filename, +ADD CONSTRAINT UQ_main_image_directory_filename UNIQUE (directory, filename);