Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] 긴급 픽스 병합 요청 #41

Merged
merged 15 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/CICD.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,5 @@ jobs:
cd ~/bitta-project
docker-compose down
docker-compose up -d --no-deps
sudo nginx -s reload
sudo nginx -s reload

2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ COPY build/libs/*.jar /app.jar

# Set the entry point to run the application
EXPOSE 8080
CMD ["java", "-jar", "-Dspring.profiles.active=prod", "/app.jar"]
CMD ["java", "-jar", "-Dspring.profiles.active=prod", "/app.jar"]
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@ services:
ports:
- "8082:8000"
environment:
- SPRING_PROFILES_ACTIVE=prod
- SPRING_PROFILES_ACTIVE=prod
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class FeedController (
}

@PostMapping
fun create(requestDto: FeedRequestDto.Create): ResponseEntity<Map<String, Any>> {
fun create(@RequestBody requestDto: FeedRequestDto.Create): ResponseEntity<Map<String, Any>> {
return ResponseEntity.ok(mapOf(
"message" to "피드를 성공적으로 등록했습니다.",
"result" to feedService.save(requestDto)
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/org/tenten/bittakotlin/feed/entity/Feed.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ data class Feed(

@CreatedDate
@Column(updatable = false, nullable = false)
val createdAt: LocalDateTime? = null,
var createdAt: LocalDateTime? = null,

@LastModifiedDate
@Column(updatable = true, nullable = false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import org.tenten.bittakotlin.feed.entity.Feed
import org.tenten.bittakotlin.feed.entity.FeedMedia
import org.tenten.bittakotlin.feed.entity.key.FeedMediaId
import org.tenten.bittakotlin.feed.repository.FeedMediaRepository
import org.tenten.bittakotlin.media.dto.MediaRequestDto
import org.tenten.bittakotlin.media.dto.MediaResponseDto
Expand All @@ -22,10 +23,12 @@ class FeedMediaServiceImpl(

uploadRequestDtos.forEach { uploadRequestDto ->
val mediaResponseDto: MediaResponseDto.Upload = mediaService.upload(uploadRequestDto, profile)
val media = mediaResponseDto.media

feedMediaRepository.save(FeedMedia(
id = FeedMediaId(feedId = feed.id!!, mediaId = media.id!!),
feed = feed,
media = mediaResponseDto.media
media = media
))

responseDto.add(MediaResponseDto.Read(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import org.tenten.bittakotlin.feed.dto.FeedRequestDto
import org.tenten.bittakotlin.feed.dto.FeedResponseDto
import org.tenten.bittakotlin.profile.entity.Profile
import org.tenten.bittakotlin.profile.service.ProfileService
import java.time.LocalDateTime

@Service
@RequiredArgsConstructor
Expand Down Expand Up @@ -80,7 +81,9 @@ class FeedServiceImpl(
val feed: Feed = feedRepository.save(Feed(
title = requestDto.title,
content = requestDto.content,
profile = profile
profile = profile,
createdAt = LocalDateTime.now(),
updatedAt = LocalDateTime.now()
))

return if (requestDto.medias != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,10 @@ class MediaResponseDto {

val media: Media
)

data class PublicUpload (
val uploadUrl: String,

val readUrl: String
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ data class Media (

@CreatedDate
@Column(nullable = false, updatable = false)
val savedAt: LocalDateTime? = null,
var savedAt: LocalDateTime? = null,

@ManyToOne
@JoinColumn
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ class MediaServiceImpl(
}

override fun upload(requestDto: MediaRequestDto.Upload, profile: Profile): MediaResponseDto.Upload {
val filename: String = UUID.randomUUID().toString()
val filetype: MediaType = checkMimetype(requestDto.mimetype)
val filename: String = UUID.randomUUID().toString() + getExtension(requestDto.mimetype)
val filesize: Int = checkFileSize(requestDto.filesize, filetype)

val media: Media = mediaRepository.save(Media(
Expand Down Expand Up @@ -94,6 +94,23 @@ class MediaServiceImpl(
throw MediaException(MediaError.WRONG_MIME_TYPE)
}

private fun getExtension(mimetype: String): String? {
val mimetypeMap = mapOf(
"image/jpeg" to "jpg",
"image/png" to "png",
"image/gif" to "gif",
"image/bmp" to "bmp",
"image/webp" to "webp",
"image/svg+xml" to "svg",
"video/mp4" to "mp4",
"video/webm" to "webm",
"video/ogg" to "ogg",
"video/x-msvideo" to "avi",
"video/x-matroska" to "mkv"
)
return mimetypeMap[mimetype]
}

private fun checkFileSize(filesize: Int, type: MediaType): Int {
val maxSize: Int = if (type == MediaType.IMAGE) {
imageMaxSize
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.tenten.bittakotlin.media.service

import org.tenten.bittakotlin.media.dto.MediaRequestDto
import org.tenten.bittakotlin.media.dto.MediaResponseDto
import org.tenten.bittakotlin.profile.entity.Profile

interface ProfileImageService {
fun upload(requestDto: MediaRequestDto.Upload, profile: Profile): MediaResponseDto.PublicUpload

fun delete(requestDto: MediaRequestDto.Delete)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package org.tenten.bittakotlin.media.service

import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import org.tenten.bittakotlin.media.constant.MediaError
import org.tenten.bittakotlin.media.constant.MediaType
import org.tenten.bittakotlin.media.dto.MediaRequestDto
import org.tenten.bittakotlin.media.dto.MediaResponseDto
import org.tenten.bittakotlin.media.entity.Media
import org.tenten.bittakotlin.media.exception.MediaException
import org.tenten.bittakotlin.media.repository.MediaRepository
import org.tenten.bittakotlin.profile.entity.Profile
import java.util.*

@Service
class ProfileImageServiceImpl (
private val mediaRepository: MediaRepository,

private val s3Service: S3Service,

@Value("\${file.max.size.image}")
private val maxsize: Int
) : ProfileImageService {
companion object {
private val logger: Logger = LoggerFactory.getLogger(ProfileImageServiceImpl::class.java)
}

@Transactional
override fun upload(requestDto: MediaRequestDto.Upload, profile: Profile): MediaResponseDto.PublicUpload {
val filetype: MediaType = checkMimetype(requestDto.mimetype)
val filename: String = UUID.randomUUID().toString() + getExtension(requestDto.mimetype)
val filesize: Int = checkFileSize(requestDto.filesize)

mediaRepository.save(Media(
filename = filename,
filetype = filetype,
filesize = filesize,
profile = profile
))

return s3Service.getPublicUploadUrl(filename, requestDto.mimetype)
}

@Transactional
override fun delete(requestDto: MediaRequestDto.Delete) {
val filename: String = requestDto.filename

mediaRepository.findByFilename(filename).orElseThrow { MediaException(MediaError.CANNOT_FOUND) }
}

private fun getExtension(mimetype: String): String? {
val mimetypeMap = mapOf(
"image/jpeg" to "jpg",
"image/png" to "png",
"image/gif" to "gif",
"image/bmp" to "bmp",
"image/webp" to "webp",
"image/svg+xml" to "svg",
)
return mimetypeMap[mimetype]
}

private fun checkMimetype(mimetype: String): MediaType {
if (mimetype.matches(Regex("image/(jpeg|png|gif|bmp|webp|svg\\+xml)"))) {
return MediaType.IMAGE
}

throw MediaException(MediaError.WRONG_MIME_TYPE)
}

private fun checkFileSize(filesize: Int): Int {
if (filesize > maxsize) {
throw MediaException(MediaError.WRONG_FILE_SIZE)
}

return filesize
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package org.tenten.bittakotlin.media.service

import org.tenten.bittakotlin.media.dto.MediaResponseDto

interface S3Service {
fun getReadUrl(name: String): String

fun getUploadUrl(name: String, contentType: String): String

fun getPublicUploadUrl(name: String, contentType: String): MediaResponseDto.PublicUpload

fun delete(filename: String): Unit
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import org.tenten.bittakotlin.media.constant.MediaError
import org.tenten.bittakotlin.media.dto.MediaResponseDto
import org.tenten.bittakotlin.media.exception.MediaException
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.s3.S3Client
import software.amazon.awssdk.services.s3.model.HeadObjectRequest
import software.amazon.awssdk.services.s3.model.NoSuchKeyException
Expand Down Expand Up @@ -56,6 +58,13 @@ class S3ServiceImpl(
return presignedPutRequest.url().toString()
}

override fun getPublicUploadUrl(name: String, contentType: String): MediaResponseDto.PublicUpload {
return MediaResponseDto.PublicUpload(
uploadUrl = getUploadUrl(name, contentType),
readUrl = "https://${s3Bucket}.s3.${Region.AP_NORTHEAST_2}.amazonaws.com/$name"
)
}

override fun delete(name: String): Unit {
existsInBucket(name)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import io.swagger.v3.oas.annotations.media.Content
import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.security.access.AccessDeniedException
Expand All @@ -14,17 +17,55 @@ import org.tenten.bittakotlin.member.dto.MemberResponseDTO
import org.tenten.bittakotlin.member.exception.MemberException
import org.tenten.bittakotlin.member.repository.MemberRepository
import org.tenten.bittakotlin.member.service.MemberService
import org.tenten.bittakotlin.security.jwt.JWTUtil
import org.tenten.bittakotlin.security.util.JwtTokenUtil

@Tag(name = "회원관리 API 컨트롤러", description = "회원과 관련된 RestAPI 제공 컨트롤러")
@RestController
@RequestMapping("/api/v1/member")
class MemberController(
private val memberService: MemberService,
private val jwtUtil: JWTUtil,
private val memberRepository: MemberRepository
private val memberRepository: MemberRepository,
private val jwtTokenUtil: JwtTokenUtil
) {

@PostMapping("/login")
fun login(@RequestBody requestDto: MemberRequestDTO.Login): ResponseEntity<Map<String, Any>> {
val responseDto: MemberResponseDTO.Login = memberService.login(requestDto)

val headers = HttpHeaders().apply {
add(HttpHeaders.SET_COOKIE, getCookieString("accessToken", responseDto.accessToken, 3600, false))
add(HttpHeaders.SET_COOKIE, getCookieString("refreshToken", responseDto.refreshToken, 604800, false))
add(HttpHeaders.SET_COOKIE, getCookieString("profileId", responseDto.profileId.toString(), 604800, true))
add(HttpHeaders.SET_COOKIE, getCookieString("profileUrl", responseDto.profileUrl, 604800, true))
}

val body = mapOf("message" to "로그인이 성공했습니다.")

return ResponseEntity(body, headers, HttpStatus.OK)
}

@PostMapping("/logout")
fun logout(request: HttpServletRequest, response: HttpServletResponse): ResponseEntity<Map<String, Any>> {
val headers = HttpHeaders().apply {
add(HttpHeaders.SET_COOKIE, getCookieString("accessToken", null, 0, false))
add(HttpHeaders.SET_COOKIE, getCookieString("refreshToken", null, 0, false))
add(HttpHeaders.SET_COOKIE, getCookieString("profileId", null, 0, true))
add(HttpHeaders.SET_COOKIE, getCookieString("profileUrl", null, 0, true))
}

val body = mapOf("message" to "로그아웃이 성공했습니다.")

return ResponseEntity(body, headers, HttpStatus.OK)
}


private fun getCookieString(name: String, token: String?, ageMax: Int, isPublic: Boolean): String {
return buildString {
append("$name=$token; Path=/; Max-Age=$ageMax;")
if (!isPublic) append(" HttpOnly;")
}
}

// 회원가입

@Operation(
Expand Down Expand Up @@ -77,7 +118,7 @@ class MemberController(
@RequestHeader("access") token: String // JWT 토큰을 헤더에서 추출
): ResponseEntity<Void> {
// 현재 로그인한 사용자 username 추출
val usernameFromToken = jwtUtil.getUsername(token)
val usernameFromToken = jwtTokenUtil.getUsername(token)

// id로 회원 정보 조회
val member = memberRepository.findById(id)
Expand All @@ -96,7 +137,7 @@ class MemberController(

@DeleteMapping("/{id}")
fun remove(@PathVariable id: Long, @RequestHeader("access") token: String): ResponseEntity<String> {
val username = jwtUtil.getUsername(token)
val username = jwtTokenUtil.getUsername(token)
val member = memberRepository.findById(id)
.orElseThrow { IllegalArgumentException("Member not Found.") }
if(member.username != username) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,13 @@ class MemberResponseDTO {
val address: String
)

data class Login(
val accessToken: String,

val refreshToken: String,

val profileId: Long,

val profileUrl: String
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,4 @@ data class Member(

@Column(nullable = false)
var role: String? = null


)
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import org.tenten.bittakotlin.member.dto.MemberRequestDTO
import org.tenten.bittakotlin.member.dto.MemberResponseDTO

interface MemberService {
fun login(requestDto: MemberRequestDTO.Login): MemberResponseDTO.Login

fun join(joinDTO: MemberRequestDTO.Join) // Join 기능 병합

Expand Down
Loading
Loading