From 9045cc9a75dc2600be037379e99208f4b7b642b6 Mon Sep 17 00:00:00 2001 From: YooJHyun Date: Fri, 1 Nov 2024 15:27:54 +0900 Subject: [PATCH 01/26] =?UTF-8?q?Feat:=20jobpost,=20apply,=20like=20?= =?UTF-8?q?=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EB=B0=8F=20=EC=B6=94=EA=B0=80=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bittakotlin/BittaKotlinApplication.kt | 4 + .../apply/controller/ApplyController.kt | 235 ++++++++++ .../advice/ApplyControllerAdvice.kt | 18 + .../tenten/bittakotlin/apply/dto/ApplyDTO.kt | 40 ++ .../apply/dto/ApplyStatusUpdateDTO.kt | 21 + .../tenten/bittakotlin/apply/entity/Apply.kt | 33 ++ .../bittakotlin/apply/entity/ApplyStatus.kt | 8 + .../apply/exception/ApplyException.kt | 14 + .../apply/exception/ApplyTaskException.kt | 13 + .../apply/repository/ApplyRepository.kt | 30 ++ .../bittakotlin/apply/service/ApplyService.kt | 25 ++ .../apply/service/ApplyServiceImpl.kt | 128 ++++++ .../bittakotlin/apply/util/ApplyProvider.kt | 20 + .../bittakotlin/global/dto/PageRequestDTO.kt | 23 + .../jobpost/controller/JobPostController.kt | 403 ++++++++++++++++++ .../controller/JobPostViewController.kt | 17 + .../advice/JobPostControllerAdvice.kt | 15 + .../bittakotlin/jobpost/dto/JobPostDTO.kt | 70 +++ .../bittakotlin/jobpost/entity/JobPost.kt | 96 +++++ .../bittakotlin/jobpost/entity/PayStatus.kt | 6 + .../jobpost/entity/WorkCategory.kt | 20 + .../jobpost/exception/JobPostException.kt | 15 + .../jobpost/exception/JobPostTaskException.kt | 11 + .../jobpost/repository/JobPostRepository.kt | 34 ++ .../jobpost/service/DayScheduler.kt | 28 ++ .../jobpost/service/JobPostService.kt | 29 ++ .../jobpost/service/JobPostServiceImpl.kt | 179 ++++++++ .../jobpost/util/JobPostProvider.kt | 16 + .../like/controller/LikeController.kt | 115 +++++ .../controller/advice/LikeControllerAdvice.kt | 15 + .../tenten/bittakotlin/like/entity/Like.kt | 34 ++ .../like/exception/LikeException.kt | 13 + .../like/exception/LikeTaskException.kt | 11 + .../like/repository/LikeRepository.kt | 20 + .../bittakotlin/like/service/LikeService.kt | 10 + .../like/service/LikeServiceImpl.kt | 57 +++ .../member/dto/MemberNicknameDTO.kt | 5 + .../bittakotlin/profile/entity/Profile.kt | 13 +- .../profile/service/ProfileProvider.kt | 14 + .../profile/service/ProfileServiceImpl.kt | 2 +- 40 files changed, 1857 insertions(+), 3 deletions(-) create mode 100644 src/main/kotlin/org/tenten/bittakotlin/apply/controller/ApplyController.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/apply/controller/advice/ApplyControllerAdvice.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/apply/dto/ApplyDTO.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/apply/dto/ApplyStatusUpdateDTO.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/apply/entity/Apply.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/apply/entity/ApplyStatus.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/apply/exception/ApplyException.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/apply/exception/ApplyTaskException.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/apply/repository/ApplyRepository.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/apply/service/ApplyService.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/apply/service/ApplyServiceImpl.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/apply/util/ApplyProvider.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/global/dto/PageRequestDTO.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/jobpost/controller/JobPostController.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/jobpost/controller/JobPostViewController.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/jobpost/controller/advice/JobPostControllerAdvice.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/jobpost/dto/JobPostDTO.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/jobpost/entity/JobPost.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/jobpost/entity/PayStatus.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/jobpost/entity/WorkCategory.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/jobpost/exception/JobPostException.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/jobpost/exception/JobPostTaskException.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/jobpost/repository/JobPostRepository.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/jobpost/service/DayScheduler.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/jobpost/service/JobPostService.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/jobpost/service/JobPostServiceImpl.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/jobpost/util/JobPostProvider.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/like/controller/LikeController.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/like/controller/advice/LikeControllerAdvice.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/like/entity/Like.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/like/exception/LikeException.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/like/exception/LikeTaskException.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/like/repository/LikeRepository.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/like/service/LikeService.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/like/service/LikeServiceImpl.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/member/dto/MemberNicknameDTO.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/profile/service/ProfileProvider.kt diff --git a/src/main/kotlin/org/tenten/bittakotlin/BittaKotlinApplication.kt b/src/main/kotlin/org/tenten/bittakotlin/BittaKotlinApplication.kt index fde40bb..107098c 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/BittaKotlinApplication.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/BittaKotlinApplication.kt @@ -2,8 +2,12 @@ package org.tenten.bittakotlin import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication +import org.springframework.data.web.config.EnableSpringDataWebSupport +import org.springframework.scheduling.annotation.EnableScheduling @SpringBootApplication +@EnableScheduling +@EnableSpringDataWebSupport(pageSerializationMode = EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO) class BittaKotlinApplication fun main(args: Array) { diff --git a/src/main/kotlin/org/tenten/bittakotlin/apply/controller/ApplyController.kt b/src/main/kotlin/org/tenten/bittakotlin/apply/controller/ApplyController.kt new file mode 100644 index 0000000..acaddb9 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/apply/controller/ApplyController.kt @@ -0,0 +1,235 @@ +package org.tenten.bittakotlin.apply.controller + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +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.validation.Valid +import lombok.RequiredArgsConstructor +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* +import org.tenten.bittakotlin.apply.dto.ApplyDTO +import org.tenten.bittakotlin.apply.dto.ApplyStatusUpdateDTO +import org.tenten.bittakotlin.apply.service.ApplyService +import org.tenten.bittakotlin.profile.entity.Profile + +@Tag(name = "지원서 API 컨트롤러", description = "지원서와 관련된 REST API를 제공하는 컨트롤러입니다.") +@RestController +@RequestMapping("api/v1/apply") +@RequiredArgsConstructor +class ApplyController( + private val applyService: ApplyService +) { + +// @Operation( +// summary = "전체 지원서 조회", +// description = "회원의 전체 지원서를 조회합니다.", +// responses = [ +// ApiResponse( +// responseCode = "200", +// description = "지원서를 성공적으로 조회했습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = APPLY_SUCCESS_READ_ALL) +// )] +// ) +// ] +// ) + @GetMapping + fun findAll(profile: Profile): ResponseEntity> { + return ResponseEntity.ok(applyService.readAll(profile)) + } + +// @Operation( +// summary = "지원서 등록", +// description = "지원서를 등록합니다.", +// responses = [ +// ApiResponse( +// responseCode = "200", +// description = "지원서를 성공적으로 등록했습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = APPLY_SUCCESS_REGISTER) +// )] +// ), +// ApiResponse( +// responseCode = "400", +// description = "지원서 등록에 실패했습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = APPLY_FAILURE_NOT_REGISTERED) +// )] +// ) +// ] +// ) + @PostMapping + fun registerApply(@Valid @RequestBody applyDTO: ApplyDTO): ResponseEntity> { + return ResponseEntity.ok(applyService.register(applyDTO)) + } + +// @Operation( +// summary = "단일 지원서 조회", +// description = "단일 지원서를 조회합니다.", +// responses = [ +// ApiResponse( +// responseCode = "200", +// description = "지원서를 조회했습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = APPLY_SUCCESS_READ) +// )] +// ), +// ApiResponse( +// responseCode = "400", +// description = "지원서가 존재하지 않습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = APPLY_FAILURE_NOT_FOUND) +// )] +// ) +// ] +// ) +// @Parameter( +// name = "id", +// description = "조회할 지원서의 ID", +// required = true, +// example = "1", +// schema = Schema(type = "integer") +// ) + @GetMapping("/{id}") + fun readApply(@PathVariable("id") id: Long): ResponseEntity { + val applyDTO = applyService.findById(id) + return ResponseEntity.ok(applyDTO) + } + +// @Operation( +// summary = "지원서 삭제", +// description = "지원서를 삭제합니다.", +// responses = [ +// ApiResponse( +// responseCode = "200", +// description = "지원서가 삭제되었습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = APPLY_SUCCESS_DELETE) +// )] +// ), +// ApiResponse( +// responseCode = "400", +// description = "지원서 삭제에 실패했습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = APPLY_FAILURE_NOT_REMOVED) +// )] +// ), +// ApiResponse( +// responseCode = "404", +// description = "삭제할 지원서가 존재하지 않습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = APPLY_FAILURE_NOT_FOUND) +// )] +// ) +// ] +// ) +// @Parameter( +// name = "id", +// description = "삭제할 지원서의 ID", +// required = true, +// example = "1", +// schema = Schema(type = "integer") +// ) + @DeleteMapping("/{id}") + fun deleteApply(@Valid @PathVariable("id") id: Long): ResponseEntity> { + applyService.delete(id) + return ResponseEntity.ok(mapOf("message" to "삭제가 완료되었습니다")) + } + +// @Operation( +// summary = "게시물의 지원서 조회", +// description = "게시물에 해당하는 지원서를 조회합니다.", +// responses = [ +// ApiResponse( +// responseCode = "200", +// description = "지원서를 성공적으로 조회했습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = APPLY_SUCCESS_READ_ALL) +// )] +// ), +// ApiResponse( +// responseCode = "404", +// description = "지원서가 존재하지 않습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = APPLY_FAILURE_NOT_FOUND) +// )] +// ) +// ] +// ) +// @Parameter( +// name = "jobPostId, memberId", +// description = "지원서의 해당 일거리 ID, 작성자의 회원 ID", +// required = true, +// example = "1, 1", +// schema = Schema(type = "integer") +// ) + @GetMapping("/job-post/{jobPostId}/member/{profileId}") + fun getApplyIntoJobPost( + @PathVariable jobPostId: Long, + @PathVariable profileId: Long + ): ResponseEntity> { + val applies = applyService.getApplyIntoJobPost(jobPostId, profileId) + return ResponseEntity.ok(applies) + } + +// @Operation( +// summary = "지원 검토 상태 수정", +// description = "게시물에 해당하는 지원의 검토 상태를 수정합니다.", +// responses = [ +// ApiResponse( +// responseCode = "200", +// description = "검토 상태를 성공적으로 수정했습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = APPLY_SUCCESS_MODIFY) +// )] +// ), +// ApiResponse( +// responseCode = "400", +// description = "검토 상태 수정에 실패했습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = APPLY_FAILURE_NOT_MODIFY) +// )] +// ), +// ApiResponse( +// responseCode = "404", +// description = "일거리가 존재하지 않습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = APPLY_FAILURE_NOT_FOUND) +// )] +// ) +// ] +// ) +// @Parameter( +// name = "applyId, profileId", +// description = "해당 게시물의 지원 ID, 게시자의 프로필 ID", +// required = true, +// example = "1, 1", +// schema = Schema(type = "integer") +// ) + @PutMapping("/{applyId}/status/{profileId}") + fun applyStatusUpdate( + @PathVariable applyId: Long, + @PathVariable profileId: Long, + @RequestBody applyStatusUpdateDTO: ApplyStatusUpdateDTO + ): ResponseEntity { + applyService.applyStatusUpdate(applyId, applyStatusUpdateDTO.applyStatus!!, profileId) + return ResponseEntity.ok("상태가 변경되었습니다") + } +} + diff --git a/src/main/kotlin/org/tenten/bittakotlin/apply/controller/advice/ApplyControllerAdvice.kt b/src/main/kotlin/org/tenten/bittakotlin/apply/controller/advice/ApplyControllerAdvice.kt new file mode 100644 index 0000000..e4d63b6 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/apply/controller/advice/ApplyControllerAdvice.kt @@ -0,0 +1,18 @@ +package org.tenten.bittakotlin.apply.controller.advice + +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.RestControllerAdvice +import org.tenten.bittakotlin.apply.exception.ApplyTaskException + +@RestControllerAdvice +class ApplyControllerAdvice { + + @ExceptionHandler(ApplyTaskException::class) + fun handleApplyTaskException(e: ApplyTaskException): ResponseEntity> { + return ResponseEntity.status(e.code) + .body(mapOf("error" to e.message)) + } +} + + diff --git a/src/main/kotlin/org/tenten/bittakotlin/apply/dto/ApplyDTO.kt b/src/main/kotlin/org/tenten/bittakotlin/apply/dto/ApplyDTO.kt new file mode 100644 index 0000000..38028fa --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/apply/dto/ApplyDTO.kt @@ -0,0 +1,40 @@ +package org.tenten.bittakotlin.apply.dto + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import lombok.AllArgsConstructor +import lombok.Builder +import lombok.Data +import lombok.NoArgsConstructor +import org.tenten.bittakotlin.apply.entity.ApplyStatus +import java.time.LocalDateTime + +@Builder +@Data +@NoArgsConstructor +@AllArgsConstructor +@Schema(title = "지원서 DTO", description = "지원서 요청 및 응답에 사용하는 DTO입니다.") +data class ApplyDTO( + + @Schema(title = "지원서 ID (PK)", description = "지원서의 고유 ID 입니다.", example = "1", minimum = "1") + @field:Min(value = 1, message = "ID는 음수가 될 수 없습니다") + var id: Long? = null, + + @Schema(title = "일거리 ID (FK)", description = "일거리의 고유 ID 입니다.", example = "1", minimum = "1") + @field:Min(value = 1, message = "ID는 음수가 될 수 없습니다") + @field:NotNull(message = "게시글의 ID가 필요합니다") + var jobPostId: Long? = null, + + @Schema(title = "프로필 ID (FK)", description = "회원의 고유 프로필 ID 입니다.", example = "1", minimum = "1") + @field:Min(value = 1, message = "ID는 음수가 될 수 없습니다") + @field:NotNull(message = "회원의 ID가 필요합니다") + var profileId: Long? = null, + + @Schema(title = "지원서 생성일시", description = "지원서가 생성된 날짜 및 시간입니다.", example = "2023-09-24T14:45:00") + var appliedAt: LocalDateTime? = null, + + @Schema(title = "지원 검토 상태", description = "지원서의 현재 검토 상태입니다", example = "PENDING") + var status: ApplyStatus? = null +) + diff --git a/src/main/kotlin/org/tenten/bittakotlin/apply/dto/ApplyStatusUpdateDTO.kt b/src/main/kotlin/org/tenten/bittakotlin/apply/dto/ApplyStatusUpdateDTO.kt new file mode 100644 index 0000000..e4c9c1a --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/apply/dto/ApplyStatusUpdateDTO.kt @@ -0,0 +1,21 @@ +package org.tenten.bittakotlin.apply.dto + +import io.swagger.v3.oas.annotations.media.Schema +import lombok.AllArgsConstructor +import lombok.Builder +import lombok.Data +import lombok.NoArgsConstructor +import org.tenten.bittakotlin.apply.entity.ApplyStatus + +@Builder +@Data +@NoArgsConstructor +@AllArgsConstructor +@Schema(title = "지원서 상태 DTO", description = "지원서의 현재 검토 상태 변경시 사용하는 DTO입니다.") +data class ApplyStatusUpdateDTO( + @Schema(title = "지원서 ID", description = "지원서의 고유 ID입니다.", example = "1") + var id: Long? = null, + + @Schema(title = "지원서 검토 상태", description = "지원서의 현재 검토 상태입니다.", example = "PENDING") + var applyStatus: ApplyStatus? = null +) diff --git a/src/main/kotlin/org/tenten/bittakotlin/apply/entity/Apply.kt b/src/main/kotlin/org/tenten/bittakotlin/apply/entity/Apply.kt new file mode 100644 index 0000000..6420df1 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/apply/entity/Apply.kt @@ -0,0 +1,33 @@ +package org.tenten.bittakotlin.apply.entity + +import jakarta.persistence.* +import org.springframework.data.annotation.CreatedDate +import org.springframework.data.jpa.domain.support.AuditingEntityListener +import org.tenten.bittakotlin.jobpost.entity.JobPost +import org.tenten.bittakotlin.profile.entity.Profile +import java.time.LocalDateTime + +@Entity +@Table(name = "application") +@EntityListeners(AuditingEntityListener::class) +data class Apply( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "job_post_id", nullable = false) + var jobPost: JobPost? = null, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "profile_id", nullable = false) + var profile: Profile? = null, + + @CreatedDate + var appliedAt: LocalDateTime? = null, + + @Column(length = 200) + var status: ApplyStatus? = null +) + + diff --git a/src/main/kotlin/org/tenten/bittakotlin/apply/entity/ApplyStatus.kt b/src/main/kotlin/org/tenten/bittakotlin/apply/entity/ApplyStatus.kt new file mode 100644 index 0000000..6ff1164 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/apply/entity/ApplyStatus.kt @@ -0,0 +1,8 @@ +package org.tenten.bittakotlin.apply.entity + +enum class ApplyStatus { + UNDETERMINED, // 미정 + PENDING, // 보류 + ACCEPTED, // 합격 + NOT_REVIEWED // 확인하지 않음 +} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/apply/exception/ApplyException.kt b/src/main/kotlin/org/tenten/bittakotlin/apply/exception/ApplyException.kt new file mode 100644 index 0000000..e7d692d --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/apply/exception/ApplyException.kt @@ -0,0 +1,14 @@ +package org.tenten.bittakotlin.apply.exception + +enum class ApplyException(private val applyTaskException: ApplyTaskException) { + NOT_FOUND("지원서를 찾을 수 없습니다", 404), + NOT_REGISTERED("지원서가 등록되지 않았습니다", 400), + NOT_REMOVED("지원서가 삭제되지 않았습니다", 400), + NOT_FETCHED("지원서 조회에 실패하였습니다", 400); + + constructor(message: String, code: Int) : this(ApplyTaskException(message, code)) + + fun get(): ApplyTaskException = applyTaskException +} + + diff --git a/src/main/kotlin/org/tenten/bittakotlin/apply/exception/ApplyTaskException.kt b/src/main/kotlin/org/tenten/bittakotlin/apply/exception/ApplyTaskException.kt new file mode 100644 index 0000000..d7ae2ce --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/apply/exception/ApplyTaskException.kt @@ -0,0 +1,13 @@ +package org.tenten.bittakotlin.apply.exception + +import lombok.AllArgsConstructor +import lombok.Getter + +@Getter +@AllArgsConstructor +class ApplyTaskException( + override val message: String, + val code: Int +) : RuntimeException(message) + + diff --git a/src/main/kotlin/org/tenten/bittakotlin/apply/repository/ApplyRepository.kt b/src/main/kotlin/org/tenten/bittakotlin/apply/repository/ApplyRepository.kt new file mode 100644 index 0000000..263ece6 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/apply/repository/ApplyRepository.kt @@ -0,0 +1,30 @@ +package org.tenten.bittakotlin.apply.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param +import org.springframework.stereotype.Repository +import org.tenten.bittakotlin.apply.dto.ApplyDTO +import org.tenten.bittakotlin.apply.entity.Apply +import org.tenten.bittakotlin.jobpost.entity.JobPost +import org.tenten.bittakotlin.profile.entity.Profile +import java.util.* + +@Repository +interface ApplyRepository : JpaRepository { + + @Query("SELECT a FROM Apply a WHERE a.profile = :profile") + fun findAllByMember(@Param("profile") profile: Profile): List + + @Query("SELECT a FROM Apply a WHERE a.jobPost.id = :jobPostId") + fun findAllByJobPostId(@Param("jobPostId") jobPostId: Long): List + + @Query("SELECT a FROM Apply a WHERE a.id = :id") + fun getApplyDTO(@Param("id") id: Long): Optional + + @Query("SELECT a FROM Apply a WHERE a.jobPost = :jobPost") + fun findAllByJobPost(@Param("jobPost") jobPost: JobPost): List + + @Query("SELECT COUNT(a) FROM Apply a WHERE a.jobPost.id = :jobPostId") + fun countByJobPostId(@Param("jobPostId") jobPostId: Long): Long +} diff --git a/src/main/kotlin/org/tenten/bittakotlin/apply/service/ApplyService.kt b/src/main/kotlin/org/tenten/bittakotlin/apply/service/ApplyService.kt new file mode 100644 index 0000000..6619af1 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/apply/service/ApplyService.kt @@ -0,0 +1,25 @@ +package org.tenten.bittakotlin.apply.service + +import org.tenten.bittakotlin.apply.dto.ApplyDTO +import org.tenten.bittakotlin.apply.entity.ApplyStatus +import org.tenten.bittakotlin.profile.entity.Profile + +interface ApplyService { + fun readAll(profile: Profile): List? + + fun register(applyDTO: ApplyDTO): Map + + fun delete(id: Long) + + fun read(id: Long): ApplyDTO + + fun getApplyIntoJobPost(jobPostId: Long, profileId: Long): List + + fun findById(id: Long): ApplyDTO + + fun getApplyCount(jobPostId: Long): Long + + fun applyStatusUpdate(applyId: Long, applyStatus: ApplyStatus, profileId: Long) +} + + diff --git a/src/main/kotlin/org/tenten/bittakotlin/apply/service/ApplyServiceImpl.kt b/src/main/kotlin/org/tenten/bittakotlin/apply/service/ApplyServiceImpl.kt new file mode 100644 index 0000000..55dd906 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/apply/service/ApplyServiceImpl.kt @@ -0,0 +1,128 @@ +package org.tenten.bittakotlin.apply.service + +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import org.tenten.bittakotlin.apply.dto.ApplyDTO +import org.tenten.bittakotlin.apply.entity.Apply +import org.tenten.bittakotlin.apply.entity.ApplyStatus +import org.tenten.bittakotlin.apply.exception.ApplyException +import org.tenten.bittakotlin.apply.repository.ApplyRepository +import org.tenten.bittakotlin.jobpost.exception.JobPostException +import org.tenten.bittakotlin.jobpost.repository.JobPostRepository +import org.tenten.bittakotlin.jobpost.util.JobPostProvider +import org.tenten.bittakotlin.profile.entity.Profile + +@Service +class ApplyServiceImpl( + private val applyRepository: ApplyRepository, + private val jobPostRepository: JobPostRepository, +// private val memberProvider: MemberProvider, + private val jobPostProvider: JobPostProvider +) : ApplyService { + + private val log = LoggerFactory.getLogger(this::class.java) + + @Transactional + override fun readAll(profile: Profile): List? { + val applies = applyRepository.findAllByMember(profile) + return if (applies.isEmpty()) null else applies.map { entityToDto(it) } + } + + @Transactional + override fun register(applyDTO: ApplyDTO): Map { + return try { + var apply = dtoToEntity(applyDTO) + apply = applyRepository.save(apply) + + val jobPost = apply.jobPost + jobPost!!.plusApplyCount() + jobPostRepository.save(jobPost) + + mapOf( + "message" to "${apply.profile!!.nickname}님 지원 완료", + "data" to entityToDto(apply) + ) + } catch (e: Exception) { + log.error(e.message) + throw ApplyException.NOT_REGISTERED.get() + } + } + + @Transactional + override fun delete(id: Long) { + val apply = applyRepository.findById(id).orElseThrow { ApplyException.NOT_FOUND.get() } + + val jobPost = apply.jobPost + if (apply.profile != null) { + apply.profile = null + } + if (apply.jobPost != null) { + jobPost!!.minusApplyCount() + jobPostRepository.save(jobPost) + apply.jobPost = null + } + applyRepository.delete(apply) + } + + @Transactional + override fun read(id: Long): ApplyDTO { + val applyDTO = applyRepository.getApplyDTO(id).map { entityToDto(it) }.orElseThrow{ApplyException.NOT_FOUND.get()} + return applyDTO + } + + override fun getApplyIntoJobPost(jobPostId: Long, profileId: Long): List { + val jobPost = jobPostRepository.findById(jobPostId).orElseThrow { JobPostException.NOT_FOUND.get() } + + if (jobPost.profile!!.id != profileId) { + throw JobPostException.BAD_REQUEST.get() + } + + val applies = applyRepository.findAllByJobPost(jobPost) + return applies//.map { entityToDto(it) } + } + + @Transactional + override fun findById(id: Long): ApplyDTO { + val applyDTO = applyRepository.findById(id) + return applyDTO.map { entityToDto(it) }.orElseThrow { ApplyException.NOT_FOUND.get() } + } + + override fun getApplyCount(jobPostId: Long): Long { + return applyRepository.countByJobPostId(jobPostId) + } + + override fun applyStatusUpdate(applyId: Long, applyStatus: ApplyStatus, profileId: Long) { + val apply = applyRepository.findById(applyId).orElseThrow { ApplyException.NOT_FOUND.get() } + + val jobPostOwnerId = apply.jobPost!!.profile!!.id + if (jobPostOwnerId != profileId) { + throw ApplyException.NOT_FOUND.get() + } + + apply.status = applyStatus + applyRepository.save(apply) + } + + private fun dtoToEntity(applyDTO: ApplyDTO): Apply { + return Apply( + id = applyDTO.id, +// profile = profileProvider.getById(applyDTO.profileId!!), + jobPost = jobPostProvider.getById(applyDTO.jobPostId!!), + appliedAt = applyDTO.appliedAt + ) + } + + private fun entityToDto(apply: Apply): ApplyDTO { + val profileId = apply.profile?.id ?: throw ApplyException.NOT_FOUND.get() + val jobPostId = apply.jobPost?.id ?: throw ApplyException.NOT_FOUND.get() + + return ApplyDTO( + id = apply.id, + profileId = profileId, + jobPostId = jobPostId, + appliedAt = apply.appliedAt + ) + } +} + diff --git a/src/main/kotlin/org/tenten/bittakotlin/apply/util/ApplyProvider.kt b/src/main/kotlin/org/tenten/bittakotlin/apply/util/ApplyProvider.kt new file mode 100644 index 0000000..429796f --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/apply/util/ApplyProvider.kt @@ -0,0 +1,20 @@ +package org.tenten.bittakotlin.apply.util + +import lombok.RequiredArgsConstructor +import org.tenten.bittakotlin.apply.entity.Apply +import org.tenten.bittakotlin.apply.repository.ApplyRepository + +import org.springframework.stereotype.Component + +@Component +@RequiredArgsConstructor +class ApplyProvider( + private val applyRepository: ApplyRepository +) { + + fun getAllByJobPost(jobPostId: Long): List? { + val applies = applyRepository.findAllByJobPostId(jobPostId) + return applies.ifEmpty { null } + } +} + diff --git a/src/main/kotlin/org/tenten/bittakotlin/global/dto/PageRequestDTO.kt b/src/main/kotlin/org/tenten/bittakotlin/global/dto/PageRequestDTO.kt new file mode 100644 index 0000000..594a381 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/global/dto/PageRequestDTO.kt @@ -0,0 +1,23 @@ +package org.tenten.bittakotlin.global.dto + +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Pageable +import org.springframework.data.domain.Sort + +data class PageRequestDTO( + @field:Min(1) + var page: Int = 1, + + @field:Min(10) + @field:Max(100) + var size: Int = 10 +) { + fun getPageable(sort: Sort): Pageable { + val pageNum = (page - 1).coerceAtLeast(0) + val sizeNum = size.coerceAtLeast(10) + + return PageRequest.of(pageNum, sizeNum, sort) + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/jobpost/controller/JobPostController.kt b/src/main/kotlin/org/tenten/bittakotlin/jobpost/controller/JobPostController.kt new file mode 100644 index 0000000..2e3db4d --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/jobpost/controller/JobPostController.kt @@ -0,0 +1,403 @@ +package org.tenten.bittakotlin.jobpost.controller + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +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.validation.Valid +import lombok.RequiredArgsConstructor +import lombok.extern.log4j.Log4j2 +import org.springframework.data.domain.Page +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.ModelAttribute +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import org.tenten.bittakotlin.apply.repository.ApplyRepository +import org.tenten.bittakotlin.apply.service.ApplyService +import org.tenten.bittakotlin.global.dto.PageRequestDTO +import org.tenten.bittakotlin.jobpost.dto.JobPostDTO +import org.tenten.bittakotlin.jobpost.repository.JobPostRepository +import org.tenten.bittakotlin.jobpost.service.JobPostService + +@Tag(name = "일거리 API 컨트롤러", description = "일거리와 관련된 REST API를 제공하는 컨트롤러입니다.") +@RestController +@RequestMapping("/api/v1/job-post") +@Log4j2 +@RequiredArgsConstructor +class JobPostController( + private val jobPostService: JobPostService, + private val applyService: ApplyService, + private val jobPostRepository: JobPostRepository, + private val applyRepository: ApplyRepository +) { + +// @Operation( +// summary = "전체 일거리 조회", +// description = "전체 일거리를 조회합니다.", +// responses = [ +// ApiResponse( +// responseCode = "200", +// description = "일거리를 성공적으로 조회했습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = JOB_POST_SUCCESS_READ_ALL) +// )] +// ), +// ApiResponse( +// responseCode = "404", +// description = "일거리가 존재하지 않습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = JOB_POST_FAILURE_NOT_FOUND) +// )] +// ) +// ] +// ) + @GetMapping + fun getList(@Valid pageRequestDTO: PageRequestDTO): ResponseEntity> = + ResponseEntity.ok(jobPostService.getList(pageRequestDTO)) + +// @Operation( +// summary = "일거리 등록", +// description = "일거리를 등록합니다.", +// responses = [ +// ApiResponse( +// responseCode = "200", +// description = "일거리를 성공적으로 등록했습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = JOB_POST_SUCCESS_REGISTER) +// )] +// ), +// ApiResponse( +// responseCode = "400", +// description = "일거리 등록에 실패했습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = JOB_POST_FAILURE_NOT_REGISTERED) +// )] +// ) +// ] +// ) + @PostMapping + fun registerJobPost(@RequestBody jobPostDTO: JobPostDTO): ResponseEntity = + ResponseEntity.ok(jobPostService.register(jobPostDTO)) + +// @Operation( +// summary = "단일 일거리 조회", +// description = "ID와 일치하는 일거리를 조회합니다.", +// responses = [ +// ApiResponse( +// responseCode = "200", +// description = "일거리를 성공적으로 조회했습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = JOB_POST_SUCCESS_READ) +// )] +// ), +// ApiResponse( +// responseCode = "404", +// description = "일거리가 존재하지 않습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = JOB_POST_FAILURE_NOT_FOUND) +// )] +// ) +// ] +// ) +// @Parameter( +// name = "id", +// description = "조회할 일거리의 ID", +// required = true, +// example = "1", +// schema = Schema(type = "integer") +// ) + @GetMapping("/{id}") + fun readJobPost(@PathVariable("id") @Valid id: Long): ResponseEntity = + ResponseEntity.ok(jobPostService.read(id)) + +// @Operation( +// summary = "일거리 수정", +// description = "ID와 일치하는 일거리를 수정합니다.", +// responses = [ +// ApiResponse( +// responseCode = "200", +// description = "일거리를 성공적으로 수정했습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = JOB_POST_SUCCESS_MODIFY) +// )] +// ), +// ApiResponse( +// responseCode = "400", +// description = "일거리 수정에 실패했습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = JOB_POST_FAILURE_NOT_MODIFIED) +// )] +// ), +// ApiResponse( +// responseCode = "404", +// description = "일거리가 존재하지 않습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = JOB_POST_FAILURE_NOT_FOUND) +// )] +// ) +// ] +// ) +// @Parameter( +// name = "id", +// description = "수정할 일거리의 ID", +// required = true, +// example = "1", +// schema = Schema(type = "integer") +// ) + @PutMapping("/{id}") + fun modifyJobPost(@RequestBody jobPostDTO: JobPostDTO, @Valid @PathVariable("id") id: Long): ResponseEntity = + ResponseEntity.ok(jobPostService.modify(jobPostDTO)) + +// @Operation( +// summary = "일거리 삭제", +// description = "ID와 일치하는 일거리를 삭제합니다.", +// responses = [ +// ApiResponse( +// responseCode = "200", +// description = "일거리를 삭제했습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = JOB_POST_SUCCESS_DELETE) +// )] +// ), +// ApiResponse( +// responseCode = "400", +// description = "일거리 삭제에 실패했습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = JOB_POST_FAILURE_NOT_REMOVED) +// )] +// ), +// ApiResponse( +// responseCode = "404", +// description = "삭제할 일거리가 존재하지 않습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = JOB_POST_FAILURE_NOT_FOUND) +// )] +// ) +// ] +// ) +// @Parameter( +// name = "id", +// description = "삭제할 일거리의 ID", +// required = true, +// example = "1", +// schema = Schema(type = "integer") +// ) + @DeleteMapping("/{id}") + fun deleteJobPost(@Valid @PathVariable("id") id: Long): ResponseEntity> { + jobPostService.removeJobPost(id) + return ResponseEntity.ok(mapOf("message" to "삭제가 완료되었습니다")) + } + +// @Operation( +// summary = "작성자의 다른 게시물 조회", +// description = "작성자의 ID와 동일한 게시물을 조회합니다", +// responses = [ +// ApiResponse( +// responseCode = "200", +// description = "작성자의 게시물 조회에 성공했습니다", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = JOB_POST_SUCCESS_READ) +// )] +// ), +// ApiResponse( +// responseCode = "404", +// description = "작성자의 게시물을 찾을 수 없습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = JOB_POST_FAILURE_NOT_FOUND) +// )] +// ) +// ] +// ) +// @Parameter( +// name = "profileId", +// description = "작성자의 ID", +// required = true, +// example = "1", +// schema = Schema(type = "integer") +// ) + @GetMapping("/member/{profileId}") + fun getJobPostByMember( + @PathVariable profileId: Long, + @ModelAttribute pageRequestDTO: PageRequestDTO + ): ResponseEntity> { + val result = jobPostService.getJobPostByMember(profileId, pageRequestDTO) + return ResponseEntity.ok(result) + } + +// @Operation( +// summary = "키워드 검색", +// description = "키워드가 포함된 게시물을 검색합니다.", +// responses = [ +// ApiResponse( +// responseCode = "200", +// description = "검색 결과를 성공적으로 조회했습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = JOB_POST_SUCCESS_READ) +// )] +// ), +// ApiResponse( +// responseCode = "404", +// description = "검색 결과가 존재하지 않습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = JOB_POST_FAILURE_NOT_FOUND) +// )] +// ) +// ] +// ) +// @Parameter( +// name = "keyword", +// description = "검색할 키워드", +// required = true, +// example = "1", +// schema = Schema(type = "string") +// ) + @GetMapping("/search/{keyword}") + fun searchJobPost( + @PathVariable("keyword") keyword: String, + @ModelAttribute pageRequestDTO: PageRequestDTO + ): ResponseEntity> { + val result = jobPostService.searchJobPosts(keyword, pageRequestDTO) + return ResponseEntity.ok(result) + } + +// @Operation( +// summary = "게시물의 지원 수 조회", +// description = "게시물에 해당하는 지원서의 수를 조회합니다.", +// responses = [ +// ApiResponse( +// responseCode = "200", +// description = "지원서 수를 성공적으로 조회했습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = JOB_POST_SUCCESS_READ) +// )] +// ), +// ApiResponse( +// responseCode = "404", +// description = "지원서가 존재하지 않습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = JOB_POST_FAILURE_NOT_FOUND) +// )] +// ) +// ] +// ) +// @Parameter( +// name = "id", +// description = "지원서의 해당 일거리 ID", +// required = true, +// example = "1", +// schema = Schema(type = "integer") +// ) + @GetMapping("/{id}/applyCount") + fun getApplyCount(@PathVariable id: Long): ResponseEntity { + val applyCount = applyService.getApplyCount(id) + return ResponseEntity.ok(applyCount) + } + +// @Operation( +// summary = "남은 일자 기준 오름차순 조회", +// description = "남은 일자가 적은 순으로 전체 일거리를 조회합니다.", +// responses = [ +// ApiResponse( +// responseCode = "200", +// description = "일거리를 성공적으로 조회했습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = JOB_POST_SUCCESS_READ) +// )] +// ), +// ApiResponse( +// responseCode = "404", +// description = "일거리가 존재하지 않습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = JOB_POST_FAILURE_NOT_FOUND) +// )] +// ) +// ] +// ) + @GetMapping("/sortByRestDate") + fun getSortByDay(@ModelAttribute pageRequestDTO: PageRequestDTO): ResponseEntity> = + ResponseEntity.ok(jobPostService.getSortByDay(pageRequestDTO)) + +// @Operation( +// summary = "지원 수 기준 오름차순 조회", +// description = "지원 수가 많은 순으로 전체 일거리를 조회합니다.", +// responses = [ +// ApiResponse( +// responseCode = "200", +// description = "일거리를 성공적으로 조회했습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = JOB_POST_SUCCESS_READ) +// )] +// ), +// ApiResponse( +// responseCode = "404", +// description = "일거리가 존재하지 않습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = JOB_POST_FAILURE_NOT_FOUND) +// )] +// ) +// ] +// ) + @GetMapping("/sortByApplyCount") + fun getSortByApplyCount(@ModelAttribute pageRequestDTO: PageRequestDTO): ResponseEntity> = + ResponseEntity.ok(jobPostService.getSortByApplyCount(pageRequestDTO)) + +// @Operation( +// summary = "생성 일자 기준 오름차순 조회", +// description = "생성 일자가 이른 순으로 전체 일거리를 조회합니다.", +// responses = [ +// ApiResponse( +// responseCode = "200", +// description = "일거리를 성공적으로 조회했습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = JOB_POST_SUCCESS_READ) +// )] +// ), +// ApiResponse( +// responseCode = "404", +// description = "일거리가 존재하지 않습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = JOB_POST_FAILURE_NOT_FOUND) +// )] +// ) +// ] +// ) + @GetMapping("/sortByCreatedAt") + fun getSortByCreatedAt(@ModelAttribute pageRequestDTO: PageRequestDTO): ResponseEntity> = + ResponseEntity.ok(jobPostService.getSortByCreatedAt(pageRequestDTO)) +} + + + + diff --git a/src/main/kotlin/org/tenten/bittakotlin/jobpost/controller/JobPostViewController.kt b/src/main/kotlin/org/tenten/bittakotlin/jobpost/controller/JobPostViewController.kt new file mode 100644 index 0000000..0557268 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/jobpost/controller/JobPostViewController.kt @@ -0,0 +1,17 @@ +package org.tenten.bittakotlin.jobpost.controller + +import org.springframework.stereotype.Controller +import org.springframework.ui.Model +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping + +@Controller +@RequestMapping("/jobpost") +class JobPostViewController { + @GetMapping + fun showJobpostPage(model: Model?): String { + return "jobpost/jobpost" + } +} + + diff --git a/src/main/kotlin/org/tenten/bittakotlin/jobpost/controller/advice/JobPostControllerAdvice.kt b/src/main/kotlin/org/tenten/bittakotlin/jobpost/controller/advice/JobPostControllerAdvice.kt new file mode 100644 index 0000000..77ba032 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/jobpost/controller/advice/JobPostControllerAdvice.kt @@ -0,0 +1,15 @@ +package org.tenten.bittakotlin.jobpost.controller.advice + +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.RestControllerAdvice +import org.tenten.bittakotlin.jobpost.exception.JobPostTaskException + +@RestControllerAdvice +class JobPostControllerAdvice { + @ExceptionHandler(JobPostTaskException::class) + fun handleArgsException(e: JobPostTaskException): ResponseEntity> { + return ResponseEntity.status(e.code) + .body(mapOf("error" to e.message)) + } +} diff --git a/src/main/kotlin/org/tenten/bittakotlin/jobpost/dto/JobPostDTO.kt b/src/main/kotlin/org/tenten/bittakotlin/jobpost/dto/JobPostDTO.kt new file mode 100644 index 0000000..8d5a651 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/jobpost/dto/JobPostDTO.kt @@ -0,0 +1,70 @@ +package org.tenten.bittakotlin.jobpost.dto + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull +import lombok.AllArgsConstructor +import lombok.Builder +import lombok.Data +import org.tenten.bittakotlin.jobpost.entity.PayStatus +import org.tenten.bittakotlin.jobpost.entity.WorkCategory +import java.time.LocalDate +import java.time.LocalDateTime + +@Data +@Builder +@AllArgsConstructor +@Schema(title = "일거리 DTO", description = "일거리의 요청 및 응답에 사용하는 DTO입니다.") +data class JobPostDTO( + @Schema(title = "일거리 ID (PK)", description = "일거리의 고유 ID 입니다.", example = "1") + @Min(1) + val id: Long? = null, + + @Schema(title = "회원 ID (FK)", description = "회원의 고유 ID 입니다.", example = "1") + @Min(1) + @field:NotNull(message = "회원 ID가 필요합니다") + val profileId: Long? = null, + + @Schema(title = "일거리 제목", description = "일거리 제목입니다.", example = "Job Title") + @field:NotBlank(message = "제목 입력은 필수적 입니다") + val title: String? = null, + + @Schema(title = "일거리 내용", description = "일거리 내용입니다.", example = "Job Content") + @field:NotBlank(message = "설명 입력은 필수적 입니다") + val description: String? = null, + + @Schema(title = "출근지", description = "출근 지역입니다.", example = "서울 특별시 광진구일대") + @field:NotNull(message = "지역명은 필수적으로 입력해야 합니다") + val location: String? = null, + + @Schema(title = "지불 유형", description = "지불 유형입니다.", example = "FREE") + @field:NotNull(message = "급여 여부는 필수적으로 입력해야 합니다") + val payStatus: PayStatus? = null, + + @Schema(title = "일거리 등록일시", description = "일거리가 등록된 날짜 및 시간입니다.", example = "2023-09-24T14:45:00") + val createdAt: LocalDateTime? = null, + + @Schema(title = "일거리 수정일시", description = "일거리가 수정된 날짜 및 시간입니다.", example = "2023-09-24T14:45:00") + val updateAt: LocalDateTime? = null, + + @Schema(title = "촬영 방법", description = "일거리의 진행 방식입니다.", example = "SNAPSHOT") + @field:NotNull(message = "작품 카테고리는 필수적으로 입력해야 합니다") + val workCategory: WorkCategory? = null, + + @Schema(title = "오디션일", description = "오디션을 진행하는 날짜입니다.", example = "2023-09-24") + val auditionDate: LocalDate? = null, + + @Schema(title = "촬영 시작일", description = "일이 시작하는 날짜입니다.", example = "2023-09-24") + val startDate: LocalDate? = null, + + @Schema(title = "촬영 종료일", description = "일이 종료되는 날짜입니다.", example = "2023-09-24") + val endDate: LocalDate? = null, + + @Schema(title = "모집 종료일", description = "모집 종료 일자입니다.", example = "2024-09-24") + val closeDate: LocalDate? = null, + + @Schema(title = "마감까지 남은 일자", description = "모집 종료까지 남은 일자입니다.", example = "38") + val restDate: Int? = null +) + diff --git a/src/main/kotlin/org/tenten/bittakotlin/jobpost/entity/JobPost.kt b/src/main/kotlin/org/tenten/bittakotlin/jobpost/entity/JobPost.kt new file mode 100644 index 0000000..be0a26d --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/jobpost/entity/JobPost.kt @@ -0,0 +1,96 @@ +package org.tenten.bittakotlin.jobpost.entity + +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.Enumerated +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.JoinColumn +import jakarta.persistence.Table +import org.springframework.data.annotation.CreatedDate +import org.springframework.data.annotation.LastModifiedDate +import org.springframework.data.jpa.domain.support.AuditingEntityListener +import org.tenten.bittakotlin.apply.entity.Apply +import java.time.LocalDate + +import jakarta.persistence.* +import lombok.* +import org.tenten.bittakotlin.like.entity.Like +import org.tenten.bittakotlin.profile.entity.Profile +import java.time.LocalDateTime + +@Data +@Entity +@Getter +@ToString +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Table(name = "job_post") +@EntityListeners(AuditingEntityListener::class) +class JobPost( + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long? = null, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "profile_id", nullable = false) + val profile: Profile? = null, // 게시글 작성자 + + @Column(length = 100, nullable = false) + var title: String, // 게시글 제목 + + @Column(length = 500, nullable = false) + var description: String, // 설명 + + @Column(length = 200, nullable = false) + var location: String, // 촬영 지역 + + @Enumerated(EnumType.STRING) + var payStatus: PayStatus? = null, // 급여 방식 + + @CreatedDate + val createdAt: LocalDateTime? = null, // 게시글 생성일자 + + @LastModifiedDate + val updatedAt: LocalDateTime? = null, // 게시글 수정일자 + + @Enumerated(EnumType.STRING) + var workCategory: WorkCategory? = null, // 작품 카테고리 + + val auditionDate: LocalDate? = null, // 오디션 일자 + + val startDate: LocalDate? = null, // 촬영 기간 시작일 + val endDate: LocalDate? = null, // 촬영 기간 종료일 + + val closeDate: LocalDate? = null, // 게시글 마감 일자 + + var restDate: Int? = null, // 마감까지 남은 일자 + + var applyCount: Int = 0, // 해당 게시글에 지원한 수 + +// @OneToOne(mappedBy = "jobPost", cascade = [CascadeType.REMOVE], orphanRemoval = true) +// var media: Media? = null, + + @OneToMany(mappedBy = "jobPost", fetch = FetchType.EAGER, cascade = [CascadeType.REMOVE], orphanRemoval = true) + val apply: List = mutableListOf(), + + @OneToMany(mappedBy = "jobPost", fetch = FetchType.EAGER, cascade = [CascadeType.REMOVE], orphanRemoval = true) + val likes: List = mutableListOf() +) { + + fun plusApplyCount() { + this.applyCount++ + } + + fun minusApplyCount() { + if (this.applyCount > 0) { + this.applyCount-- + } + } +} + + + diff --git a/src/main/kotlin/org/tenten/bittakotlin/jobpost/entity/PayStatus.kt b/src/main/kotlin/org/tenten/bittakotlin/jobpost/entity/PayStatus.kt new file mode 100644 index 0000000..dff79b5 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/jobpost/entity/PayStatus.kt @@ -0,0 +1,6 @@ +package org.tenten.bittakotlin.jobpost.entity + +enum class PayStatus { + FREE, PAID // 모집 방식 (무급, 유급) +} + diff --git a/src/main/kotlin/org/tenten/bittakotlin/jobpost/entity/WorkCategory.kt b/src/main/kotlin/org/tenten/bittakotlin/jobpost/entity/WorkCategory.kt new file mode 100644 index 0000000..cb87cae --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/jobpost/entity/WorkCategory.kt @@ -0,0 +1,20 @@ +package org.tenten.bittakotlin.jobpost.entity + +enum class WorkCategory { + FEATURE_FILM, // 장편 영화 + SHORT_FILM, // 단편 영화 + FEATURE_WEBDRAMA, // 장편 웹드라마 + SHORT_WEBDRAMA, // 단편 웹드라마 + MUSIC_VIDEO, // 뮤직비디오 + COMMERCIAL, // 광고 + DOCUMENTARY, // 다큐멘터리 + PHOTOGRAPHY, // 사진 촬영 + COVER_MODEL, // 표지 모델 + SNAPSHOT, // 스냅샷 + PHOTO_SHOOT, // 화보 촬영 + FASHION_FILM, // 패션 필름 + VLOG, // 브이로그 + INTERVIEW, // 인터뷰 영상 + EVENT_VIDEO, // 행사 영상 + YOUTUBE // 유튜브 섭외 +} diff --git a/src/main/kotlin/org/tenten/bittakotlin/jobpost/exception/JobPostException.kt b/src/main/kotlin/org/tenten/bittakotlin/jobpost/exception/JobPostException.kt new file mode 100644 index 0000000..012f395 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/jobpost/exception/JobPostException.kt @@ -0,0 +1,15 @@ +package org.tenten.bittakotlin.jobpost.exception + +enum class JobPostException(private val jobPostTaskException: JobPostTaskException) { + BAD_REQUEST("잘못된 접근입니다", 400), + NOT_FOUND("게시글을 찾을 수 없습니다", 404), + NOT_REGISTERED("게시글을 등록할 수 없습니다", 400), + NOT_MODIFIED("게시글을 수정할 수 없습니다", 400), + NOT_REMOVED("게시글을 삭제할 수 없습니다", 400), + NOT_FETCHED("게시글을 조회할 수 없습니다", 400); + + constructor(message: String, code: Int) : this(JobPostTaskException(message, code)) + + fun get(): JobPostTaskException = jobPostTaskException +} + diff --git a/src/main/kotlin/org/tenten/bittakotlin/jobpost/exception/JobPostTaskException.kt b/src/main/kotlin/org/tenten/bittakotlin/jobpost/exception/JobPostTaskException.kt new file mode 100644 index 0000000..e3a36a8 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/jobpost/exception/JobPostTaskException.kt @@ -0,0 +1,11 @@ +package org.tenten.bittakotlin.jobpost.exception + +import lombok.AllArgsConstructor +import lombok.Getter + +@Getter +@AllArgsConstructor +class JobPostTaskException ( + override val message: String, + val code: Int +) : RuntimeException(message) diff --git a/src/main/kotlin/org/tenten/bittakotlin/jobpost/repository/JobPostRepository.kt b/src/main/kotlin/org/tenten/bittakotlin/jobpost/repository/JobPostRepository.kt new file mode 100644 index 0000000..4d171f7 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/jobpost/repository/JobPostRepository.kt @@ -0,0 +1,34 @@ +package org.tenten.bittakotlin.jobpost.repository + +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param +import org.tenten.bittakotlin.jobpost.entity.JobPost +import java.util.Optional + +interface JobPostRepository : JpaRepository { + + @Query("SELECT j FROM JobPost j WHERE j.id = :id") + fun getJobPost(@Param("id") id: Long): Optional + + @Query("SELECT j FROM JobPost j ORDER BY j.id DESC") + fun getList(pageable: Pageable): Page + + @Query("SELECT j FROM JobPost j WHERE j.profile.id = :profileId") + fun findJobPostByMember(@Param("profileId") profileId: Long, pageable: Pageable): Page + + @Query("SELECT j FROM JobPost j WHERE LOWER(j.title) LIKE LOWER(CONCAT('%', :keyword, '%')) OR LOWER(j.description) LIKE LOWER(CONCAT('%', :keyword, '%'))") + fun searchByKeyword(@Param("keyword") keyword: String, pageable: Pageable): Page + + @Query("SELECT j FROM JobPost j WHERE j.restDate IS NOT NULL ORDER BY j.restDate ASC") + fun findSortedByRestDate(pageable: Pageable): Page + + @Query("SELECT j FROM JobPost j WHERE j.applyCount IS NOT NULL ORDER BY j.applyCount DESC") + fun findSortedByApplyCountDesc(pageable: Pageable): Page + + @Query("SELECT j FROM JobPost j WHERE j.createdAt IS NOT NULL ORDER BY j.createdAt ASC") + fun findSortedByCreatedAt(pageable: Pageable): Page +} + diff --git a/src/main/kotlin/org/tenten/bittakotlin/jobpost/service/DayScheduler.kt b/src/main/kotlin/org/tenten/bittakotlin/jobpost/service/DayScheduler.kt new file mode 100644 index 0000000..1b980eb --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/jobpost/service/DayScheduler.kt @@ -0,0 +1,28 @@ +package org.tenten.bittakotlin.jobpost.service + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Service +import org.tenten.bittakotlin.jobpost.repository.JobPostRepository +import java.time.LocalDate +import java.time.temporal.ChronoUnit + +@Service +class DayScheduler @Autowired constructor( + private val jobPostRepository: JobPostRepository +) { + + @Scheduled(cron = "0 0 0 * * ?") // 자정마다 실행 + fun checkDday() { + val posts = jobPostRepository.findAll() + + for (post in posts) { + val today = LocalDate.now() + val endDate = post.endDate + val restDate = ChronoUnit.DAYS.between(today, endDate).toInt() + + post.restDate = restDate + jobPostRepository.save(post) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/jobpost/service/JobPostService.kt b/src/main/kotlin/org/tenten/bittakotlin/jobpost/service/JobPostService.kt new file mode 100644 index 0000000..3134c75 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/jobpost/service/JobPostService.kt @@ -0,0 +1,29 @@ +package org.tenten.bittakotlin.jobpost.service + +import org.springframework.data.domain.Page +import org.tenten.bittakotlin.global.dto.PageRequestDTO +import org.tenten.bittakotlin.jobpost.dto.JobPostDTO + +interface JobPostService { + + fun register(jobPostDTO: JobPostDTO): JobPostDTO + + fun read(id: Long): JobPostDTO + + fun modify(jobPostDTO: JobPostDTO): JobPostDTO + + fun getList(pageRequestDTO: PageRequestDTO): Page + + fun removeJobPost(jobPostId: Long) + + fun getJobPostByMember(memberId: Long, pageRequestDTO: PageRequestDTO): Page + + fun searchJobPosts(keyword: String, pageRequestDTO: PageRequestDTO): Page + + fun getSortByDay(pageRequestDTO: PageRequestDTO): Page + + fun getSortByApplyCount(pageRequestDTO: PageRequestDTO): Page + + fun getSortByCreatedAt(pageRequestDTO: PageRequestDTO): Page +} + diff --git a/src/main/kotlin/org/tenten/bittakotlin/jobpost/service/JobPostServiceImpl.kt b/src/main/kotlin/org/tenten/bittakotlin/jobpost/service/JobPostServiceImpl.kt new file mode 100644 index 0000000..8e432f8 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/jobpost/service/JobPostServiceImpl.kt @@ -0,0 +1,179 @@ +package org.tenten.bittakotlin.jobpost.service + +import org.hibernate.query.sqm.tree.SqmNode.log +import org.springframework.data.domain.PageRequest +import org.tenten.bittakotlin.apply.repository.ApplyRepository +import org.tenten.bittakotlin.apply.util.ApplyProvider +import org.tenten.bittakotlin.jobpost.dto.JobPostDTO +import org.tenten.bittakotlin.jobpost.entity.JobPost +import org.tenten.bittakotlin.jobpost.exception.JobPostException +import org.tenten.bittakotlin.jobpost.repository.JobPostRepository + +import org.springframework.data.domain.Page +import org.springframework.data.domain.Sort +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import org.tenten.bittakotlin.apply.dto.ApplyDTO +import org.tenten.bittakotlin.apply.entity.Apply +import org.tenten.bittakotlin.global.dto.PageRequestDTO +import org.tenten.bittakotlin.profile.repository.ProfileRepository +import org.tenten.bittakotlin.profile.service.ProfileProvider + +@Service +class JobPostServiceImpl( + private val jobPostRepository: JobPostRepository, + private val profileProvider: ProfileProvider, + private val applyProvider: ApplyProvider, +// private val mediaService: MediaService, + private val applyRepository: ApplyRepository, + private val profileRepository: ProfileRepository +) : JobPostService { + + @Transactional + override fun getList(pageRequestDTO: PageRequestDTO): Page { + val sort = Sort.by("id").descending() + val pageable = pageRequestDTO.getPageable(sort) + val jobPosts = jobPostRepository.getList(pageable) + return jobPosts.map { entityToDto(it) } + } + + @Transactional + override fun register(jobPostDTO: JobPostDTO): JobPostDTO { + return try { + var jobPost = dtoToEntity(jobPostDTO) + jobPost = jobPostRepository.save(jobPost) + entityToDto(jobPost) + } catch (e: Exception) { + log.error(e.message) + throw JobPostException.NOT_REGISTERED.get() + } + } + + @Transactional + override fun read(jobPostId: Long): JobPostDTO { + val jobPost = jobPostRepository.getJobPost(jobPostId) + return jobPost.map { entityToDto(it) }.orElseThrow { JobPostException.NOT_REGISTERED.get() } + } + + @Transactional + override fun modify(jobPostDTO: JobPostDTO): JobPostDTO { + val jobPost = jobPostRepository.findById(jobPostDTO.id!!) + .orElseThrow { JobPostException.NOT_FOUND.get() } + + return try { + jobPost.title = jobPostDTO.title!! + jobPost.description = jobPostDTO.description!! + jobPost.location = jobPostDTO.location!! + jobPost.payStatus = jobPostDTO.payStatus + jobPost.workCategory = jobPostDTO.workCategory + + jobPostRepository.save(jobPost) + entityToDto(jobPost) + } catch (e: Exception) { + log.error(e.message) + throw JobPostException.NOT_MODIFIED.get() + } + } + + @Transactional + override fun removeJobPost(jobPostId: Long) { + val jobPost = jobPostRepository.findById(jobPostId).orElseThrow { JobPostException.NOT_FOUND.get() } + val applies = jobPost.apply + applies?.let { + if (it.isNotEmpty()) { + it.forEach { apply -> apply.jobPost = null } + applyRepository.deleteAllInBatch(it) + } + } +// jobPost.media?.let { +// mediaService.delete(it) +// jobPost.media = null +// } + jobPostRepository.delete(jobPost) + } + + override fun getJobPostByMember(memberId: Long, pageRequestDTO: PageRequestDTO): Page { + val pageable = PageRequest.of(pageRequestDTO.page, pageRequestDTO.size) + val jobPostPage = jobPostRepository.findJobPostByMember(memberId, pageable) + return jobPostPage.map { entityToDto(it) } + } + + override fun searchJobPosts(keyword: String, pageRequestDTO: PageRequestDTO): Page { + val sort = Sort.by("id").descending() + val pageable = pageRequestDTO.getPageable(sort) + val jobPosts = jobPostRepository.searchByKeyword(keyword, pageable) + return jobPosts.map { entityToDto(it) } + } + + @Transactional + override fun getSortByDay(pageRequestDTO: PageRequestDTO): Page { + val sort = Sort.by("restDate").ascending() + val pageable = pageRequestDTO.getPageable(sort) + val jobPosts = jobPostRepository.findSortedByRestDate(pageable) + return jobPosts.map { entityToDto(it) } + } + + @Transactional + override fun getSortByApplyCount(pageRequestDTO: PageRequestDTO): Page { + val sort = Sort.by("applyCount").descending() + val pageable = pageRequestDTO.getPageable(sort) + val jobPosts = jobPostRepository.findSortedByApplyCountDesc(pageable) + return jobPosts.map { entityToDto(it) } + } + + override fun getSortByCreatedAt(pageRequestDTO: PageRequestDTO): Page { + val sort = Sort.by("createdAt").ascending() + val pageable = pageRequestDTO.getPageable(sort) + val jobPosts = jobPostRepository.findSortedByCreatedAt(pageable) + return jobPosts.map { entityToDto(it) } + } + + private fun entityToDto(apply: Apply): ApplyDTO { + return ApplyDTO( + id = apply.id, + profileId = apply.profile?.id, + jobPostId = apply.jobPost?.id, + appliedAt = apply.appliedAt + ) + } + + private fun entityToDto(jobPost: JobPost): JobPostDTO { + return JobPostDTO( + id = jobPost.id, + title = jobPost.title, + description = jobPost.description, + location = jobPost.location, + payStatus = jobPost.payStatus, + closeDate = jobPost.closeDate, + workCategory = jobPost.workCategory, + auditionDate = jobPost.auditionDate, + startDate = jobPost.startDate, + endDate = jobPost.endDate, + updateAt = jobPost.updatedAt, + restDate = jobPost.restDate, + profileId = jobPost.profile?.id + ) + } + + private fun dtoToEntity(jobPostDTO: JobPostDTO): JobPost { + return JobPost( + id = jobPostDTO.id, + title = jobPostDTO.title!!, + description = jobPostDTO.description!!, + location = jobPostDTO.location!!, + payStatus = jobPostDTO.payStatus, + closeDate = jobPostDTO.closeDate, + workCategory = jobPostDTO.workCategory, + auditionDate = jobPostDTO.auditionDate, + startDate = jobPostDTO.startDate, + endDate = jobPostDTO.endDate, + updatedAt = jobPostDTO.updateAt, + restDate = jobPostDTO.restDate, + profile = profileProvider.getById(jobPostDTO.profileId!!)!!, + apply = applyProvider.getAllByJobPost(jobPostDTO.id!!)!! + ) + } +} + + + diff --git a/src/main/kotlin/org/tenten/bittakotlin/jobpost/util/JobPostProvider.kt b/src/main/kotlin/org/tenten/bittakotlin/jobpost/util/JobPostProvider.kt new file mode 100644 index 0000000..8aa55bd --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/jobpost/util/JobPostProvider.kt @@ -0,0 +1,16 @@ +package org.tenten.bittakotlin.jobpost.util + +import org.tenten.bittakotlin.jobpost.entity.JobPost +import org.tenten.bittakotlin.jobpost.repository.JobPostRepository + +import org.springframework.stereotype.Component + +@Component +class JobPostProvider( + private val jobPostRepository: JobPostRepository +) { + + fun getById(id: Long): JobPost? { + return jobPostRepository.findById(id).orElse(null) + } +} diff --git a/src/main/kotlin/org/tenten/bittakotlin/like/controller/LikeController.kt b/src/main/kotlin/org/tenten/bittakotlin/like/controller/LikeController.kt new file mode 100644 index 0000000..c311e3b --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/like/controller/LikeController.kt @@ -0,0 +1,115 @@ +package org.tenten.bittakotlin.like.controller + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.media.Content +import io.swagger.v3.oas.annotations.media.Schema +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* +import org.apache.logging.log4j.Logger +import org.apache.logging.log4j.LogManager +import org.tenten.bittakotlin.like.service.LikeService +import org.tenten.bittakotlin.member.dto.MemberNicknameDTO +import org.tenten.bittakotlin.profile.entity.Profile + +@Tag(name = "좋아요 API 컨트롤러", description = "좋아요 기능과 관련된 REST API를 제공하는 컨트롤러입니다") +@RestController +@RequestMapping("/api/v1/like") +class LikeController ( + private val likeService: LikeService +) { + private val log: Logger = LogManager.getLogger(LikeController::class.java) + +// @Operation( +// summary = "좋아요 등록", +// description = "좋아요를 등록합니다.", +// responses = [ +// ApiResponse( +// responseCode = "200", +// description = "좋아요를 성공적으로 등록했습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = LIKE_SUCCESS_REGISTER) +// )] +// ), +// ApiResponse( +// responseCode = "400", +// description = "좋아요 등록에 실패했습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = LIKE_FAILURE_NOT_REGISTERED) +// )] +// ) +// ] +// ) + @PostMapping("/jobPost/{jobPostId}/member/{profileId}") + fun addLike(@PathVariable jobPostId: Long, @PathVariable profileId: Long): ResponseEntity { + likeService.addLike(jobPostId, profileId) + return ResponseEntity.ok("좋아요를 눌렀습니다") + } + +// @Operation( +// summary = "좋아요 삭제", +// description = "ID와 일치하는 좋아요를 취소합니다.", +// responses = [ +// ApiResponse( +// responseCode = "200", +// description = "좋아요를 취소했습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = LIKE_SUCCESS_DELETE) +// )] +// ), +// ApiResponse( +// responseCode = "400", +// description = "좋아요 취소에 실패했습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = LIKE_FAILURE_NOT_REMOVED) +// )] +// ), +// ApiResponse( +// responseCode = "404", +// description = "취소할 좋아요가 존재하지 않습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = LIKE_FAILURE_NOT_FOUND) +// )] +// ) +// ] +// ) + @DeleteMapping("/jobPost/{jobPostId}/member/{profileId}") + fun removeLike(@PathVariable jobPostId: Long, @PathVariable profileId: Long): ResponseEntity { + likeService.removeLike(jobPostId, profileId) + return ResponseEntity.ok("좋아요를 취소했습니다") + } + +// @Operation( +// summary = "좋아요를 누른 회원 조회", +// description = "해당 게시물에 좋아요를 누른 회원을 조회합니다.", +// responses = [ +// ApiResponse( +// responseCode = "200", +// description = "좋아요를 누른 회원을 성공적으로 조회했습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = LIKE_SUCCESS_READ_ALL) +// )] +// ), +// ApiResponse( +// responseCode = "404", +// description = "좋아요가 존재하지 않습니다.", +// content = [Content( +// mediaType = "application/json", +// schema = Schema(example = LIKE_FAILURE_NOT_FOUND) +// )] +// ) +// ] +// ) + @GetMapping("/jobPost/{jobPostId}") + fun getLikes(@PathVariable jobPostId: Long): ResponseEntity> { + val members = likeService.getLikesForJobPost(jobPostId) + return ResponseEntity.ok(members) + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/like/controller/advice/LikeControllerAdvice.kt b/src/main/kotlin/org/tenten/bittakotlin/like/controller/advice/LikeControllerAdvice.kt new file mode 100644 index 0000000..8f9f0ec --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/like/controller/advice/LikeControllerAdvice.kt @@ -0,0 +1,15 @@ +package org.tenten.bittakotlin.like.controller.advice + +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.RestControllerAdvice +import org.tenten.bittakotlin.jobpost.exception.JobPostTaskException + +@RestControllerAdvice +class LikeControllerAdvice { + @ExceptionHandler(JobPostTaskException::class) + fun handleArgsException(e: JobPostTaskException): ResponseEntity> { + return ResponseEntity.status(e.code) + .body(mapOf("error" to e.message)) + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/like/entity/Like.kt b/src/main/kotlin/org/tenten/bittakotlin/like/entity/Like.kt new file mode 100644 index 0000000..c794ba4 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/like/entity/Like.kt @@ -0,0 +1,34 @@ +package org.tenten.bittakotlin.like.entity + +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.Table +import org.springframework.data.jpa.domain.support.AuditingEntityListener +import org.tenten.bittakotlin.jobpost.entity.JobPost +import org.tenten.bittakotlin.profile.entity.Profile +import java.time.LocalDateTime + +@Entity +@Table(name = "heart") +@EntityListeners(AuditingEntityListener::class) +data class Like( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long? = null, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "profile_id", nullable = false) + var profile: Profile? = null, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "job_post_id", nullable = false) + var jobPost: JobPost? = null, + + var likedAt: LocalDateTime? = null +) diff --git a/src/main/kotlin/org/tenten/bittakotlin/like/exception/LikeException.kt b/src/main/kotlin/org/tenten/bittakotlin/like/exception/LikeException.kt new file mode 100644 index 0000000..d5fc276 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/like/exception/LikeException.kt @@ -0,0 +1,13 @@ +package org.tenten.bittakotlin.like.exception + +enum class LikeException(private val likeTaskException: LikeTaskException) { + BAD_REQUEST("잘못된 접근입니다", 400), + NOT_FOUND("좋아요를 찾을 수 없습니다", 404), + NOT_REGISTERED("좋아요를 등록할 수 없습니다", 400), + NOT_REMOVED("좋아요를 취소할 수 없습니다", 400), + NOT_FETCHED("좋아요를 누른 회원을 조회할 수 없습니다", 400); + + constructor(message: String, code: Int) : this(LikeTaskException(message, code)) + + fun get(): LikeTaskException = likeTaskException +} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/like/exception/LikeTaskException.kt b/src/main/kotlin/org/tenten/bittakotlin/like/exception/LikeTaskException.kt new file mode 100644 index 0000000..c4432aa --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/like/exception/LikeTaskException.kt @@ -0,0 +1,11 @@ +package org.tenten.bittakotlin.like.exception + +import lombok.AllArgsConstructor +import lombok.Getter + +@Getter +@AllArgsConstructor +class LikeTaskException( + override val message: String, + val code: Int +) : RuntimeException(message) \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/like/repository/LikeRepository.kt b/src/main/kotlin/org/tenten/bittakotlin/like/repository/LikeRepository.kt new file mode 100644 index 0000000..3fb98a1 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/like/repository/LikeRepository.kt @@ -0,0 +1,20 @@ +package org.tenten.bittakotlin.like.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import org.tenten.bittakotlin.jobpost.entity.JobPost +import org.tenten.bittakotlin.like.entity.Like +import org.tenten.bittakotlin.profile.entity.Profile +import java.util.Optional + +interface LikeRepository : JpaRepository { + + @Query("SELECT l FROM Like l WHERE l.jobPost = :jobPost") + fun findByJobPost(jobPost: JobPost): List + + @Query("SELECT CASE WHEN COUNT(l) > 0 THEN true ELSE false END FROM Like l WHERE l.jobPost = :jobPost AND l.profile = :profile") + fun existsByJobPostAndProfile(jobPost: JobPost, profile: Profile): Boolean + + @Query("SELECT l FROM Like l WHERE l.jobPost.id = :jobPostId AND l.profile.id = :profileId") + fun findByJobPostIdAndProfileId(jobPostId: Long, profileId: Long): Optional +} diff --git a/src/main/kotlin/org/tenten/bittakotlin/like/service/LikeService.kt b/src/main/kotlin/org/tenten/bittakotlin/like/service/LikeService.kt new file mode 100644 index 0000000..a502b41 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/like/service/LikeService.kt @@ -0,0 +1,10 @@ +package org.tenten.bittakotlin.like.service + +import org.tenten.bittakotlin.member.dto.MemberNicknameDTO + +interface LikeService { + fun addLike(jobPostId: Long, profileId: Long) + fun getLikesForJobPost(jobPostId: Long): List + fun removeLike(jobPostId: Long, profileId: Long) +} + diff --git a/src/main/kotlin/org/tenten/bittakotlin/like/service/LikeServiceImpl.kt b/src/main/kotlin/org/tenten/bittakotlin/like/service/LikeServiceImpl.kt new file mode 100644 index 0000000..07ec88f --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/like/service/LikeServiceImpl.kt @@ -0,0 +1,57 @@ +package org.tenten.bittakotlin.like.service + +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import org.tenten.bittakotlin.jobpost.exception.JobPostException +import org.tenten.bittakotlin.jobpost.repository.JobPostRepository +import org.tenten.bittakotlin.like.entity.Like +import org.tenten.bittakotlin.like.exception.LikeException +import org.tenten.bittakotlin.like.repository.LikeRepository +import org.tenten.bittakotlin.member.dto.MemberNicknameDTO +import org.tenten.bittakotlin.profile.repository.ProfileRepository +import java.time.LocalDateTime + +@Service +class LikeServiceImpl( + private val likeRepository: LikeRepository, + private val profileRepository: ProfileRepository, + private val jobPostRepository: JobPostRepository +) : LikeService { + + @Transactional + override fun addLike(jobPostId: Long, profileId: Long) { + val jobPost = jobPostRepository.findById(jobPostId).orElseThrow { LikeException.NOT_FOUND.get() } + val profile = profileRepository.findById(profileId).orElseThrow { LikeException.NOT_FOUND.get() } + + if (!likeRepository.existsByJobPostAndProfile(jobPost, profile)) { + val like = Like().apply { + this.jobPost = jobPost + this.profile = profile + this.likedAt = LocalDateTime.now() + } + likeRepository.save(like) + } + } + + @Transactional + override fun getLikesForJobPost(jobPostId: Long): List { + val jobPost = jobPostRepository.findById(jobPostId).orElseThrow { RuntimeException() } + return likeRepository.findByJobPost(jobPost).map { like -> + MemberNicknameDTO(like.profile!!.nickname) + } + } + + @Transactional + override fun removeLike(jobPostId: Long, profileId: Long) { + val like = likeRepository.findByJobPostIdAndProfileId(jobPostId, profileId) + .orElseThrow { JobPostException.NOT_FOUND.get() } as Like + + like.jobPost = null + like.profile = null + + likeRepository.delete(like) + } +} + + + diff --git a/src/main/kotlin/org/tenten/bittakotlin/member/dto/MemberNicknameDTO.kt b/src/main/kotlin/org/tenten/bittakotlin/member/dto/MemberNicknameDTO.kt new file mode 100644 index 0000000..323f817 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/member/dto/MemberNicknameDTO.kt @@ -0,0 +1,5 @@ +package org.tenten.bittakotlin.member.dto + +data class MemberNicknameDTO( + val nickname: String +) diff --git a/src/main/kotlin/org/tenten/bittakotlin/profile/entity/Profile.kt b/src/main/kotlin/org/tenten/bittakotlin/profile/entity/Profile.kt index 7d36cb1..a76beed 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/profile/entity/Profile.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/profile/entity/Profile.kt @@ -2,6 +2,9 @@ package org.tenten.bittakotlin.profile.entity import jakarta.persistence.* +import org.tenten.bittakotlin.apply.entity.Apply +import org.tenten.bittakotlin.jobpost.entity.JobPost +import org.tenten.bittakotlin.like.entity.Like import org.tenten.bittakotlin.member.entity.Member import org.tenten.bittakotlin.profile.constant.Job @@ -13,7 +16,7 @@ class Profile( @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id", nullable = false) - val member: Member, + val member: Member? = null, @Column(length = 20, nullable = false) var nickname: String, @@ -29,5 +32,11 @@ class Profile( var job: Job? = null, @Column(columnDefinition = "JSON") - var socialMedia: String? = null + var socialMedia: String? = null, + + @OneToMany(mappedBy = "profile", fetch = FetchType.EAGER, cascade = [CascadeType.REMOVE], orphanRemoval = true) + val apply: List = mutableListOf(), + + @OneToMany(mappedBy = "profile", fetch = FetchType.EAGER, cascade = [CascadeType.REMOVE], orphanRemoval = true) + val like: List = mutableListOf() ) \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/profile/service/ProfileProvider.kt b/src/main/kotlin/org/tenten/bittakotlin/profile/service/ProfileProvider.kt new file mode 100644 index 0000000..f9cb54d --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/profile/service/ProfileProvider.kt @@ -0,0 +1,14 @@ +package org.tenten.bittakotlin.profile.service + +import org.springframework.stereotype.Service +import org.tenten.bittakotlin.profile.entity.Profile +import org.tenten.bittakotlin.profile.repository.ProfileRepository + +@Service +class ProfileProvider( + private val profileRepository: ProfileRepository +) { + fun getById(id: Long): Profile? { + return profileRepository.findById(id).orElse(null) + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/profile/service/ProfileServiceImpl.kt b/src/main/kotlin/org/tenten/bittakotlin/profile/service/ProfileServiceImpl.kt index 044fc4b..164ad24 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/profile/service/ProfileServiceImpl.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/profile/service/ProfileServiceImpl.kt @@ -92,7 +92,7 @@ class ProfileServiceImpl( private fun toDto(profile: Profile): ProfileDTO { return ProfileDTO( - memberId = profile.member.id ?: throw IllegalStateException("Member ID is missing"), + memberId = profile.member!!.id ?: throw IllegalStateException("Member ID is missing"), nickname = profile.nickname, profileUrl = profile.profileUrl, description = profile.description, From 280f680f48be965f562f181db68f95c3b9207fdb Mon Sep 17 00:00:00 2001 From: ghtndl Date: Tue, 5 Nov 2024 09:42:45 +0900 Subject: [PATCH 02/26] Refactor: uri modify --- .../org/tenten/bittakotlin/security/config/SecurityConfig.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/org/tenten/bittakotlin/security/config/SecurityConfig.kt b/src/main/kotlin/org/tenten/bittakotlin/security/config/SecurityConfig.kt index 815d9a2..0e7f010 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/security/config/SecurityConfig.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/security/config/SecurityConfig.kt @@ -95,7 +95,7 @@ class SecurityConfig( http.addFilterBefore(JWTFilter(jwtUtil), LoginFilter::class.java) val loginFilter = LoginFilter(authenticationManager(), jwtUtil, refreshRepository) - loginFilter.setFilterProcessesUrl("/api/member/login") + loginFilter.setFilterProcessesUrl("/api/v1/member/login") http.addFilterAt(loginFilter, UsernamePasswordAuthenticationFilter::class.java) http.addFilterBefore(CustomLogoutFilter(jwtUtil, refreshRepository), LogoutFilter::class.java) From 44a0a7322957910eae4ec79e6ed150b9702d767e Mon Sep 17 00:00:00 2001 From: YooJHyun Date: Tue, 5 Nov 2024 09:58:17 +0900 Subject: [PATCH 03/26] =?UTF-8?q?Feat:=20Calendar=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apply/controller/ApplyController.kt | 7 +++ .../bittakotlin/apply/service/ApplyService.kt | 8 ++++ .../apply/service/ApplyServiceImpl.kt | 46 +++++++++++++++++-- .../controller/EventCalendarController.kt | 22 +++++++++ .../calendar/dto/EventCalendarDTO.kt | 24 ++++++++++ .../calendar/entity/EventCalendar.kt | 29 ++++++++++++ .../repository/EventCalendarRepository.kt | 10 ++++ .../calendar/service/EventCalendarService.kt | 7 +++ .../service/EventCalendarServiceImpl.kt | 31 +++++++++++++ .../controller/JobPostViewController.kt | 1 - .../bittakotlin/jobpost/dto/JobPostDTO.kt | 2 +- .../bittakotlin/jobpost/entity/JobPost.kt | 4 +- 12 files changed, 183 insertions(+), 8 deletions(-) create mode 100644 src/main/kotlin/org/tenten/bittakotlin/calendar/controller/EventCalendarController.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/calendar/dto/EventCalendarDTO.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/calendar/entity/EventCalendar.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/calendar/repository/EventCalendarRepository.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/calendar/service/EventCalendarService.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/calendar/service/EventCalendarServiceImpl.kt diff --git a/src/main/kotlin/org/tenten/bittakotlin/apply/controller/ApplyController.kt b/src/main/kotlin/org/tenten/bittakotlin/apply/controller/ApplyController.kt index acaddb9..3620635 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/apply/controller/ApplyController.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/apply/controller/ApplyController.kt @@ -231,5 +231,12 @@ class ApplyController( applyService.applyStatusUpdate(applyId, applyStatusUpdateDTO.applyStatus!!, profileId) return ResponseEntity.ok("상태가 변경되었습니다") } + + @PostMapping("/calendar") + fun applyToCalendar(@RequestParam jobPostId: Long, @RequestParam profileId: Long): ResponseEntity { + applyService.applyToCalendar(jobPostId, profileId) + return ResponseEntity.ok("캘린더가 등록되었습니다") + } + } diff --git a/src/main/kotlin/org/tenten/bittakotlin/apply/service/ApplyService.kt b/src/main/kotlin/org/tenten/bittakotlin/apply/service/ApplyService.kt index 6619af1..9987ff6 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/apply/service/ApplyService.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/apply/service/ApplyService.kt @@ -1,7 +1,9 @@ package org.tenten.bittakotlin.apply.service import org.tenten.bittakotlin.apply.dto.ApplyDTO +import org.tenten.bittakotlin.apply.entity.Apply import org.tenten.bittakotlin.apply.entity.ApplyStatus +import org.tenten.bittakotlin.calendar.entity.EventCalendar import org.tenten.bittakotlin.profile.entity.Profile interface ApplyService { @@ -20,6 +22,12 @@ interface ApplyService { fun getApplyCount(jobPostId: Long): Long fun applyStatusUpdate(applyId: Long, applyStatus: ApplyStatus, profileId: Long) + + fun setCalendar(apply: Apply?) + + fun getCalendar(profileId: Long?): List? + + fun applyToCalendar(jobPostId: Long?, profileId: Long?) } diff --git a/src/main/kotlin/org/tenten/bittakotlin/apply/service/ApplyServiceImpl.kt b/src/main/kotlin/org/tenten/bittakotlin/apply/service/ApplyServiceImpl.kt index 55dd906..332a01d 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/apply/service/ApplyServiceImpl.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/apply/service/ApplyServiceImpl.kt @@ -8,17 +8,21 @@ import org.tenten.bittakotlin.apply.entity.Apply import org.tenten.bittakotlin.apply.entity.ApplyStatus import org.tenten.bittakotlin.apply.exception.ApplyException import org.tenten.bittakotlin.apply.repository.ApplyRepository +import org.tenten.bittakotlin.calendar.entity.EventCalendar +import org.tenten.bittakotlin.calendar.repository.EventCalendarRepository import org.tenten.bittakotlin.jobpost.exception.JobPostException import org.tenten.bittakotlin.jobpost.repository.JobPostRepository import org.tenten.bittakotlin.jobpost.util.JobPostProvider import org.tenten.bittakotlin.profile.entity.Profile +import org.tenten.bittakotlin.profile.service.ProfileProvider @Service class ApplyServiceImpl( private val applyRepository: ApplyRepository, private val jobPostRepository: JobPostRepository, -// private val memberProvider: MemberProvider, - private val jobPostProvider: JobPostProvider + private val profileProvider: ProfileProvider, + private val jobPostProvider: JobPostProvider, + private val eventCalendarRepository: EventCalendarRepository ) : ApplyService { private val log = LoggerFactory.getLogger(this::class.java) @@ -39,6 +43,8 @@ class ApplyServiceImpl( jobPost!!.plusApplyCount() jobPostRepository.save(jobPost) + addCalendar(apply) + mapOf( "message" to "${apply.profile!!.nickname}님 지원 완료", "data" to entityToDto(apply) @@ -49,6 +55,18 @@ class ApplyServiceImpl( } } + private fun addCalendar(apply: Apply) { + val jobPost = apply.jobPost + val event = EventCalendar( + profile = apply.profile, + title = jobPost!!.title, + startDate = jobPost.startDate, + endDate = jobPost.endDate, + auditionDate = jobPost.auditionDate + ) + eventCalendarRepository.save(event) + } + @Transactional override fun delete(id: Long) { val apply = applyRepository.findById(id).orElseThrow { ApplyException.NOT_FOUND.get() } @@ -79,7 +97,7 @@ class ApplyServiceImpl( } val applies = applyRepository.findAllByJobPost(jobPost) - return applies//.map { entityToDto(it) } + return applies } @Transactional @@ -104,10 +122,30 @@ class ApplyServiceImpl( applyRepository.save(apply) } + override fun setCalendar(apply: Apply?) { + val event = EventCalendar( + profile = apply!!.profile, + title = apply.jobPost!!.title, + startDate = apply.jobPost!!.startDate, + endDate = apply.jobPost!!.endDate, + auditionDate = apply.jobPost!!.auditionDate + ) + eventCalendarRepository.save(event) + } + + override fun getCalendar(profileId: Long?): List? { + return eventCalendarRepository.findAllByProfileId(profileId!!) + } + + @Transactional + override fun applyToCalendar(jobPostId: Long?, profileId: Long?) { + val apply = Apply() + } + private fun dtoToEntity(applyDTO: ApplyDTO): Apply { return Apply( id = applyDTO.id, -// profile = profileProvider.getById(applyDTO.profileId!!), + profile = profileProvider.getById(applyDTO.profileId!!), jobPost = jobPostProvider.getById(applyDTO.jobPostId!!), appliedAt = applyDTO.appliedAt ) diff --git a/src/main/kotlin/org/tenten/bittakotlin/calendar/controller/EventCalendarController.kt b/src/main/kotlin/org/tenten/bittakotlin/calendar/controller/EventCalendarController.kt new file mode 100644 index 0000000..f501eb9 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/calendar/controller/EventCalendarController.kt @@ -0,0 +1,22 @@ +package org.tenten.bittakotlin.calendar.controller + +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import org.tenten.bittakotlin.calendar.dto.EventCalendarDTO +import org.tenten.bittakotlin.calendar.service.EventCalendarService + +@RestController +@RequestMapping("/api/v1/calendar") +class EventCalendarController( + private val eventCalendarService: EventCalendarService +) { + + @GetMapping("/{profileId}") + fun getCalendar(@PathVariable profileId: Long): ResponseEntity> { + val events = eventCalendarService.getEventCalendar(profileId) + return ResponseEntity.ok(events) + } +} diff --git a/src/main/kotlin/org/tenten/bittakotlin/calendar/dto/EventCalendarDTO.kt b/src/main/kotlin/org/tenten/bittakotlin/calendar/dto/EventCalendarDTO.kt new file mode 100644 index 0000000..b8ecac5 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/calendar/dto/EventCalendarDTO.kt @@ -0,0 +1,24 @@ +package org.tenten.bittakotlin.calendar.dto + +import java.time.LocalDate +import java.time.LocalDateTime +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import io.swagger.v3.oas.annotations.media.Schema + +@Schema(title = "캘린더 DTO", description = "회원의 일정을 확인할 때 사용하는 DTO입니다.") +data class EventCalendarDTO( + @Schema(title = "캘린더 ID (PK)", description = "캘린더의 고유 ID 입니다.", example = "1", minimum = "1") + @field:Min(value = 1, message = "ID는 음수가 될 수 없습니다") + val id: Long? = null, + + @Schema(title = "회원 ID", description = "회원의 고유 ID 입니다.", example = "1", minimum = "1") + @field:Min(value = 1, message = "ID는 음수가 될 수 없습니다") + @field:NotNull(message = "회원의 ID가 필요합니다") + val profileId: Long? = null, + + val startDate: LocalDate? = null, + val endDate: LocalDate? = null, + val auditionDate: LocalDateTime? = null, + val title: String? = null +) diff --git a/src/main/kotlin/org/tenten/bittakotlin/calendar/entity/EventCalendar.kt b/src/main/kotlin/org/tenten/bittakotlin/calendar/entity/EventCalendar.kt new file mode 100644 index 0000000..faa7ab3 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/calendar/entity/EventCalendar.kt @@ -0,0 +1,29 @@ +package org.tenten.bittakotlin.calendar.entity + +import jakarta.persistence.* +import org.springframework.data.jpa.domain.support.AuditingEntityListener +import org.tenten.bittakotlin.profile.entity.Profile +import java.time.LocalDate +import java.time.LocalDateTime + +@Entity +@Table(name = "event_calendar") +@EntityListeners(AuditingEntityListener::class) +data class EventCalendar( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long? = null, + + @ManyToOne + @JoinColumn(name = "profileId", nullable = false) + val profile: Profile? = null, + + @Column(nullable = false) + val title: String, + + val startDate: LocalDate? = null, + + val endDate: LocalDate? = null, + + val auditionDate: LocalDateTime? = null +) diff --git a/src/main/kotlin/org/tenten/bittakotlin/calendar/repository/EventCalendarRepository.kt b/src/main/kotlin/org/tenten/bittakotlin/calendar/repository/EventCalendarRepository.kt new file mode 100644 index 0000000..1ea2ba4 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/calendar/repository/EventCalendarRepository.kt @@ -0,0 +1,10 @@ +package org.tenten.bittakotlin.calendar.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import org.tenten.bittakotlin.calendar.entity.EventCalendar + +interface EventCalendarRepository : JpaRepository { + @Query("SELECT c FROM EventCalendar c WHERE c.profile.id = :profileId") + fun findAllByProfileId(profileId: Long): List +} diff --git a/src/main/kotlin/org/tenten/bittakotlin/calendar/service/EventCalendarService.kt b/src/main/kotlin/org/tenten/bittakotlin/calendar/service/EventCalendarService.kt new file mode 100644 index 0000000..b3d7f5a --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/calendar/service/EventCalendarService.kt @@ -0,0 +1,7 @@ +package org.tenten.bittakotlin.calendar.service + +import org.tenten.bittakotlin.calendar.dto.EventCalendarDTO + +interface EventCalendarService { + fun getEventCalendar(profileId: Long): List +} diff --git a/src/main/kotlin/org/tenten/bittakotlin/calendar/service/EventCalendarServiceImpl.kt b/src/main/kotlin/org/tenten/bittakotlin/calendar/service/EventCalendarServiceImpl.kt new file mode 100644 index 0000000..ea73833 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/calendar/service/EventCalendarServiceImpl.kt @@ -0,0 +1,31 @@ +package org.tenten.bittakotlin.calendar.service + +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.tenten.bittakotlin.calendar.dto.EventCalendarDTO +import org.tenten.bittakotlin.calendar.entity.EventCalendar +import org.tenten.bittakotlin.calendar.repository.EventCalendarRepository + +@Service +class EventCalendarServiceImpl( + private val eventCalendarRepository: EventCalendarRepository +) : EventCalendarService { + + private val logger = LoggerFactory.getLogger(EventCalendarServiceImpl::class.java) + + override fun getEventCalendar(profileId: Long): List { + val events = eventCalendarRepository.findAllByProfileId(profileId) + return events.map { entityToDto(it) } + } + + private fun entityToDto(event: EventCalendar): EventCalendarDTO { + return EventCalendarDTO( + id = event.id, + profileId = event.profile!!.id, + startDate = event.startDate, + endDate = event.endDate, + title = event.title, + auditionDate = event.auditionDate + ) + } +} diff --git a/src/main/kotlin/org/tenten/bittakotlin/jobpost/controller/JobPostViewController.kt b/src/main/kotlin/org/tenten/bittakotlin/jobpost/controller/JobPostViewController.kt index 0557268..dd4b733 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/jobpost/controller/JobPostViewController.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/jobpost/controller/JobPostViewController.kt @@ -14,4 +14,3 @@ class JobPostViewController { } } - diff --git a/src/main/kotlin/org/tenten/bittakotlin/jobpost/dto/JobPostDTO.kt b/src/main/kotlin/org/tenten/bittakotlin/jobpost/dto/JobPostDTO.kt index 8d5a651..633936c 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/jobpost/dto/JobPostDTO.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/jobpost/dto/JobPostDTO.kt @@ -53,7 +53,7 @@ data class JobPostDTO( val workCategory: WorkCategory? = null, @Schema(title = "오디션일", description = "오디션을 진행하는 날짜입니다.", example = "2023-09-24") - val auditionDate: LocalDate? = null, + val auditionDate: LocalDateTime? = null, @Schema(title = "촬영 시작일", description = "일이 시작하는 날짜입니다.", example = "2023-09-24") val startDate: LocalDate? = null, diff --git a/src/main/kotlin/org/tenten/bittakotlin/jobpost/entity/JobPost.kt b/src/main/kotlin/org/tenten/bittakotlin/jobpost/entity/JobPost.kt index be0a26d..2ab9338 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/jobpost/entity/JobPost.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/jobpost/entity/JobPost.kt @@ -60,7 +60,7 @@ class JobPost( @Enumerated(EnumType.STRING) var workCategory: WorkCategory? = null, // 작품 카테고리 - val auditionDate: LocalDate? = null, // 오디션 일자 + val auditionDate: LocalDateTime? = null, // 오디션 일자 val startDate: LocalDate? = null, // 촬영 기간 시작일 val endDate: LocalDate? = null, // 촬영 기간 종료일 @@ -78,7 +78,7 @@ class JobPost( val apply: List = mutableListOf(), @OneToMany(mappedBy = "jobPost", fetch = FetchType.EAGER, cascade = [CascadeType.REMOVE], orphanRemoval = true) - val likes: List = mutableListOf() + val like: List = mutableListOf() ) { fun plusApplyCount() { From 15235a6e1d15b9f64e0cc502356cf2e3bd869806 Mon Sep 17 00:00:00 2001 From: deveunhwa Date: Tue, 5 Nov 2024 10:05:49 +0900 Subject: [PATCH 04/26] =?UTF-8?q?Fix:=20Dockerfile=20jdk=20version=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 9ee93cb..35c8f11 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Use an official Gradle image to build the backend -FROM gradle:7.5.1-jdk11 AS build +FROM openjdk:17 # Set working directory WORKDIR /app From 1789b819e9931c3b86a4ba891762e75d76382bcc Mon Sep 17 00:00:00 2001 From: deveunhwa Date: Tue, 5 Nov 2024 10:09:38 +0900 Subject: [PATCH 05/26] =?UTF-8?q?Fix:=20Dockerfile=20=EC=9B=90=EC=83=81?= =?UTF-8?q?=EB=B3=B5=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/Dockerfile b/Dockerfile index 35c8f11..c4469d1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,8 @@ -# Use an official Gradle image to build the backend +# Use OpenJDK 17 image FROM openjdk:17 -# Set working directory -WORKDIR /app - -# Copy and build the application -COPY . . -RUN gradle build -x test - -# Use a lightweight JRE image for runtime -FROM openjdk:11-jre-slim -COPY --from=build /app/build/libs/*.jar /app/app.jar - -EXPOSE 8080 -CMD ["java", "-jar", "-Dspring.profiles.active=prod", "/app.jar"] +# Copy the built jar file +COPY build/libs/*.jar app.jar +# Set the entry point to run the application +ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=prod", "/app.jar"] From 39127c0133411db4f5c1b299c25069543edd5128 Mon Sep 17 00:00:00 2001 From: ghtndl Date: Tue, 5 Nov 2024 10:30:57 +0900 Subject: [PATCH 06/26] Refactor: member --- .../org/tenten/bittakotlin/security/config/SecurityConfig.kt | 4 ++-- .../org/tenten/bittakotlin/security/jwt/CustomLogoutFilter.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/org/tenten/bittakotlin/security/config/SecurityConfig.kt b/src/main/kotlin/org/tenten/bittakotlin/security/config/SecurityConfig.kt index 0e7f010..fb8f9aa 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/security/config/SecurityConfig.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/security/config/SecurityConfig.kt @@ -85,8 +85,8 @@ class SecurityConfig( "/job-post/**", "/api/v1/like/**").hasRole("USER") - .requestMatchers(HttpMethod.DELETE,"/api/member/{id}").authenticated() - .requestMatchers(HttpMethod.PUT,"/api/member/{id}").authenticated() + .requestMatchers(HttpMethod.DELETE,"/api/v1/member/{id}").authenticated() + .requestMatchers(HttpMethod.PUT,"/api/v1/member/{id}").authenticated() .requestMatchers("/api/v1/chat/**").authenticated() .anyRequest().authenticated() diff --git a/src/main/kotlin/org/tenten/bittakotlin/security/jwt/CustomLogoutFilter.kt b/src/main/kotlin/org/tenten/bittakotlin/security/jwt/CustomLogoutFilter.kt index 5710965..470f716 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/security/jwt/CustomLogoutFilter.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/security/jwt/CustomLogoutFilter.kt @@ -31,7 +31,7 @@ class CustomLogoutFilter( val requestUri = request.requestURI logger.info("Incoming request URI: $requestUri") - if (!requestUri.matches("^/api/member/logout$".toRegex())) { + if (!requestUri.matches("^/api/v1/member/logout$".toRegex())) { logger.warn("Invalid logout request URI: $requestUri") filterChain.doFilter(request, response) return From 5b3434cc45c503f0bb8a9c06c311a432dd27a337 Mon Sep 17 00:00:00 2001 From: deveunhwa Date: Tue, 5 Nov 2024 10:58:25 +0900 Subject: [PATCH 07/26] =?UTF-8?q?Fix:=20workflow=20CICD=20=EB=B3=B4?= =?UTF-8?q?=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/CICD.yml | 112 ++++++++++++++++++++----------------- 1 file changed, 61 insertions(+), 51 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 52e4c5c..a67bf14 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -14,43 +14,45 @@ jobs: contents: read steps: - - uses: actions/checkout@v4 - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - - # application-prod.properties 암호화 - - name: add secrets into properties - run: | - echo "${{ secrets.APPLICATION_PROD }}" | base64 --decode >> ./src/main/resources/application-prod.properties - - # 환경 설정 - - name: Setup Gradle - uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 - - # 실행 권한 부여 - - name: Grant execute permission for Gradlew - run: chmod +x gradlew - - # 프로젝트 빌드 (clean bootJar로 빌드만 수행) - - name: Build with Gradle Wrapper - run: ./gradlew build - - # Docker Hub 로그인 - - name: Log in to Docker Hub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - # Docker 이미지 빌드 및 푸시 - - name: Build and push Docker image - run: | - docker build -t ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest . - docker push ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest - + - uses: actions/checkout@v4 + + # JDK 설정 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + # application-prod.properties 암호화 + - name: Add secrets into properties + run: | + echo "${{ secrets.APPLICATION_PROD }}" | base64 --decode > ./src/main/resources/application-prod.properties + + # Gradle 설정 + - name: Setup Gradle + uses: gradle/setup-gradle@v2 + + # Gradlew 실행 권한 부여 + - name: Grant execute permission for Gradlew + run: chmod +x gradlew + + # 프로젝트 빌드 + - name: Build with Gradle Wrapper + run: ./gradlew build + + # Docker Hub 로그인 + - name: Log in to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + # Docker 이미지 빌드 및 푸시 + - name: Build and push Docker image + run: | + docker build -t ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest . + docker push ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest + deploy: if: github.repository == 'prgrms-be-devcourse/NBB1_2_3_Team10' && github.ref == 'refs/heads/release' needs: build @@ -60,22 +62,30 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' # EC2 서버 배포 - name: Deploy to EC2 uses: appleboy/ssh-action@v0.1.6 with: - host: ${{ secrets.EC2_HOST }} - username: ${{ secrets.EC2_USER }} - key: ${{ secrets.EC2_SSH_KEY }} - script: | - echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin - docker pull ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest - docker stop my-container || true - docker rm my-container || true - docker run -d --name my-container -p 80:80 ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USER }} + key: ${{ secrets.EC2_SSH_KEY }} + script: | + # Docker 로그인 및 최신 이미지 가져오기 + echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin + docker pull ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest + + # blue/green 컨테이너 전환 + CURRENT_CONTAINER=$(docker ps --filter "name=backend-blue" -q) + TARGET_CONTAINER="backend-green" + [ -z "$CURRENT_CONTAINER" ] && TARGET_CONTAINER="backend-blue" + + # 새 컨테이너 실행 + docker stop $TARGET_CONTAINER || true + docker rm $TARGET_CONTAINER || true + docker run -d --name $TARGET_CONTAINER -p 8080:80 \ + -e NODE_ENV=production \ + ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest + + # Nginx 리로드하여 트래픽 전환 + sudo systemctl reload nginx From 75bd6ea3e8f106565aa3fa2853c0749bff59c6cc Mon Sep 17 00:00:00 2001 From: deveunhwa Date: Tue, 5 Nov 2024 11:12:32 +0900 Subject: [PATCH 08/26] =?UTF-8?q?Fix:=20gradle=20=EC=B0=B8=EC=A1=B0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/CICD.yml | 128 ++++++++++++++++++++----------------- 1 file changed, 69 insertions(+), 59 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index a67bf14..f371222 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -14,44 +14,42 @@ jobs: contents: read steps: - - uses: actions/checkout@v4 - - # JDK 설정 - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - - # application-prod.properties 암호화 - - name: Add secrets into properties - run: | - echo "${{ secrets.APPLICATION_PROD }}" | base64 --decode > ./src/main/resources/application-prod.properties - - # Gradle 설정 - - name: Setup Gradle - uses: gradle/setup-gradle@v2 - - # Gradlew 실행 권한 부여 - - name: Grant execute permission for Gradlew - run: chmod +x gradlew - - # 프로젝트 빌드 - - name: Build with Gradle Wrapper - run: ./gradlew build - - # Docker Hub 로그인 - - name: Log in to Docker Hub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - # Docker 이미지 빌드 및 푸시 - - name: Build and push Docker image - run: | - docker build -t ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest . - docker push ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + # application-prod.properties 암호화 + - name: add secrets into properties + run: | + echo "${{ secrets.APPLICATION_PROD }}" | base64 --decode >> ./src/main/resources/application-prod.properties + + # 환경 설정 + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + + # 실행 권한 부여 + - name: Grant execute permission for Gradlew + run: chmod +x gradlew + + # 프로젝트 빌드 (clean bootJar로 빌드만 수행) + - name: Build with Gradle Wrapper + run: ./gradlew build + + # Docker Hub 로그인 + - name: Log in to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + # Docker 이미지 빌드 및 푸시 + - name: Build and push Docker image + run: | + docker build -t ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest . + docker push ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest deploy: if: github.repository == 'prgrms-be-devcourse/NBB1_2_3_Team10' && github.ref == 'refs/heads/release' @@ -62,30 +60,42 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' # EC2 서버 배포 - name: Deploy to EC2 uses: appleboy/ssh-action@v0.1.6 with: - host: ${{ secrets.EC2_HOST }} - username: ${{ secrets.EC2_USER }} - key: ${{ secrets.EC2_SSH_KEY }} - script: | - # Docker 로그인 및 최신 이미지 가져오기 - echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin - docker pull ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest - - # blue/green 컨테이너 전환 - CURRENT_CONTAINER=$(docker ps --filter "name=backend-blue" -q) + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USER }} + key: ${{ secrets.EC2_SSH_KEY }} + script: | + echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin + docker pull ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest + docker stop my-container || true + docker rm my-container || true + docker run -d --name my-container -p 80:80 ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest + + # 현재 활성화된 blue/green 컨테이너 확인 + CURRENT_CONTAINER=$(docker ps --filter "name=backend-blue" -q) + if [ -n "$CURRENT_CONTAINER" ]; then + # blue가 활성화된 경우 green으로 전환 TARGET_CONTAINER="backend-green" - [ -z "$CURRENT_CONTAINER" ] && TARGET_CONTAINER="backend-blue" - - # 새 컨테이너 실행 - docker stop $TARGET_CONTAINER || true - docker rm $TARGET_CONTAINER || true - docker run -d --name $TARGET_CONTAINER -p 8080:80 \ - -e NODE_ENV=production \ - ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest - - # Nginx 리로드하여 트래픽 전환 - sudo systemctl reload nginx + else + # green이 활성화된 경우 blue로 전환 + TARGET_CONTAINER="backend-blue" + fi + + # 새 컨테이너 시작 + docker stop $TARGET_CONTAINER || true + docker rm $TARGET_CONTAINER || true + docker run -d --name $TARGET_CONTAINER -p 8080:80 \ + -e NODE_ENV=production \ + ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest + + # Nginx 리로드하여 트래픽 전환 + sudo systemctl reload nginx \ No newline at end of file From d8f3b4673f9fa8c2a6de653be846bf5cc109d169 Mon Sep 17 00:00:00 2001 From: deveunhwa Date: Tue, 5 Nov 2024 11:23:37 +0900 Subject: [PATCH 09/26] =?UTF-8?q?Fix:=20workflows=20CICD=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/CICD.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index f371222..c8aa19b 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -24,12 +24,12 @@ jobs: # application-prod.properties 암호화 - name: add secrets into properties run: | - echo "${{ secrets.APPLICATION_PROD }}" | base64 --decode >> ./src/main/resources/application-prod.properties + echo "${{ secrets.APPLICATION_PROD }}" | base64 --decode > ./src/main/resources/application-prod.properties # 환경 설정 - name: Setup Gradle - uses: gradle/gradle-build-action@v2 - + uses: gradle/gradle-build-action + # 실행 권한 부여 - name: Grant execute permission for Gradlew run: chmod +x gradlew @@ -78,15 +78,13 @@ jobs: docker pull ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest docker stop my-container || true docker rm my-container || true - docker run -d --name my-container -p 80:80 ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest + docker run -d --name my-container -p 8080:80 ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest # 현재 활성화된 blue/green 컨테이너 확인 CURRENT_CONTAINER=$(docker ps --filter "name=backend-blue" -q) if [ -n "$CURRENT_CONTAINER" ]; then - # blue가 활성화된 경우 green으로 전환 TARGET_CONTAINER="backend-green" else - # green이 활성화된 경우 blue로 전환 TARGET_CONTAINER="backend-blue" fi From 512f979a17010c029235bacb93bc30ee627cd534 Mon Sep 17 00:00:00 2001 From: deveunhwa Date: Tue, 5 Nov 2024 11:33:20 +0900 Subject: [PATCH 10/26] =?UTF-8?q?Fix:=20gradle=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/CICD.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index c8aa19b..52bc78c 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -28,7 +28,10 @@ jobs: # 환경 설정 - name: Setup Gradle - uses: gradle/gradle-build-action + uses: gradle/gradle-build-action@v2 + with: + gradle-version: '7.5' + cache-enabled: true # 실행 권한 부여 - name: Grant execute permission for Gradlew From 6ff872a0abe0dabd4f06c4d74bfacda0d7827924 Mon Sep 17 00:00:00 2001 From: eunhwa shin Date: Tue, 5 Nov 2024 11:51:32 +0900 Subject: [PATCH 11/26] =?UTF-8?q?Fix:=20workflows=20CICD=20=EB=B3=B5?= =?UTF-8?q?=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/CICD.yml | 129 +++++++++++++++++-------------------- 1 file changed, 59 insertions(+), 70 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 52bc78c..a67bf14 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -14,45 +14,44 @@ jobs: contents: read steps: - - uses: actions/checkout@v4 - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - - # application-prod.properties 암호화 - - name: add secrets into properties - run: | - echo "${{ secrets.APPLICATION_PROD }}" | base64 --decode > ./src/main/resources/application-prod.properties - - # 환경 설정 - - name: Setup Gradle - uses: gradle/gradle-build-action@v2 - with: - gradle-version: '7.5' - cache-enabled: true - - # 실행 권한 부여 - - name: Grant execute permission for Gradlew - run: chmod +x gradlew - - # 프로젝트 빌드 (clean bootJar로 빌드만 수행) - - name: Build with Gradle Wrapper - run: ./gradlew build - - # Docker Hub 로그인 - - name: Log in to Docker Hub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - # Docker 이미지 빌드 및 푸시 - - name: Build and push Docker image - run: | - docker build -t ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest . - docker push ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest + - uses: actions/checkout@v4 + + # JDK 설정 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + # application-prod.properties 암호화 + - name: Add secrets into properties + run: | + echo "${{ secrets.APPLICATION_PROD }}" | base64 --decode > ./src/main/resources/application-prod.properties + + # Gradle 설정 + - name: Setup Gradle + uses: gradle/setup-gradle@v2 + + # Gradlew 실행 권한 부여 + - name: Grant execute permission for Gradlew + run: chmod +x gradlew + + # 프로젝트 빌드 + - name: Build with Gradle Wrapper + run: ./gradlew build + + # Docker Hub 로그인 + - name: Log in to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + # Docker 이미지 빌드 및 푸시 + - name: Build and push Docker image + run: | + docker build -t ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest . + docker push ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest deploy: if: github.repository == 'prgrms-be-devcourse/NBB1_2_3_Team10' && github.ref == 'refs/heads/release' @@ -63,40 +62,30 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' # EC2 서버 배포 - name: Deploy to EC2 uses: appleboy/ssh-action@v0.1.6 with: - host: ${{ secrets.EC2_HOST }} - username: ${{ secrets.EC2_USER }} - key: ${{ secrets.EC2_SSH_KEY }} - script: | - echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin - docker pull ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest - docker stop my-container || true - docker rm my-container || true - docker run -d --name my-container -p 8080:80 ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest - - # 현재 활성화된 blue/green 컨테이너 확인 - CURRENT_CONTAINER=$(docker ps --filter "name=backend-blue" -q) - if [ -n "$CURRENT_CONTAINER" ]; then + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USER }} + key: ${{ secrets.EC2_SSH_KEY }} + script: | + # Docker 로그인 및 최신 이미지 가져오기 + echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin + docker pull ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest + + # blue/green 컨테이너 전환 + CURRENT_CONTAINER=$(docker ps --filter "name=backend-blue" -q) TARGET_CONTAINER="backend-green" - else - TARGET_CONTAINER="backend-blue" - fi - - # 새 컨테이너 시작 - docker stop $TARGET_CONTAINER || true - docker rm $TARGET_CONTAINER || true - docker run -d --name $TARGET_CONTAINER -p 8080:80 \ - -e NODE_ENV=production \ - ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest - - # Nginx 리로드하여 트래픽 전환 - sudo systemctl reload nginx \ No newline at end of file + [ -z "$CURRENT_CONTAINER" ] && TARGET_CONTAINER="backend-blue" + + # 새 컨테이너 실행 + docker stop $TARGET_CONTAINER || true + docker rm $TARGET_CONTAINER || true + docker run -d --name $TARGET_CONTAINER -p 8080:80 \ + -e NODE_ENV=production \ + ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest + + # Nginx 리로드하여 트래픽 전환 + sudo systemctl reload nginx From 8496775ad9f8a2a43bde6177b507a27cc7792297 Mon Sep 17 00:00:00 2001 From: deveunhwa Date: Tue, 5 Nov 2024 12:31:55 +0900 Subject: [PATCH 12/26] =?UTF-8?q?Fix:=20workflow=20CI/CD=20=EC=8B=A4?= =?UTF-8?q?=ED=96=89=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/CICD.yml | 131 +++++++++++++++++-------------------- 1 file changed, 60 insertions(+), 71 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 52bc78c..260f69c 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -1,4 +1,4 @@ -name: Java & React CI/CD +name: Java CI/CD on: push: @@ -14,45 +14,44 @@ jobs: contents: read steps: - - uses: actions/checkout@v4 - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - - # application-prod.properties 암호화 - - name: add secrets into properties - run: | - echo "${{ secrets.APPLICATION_PROD }}" | base64 --decode > ./src/main/resources/application-prod.properties - - # 환경 설정 - - name: Setup Gradle - uses: gradle/gradle-build-action@v2 - with: - gradle-version: '7.5' - cache-enabled: true - - # 실행 권한 부여 - - name: Grant execute permission for Gradlew - run: chmod +x gradlew - - # 프로젝트 빌드 (clean bootJar로 빌드만 수행) - - name: Build with Gradle Wrapper - run: ./gradlew build - - # Docker Hub 로그인 - - name: Log in to Docker Hub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - # Docker 이미지 빌드 및 푸시 - - name: Build and push Docker image - run: | - docker build -t ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest . - docker push ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest + - uses: actions/checkout@v4 + + # JDK 설정 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + # application-prod.properties 암호화 + - name: Add secrets into properties + run: | + echo "${{ secrets.APPLICATION_PROD }}" | base64 --decode > ./src/main/resources/application-prod.properties + + # Gradle 설정 + - name: Setup Gradle + uses: gradle/setup-gradle@v2 + + # Gradlew 실행 권한 부여 + - name: Grant execute permission for Gradlew + run: chmod +x gradlew + + # 프로젝트 빌드 + - name: Build with Gradle Wrapper + run: ./gradlew build + + # Docker Hub 로그인 + - name: Log in to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + # Docker 이미지 빌드 및 푸시 + - name: Build and push Docker image + run: | + docker build -t ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest . + docker push ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest deploy: if: github.repository == 'prgrms-be-devcourse/NBB1_2_3_Team10' && github.ref == 'refs/heads/release' @@ -63,40 +62,30 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' # EC2 서버 배포 - name: Deploy to EC2 uses: appleboy/ssh-action@v0.1.6 with: - host: ${{ secrets.EC2_HOST }} - username: ${{ secrets.EC2_USER }} - key: ${{ secrets.EC2_SSH_KEY }} - script: | - echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin - docker pull ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest - docker stop my-container || true - docker rm my-container || true - docker run -d --name my-container -p 8080:80 ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest - - # 현재 활성화된 blue/green 컨테이너 확인 - CURRENT_CONTAINER=$(docker ps --filter "name=backend-blue" -q) - if [ -n "$CURRENT_CONTAINER" ]; then + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USER }} + key: ${{ secrets.EC2_SSH_KEY }} + script: | + # Docker 로그인 및 최신 이미지 가져오기 + echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin + docker pull ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest + + # blue/green 컨테이너 전환 + CURRENT_CONTAINER=$(docker ps --filter "name=backend-blue" -q) TARGET_CONTAINER="backend-green" - else - TARGET_CONTAINER="backend-blue" - fi - - # 새 컨테이너 시작 - docker stop $TARGET_CONTAINER || true - docker rm $TARGET_CONTAINER || true - docker run -d --name $TARGET_CONTAINER -p 8080:80 \ - -e NODE_ENV=production \ - ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest - - # Nginx 리로드하여 트래픽 전환 - sudo systemctl reload nginx \ No newline at end of file + [ -z "$CURRENT_CONTAINER" ] && TARGET_CONTAINER="backend-blue" + + # 새 컨테이너 실행 + docker stop $TARGET_CONTAINER || true + docker rm $TARGET_CONTAINER || true + docker run -d --name $TARGET_CONTAINER -p 8080:80 \ + -e NODE_ENV=production \ + ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest + + # Nginx 리로드하여 트래픽 전환 + sudo systemctl reload nginx \ No newline at end of file From 26296b4d9e74aca72ae25cd3e736923a67597e3a Mon Sep 17 00:00:00 2001 From: deveunhwa Date: Tue, 5 Nov 2024 12:37:35 +0900 Subject: [PATCH 13/26] =?UTF-8?q?Fix:=20workflows=20CI/CD=20Gradle=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/CICD.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 40aca1c..2cfbe8a 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -30,7 +30,7 @@ jobs: # Gradle 설정 - name: Setup Gradle - uses: gradle/setup-gradle@v2 + uses: gradle/gradle-build-action@v2 # Gradlew 실행 권한 부여 - name: Grant execute permission for Gradlew From 4307b213107da435b10d95ab16c62fa7806cdee8 Mon Sep 17 00:00:00 2001 From: ghtndl Date: Tue, 5 Nov 2024 15:44:36 +0900 Subject: [PATCH 14/26] Feat: Swagger Setting --- .../global/config/SwaggerConfig.kt | 21 +++++++++++++++++++ .../security/config/SecurityConfig.kt | 8 +++++++ 2 files changed, 29 insertions(+) create mode 100644 src/main/kotlin/org/tenten/bittakotlin/global/config/SwaggerConfig.kt diff --git a/src/main/kotlin/org/tenten/bittakotlin/global/config/SwaggerConfig.kt b/src/main/kotlin/org/tenten/bittakotlin/global/config/SwaggerConfig.kt new file mode 100644 index 0000000..f44ec1d --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/global/config/SwaggerConfig.kt @@ -0,0 +1,21 @@ +package org.tenten.bittakotlin.global.config + +import io.swagger.v3.oas.annotations.OpenAPIDefinition +import io.swagger.v3.oas.models.OpenAPI +import io.swagger.v3.oas.models.info.Info +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@OpenAPIDefinition +@Configuration +class SwaggerConfig { + + @Bean + fun openAPI(): OpenAPI { + return OpenAPI().info( + Info().title("BitTa Kotlin") + .version("1.0") + .description("Bitta Kotlin") + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/security/config/SecurityConfig.kt b/src/main/kotlin/org/tenten/bittakotlin/security/config/SecurityConfig.kt index fb8f9aa..beee2e0 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/security/config/SecurityConfig.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/security/config/SecurityConfig.kt @@ -78,6 +78,14 @@ class SecurityConfig( "/member/join", "/api/v1/member/reissue").permitAll() + .requestMatchers( + "/swagger", + "/swagger-ui.html", + "/swagger-ui/**", + "/api-docs", + "/api-docs/**", + "/v3/api-docs/**").permitAll() + .requestMatchers( "/api/v1/member/{id}/**", "member/{id}/**", From 81a8771937be2a4faccfeb6a3fda4bb919340784 Mon Sep 17 00:00:00 2001 From: Jae Hyung Choi Date: Tue, 5 Nov 2024 16:03:52 +0900 Subject: [PATCH 15/26] =?UTF-8?q?Revert=20"Feat:=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=EC=88=98/=EC=A2=8B=EC=95=84=EC=9A=94=20=EC=8B=9C=EC=8A=A4?= =?UTF-8?q?=ED=85=9C=20=EA=B5=AC=ED=98=84"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../like/controller/LikeController.kt | 24 ---------- .../feedInteraction/like/dto/LikeDTO.kt | 7 --- .../feedInteraction/like/entity/Like.kt | 26 ----------- .../like/repository/LikeRepository.kt | 13 ------ .../like/service/LikeService.kt | 8 ---- .../like/service/LikeServiceImpl.kt | 46 ------------------- .../controller/ViewCountController.kt | 25 ---------- .../viewCount/dto/ViewCountDTO.kt | 10 ---- .../viewCount/entity/ViewCount.kt | 23 ---------- .../repository/ViewCountRepository.kt | 10 ---- .../viewCount/service/ViewCountService.kt | 8 ---- .../viewCount/service/ViewCountServiceImpl.kt | 45 ------------------ .../profile/controller/ProfileController.kt | 2 - .../bittakotlin/profile/entity/Profile.kt | 1 + 14 files changed, 1 insertion(+), 247 deletions(-) delete mode 100644 src/main/kotlin/org/tenten/bittakotlin/feedInteraction/like/controller/LikeController.kt delete mode 100644 src/main/kotlin/org/tenten/bittakotlin/feedInteraction/like/dto/LikeDTO.kt delete mode 100644 src/main/kotlin/org/tenten/bittakotlin/feedInteraction/like/entity/Like.kt delete mode 100644 src/main/kotlin/org/tenten/bittakotlin/feedInteraction/like/repository/LikeRepository.kt delete mode 100644 src/main/kotlin/org/tenten/bittakotlin/feedInteraction/like/service/LikeService.kt delete mode 100644 src/main/kotlin/org/tenten/bittakotlin/feedInteraction/like/service/LikeServiceImpl.kt delete mode 100644 src/main/kotlin/org/tenten/bittakotlin/feedInteraction/viewCount/controller/ViewCountController.kt delete mode 100644 src/main/kotlin/org/tenten/bittakotlin/feedInteraction/viewCount/dto/ViewCountDTO.kt delete mode 100644 src/main/kotlin/org/tenten/bittakotlin/feedInteraction/viewCount/entity/ViewCount.kt delete mode 100644 src/main/kotlin/org/tenten/bittakotlin/feedInteraction/viewCount/repository/ViewCountRepository.kt delete mode 100644 src/main/kotlin/org/tenten/bittakotlin/feedInteraction/viewCount/service/ViewCountService.kt delete mode 100644 src/main/kotlin/org/tenten/bittakotlin/feedInteraction/viewCount/service/ViewCountServiceImpl.kt diff --git a/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/like/controller/LikeController.kt b/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/like/controller/LikeController.kt deleted file mode 100644 index caedfcd..0000000 --- a/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/like/controller/LikeController.kt +++ /dev/null @@ -1,24 +0,0 @@ -package org.tenten.bittakotlin.feedInteraction.like.controller - -import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.* -import org.tenten.bittakotlin.feedInteraction.like.dto.LikeDTO -import org.tenten.bittakotlin.feedInteraction.like.service.LikeService - - -@RestController -@RequestMapping("/api/v1/feed/like") -class LikeController(private val likeService: LikeService) { - - @PostMapping("/{feedId}") - fun toggleLike(@PathVariable feedId: Long, @RequestParam profileId: Long): ResponseEntity { - val likeDTO = likeService.toggleLike(feedId, profileId) - return ResponseEntity.ok(likeDTO) - } - - @GetMapping("/{feedId}/count") - fun getLikeCount(@PathVariable feedId: Long): ResponseEntity { - val likeCount = likeService.getLikeCount(feedId) - return ResponseEntity.ok(likeCount) - } -} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/like/dto/LikeDTO.kt b/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/like/dto/LikeDTO.kt deleted file mode 100644 index ee77b21..0000000 --- a/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/like/dto/LikeDTO.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.tenten.bittakotlin.feedInteraction.like.dto - -data class LikeDTO( - val feedId: Long?, - val profileId: Long?, - val isLiked: Boolean -) \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/like/entity/Like.kt b/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/like/entity/Like.kt deleted file mode 100644 index 35581cc..0000000 --- a/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/like/entity/Like.kt +++ /dev/null @@ -1,26 +0,0 @@ -package org.tenten.bittakotlin.feedInteraction.like.entity - -import jakarta.persistence.* -import lombok.AllArgsConstructor -import lombok.Builder -import lombok.Data -import lombok.NoArgsConstructor -import org.tenten.bittakotlin.feed.entity.Feed -import org.tenten.bittakotlin.profile.entity.Profile - -data class Like( - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - val id: Long? = null, - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "feed_id", nullable = false) - var feed: Feed, - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "profile_id", nullable = false) - var profile: Profile, - - @Column(nullable = false) - var liked: Boolean = false -) \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/like/repository/LikeRepository.kt b/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/like/repository/LikeRepository.kt deleted file mode 100644 index 723ea7a..0000000 --- a/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/like/repository/LikeRepository.kt +++ /dev/null @@ -1,13 +0,0 @@ -package org.tenten.bittakotlin.feedInteraction.like.repository - -import org.springframework.data.jpa.repository.JpaRepository -import org.tenten.bittakotlin.feed.entity.Feed -import org.tenten.bittakotlin.feedInteraction.like.entity.Like -import org.tenten.bittakotlin.profile.entity.Profile -import java.util.* - - -interface LikeRepository : JpaRepository { - fun findByFeedAndProfile(feed: Feed, profile: Profile): Optional - fun countByFeedAndLikedTrue(feed: Feed): Long -} diff --git a/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/like/service/LikeService.kt b/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/like/service/LikeService.kt deleted file mode 100644 index 634fcf5..0000000 --- a/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/like/service/LikeService.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.tenten.bittakotlin.feedInteraction.like.service - -import org.tenten.bittakotlin.feedInteraction.like.dto.LikeDTO - -interface LikeService { - fun toggleLike(feedId: Long, profileId: Long): LikeDTO - fun getLikeCount(feedId: Long): Long -} diff --git a/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/like/service/LikeServiceImpl.kt b/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/like/service/LikeServiceImpl.kt deleted file mode 100644 index 508919c..0000000 --- a/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/like/service/LikeServiceImpl.kt +++ /dev/null @@ -1,46 +0,0 @@ -package org.tenten.bittakotlin.feedInteraction.like.service - -import jakarta.persistence.EntityNotFoundException -import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional -import org.tenten.bittakotlin.feed.repository.FeedRepository -import org.tenten.bittakotlin.feedInteraction.like.dto.LikeDTO -import org.tenten.bittakotlin.feedInteraction.like.entity.Like -import org.tenten.bittakotlin.feedInteraction.like.repository.LikeRepository -import org.tenten.bittakotlin.profile.repository.ProfileRepository - - -@Service -class LikeServiceImpl( - private val likeRepository: LikeRepository, - private val feedRepository: FeedRepository, - private val profileRepository: ProfileRepository -) : LikeService { - - @Transactional - override fun toggleLike(feedId: Long, profileId: Long): LikeDTO { - val feed = feedRepository.findById(feedId) - .orElseThrow { EntityNotFoundException("Feed not found for id: $feedId") } - val profile = profileRepository.findById(profileId) - .orElseThrow { EntityNotFoundException("Profile not found for id: $profileId") } - - val like = likeRepository.findByFeedAndProfile(feed, profile).orElseGet { - val newLike = Like(feed = feed, profile = profile, liked = true) - likeRepository.save(newLike) - newLike - } - - like.liked = !like.liked - likeRepository.save(like) - - return LikeDTO(feed.id, profile.id, like.liked) - } - - @Transactional(readOnly = true) - override fun getLikeCount(feedId: Long): Long { - val feed = feedRepository.findById(feedId) - .orElseThrow { EntityNotFoundException("Feed not found for id: $feedId") } - - return likeRepository.countByFeedAndLikedTrue(feed) - } -} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/viewCount/controller/ViewCountController.kt b/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/viewCount/controller/ViewCountController.kt deleted file mode 100644 index b3999de..0000000 --- a/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/viewCount/controller/ViewCountController.kt +++ /dev/null @@ -1,25 +0,0 @@ -package org.tenten.bittakotlin.feedInteraction.viewCount.controller - -import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.* -import org.tenten.bittakotlin.feedInteraction.viewCount.dto.ViewCountDTO -import org.tenten.bittakotlin.feedInteraction.viewCount.service.ViewCountService - - - -@RestController -@RequestMapping("/api/v1/feed/view") -class ViewCountController(private val viewCountService: ViewCountService) { - - @PostMapping("/{feedId}") - fun addView(@PathVariable feedId: Long): ResponseEntity { - val viewCountDTO = viewCountService.addView(feedId) - return ResponseEntity.ok(viewCountDTO) - } - - @GetMapping("/{feedId}/count") - fun getViewCount(@PathVariable feedId: Long): ResponseEntity { - val viewCountDTO = viewCountService.getViewCount(feedId) - return ResponseEntity.ok(viewCountDTO) - } -} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/viewCount/dto/ViewCountDTO.kt b/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/viewCount/dto/ViewCountDTO.kt deleted file mode 100644 index 9b60087..0000000 --- a/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/viewCount/dto/ViewCountDTO.kt +++ /dev/null @@ -1,10 +0,0 @@ -package org.tenten.bittakotlin.feedInteraction.viewCount.dto - -import lombok.Getter -import lombok.Setter - - -data class ViewCountDTO( - val feedId: Long?, - val viewCount: Long -) \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/viewCount/entity/ViewCount.kt b/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/viewCount/entity/ViewCount.kt deleted file mode 100644 index 7f7a1f8..0000000 --- a/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/viewCount/entity/ViewCount.kt +++ /dev/null @@ -1,23 +0,0 @@ -package org.tenten.bittakotlin.feedInteraction.viewCount.entity - -import jakarta.persistence.* -import lombok.AllArgsConstructor -import lombok.Builder -import lombok.Data -import lombok.NoArgsConstructor -import org.tenten.bittakotlin.feed.entity.Feed - - -@Entity -class ViewCount( - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - val id: Long = 0L, - - @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "feed_id", nullable = false) - var feed: Feed, - - @Column(name = "count", nullable = false) - var count: Long = 0L -) \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/viewCount/repository/ViewCountRepository.kt b/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/viewCount/repository/ViewCountRepository.kt deleted file mode 100644 index 83c7405..0000000 --- a/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/viewCount/repository/ViewCountRepository.kt +++ /dev/null @@ -1,10 +0,0 @@ -package org.tenten.bittakotlin.feedInteraction.viewCount.repository - -import org.springframework.data.jpa.repository.JpaRepository -import org.tenten.bittakotlin.feed.entity.Feed -import org.tenten.bittakotlin.feedInteraction.viewCount.entity.ViewCount -import java.util.* - -interface ViewCountRepository : JpaRepository { - fun findByFeed(feed: Feed): Optional -} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/viewCount/service/ViewCountService.kt b/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/viewCount/service/ViewCountService.kt deleted file mode 100644 index 200395f..0000000 --- a/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/viewCount/service/ViewCountService.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.tenten.bittakotlin.feedInteraction.viewCount.service - -import org.tenten.bittakotlin.feedInteraction.viewCount.dto.ViewCountDTO - -interface ViewCountService { - fun addView(feedId: Long): ViewCountDTO - fun getViewCount(feedId: Long): ViewCountDTO -} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/viewCount/service/ViewCountServiceImpl.kt b/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/viewCount/service/ViewCountServiceImpl.kt deleted file mode 100644 index 0df3825..0000000 --- a/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/viewCount/service/ViewCountServiceImpl.kt +++ /dev/null @@ -1,45 +0,0 @@ -package org.tenten.bittakotlin.feedInteraction.viewCount.service - -import jakarta.persistence.EntityNotFoundException -import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional -import org.tenten.bittakotlin.feed.repository.FeedRepository -import org.tenten.bittakotlin.feedInteraction.viewCount.dto.ViewCountDTO -import org.tenten.bittakotlin.feedInteraction.viewCount.entity.ViewCount -import org.tenten.bittakotlin.feedInteraction.viewCount.repository.ViewCountRepository - - -@Service -class ViewCountServiceImpl( - private val viewCountRepository: ViewCountRepository, - private val feedRepository: FeedRepository -) : ViewCountService { - - @Transactional - override fun addView(feedId: Long): ViewCountDTO { - val feed = feedRepository.findById(feedId) - .orElseThrow { EntityNotFoundException("Feed not found for id: $feedId") } - - val viewCount = viewCountRepository.findByFeed(feed).orElseGet { - val newViewCount = ViewCount(feed = feed, count = 0L) - viewCountRepository.save(newViewCount) - } - - viewCount.count += 1 - viewCountRepository.save(viewCount) - - return ViewCountDTO(feed.id, viewCount.count) - } - - @Transactional(readOnly = true) - override fun getViewCount(feedId: Long): ViewCountDTO { - val feed = feedRepository.findById(feedId) - .orElseThrow { EntityNotFoundException("Feed not found for id: $feedId") } - - val count = viewCountRepository.findByFeed(feed) - .map { it.count } - .orElse(0L) - - return ViewCountDTO(feed.id, count) - } -} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/profile/controller/ProfileController.kt b/src/main/kotlin/org/tenten/bittakotlin/profile/controller/ProfileController.kt index c4889d5..791e40b 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/profile/controller/ProfileController.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/profile/controller/ProfileController.kt @@ -31,8 +31,6 @@ class ProfileController( return ResponseEntity.ok(updatedProfile) } - - companion object { private val logger: Logger = LoggerFactory.getLogger(ProfileController::class.java) } diff --git a/src/main/kotlin/org/tenten/bittakotlin/profile/entity/Profile.kt b/src/main/kotlin/org/tenten/bittakotlin/profile/entity/Profile.kt index ef90f0c..78075f6 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/profile/entity/Profile.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/profile/entity/Profile.kt @@ -8,6 +8,7 @@ import org.tenten.bittakotlin.member.entity.Member import org.tenten.bittakotlin.profile.constant.Job import org.tenten.bittakotlin.scout.entity.ScoutRequest +//data class 로 변경 @Entity class Profile( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) From 73c9ee7edeafff9077b4293dd5aa8eeb1fb81323 Mon Sep 17 00:00:00 2001 From: juwon-code Date: Tue, 5 Nov 2024 16:29:06 +0900 Subject: [PATCH 16/26] =?UTF-8?q?Chore:=20=EA=B8=B0=EB=B3=B8=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20URL=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기본 프로필 이미지 URL을 수호님 사진으로 대체합니다. Related to: prgrms-be-devcourse/NBB1_2_3_Team10#37 --- src/main/resources/application.properties | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 3ade8d4..428eb78 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -3,4 +3,7 @@ spring.application.name=bitta-kotlin # file size limit file.max.size.image=10485760 -file.max.size.video=31457280 \ No newline at end of file +file.max.size.video=31457280 + +# default profile image url +default.profile.image = https://project-bitta-s3-bucket.s3.ap-northeast-2.amazonaws.com/profile.jpg \ No newline at end of file From c811ff12c99c3ee6911ac0893d143542afe57d30 Mon Sep 17 00:00:00 2001 From: deveunhwa Date: Tue, 5 Nov 2024 16:09:05 +0900 Subject: [PATCH 17/26] =?UTF-8?q?Fix:=20=EB=B0=B0=ED=8F=AC=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/CICD.yml | 26 ++++++++++++++++++++------ Dockerfile | 8 ++++++-- docker-compose.yml | 8 ++++---- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 2cfbe8a..bff4eba 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -1,4 +1,4 @@ -name: Java CI/CD +name: Backend CI/CD Pipeline on: push: @@ -53,8 +53,12 @@ jobs: docker build -t ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest . docker push ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest + # application-prod.properties 빌드 후 제거 - 안전성 확보 + - name: Clean up application-prod.properties + run: rm ./src/main/resources/application-prod.properties + deploy: - if: github.repository == 'prgrms-be-devcourse/NBB1_2_3_Team10' && github.ref == 'refs/heads/release' + if: github.repository == 'prgrms-be-devcourse/NBB1_2_3_Team10' && github.ref == 'refs/heads/release' needs: build runs-on: ubuntu-latest permissions: @@ -71,11 +75,10 @@ jobs: username: ${{ secrets.EC2_USER }} key: ${{ secrets.EC2_SSH_KEY }} script: | - # Docker 로그인 및 최신 이미지 가져오기 echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin docker pull ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest - # blue/green 컨테이너 전환 + # Blue-Green 배포: blue와 green 컨테이너 전환 CURRENT_CONTAINER=$(docker ps --filter "name=backend-blue" -q) TARGET_CONTAINER="backend-green" [ -z "$CURRENT_CONTAINER" ] && TARGET_CONTAINER="backend-blue" @@ -83,9 +86,20 @@ jobs: # 새 컨테이너 실행 docker stop $TARGET_CONTAINER || true docker rm $TARGET_CONTAINER || true - docker run -d --name $TARGET_CONTAINER -p 8080:80 \ - -e NODE_ENV=production \ + docker run -d --name $TARGET_CONTAINER -p 8080:8080 \ + -e SPRING_PROFILES_ACTIVE=prod \ + -e AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }} \ + -e AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }} \ ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest # Nginx 리로드하여 트래픽 전환 sudo systemctl reload nginx + + # Blue-Green 설정 Docker Compose 실행 + - name: Deploy with Docker Compose on EC2 + run: | + ssh -o StrictHostKeyChecking=no ec2-user@${{ secrets.EC2_HOST }} + docker-compose down + docker-compose up -d" + env: + SSH_KEY: ${{ secrets.EC2_SSH_KEY }} diff --git a/Dockerfile b/Dockerfile index c4469d1..b7436a5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,12 @@ # Use OpenJDK 17 image -FROM openjdk:17 +FROM openjdk:17-jdk-alpine + +# Set working directory +WORKDIR /app # Copy the built jar file COPY build/libs/*.jar app.jar # Set the entry point to run the application -ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=prod", "/app.jar"] +EXPOSE 8080 +CMD ["java", "-jar", "-Dspring.profiles.active=prod", "/app.jar"] diff --git a/docker-compose.yml b/docker-compose.yml index dc69adc..fb54378 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,17 +3,17 @@ services: backend-blue: build: context: . - dockerfile: ./Dockerfile + dockerfile: Dockerfile ports: - "8081:8080" environment: - - SPRING_PROFILES_ACTIVE=production + - SPRING_PROFILES_ACTIVE=prod backend-green: build: context: . - dockerfile: ./Dockerfile + dockerfile: Dockerfile ports: - "8082:8080" environment: - - SPRING_PROFILES_ACTIVE=production + - SPRING_PROFILES_ACTIVE=prod From 7970e974eb6c8e983d9dc54596d0609bf36c59a9 Mon Sep 17 00:00:00 2001 From: Preta3418 Date: Tue, 5 Nov 2024 16:07:37 +0900 Subject: [PATCH 18/26] =?UTF-8?q?Refactor:=20=ED=81=B4=EB=9E=98=EC=8A=A4?= =?UTF-8?q?=EB=84=A4=EC=9E=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bean 충돌이 생겨서 클래스 이름을 좀 변경 했습니다 --- .../feedInteraction/feedLike/dto/FeedLikeDTO.kt | 7 +++++++ .../feedInteraction/feedLike/service/FeedLikeService.kt | 8 ++++++++ 2 files changed, 15 insertions(+) create mode 100644 src/main/kotlin/org/tenten/bittakotlin/feedInteraction/feedLike/dto/FeedLikeDTO.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/feedInteraction/feedLike/service/FeedLikeService.kt diff --git a/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/feedLike/dto/FeedLikeDTO.kt b/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/feedLike/dto/FeedLikeDTO.kt new file mode 100644 index 0000000..6b70070 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/feedLike/dto/FeedLikeDTO.kt @@ -0,0 +1,7 @@ +package org.tenten.bittakotlin.feedInteraction.feedLike.dto + +data class FeedLikeDTO( + val feedId: Long?, + val profileId: Long?, + val isLiked: Boolean +) \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/feedLike/service/FeedLikeService.kt b/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/feedLike/service/FeedLikeService.kt new file mode 100644 index 0000000..15b72ce --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/feedInteraction/feedLike/service/FeedLikeService.kt @@ -0,0 +1,8 @@ +package org.tenten.bittakotlin.feedInteraction.feedLike.service + +import org.tenten.bittakotlin.feedInteraction.feedLike.dto.FeedLikeDTO + +interface FeedLikeService { + fun toggleLike(feedId: Long, profileId: Long): FeedLikeDTO + fun getLikeCount(feedId: Long): Long +} From 69d28147c9002745795f46d04925997ccc8e81b5 Mon Sep 17 00:00:00 2001 From: deveunhwa Date: Tue, 5 Nov 2024 16:18:48 +0900 Subject: [PATCH 19/26] =?UTF-8?q?Fix:=20workflows=20blue-green=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/CICD.yml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index bff4eba..bea8bae 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -58,7 +58,7 @@ jobs: run: rm ./src/main/resources/application-prod.properties deploy: - if: github.repository == 'prgrms-be-devcourse/NBB1_2_3_Team10' && github.ref == 'refs/heads/release' + if: github.repository == 'prgrms-be-devcourse/NBB1_2_3_Team10' && github.ref == 'refs/heads/release' needs: build runs-on: ubuntu-latest permissions: @@ -97,9 +97,11 @@ jobs: # Blue-Green 설정 Docker Compose 실행 - name: Deploy with Docker Compose on EC2 - run: | - ssh -o StrictHostKeyChecking=no ec2-user@${{ secrets.EC2_HOST }} - docker-compose down - docker-compose up -d" - env: - SSH_KEY: ${{ secrets.EC2_SSH_KEY }} + uses: appleboy/ssh-action@v0.1.6 + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USER }} + key: ${{ secrets.EC2_SSH_KEY }} + script: | + docker-compose down + docker-compose up -d From 00b7b546bd6851d1c3450cd975b8545e578fdc4a Mon Sep 17 00:00:00 2001 From: ghtndl Date: Tue, 5 Nov 2024 16:30:06 +0900 Subject: [PATCH 20/26] Feat: Member Swagger --- .../member/controller/MemberController.kt | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/main/kotlin/org/tenten/bittakotlin/member/controller/MemberController.kt b/src/main/kotlin/org/tenten/bittakotlin/member/controller/MemberController.kt index b65d44a..bc9f56e 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/member/controller/MemberController.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/member/controller/MemberController.kt @@ -1,5 +1,10 @@ package org.tenten.bittakotlin.member.controller +import io.swagger.v3.oas.annotations.Operation +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 org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.security.access.AccessDeniedException @@ -11,6 +16,7 @@ import org.tenten.bittakotlin.member.repository.MemberRepository import org.tenten.bittakotlin.member.service.MemberService import org.tenten.bittakotlin.security.jwt.JWTUtil +@Tag(name = "회원관리 API 컨트롤러", description = "회원과 관련된 RestAPI 제공 컨트롤러") @RestController @RequestMapping("/api/v1/member") class MemberController( @@ -20,6 +26,26 @@ class MemberController( ) { // 회원가입 + + @Operation( + summary = "회원가입", + description = "회원가입을 진행합니다.", + responses = [ + ApiResponse( + responseCode = "200", + description = "회원가입 성공", + content = [Content( + mediaType = "application/json", + schema = Schema(example = "MEMBER_SUCCESS_SIGN_UP") // 예시 값은 문자열로 처리 + )] + ), + ApiResponse( + responseCode = "400", + description = "회원가입 실패", + content = [Content()] + ) + ] + ) @PostMapping("/join") fun join(@RequestBody joinDTO: MemberRequestDTO.Join): ResponseEntity { memberService.join(joinDTO) @@ -27,6 +53,16 @@ class MemberController( } // 회원 정보 조회 + + @Operation( + summary = "회원 정보 조회", + description = "회원의 ID를 사용해 회원 정보를 조회합니다.", + responses = [ApiResponse( + responseCode = "200", + description = "회원 정보 조회 성공", + content = [Content(mediaType = "application/json")] + ), ApiResponse(responseCode = "404", description = "회원 정보 조회 실패", content = [Content()])] + ) @GetMapping("/{id}") fun read(@PathVariable id: Long): ResponseEntity { val memberInfo = memberService.read(id) From c7053ac8100b2e439d0dcce533973dbca1a617ec Mon Sep 17 00:00:00 2001 From: deveunhwa Date: Tue, 5 Nov 2024 18:59:44 +0900 Subject: [PATCH 21/26] =?UTF-8?q?Fix:=20docker-compose=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/CICD.yml | 21 ++++++++++++++++----- docker-compose.yml | 20 +++++++++----------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index bea8bae..b95ff0a 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -86,11 +86,21 @@ jobs: # 새 컨테이너 실행 docker stop $TARGET_CONTAINER || true docker rm $TARGET_CONTAINER || true - docker run -d --name $TARGET_CONTAINER -p 8080:8080 \ - -e SPRING_PROFILES_ACTIVE=prod \ - -e AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }} \ - -e AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }} \ - ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest + + # 조건문을 사용해 컨테이너에 맞는 포트로 실행 + if [ "$TARGET_CONTAINER" == "backend-blue" ]; then + docker run -d --name backend-blue -p 8081:8080 \ + -e SPRING_PROFILES_ACTIVE=prod \ + -e AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }} \ + -e AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }} \ + ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest + else + docker run -d --name backend-green -p 8082:8080 \ + -e SPRING_PROFILES_ACTIVE=prod \ + -e AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }} \ + -e AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }} \ + ${{ secrets.DOCKER_USERNAME }}/bitta-kotlin:latest + fi # Nginx 리로드하여 트래픽 전환 sudo systemctl reload nginx @@ -103,5 +113,6 @@ jobs: username: ${{ secrets.EC2_USER }} key: ${{ secrets.EC2_SSH_KEY }} script: | + cd ~/bitta-front docker-compose down docker-compose up -d diff --git a/docker-compose.yml b/docker-compose.yml index fb54378..b448ef0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,19 +1,17 @@ version: '3' services: - backend-blue: - build: - context: . - dockerfile: Dockerfile + backend: + container_name: backend-blue + image: publicdeveh/bitta-kotlin:latest ports: - - "8081:8080" + - "8081:8000" environment: - SPRING_PROFILES_ACTIVE=prod - backend-green: - build: - context: . - dockerfile: Dockerfile + backend_alternate: + container_name: backend-green + image: publicdeveh/bitta-kotlin:latest ports: - - "8082:8080" + - "8082:8000" environment: - - SPRING_PROFILES_ACTIVE=prod + - SPRING_PROFILES_ACTIVE=prod \ No newline at end of file From 1261339b0f68471e6cc40a2f6307ec6d3b5fbbf2 Mon Sep 17 00:00:00 2001 From: deveunhwa Date: Wed, 6 Nov 2024 04:38:35 +0900 Subject: [PATCH 22/26] =?UTF-8?q?Fix:=20docker-compose=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/CICD.yml | 29 +++++++++++++++++++---------- docker-compose.yml | 22 +++++++++++++++++----- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index b95ff0a..ac9752d 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -106,13 +106,22 @@ jobs: sudo systemctl reload nginx # Blue-Green 설정 Docker Compose 실행 - - name: Deploy with Docker Compose on EC2 - uses: appleboy/ssh-action@v0.1.6 - with: - host: ${{ secrets.EC2_HOST }} - username: ${{ secrets.EC2_USER }} - key: ${{ secrets.EC2_SSH_KEY }} - script: | - cd ~/bitta-front - docker-compose down - docker-compose up -d + - name: Deploy with Docker Compose on EC2 + uses: appleboy/ssh-action@v0.1.6 + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USER }} + key: ${{ secrets.EC2_SSH_KEY }} + script: | + cd ~/bitta-project + docker-compose down + docker-compose up -d --no-deps + sudo nginx -s reload + + # test + - name: Test Frontend Response + run: curl --fail http://${{ secrets.EC2_HOST }} || exit 1 + + - name: Test Backend Response + run: curl --fail http://${{ secrets.EC2_HOST }}/api/ || exit 1 + diff --git a/docker-compose.yml b/docker-compose.yml index b448ef0..e82540b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,17 +1,29 @@ version: '3' services: - backend: - container_name: backend-blue + frontend-blue: + image: publicdeveh/bitta-front:latest + ports: + - "3001:3000" + environment: + - NODE_ENV=production + + frontend-green: + image: publicdeveh/bitta-front:latest + ports: + - "3002:3000" + environment: + - NODE_ENV=production + + backend-blue: image: publicdeveh/bitta-kotlin:latest ports: - "8081:8000" environment: - SPRING_PROFILES_ACTIVE=prod - backend_alternate: - container_name: backend-green + backend-green: image: publicdeveh/bitta-kotlin:latest ports: - "8082:8000" environment: - - SPRING_PROFILES_ACTIVE=prod \ No newline at end of file + - SPRING_PROFILES_ACTIVE=prod From f7cb32d27f26a8530cdf66f08a3a71e31dcbeade Mon Sep 17 00:00:00 2001 From: deveunhwa Date: Wed, 6 Nov 2024 04:46:21 +0900 Subject: [PATCH 23/26] =?UTF-8?q?Fix:=20workflows=20=EB=9D=84=EC=96=B4?= =?UTF-8?q?=EC=93=B0=EA=B8=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/CICD.yml | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index ac9752d..36b77f7 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -106,22 +106,21 @@ jobs: sudo systemctl reload nginx # Blue-Green 설정 Docker Compose 실행 - - name: Deploy with Docker Compose on EC2 - uses: appleboy/ssh-action@v0.1.6 - with: - host: ${{ secrets.EC2_HOST }} - username: ${{ secrets.EC2_USER }} - key: ${{ secrets.EC2_SSH_KEY }} - script: | - cd ~/bitta-project - docker-compose down - docker-compose up -d --no-deps - sudo nginx -s reload - - # test + - name: Deploy with Docker Compose on EC2 + uses: appleboy/ssh-action@v0.1.6 + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USER }} + key: ${{ secrets.EC2_SSH_KEY }} + script: | + cd ~/bitta-project + docker-compose down + docker-compose up -d --no-deps + sudo nginx -s reload + + # 테스트 단계 - name: Test Frontend Response run: curl --fail http://${{ secrets.EC2_HOST }} || exit 1 - name: Test Backend Response - run: curl --fail http://${{ secrets.EC2_HOST }}/api/ || exit 1 - + run: curl --fail http://${{ secrets.EC2_HOST }}/api/ || exit 1 \ No newline at end of file From 728aca8f5c4d3bb942fa1da8e13d6cdae6fc0b04 Mon Sep 17 00:00:00 2001 From: deveunhwa Date: Wed, 6 Nov 2024 04:59:49 +0900 Subject: [PATCH 24/26] =?UTF-8?q?Fix:=20Dockerfile=20=EB=A3=A8=ED=8A=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index b7436a5..d9d1fe5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,8 +5,8 @@ FROM openjdk:17-jdk-alpine WORKDIR /app # Copy the built jar file -COPY build/libs/*.jar app.jar +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"] \ No newline at end of file From b93f8d137a22b70972cb02ecc29b4776f703305e Mon Sep 17 00:00:00 2001 From: deveunhwa Date: Wed, 6 Nov 2024 05:03:45 +0900 Subject: [PATCH 25/26] =?UTF-8?q?Fix:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=ED=99=95=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/CICD.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 36b77f7..544a78a 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -120,7 +120,10 @@ jobs: # 테스트 단계 - name: Test Frontend Response - run: curl --fail http://${{ secrets.EC2_HOST }} || exit 1 + run: | + for i in {1..5}; do + curl --fail http://${{ secrets.EC2_HOST }} && break || sleep 15 + done || exit 1 - name: Test Backend Response run: curl --fail http://${{ secrets.EC2_HOST }}/api/ || exit 1 \ No newline at end of file From 7ec0da05dc9f3eb516b767f33984df3c573cbe92 Mon Sep 17 00:00:00 2001 From: deveunhwa Date: Wed, 6 Nov 2024 06:03:00 +0900 Subject: [PATCH 26/26] =?UTF-8?q?Fix:=20workflows=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/CICD.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 544a78a..36b77f7 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -120,10 +120,7 @@ jobs: # 테스트 단계 - name: Test Frontend Response - run: | - for i in {1..5}; do - curl --fail http://${{ secrets.EC2_HOST }} && break || sleep 15 - done || exit 1 + run: curl --fail http://${{ secrets.EC2_HOST }} || exit 1 - name: Test Backend Response run: curl --fail http://${{ secrets.EC2_HOST }}/api/ || exit 1 \ No newline at end of file