Skip to content
Open
3 changes: 3 additions & 0 deletions server/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
runtimeOnly("org.postgresql:postgresql")

// UUID v7
implementation("com.github.f4b6a3:uuid-creator:6.1.1")

// Swagger/OpenAPI
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.0")

Expand Down
2 changes: 2 additions & 0 deletions server/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ services:
POSTGRES_DB: ${DB_NAME:?DB_NAME is required}
POSTGRES_USER: ${DB_USER:?DB_USER is required}
POSTGRES_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required}
TZ: Asia/Seoul
PGTZ: Asia/Seoul
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.andone.memorip.common.config

import com.andone.memorip.common.converter.StringToUuidConverter
import org.springframework.context.annotation.Configuration
import org.springframework.format.FormatterRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer

@Configuration
class WebConfig(
private val stringToUuidConverter: StringToUuidConverter
) : WebMvcConfigurer {

override fun addFormatters(registry: FormatterRegistry) {
registry.addConverter(stringToUuidConverter)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.andone.memorip.common.converter

import com.andone.memorip.common.exception.BusinessException
import com.andone.memorip.common.exception.CommonExceptionCode
import org.springframework.core.convert.converter.Converter
import org.springframework.stereotype.Component
import java.util.UUID

@Component
class StringToUuidConverter : Converter<String, UUID> {

override fun convert(id: String): UUID {
return try {
UUID.fromString(id)
} catch (e: IllegalArgumentException) {
throw BusinessException(
code = CommonExceptionCode.INVALID_PARAMETER,
fields = listOf("UUID 형식이 올바르지 않습니다: $id")
)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,12 @@
package com.andone.memorip.common.entity

import jakarta.persistence.Column
import jakarta.persistence.EntityListeners
import jakarta.persistence.MappedSuperclass
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.jpa.domain.support.AuditingEntityListener
import java.time.LocalDateTime
import jakarta.persistence.*
import java.util.UUID

@MappedSuperclass
@EntityListeners(AuditingEntityListener::class)
abstract class BaseEntity protected constructor() {

@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
var createdAt: LocalDateTime? = null
protected set

@LastModifiedDate
@Column(name = "updated_at", nullable = false)
var updatedAt: LocalDateTime? = null
protected set

@Column(name = "deleted_at")
var deletedAt: LocalDateTime? = null
abstract class BaseEntity {
@Id
@Column(columnDefinition = "UUID")
open var id: UUID? = null
protected set
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.andone.memorip.common.entity

import jakarta.persistence.Column
import jakarta.persistence.EntityListeners
import jakarta.persistence.MappedSuperclass
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.jpa.domain.support.AuditingEntityListener
import java.time.LocalDateTime

@MappedSuperclass
@EntityListeners(AuditingEntityListener::class)
abstract class BaseTimeEntity : BaseEntity() {

@CreatedDate
@Column(nullable = false, updatable = false, columnDefinition = "TIMESTAMPTZ")
var createdAt: LocalDateTime = LocalDateTime.now()
protected set

@LastModifiedDate
@Column(nullable = false, columnDefinition = "TIMESTAMPTZ")
var updatedAt: LocalDateTime = LocalDateTime.now()
protected set

@Column(name = "deleted_at", columnDefinition = "TIMESTAMPTZ")
var deletedAt: LocalDateTime? = null
protected set

fun isDeleted(): Boolean = deletedAt != null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.andone.memorip.common.entity

import jakarta.persistence.Column
import jakarta.persistence.MappedSuperclass
import java.time.LocalDateTime

@MappedSuperclass
abstract class BaseTimeSyncEntity : BaseTimeEntity() {

@Column(name = "synced_at", columnDefinition = "TIMESTAMPTZ")
var syncedAt: LocalDateTime? = null
protected set

fun sync() {
syncedAt = LocalDateTime.now()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.andone.memorip.common.util

import com.github.f4b6a3.uuid.UuidCreator
import java.util.UUID

object UuidV7Generator {
fun generate(): UUID {
return UuidCreator.getTimeOrderedEpoch()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.andone.memorip.domain.group.entity

import com.andone.memorip.common.entity.BaseTimeSyncEntity
import com.andone.memorip.common.util.UuidV7Generator
import com.andone.memorip.domain.user.entity.User
import jakarta.persistence.*
import org.hibernate.annotations.SQLDelete
import org.hibernate.annotations.SQLRestriction
import java.util.UUID

enum class Visibility {
PRIVATE,
PUBLIC
}

@Entity
@Table(name = "groups")
@SQLDelete(sql = "UPDATE groups SET deleted_at = CURRENT_TIMESTAMP WHERE id = ?")
@SQLRestriction("deleted_at IS NULL")
class Group protected constructor(
id: UUID? = null,
owner: User,
title: String,
visibility: Visibility = Visibility.PRIVATE
) : BaseTimeSyncEntity() {

init {
this.id = id
}

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "owner_id", nullable = false)
var owner: User = owner
internal set

@Column(nullable = false, length = 50)
var title: String = title
internal set

@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
var visibility: Visibility = visibility
internal set

fun updateTitle(title: String) {
require(title.isNotBlank()) { "제목은 필수입니다" }
require(title.length <= 50) { "제목은 50자 이하여야 합니다" }
this.title = title
}

fun updateVisibility(visibility: Visibility) {
this.visibility = visibility
}

companion object {
fun create(
id: UUID? = null,
owner: User,
title: String,
visibility: Visibility = Visibility.PRIVATE
): Group {
require(title.isNotBlank()) { "제목은 필수입니다" }
require(title.length <= 50) { "제목은 50자 이하여야 합니다" }

val generatedId = id ?: UuidV7Generator.generate()
return Group(generatedId, owner, title, visibility)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.andone.memorip.domain.place.entity

import jakarta.persistence.Column
import jakarta.persistence.Embeddable

@Embeddable
data class Address(
@Column(name = "region_1depth", nullable = false, length = 20)
val region1Depth: String, // 시/도 (ex. 서울)

@Column(name = "region_2depth", length = 20)
val region2Depth: String? = null, // 구/군 (ex. 강남구)

@Column(name = "region_3depth", length = 20)
val region3Depth: String? = null, // 동/읍/면 (ex. 역삼동)

@Column(name = "full_address", nullable = false, length = 255)
val fullAddress: String // 전체 주소
) {
init {
require(region1Depth.isNotBlank()) { "시/도 정보는 필수입니다" }
require(fullAddress.isNotBlank()) { "전체 주소는 필수입니다" }
}
protected constructor() : this("", null, null, "")
}
Loading