diff --git a/.github/ISSUE_TEMPLATE/01. main-issue.yml b/.github/ISSUE_TEMPLATE/01. main-issue.yml
index 6f55e66..43a3d7d 100644
--- a/.github/ISSUE_TEMPLATE/01. main-issue.yml
+++ b/.github/ISSUE_TEMPLATE/01. main-issue.yml
@@ -1,6 +1,6 @@
name: Main Issue
description: Describe the task list.
-title: "[MAIN] "
+title: "βοΈ "
#labels: []
#projects: []
#assignees:
@@ -9,7 +9,7 @@ body:
- type: markdown
attributes:
value: |
- Thanks for taking the time to fill out this bug report!
+ Thanks for taking the time to fill out this issue!
# - type: input
# id: contact
# attributes:
diff --git a/.github/ISSUE_TEMPLATE/03. bug-report.yml b/.github/ISSUE_TEMPLATE/03. bug-report.yml
new file mode 100644
index 0000000..d445a79
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/03. bug-report.yml
@@ -0,0 +1,93 @@
+name: Bug Report
+description: Describe the bug you encountered.
+title: "π "
+labels: ["type: bug"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for taking the time to fill out this bug report!
+ - type: dropdown
+ id: bug-type
+ attributes:
+ label: π Bug Types (Multiple)
+ description: |
+ μ΄λ€ μ’
λ₯μ λ¬Έμ μΈκ°μ?
+
+ - π μ€λ₯ (μλ¬, μμΈ λ°μ, λ
Όλ¦¬μ μ€λ₯ λ±)
+ - π 컨벀μ
·리ν©ν°λ§ (μ½λ μ€νμΌ, μν€ν
μ² λ±)
+ - β‘οΈ μ±λ₯ (μ§μ°, κ³Όλν μμ, λ³λͺ© νμ λ±)
+ - βοΈ μ€νΒ·μ€λͺ
μ€λ₯ (λ¬Έμ, λ¬Έμμ΄, μ£Όμ, μλ³μ λ±)
+ - π§ κΈ°ν (λΉλ, νκ²½λ³μ λ±)
+
+ **βοΈ λ³΄μ μ·¨μ½μ μ pkw19961027@gmail.comμΌλ‘ λ©μΌ λΆνλ립λλ€.**
+
+
\* λ€μ€ μ ν
+ multiple: true
+ options:
+ - π μ€λ₯
+ - π 컨벀μ
·리ν©ν°λ§
+ - β‘οΈ μ±λ₯
+ - βοΈ μ€νΒ·μ€λͺ
μ€λ₯
+ - π§ κΈ°ν
+ default: 0
+ validations:
+ required: true
+ - type: input
+ id: summary
+ attributes:
+ label:
π¬ Summary of the Bug
+ description: λ²κ·Έλ₯Ό ν λ¬Έμ₯ μ λλ‘ μμ½ν΄ μ£ΌμΈμ.
+ placeholder: ex. λ‘κ·ΈμΈ ν νλ©΄μ΄ λ©μΆ₯λλ€.
+ validations:
+ required: true
+ - type: textarea
+ id: steps-to-reproduce
+ attributes:
+ label:
π μ€λ₯ μ¬ν (Reproduction Steps)
+ description: λ²κ·Έλ₯Ό μ¬ννκΈ° μν ꡬ체μ μΈ λ¨κ³λ€μ μμλλ‘ μ μ΄ μ£ΌμΈμ.
+ placeholder: |
+ 1.
+ 2.
+ 3.
+
+ validations:
+ required: false
+ - type: textarea
+ id: logs
+ attributes:
+ label:
π κ΄λ ¨ λ‘κ·Έ (Logs)
+ description: μλ¬ λ©μμ§λ λ‘κ·Έκ° μλ€λ©΄ 볡μ¬ν΄μ λΆμ¬ μ£ΌμΈμ.
+ placeholder: |
+ Error: Cannot read property 'foo' of undefined
+ at β¦
+ render: console
+ validations:
+ required: false
+ - type: textarea
+ id: expected-behavior
+ attributes:
+ label:
π― κΈ°λ λμ (Expected Behavior)
+ description: μ μμ μΌλ‘ λμνμ λ μ΄λ€ κ²°κ³Όκ° λμμΌ νλμ§ μ€λͺ
ν΄ μ£ΌμΈμ.
+ placeholder: ex. λ‘κ·ΈμΈ ν λμ보λ νμ΄μ§κ° 보μ¬μΌ ν©λλ€.
+ validations:
+ required: true
+ - type: textarea
+ id: actual-behavior
+ attributes:
+ label:
π μ€μ λμ (Actual Behavior)
+ description: μ΄λ€ λμμ΄ λνλλμ§ μ€λͺ
ν΄ μ£ΌμΈμ.
+ placeholder: ex. λ‘κ·ΈμΈ ν λ²νΌμ΄ λ°μνμ§ μμ΅λλ€.
+ validations:
+ required: true
+ - type: textarea
+ id: additional-note
+ attributes:
+ label:
λΆμ° μ€λͺ
(Additional Notes)
+ description: κ΄λ ¨ λΈλμΉλ 컀λ°, Active profile, νκ²½λ³μ λ± κΈ°ν μ°Έκ³ ν λ§ν μ λ³΄κ° μμΌλ©΄ μ μ΄ μ£ΌμΈμ.
+ placeholder: Tell us any other details you think will help us fix this!
+ value: |
+
+
+ validations:
+ required: false
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 6ce1225..2913818 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,5 +1,3 @@
-# Pull Request
-
## Issues
- Resolves #0
@@ -11,12 +9,26 @@
- μ£Όμ λ³κ²½ μ¬νμ λν κ°λ¨ν μ€λͺ
μ μμ±ν΄ μ£ΌμΈμ.
- κ΄λ ¨ μ΄μ λ²νΈλ₯Ό ν¬ν¨ν΄ μ£ΌμΈμ (μ: `#123`).
+
+
+## Review Points
+
+
+
+
+- μμΈν 리뷰λ₯Ό μνλ μμμ μμ
μλμ ν¨κ» μ€λͺ
ν΄ μ£ΌμΈμ.
+
+
+
## How Has This Been Tested?
- λ³κ²½ μ¬νμ ν
μ€νΈνλ λ°©λ²μ λν΄ μ€λͺ
ν΄ μ£ΌμΈμ.
- μ΄λ€ νκ²½μμ ν
μ€νΈκ° μ΄λ£¨μ΄μ‘λμ§ λͺ
μν΄ μ£ΌμΈμ.
+
+
+
+
## Additional Notes
- μ΄ PRκ³Ό κ΄λ ¨λ μΆκ°μ μΈ μ λ³΄κ° μλ€λ©΄ μ¬κΈ°μ κΈ°μ¬ν΄ μ£ΌμΈμ.
-
diff --git a/build.gradle.kts b/build.gradle.kts
index dcc14bb..1a3bcbc 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -10,9 +10,6 @@ plugins {
}
allprojects {
- group = "me.nettee"
- version = "1.0-SNAPSHOT"
-
repositories {
mavenCentral()
}
@@ -34,7 +31,18 @@ subprojects {
}
}
+ configurations {
+ compileOnly {
+ extendsFrom(configurations.annotationProcessor.get())
+ }
+ configureEach {
+ exclude(group = "org.springframework.boot", module = "spring-boot-starter-logging")
+ }
+ }
+
dependencies {
+ // FIXME determine the placement of logging library later
+ implementation("org.springframework.boot:spring-boot-starter-log4j2")
if(project.name != "common") {
api(project(":common"))
}
diff --git a/core/core.settings.gradle.kts b/core/core.settings.gradle.kts
index 242a7ed..da76ea8 100644
--- a/core/core.settings.gradle.kts
+++ b/core/core.settings.gradle.kts
@@ -9,10 +9,14 @@ include(
":jpa-core",
":exception-handler-core",
":cors-api",
- ":cors-webmvc"
+ ":cors-webmvc",
+ ":snowflake-id-api",
+ ":snowflake-id-hibernate",
)
project(":jpa-core").projectDir = core["jpa-core"]!!
project(":exception-handler-core").projectDir = core["exception-handler-core"]!!
project(":cors-webmvc").projectDir = core["nettee-cors-webmvc"]!!
-project(":cors-api").projectDir = core["nettee-cors-api"]!!
\ No newline at end of file
+project(":cors-api").projectDir = core["nettee-cors-api"]!!
+project(":snowflake-id-api").projectDir = core["nettee-snowflake-id-api"]!!
+project(":snowflake-id-hibernate").projectDir = core["nettee-snowflake-id-hibernate"]!!
\ No newline at end of file
diff --git a/core/jpa-core/build.gradle.kts b/core/jpa-core/build.gradle.kts
index 4af48f6..db413e1 100644
--- a/core/jpa-core/build.gradle.kts
+++ b/core/jpa-core/build.gradle.kts
@@ -1,5 +1,6 @@
dependencies {
- implementation("org.springframework.boot:spring-boot-starter-data-jpa")
+ api(project(":snowflake-id-hibernate"))
+ api("org.springframework.boot:spring-boot-starter-data-jpa")
// querydsl
implementation("com.querydsl:querydsl-jpa:${dependencyManagement.importedProperties["querydsl.version"]}:jakarta")
diff --git a/core/jpa-core/src/main/java/nettee/jpa/support/SnowflakeBaseEntity.java b/core/jpa-core/src/main/java/nettee/jpa/support/SnowflakeBaseEntity.java
new file mode 100644
index 0000000..a23b765
--- /dev/null
+++ b/core/jpa-core/src/main/java/nettee/jpa/support/SnowflakeBaseEntity.java
@@ -0,0 +1,16 @@
+package nettee.jpa.support;
+
+import jakarta.persistence.Id;
+import jakarta.persistence.MappedSuperclass;
+import lombok.Getter;
+import nettee.hibenate.annotation.SnowflakeGenerated;
+
+import java.io.Serializable;
+
+@Getter
+@MappedSuperclass
+public abstract class SnowflakeBaseEntity implements Serializable {
+ @Id
+ @SnowflakeGenerated
+ private Long id;
+}
diff --git a/core/jpa-core/src/main/java/nettee/jpa/support/SnowflakeBaseTimeEntity.java b/core/jpa-core/src/main/java/nettee/jpa/support/SnowflakeBaseTimeEntity.java
new file mode 100644
index 0000000..150c6aa
--- /dev/null
+++ b/core/jpa-core/src/main/java/nettee/jpa/support/SnowflakeBaseTimeEntity.java
@@ -0,0 +1,21 @@
+package nettee.jpa.support;
+
+import jakarta.persistence.EntityListeners;
+import jakarta.persistence.MappedSuperclass;
+import lombok.Getter;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import java.time.Instant;
+
+@Getter
+@MappedSuperclass
+@EntityListeners(AuditingEntityListener.class)
+public class SnowflakeBaseTimeEntity extends SnowflakeBaseEntity {
+ @CreatedDate
+ private Instant createdAt;
+
+ @LastModifiedDate
+ private Instant updatedAt;
+}
diff --git a/core/snowflake/nettee-snowflake-id-api/build.gradle.kts b/core/snowflake/nettee-snowflake-id-api/build.gradle.kts
new file mode 100644
index 0000000..1355541
--- /dev/null
+++ b/core/snowflake/nettee-snowflake-id-api/build.gradle.kts
@@ -0,0 +1,3 @@
+dependencies {
+ compileOnly("org.springframework.boot:spring-boot-autoconfigure:3.4.3")
+}
\ No newline at end of file
diff --git a/core/snowflake/nettee-snowflake-id-api/src/main/java/nettee/snowflake/constants/SnowflakeConstants.java b/core/snowflake/nettee-snowflake-id-api/src/main/java/nettee/snowflake/constants/SnowflakeConstants.java
new file mode 100644
index 0000000..6c06c2b
--- /dev/null
+++ b/core/snowflake/nettee-snowflake-id-api/src/main/java/nettee/snowflake/constants/SnowflakeConstants.java
@@ -0,0 +1,25 @@
+package nettee.snowflake.constants;
+
+public final class SnowflakeConstants {
+ // νκ΅ μκ°(KST): 2025-03-26 23:40:00 κΈ°μ€μ
+ public static final long NETTEE_EPOCH = 1_743_000_000_000L;
+ public static final String PREFIX = "nettee.persistence.snowflake";
+
+ private SnowflakeConstants() {}
+
+ public static final class SnowflakeDefault {
+ public static final int WORKER_ID_BIT_SIZE = 5;
+ public static final int DATACENTER_ID_BIT_SIZE = 5;
+ public static final int SEQUENCE_BIT_SIZE = 12;
+
+ public static final int WORKER_ID_SHIFT = SEQUENCE_BIT_SIZE;
+ public static final int DATACENTER_ID_SHIFT = SEQUENCE_BIT_SIZE + WORKER_ID_BIT_SIZE;
+ public static final int TIMESTAMP_LEFT_SHIFT = SEQUENCE_BIT_SIZE + WORKER_ID_BIT_SIZE + DATACENTER_ID_BIT_SIZE;
+
+ public static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BIT_SIZE);
+ public static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BIT_SIZE);
+ public static final long MAX_DATACENTER_ID = ~(-1L << DATACENTER_ID_BIT_SIZE);
+
+ private SnowflakeDefault() {}
+ }
+}
diff --git a/core/snowflake/nettee-snowflake-id-api/src/main/java/nettee/snowflake/exception/InvalidDatacenterIdException.java b/core/snowflake/nettee-snowflake-id-api/src/main/java/nettee/snowflake/exception/InvalidDatacenterIdException.java
new file mode 100644
index 0000000..2877234
--- /dev/null
+++ b/core/snowflake/nettee-snowflake-id-api/src/main/java/nettee/snowflake/exception/InvalidDatacenterIdException.java
@@ -0,0 +1,18 @@
+package nettee.snowflake.exception;
+
+import static nettee.snowflake.constants.SnowflakeConstants.SnowflakeDefault.MAX_DATACENTER_ID;
+
+public class InvalidDatacenterIdException extends RuntimeException {
+
+ private final long datacenterId;
+
+ public InvalidDatacenterIdException(long datacenterId) {
+ super("Datacenter ID can't be greater than %d or less than 0. Input: %d"
+ .formatted(MAX_DATACENTER_ID, datacenterId));
+ this.datacenterId = datacenterId;
+ }
+
+ public long getDatacenterId() {
+ return datacenterId;
+ }
+}
diff --git a/core/snowflake/nettee-snowflake-id-api/src/main/java/nettee/snowflake/exception/InvalidWorkerIdException.java b/core/snowflake/nettee-snowflake-id-api/src/main/java/nettee/snowflake/exception/InvalidWorkerIdException.java
new file mode 100644
index 0000000..68590f8
--- /dev/null
+++ b/core/snowflake/nettee-snowflake-id-api/src/main/java/nettee/snowflake/exception/InvalidWorkerIdException.java
@@ -0,0 +1,18 @@
+package nettee.snowflake.exception;
+
+import static nettee.snowflake.constants.SnowflakeConstants.SnowflakeDefault.MAX_WORKER_ID;
+
+public class InvalidWorkerIdException extends RuntimeException{
+
+ private final long workerId;
+
+ public InvalidWorkerIdException(final long workerId) {
+ super("Worker ID can't be greater than %d or less than 0. Input: %d"
+ .formatted(MAX_WORKER_ID, workerId));
+ this.workerId = workerId;
+ }
+
+ public long getWorkerId() {
+ return workerId;
+ }
+}
diff --git a/core/snowflake/nettee-snowflake-id-api/src/main/java/nettee/snowflake/persistence/id/Snowflake.java b/core/snowflake/nettee-snowflake-id-api/src/main/java/nettee/snowflake/persistence/id/Snowflake.java
new file mode 100644
index 0000000..e7e58f4
--- /dev/null
+++ b/core/snowflake/nettee-snowflake-id-api/src/main/java/nettee/snowflake/persistence/id/Snowflake.java
@@ -0,0 +1,66 @@
+package nettee.snowflake.persistence.id;
+
+import nettee.snowflake.properties.SnowflakeProperties;
+import nettee.snowflake.validator.SnowflakeConstructingValidator;
+
+import static nettee.snowflake.constants.SnowflakeConstants.NETTEE_EPOCH;
+import static nettee.snowflake.constants.SnowflakeConstants.SnowflakeDefault.*;
+
+public class Snowflake {
+ private final long datacenterId;
+ private final long workerId;
+ private final long epoch;
+
+ private long sequence = 0L;
+ private long lastTimestamp = -1L;
+
+ public Snowflake(SnowflakeProperties properties) {
+ this(properties.datacenterId(), properties.workerId(), properties.epoch());
+ }
+
+ public Snowflake(long datacenterId, long workerId, long epoch) {
+ SnowflakeConstructingValidator.validateDatacenterId(datacenterId);
+ SnowflakeConstructingValidator.validateWorkerId(workerId);
+
+ this.workerId = workerId;
+ this.datacenterId = datacenterId;
+ this.epoch = epoch >= 0 ? epoch : NETTEE_EPOCH;
+ }
+
+ public synchronized long nextId() {
+ long timestamp = timeGen();
+
+ if (timestamp < lastTimestamp) {
+ throw new RuntimeException(String.format(
+ "Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp
+ ));
+ }
+
+ if (lastTimestamp == timestamp) {
+ sequence = (sequence + 1) & SEQUENCE_MASK;
+ if (sequence == 0) {
+ timestamp = tilNextMillis(lastTimestamp);
+ }
+ } else {
+ sequence = 0L;
+ }
+
+ lastTimestamp = timestamp;
+ return ((timestamp - epoch) << TIMESTAMP_LEFT_SHIFT) |
+ (datacenterId << DATACENTER_ID_SHIFT) |
+ (workerId << WORKER_ID_SHIFT) |
+ sequence;
+ }
+
+ private long tilNextMillis(long lastTimestamp) {
+ long timestamp = timeGen();
+ while (timestamp <= lastTimestamp) {
+ timestamp = timeGen();
+ }
+ return timestamp;
+ }
+
+ private long timeGen() {
+ return System.currentTimeMillis();
+ }
+}
diff --git a/core/snowflake/nettee-snowflake-id-api/src/main/java/nettee/snowflake/properties/SnowflakeProperties.java b/core/snowflake/nettee-snowflake-id-api/src/main/java/nettee/snowflake/properties/SnowflakeProperties.java
new file mode 100644
index 0000000..1d65f9a
--- /dev/null
+++ b/core/snowflake/nettee-snowflake-id-api/src/main/java/nettee/snowflake/properties/SnowflakeProperties.java
@@ -0,0 +1,26 @@
+package nettee.snowflake.properties;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.util.Objects;
+
+import static nettee.snowflake.constants.SnowflakeConstants.NETTEE_EPOCH;
+import static nettee.snowflake.constants.SnowflakeConstants.PREFIX;
+
+@ConfigurationProperties(PREFIX)
+public record SnowflakeProperties(
+ Long datacenterId,
+ Long workerId,
+ Long epoch
+) {
+ public SnowflakeProperties {
+ Objects.requireNonNull(datacenterId, PREFIX + ".datacenter-id must not be null.");
+ Objects.requireNonNull(workerId, PREFIX + ".worker-id must not be null.");
+
+ if (epoch == null) {
+ epoch = NETTEE_EPOCH;
+ } else if (epoch < 0) {
+ epoch = NETTEE_EPOCH;
+ }
+ }
+}
diff --git a/core/snowflake/nettee-snowflake-id-api/src/main/java/nettee/snowflake/validator/SnowflakeConstructingValidator.java b/core/snowflake/nettee-snowflake-id-api/src/main/java/nettee/snowflake/validator/SnowflakeConstructingValidator.java
new file mode 100644
index 0000000..1d76114
--- /dev/null
+++ b/core/snowflake/nettee-snowflake-id-api/src/main/java/nettee/snowflake/validator/SnowflakeConstructingValidator.java
@@ -0,0 +1,23 @@
+package nettee.snowflake.validator;
+
+import nettee.snowflake.exception.InvalidDatacenterIdException;
+import nettee.snowflake.exception.InvalidWorkerIdException;
+
+import static nettee.snowflake.constants.SnowflakeConstants.SnowflakeDefault.MAX_DATACENTER_ID;
+import static nettee.snowflake.constants.SnowflakeConstants.SnowflakeDefault.MAX_WORKER_ID;
+
+public class SnowflakeConstructingValidator {
+ private SnowflakeConstructingValidator() {}
+
+ public static void validateDatacenterId(long datacenterId) {
+ if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) {
+ throw new InvalidDatacenterIdException(datacenterId);
+ }
+ }
+
+ public static void validateWorkerId(long workerId) {
+ if (workerId > MAX_WORKER_ID || workerId < 0) {
+ throw new InvalidWorkerIdException(workerId);
+ }
+ }
+}
diff --git a/core/snowflake/nettee-snowflake-id-hibernate/build.gradle.kts b/core/snowflake/nettee-snowflake-id-hibernate/build.gradle.kts
new file mode 100644
index 0000000..7f91fd0
--- /dev/null
+++ b/core/snowflake/nettee-snowflake-id-hibernate/build.gradle.kts
@@ -0,0 +1,4 @@
+dependencies{
+ api(project(":snowflake-id-api"))
+ compileOnly("org.hibernate.orm:hibernate-core")
+}
\ No newline at end of file
diff --git a/core/snowflake/nettee-snowflake-id-hibernate/src/main/java/nettee/hibenate/annotation/SnowflakeGenerated.java b/core/snowflake/nettee-snowflake-id-hibernate/src/main/java/nettee/hibenate/annotation/SnowflakeGenerated.java
new file mode 100644
index 0000000..b2cb4df
--- /dev/null
+++ b/core/snowflake/nettee-snowflake-id-hibernate/src/main/java/nettee/hibenate/annotation/SnowflakeGenerated.java
@@ -0,0 +1,16 @@
+package nettee.hibenate.annotation;
+
+import nettee.hibenate.generator.SnowflakeIdGenerator;
+import org.hibernate.annotations.IdGeneratorType;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+@Retention(RUNTIME)
+@Target(FIELD)
+@IdGeneratorType(SnowflakeIdGenerator.class)
+public @interface SnowflakeGenerated {
+}
diff --git a/core/snowflake/nettee-snowflake-id-hibernate/src/main/java/nettee/hibenate/generator/SnowflakeIdGenerator.java b/core/snowflake/nettee-snowflake-id-hibernate/src/main/java/nettee/hibenate/generator/SnowflakeIdGenerator.java
new file mode 100644
index 0000000..44af57f
--- /dev/null
+++ b/core/snowflake/nettee-snowflake-id-hibernate/src/main/java/nettee/hibenate/generator/SnowflakeIdGenerator.java
@@ -0,0 +1,20 @@
+package nettee.hibenate.generator;
+
+import nettee.snowflake.persistence.id.Snowflake;
+import nettee.snowflake.properties.SnowflakeProperties;
+import org.hibernate.engine.spi.SharedSessionContractImplementor;
+import org.hibernate.id.IdentifierGenerator;
+
+public class SnowflakeIdGenerator implements IdentifierGenerator {
+
+ private final Snowflake snowflake;
+
+ public SnowflakeIdGenerator(SnowflakeProperties snowflakeProperties) {
+ this.snowflake = new Snowflake(snowflakeProperties);
+ }
+
+ @Override
+ public Long generate(SharedSessionContractImplementor sharedSessionContractImplementor, Object o) {
+ return snowflake.nextId();
+ }
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..1fd9672
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,89 @@
+# β¬β¬β¬β¬β¬β¬β¬β¬β¬β¬β¬β¬β¬β¬β¬β¬
+# β¬β¬β¬β¬β¬β¬β¬β¬β¬β¬β¬β¬π¦π¦π¦β¬
+# β¬β¬β¬β¬β¬β¬β¬β¬β¬β¬β¬β¬π¦π¦π¦π¦
+# β¬β¬β¬β¬π¦π¦π¦π¦π¦π¦β¬β¬β¬β¬π¦π¦
+# β¬β¬β¬π¦π¦π¦π¦π¦π¦π¦π¦π¦β¬β¬π¦π¦ Gradle properties are classified with 3 categories.
+# β¬β¬π¦β¬π¦π¦π¦π¦π¦π¦β¬π¦π¦π¦π¦π¦
+# β¬π¦π¦π¦β¬π¦β¬π¦π¦π¦π¦π¦π¦π¦π¦β¬ 1. System prop
+# β¬π¦π¦π¦π¦β¬π¦π¦π¦π¦π¦π¦π¦π¦π¦β¬ 2. Gradle config prop
+# π¦π¦π¦π¦π¦π¦π¦π¦π¦π¦π¦π¦π¦β¬β¬β¬ 3. Project prop
+# π¦π¦π¦π¦π¦π¦π¦π¦π¦π¦π¦π¦β¬β¬β¬β¬
+# π¦π¦π¦β¬π¦π¦π¦π¦β¬π¦π¦π¦β¬β¬β¬β¬
+# π¦π¦β¬β¬β¬π¦π¦β¬β¬β¬π¦π¦β¬β¬β¬β¬
+# β¬β¬β¬β¬β¬β¬β¬β¬β¬β¬β¬β¬β¬β¬β¬β¬
+
+## β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’
+## System Properties
+## β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’
+
+# λ΄μ₯λ μμ€ν
κ΄λ ¨ λ³μλ₯Ό μμ ν μ μμ΅λλ€. (λλ¬Όκ² μ¬μ©λλ©°, μΌλ°μ μΌλ‘ μλ΅ν μ μμ΅λλ€.)
+
+# https.protocols=TLSv1.2,TLSv1.3
+# http.proxyHost=www.somehost.org
+# http.proxyPort=8080
+# http.proxyUser=userid
+# http.proxyPassword=password
+# https.proxyHost=...
+# https.proxyPort=...
+# https.proxyUser=...
+# https.proxyPassword=...
+# http.nonProxyHosts=*.nonproxyrepos.com|localhost
+
+## β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’
+## Gradle Configuration Properties
+## β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’
+
+org.gradle.parallel=true
+org.gradle.caching=true
+
+## β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’
+## Project Common Properties
+## β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’
+
+# NOTE project.groupμ μ΄ λ΄μ©μ΄ λ΄κΈ°κΈ° λλ¬Έμ allprojects { group = "..." } νκΈ°λ₯Ό μλ΅ν μ μμ΅λλ€.
+group=me.nettee
+version=0.1.0
+
+## β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’
+## Service Modules
+## β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’β€β’
+
+# article
+article=:article
+articleApi=:article:article-api
+articleDomain=:article:article-api:article-domain
+articleException=:article:article-api:article-exception
+articleReadModel=:article:article-api:article-readmodel
+articleApplication=:article:article-application
+articleRdbAdapter=:article:article-rdb-adapter
+articleWebMvcAdapter=:article:article-webmvc
+
+# board
+board=:board
+boardApi=:board:board-api
+boardDomain=:board:board-api:board-domain
+boardException=:board:board-api:board-exception
+boardReadModel=:board:board-api:board-readmodel
+boardApplication=:board:board-application
+boardRdbAdapter=:board:board-rdb-adapter
+boardWebMvcAdapter=:board:board-webmvc
+
+# comment
+comment=:comment
+commentApi=:comment:comment-api
+commentDomain=:comment:comment-api:comment-domain
+commentException=:comment:comment-api:comment-exception
+commentReadModel=:comment:comment-api:comment-readmodel
+commentApplication=:comment:comment-application
+commentRdbAdapter=:comment:comment-rdb-adapter
+commentWebMvcAdapter=:comment:comment-webmvc
+
+# views
+views=:views
+viewsApi=:views:views-api
+viewsDomain=:views:views-api:views-domain
+viewsException=:views:views-api:views-exception
+viewsReadModel=:views:views-api:views-readmodel
+viewsApplication=:views:views-application
+viewsRdbAdapter=:views:views-rdb-adapter
+viewsWebMvcAdapter=:views:views-webmvc
diff --git a/monolith/main-runner/build.gradle.kts b/monolith/main-runner/build.gradle.kts
index 0098171..27bf804 100644
--- a/monolith/main-runner/build.gradle.kts
+++ b/monolith/main-runner/build.gradle.kts
@@ -1,5 +1,8 @@
import org.springframework.boot.gradle.tasks.bundling.BootJar
+val board: String by project
+val comment: String by project
+
version = "0.0.1-SNAPSHOT"
dependencies {
@@ -7,15 +10,22 @@ dependencies {
implementation(project(":exception-handler-core"))
implementation(project(":jpa-core"))
implementation(project(":cors-webmvc"))
- // service
- implementation(project(":board"))
+
+ // services
+ implementation(project(board))
+ implementation(project(comment))
+
// webmvc
implementation("org.springframework.boot:spring-boot-starter-web")
+
// db
runtimeOnly("org.postgresql:postgresql:42.7.4")
+
// flyway
implementation("org.flywaydb:flyway-database-postgresql")
+
// test
+ testImplementation("com.h2database:h2")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("com.fasterxml.jackson.core:jackson-databind")
testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin")
diff --git a/monolith/main-runner/src/main/resources/application.yml b/monolith/main-runner/src/main/resources/application.yml
index 9fe712b..66934f0 100644
--- a/monolith/main-runner/src/main/resources/application.yml
+++ b/monolith/main-runner/src/main/resources/application.yml
@@ -4,8 +4,10 @@ spring:
config:
import:
- - properties.web/main.cors.yml
+ - properties/web/main.cors.yml
+ - properties/persistence/main.snowflake.yml
- board.yml
+ - comment.yml
server:
port: 5000
diff --git a/monolith/main-runner/src/main/resources/properties/persistence/main.snowflake.yml b/monolith/main-runner/src/main/resources/properties/persistence/main.snowflake.yml
new file mode 100644
index 0000000..b097f61
--- /dev/null
+++ b/monolith/main-runner/src/main/resources/properties/persistence/main.snowflake.yml
@@ -0,0 +1,3 @@
+nettee.persistence.snowflake:
+ datacenter-id: ${SNOWFLAKE_DC_ID:0}
+ worker-id: ${SNOWFLAKE_WORKER_ID:0}
\ No newline at end of file
diff --git a/monolith/main-runner/src/main/resources/properties.web/main.cors.yml b/monolith/main-runner/src/main/resources/properties/web/main.cors.yml
similarity index 100%
rename from monolith/main-runner/src/main/resources/properties.web/main.cors.yml
rename to monolith/main-runner/src/main/resources/properties/web/main.cors.yml
diff --git a/monolith/main-runner/src/test/java/nettee/main/sample/entity/Sample.java b/monolith/main-runner/src/test/java/nettee/main/sample/entity/Sample.java
new file mode 100644
index 0000000..f5d4cdf
--- /dev/null
+++ b/monolith/main-runner/src/test/java/nettee/main/sample/entity/Sample.java
@@ -0,0 +1,10 @@
+package nettee.main.sample.entity;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.Table;
+import nettee.jpa.support.SnowflakeBaseEntity;
+
+@Entity
+@Table(name = "sample")
+public class Sample extends SnowflakeBaseEntity {
+}
diff --git a/monolith/main-runner/src/test/java/nettee/main/sample/persistence/SampleRepository.java b/monolith/main-runner/src/test/java/nettee/main/sample/persistence/SampleRepository.java
new file mode 100644
index 0000000..2accad8
--- /dev/null
+++ b/monolith/main-runner/src/test/java/nettee/main/sample/persistence/SampleRepository.java
@@ -0,0 +1,7 @@
+package nettee.main.sample.persistence;
+
+import nettee.main.sample.entity.Sample;
+import org.springframework.data.repository.CrudRepository;
+
+public interface SampleRepository extends CrudRepository {
+}
\ No newline at end of file
diff --git a/monolith/main-runner/src/test/kotlin/nettee/main/snowflake/SnowflakeTest.kt b/monolith/main-runner/src/test/kotlin/nettee/main/snowflake/SnowflakeTest.kt
new file mode 100644
index 0000000..52b6ae5
--- /dev/null
+++ b/monolith/main-runner/src/test/kotlin/nettee/main/snowflake/SnowflakeTest.kt
@@ -0,0 +1,80 @@
+package nettee.main.snowflake
+
+import io.kotest.core.spec.style.FreeSpec
+import io.kotest.extensions.spring.SpringExtension
+import io.kotest.matchers.collections.shouldHaveSize
+import io.kotest.matchers.longs.shouldBeGreaterThan
+import io.kotest.matchers.shouldBe
+import io.kotest.matchers.shouldNotBe
+import kotlinx.coroutines.*
+import nettee.main.sample.entity.Sample
+import nettee.main.sample.persistence.SampleRepository
+import nettee.snowflake.properties.SnowflakeProperties
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
+import org.springframework.boot.test.context.TestConfiguration
+import org.springframework.context.annotation.Bean
+import java.util.concurrent.ConcurrentHashMap
+
+@DataJpaTest
+class SnowflakeTest(
+ @Autowired val repository: SampleRepository
+) : FreeSpec({
+
+ "[POST] Snowflake ID μ±λ² μ μ±
" - {
+ "Snowflake IDκ° μμ±μ΄ λμ λ" - {
+ val sample = repository.save(Sample())
+
+ "Snowflake ID μ μμ μΌλ‘ μμ±λλ€" {
+ sample.id shouldNotBe null
+ }
+
+ "Snowflake IDκ° μμμ΄λ€" {
+ sample.id shouldBeGreaterThan 0L
+ }
+
+ "Snowflake IDμ ν¬κΈ°λ λΆνΈ λΉνΈλ₯Ό μ μΈν 63bit μ΄λ΄λ₯Ό μΆ©μ‘±νλ€" {
+ // 63λ²μ§Έ λΉνΈκ° κΊΌμ Έ μλμ§(0) νμΈνμ¬, Snowflake IDκ° 63λΉνΈ μ΄λ΄(μμ λ²μ)μμ 보μ₯νλ€.
+ // 0 and 1 => 0 μ΄κ³ 1 and 1 -> 1
+ sample.id and (1L.shl(63)) shouldBe 0L
+ }
+ }
+ }
+
+ "[POST] Snowflake ID λμμ± ν
μ€νΈ" - {
+ "[λ³λ ¬ μ²λ¦¬] 100κ°μ Snowflake IDμ λμ μμ± μμ²" - {
+ // Setμ μ΄μ©νμ¬ ν€λ₯Ό μ μ₯νκ³ μ€λ³΅μλ μ§λ₯Ό νλ¨
+ val concurrentSet = ConcurrentHashMap.newKeySet()
+
+ // μ½λ£¨ν΄ μ€ν
+ val coroutineScope = CoroutineScope(Dispatchers.Default)
+
+ val jobs = List(100) {
+ // λ³λ ¬ μμ
μμ±
+ coroutineScope.launch {
+ // μλ‘μ΄ sample μν°ν° μ μ₯ μμ²
+ val sample = repository.save(Sample())
+ println("Saved id=${sample.id}, time=${System.currentTimeMillis()}")
+ concurrentSet.add(sample.id)
+ }
+ }
+
+ jobs.joinAll()
+
+ "ID μ€λ³΅μ΄ μμ΄μΌ νλ€" {
+ concurrentSet shouldHaveSize 100
+ }
+ }
+ }
+}) {
+ override fun extensions() = listOf(SpringExtension)
+
+ @TestConfiguration
+ class SnowflakeTestConfig {
+
+ @Bean
+ fun snowflakeProperties(): SnowflakeProperties {
+ return SnowflakeProperties(1L, 1L, null)
+ }
+ }
+}
\ No newline at end of file
diff --git a/monolith/main-runner/src/test/resources/application.yml b/monolith/main-runner/src/test/resources/application.yml
new file mode 100644
index 0000000..edd2dbb
--- /dev/null
+++ b/monolith/main-runner/src/test/resources/application.yml
@@ -0,0 +1,38 @@
+spring:
+ application.name: multi-module
+ h2:
+ console:
+ enabled: true
+ path: /h2-console
+ datasource:
+ driver-class-name: org.h2.Driver
+ # https://www.h2database.com/html/features.html#in_memory_databases μ°Έμ‘°
+ url: jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;
+ username: sa
+ password:
+ jpa:
+ generate-ddl: 'true'
+ hibernate:
+ ddl-auto: create
+ properties:
+ hibernate:
+ show_sql: true
+ format_sql: true
+ use_sql_comments: true
+ flyway:
+ baseline-on-migrate: false
+ enabled: false
+
+app.cors.endpoints:
+ - path: "/**"
+ allowed:
+ headers: "*"
+ methods: "*"
+ origins:
+ - http://localhost:3000
+ - https://localhost:3000
+ credentials: true
+ exposed:
+ headers: "*"
+ max-age: 3_600
+
diff --git a/services/board/api/build.gradle.kts b/services/board/api/build.gradle.kts
index 0dcf7c2..1f79251 100644
--- a/services/board/api/build.gradle.kts
+++ b/services/board/api/build.gradle.kts
@@ -1,5 +1,9 @@
+val boardDomain: String by project
+val boardException: String by project
+val boardReadModel: String by project
+
dependencies {
- api(project(":board:api:domain"))
- api(project(":board:api:exception"))
- api(project(":board:api:readmodel"))
+ api(project(boardDomain))
+ api(project(boardException))
+ api(project(boardReadModel))
}
\ No newline at end of file
diff --git a/services/board/application/build.gradle.kts b/services/board/application/build.gradle.kts
index dd0d3a4..0d3814b 100644
--- a/services/board/application/build.gradle.kts
+++ b/services/board/application/build.gradle.kts
@@ -1,3 +1,3 @@
dependencies {
- api(project(":board:api"))
+ api(project(":board:board-api"))
}
\ No newline at end of file
diff --git a/services/board/board.settings.gradle.kts b/services/board/board.settings.gradle.kts
index 46f080a..9550d88 100644
--- a/services/board/board.settings.gradle.kts
+++ b/services/board/board.settings.gradle.kts
@@ -1,3 +1,12 @@
+val board: String by settings
+val boardApi: String by settings
+val boardDomain: String by settings
+val boardException: String by settings
+val boardReadModel: String by settings
+val boardApplication: String by settings
+val boardRdbAdapter: String by settings
+val boardWebMvcAdapter: String by settings
+
fun getDirectories(vararg names: String): (String) -> File {
var dir = rootDir
for (name in names) {
@@ -11,25 +20,25 @@ fun getDirectories(vararg names: String): (String) -> File {
}
}
-val board = getDirectories("services", "board")
+val boardDirectory = getDirectories("services", "board")
// SERVICE/BOARD
include(
- ":board",
- ":board:api",
- ":board:api:domain",
- ":board:api:exception",
- ":board:api:readmodel",
- ":board:application",
- ":board:rdb-adapter",
- ":board:webmvc-adapter",
+ board,
+ boardApi,
+ boardDomain,
+ boardException,
+ boardReadModel,
+ boardApplication,
+ boardRdbAdapter,
+ boardWebMvcAdapter,
)
-project(":board").projectDir = board("board")
-project(":board:api").projectDir = board("api")
-project(":board:api:domain").projectDir = board("domain")
-project(":board:api:exception").projectDir = board("exception")
-project(":board:api:readmodel").projectDir = board("readmodel")
-project(":board:application").projectDir = board("application")
-project(":board:rdb-adapter").projectDir = board("rdb")
-project(":board:webmvc-adapter").projectDir = board("web-mvc")
+project(board).projectDir = boardDirectory("board")
+project(boardApi).projectDir = boardDirectory("api")
+project(boardDomain).projectDir = boardDirectory("domain")
+project(boardException).projectDir = boardDirectory("exception")
+project(boardReadModel).projectDir = boardDirectory("readmodel")
+project(boardApplication).projectDir = boardDirectory("application")
+project(boardRdbAdapter).projectDir = boardDirectory("rdb")
+project(boardWebMvcAdapter).projectDir = boardDirectory("web-mvc")
diff --git a/services/board/build.gradle.kts b/services/board/build.gradle.kts
index 3565f94..5f9bb4b 100644
--- a/services/board/build.gradle.kts
+++ b/services/board/build.gradle.kts
@@ -1,6 +1,11 @@
+val boardApi: String by project
+val boardApplication: String by project
+val boardRdbAdapter: String by project
+val boardWebMvcAdapter: String by project
+
dependencies {
- api(project(":board:api"))
- api(project(":board:application"))
- api(project(":board:rdb-adapter"))
- api(project(":board:webmvc-adapter"))
+ api(project(boardApi))
+ api(project(boardApplication))
+ api(project(boardRdbAdapter))
+ api(project(boardWebMvcAdapter))
}
\ No newline at end of file
diff --git a/services/board/driven/rdb/build.gradle.kts b/services/board/driven/rdb/build.gradle.kts
index 9617183..34e30c1 100644
--- a/services/board/driven/rdb/build.gradle.kts
+++ b/services/board/driven/rdb/build.gradle.kts
@@ -1,8 +1,8 @@
dependencies {
val bom = dependencyManagement.importedProperties
- api(project(":board:api"))
- api(project(":board:application"))
+ api(project(":board:board-api"))
+ api(project(":board:board-application"))
api(project(":jpa-core"))
// spring
diff --git a/services/board/driven/rdb/src/main/resources/properties/db/board.database-local.yml b/services/board/driven/rdb/src/main/resources/properties/db/board.database-local.yml
index 2532871..5e721ef 100644
--- a/services/board/driven/rdb/src/main/resources/properties/db/board.database-local.yml
+++ b/services/board/driven/rdb/src/main/resources/properties/db/board.database-local.yml
@@ -1,5 +1,11 @@
spring:
datasource:
+ driver-class-name: org.postgresql.Driver
url: ${BOARD_POSTGRESQL_URL:jdbc:postgresql://localhost:5433/demo}
username: ${BOARD_POSTGRESQL_USERNAME:root}
password: ${BOARD_POSTGRESQL_PASSWORD:root}
+
+ flyway:
+ baseline-on-migrate: true
+ locations:
+ - db/postgresql/migration/v1_0
\ No newline at end of file
diff --git a/services/board/driving/web-mvc/build.gradle.kts b/services/board/driving/web-mvc/build.gradle.kts
index 1f453c7..19a4629 100644
--- a/services/board/driving/web-mvc/build.gradle.kts
+++ b/services/board/driving/web-mvc/build.gradle.kts
@@ -1,6 +1,6 @@
dependencies {
- api(project(":board:api"))
- api(project(":board:application"))
+ api(project(":board:board-api"))
+ api(project(":board:board-application"))
// validation
compileOnly("jakarta.validation:jakarta.validation-api")
diff --git a/services/board/driving/web-mvc/src/main/java/nettee/board/web/BoardCommandApi.java b/services/board/driving/web-mvc/src/main/java/nettee/board/web/BoardCommandApi.java
index 8e5e54b..2f26934 100644
--- a/services/board/driving/web-mvc/src/main/java/nettee/board/web/BoardCommandApi.java
+++ b/services/board/driving/web-mvc/src/main/java/nettee/board/web/BoardCommandApi.java
@@ -15,7 +15,7 @@
import org.springframework.web.bind.annotation.RestController;
@RestController
-@RequestMapping("boards")
+@RequestMapping("/boards")
@RequiredArgsConstructor
public class BoardCommandApi {
private final BoardCreateUseCase boardCreateUseCase;
diff --git a/services/comment/api/build.gradle.kts b/services/comment/api/build.gradle.kts
new file mode 100644
index 0000000..f334790
--- /dev/null
+++ b/services/comment/api/build.gradle.kts
@@ -0,0 +1,9 @@
+val commentDomain: String by project
+val commentException: String by project
+val commentReadModel: String by project
+
+dependencies {
+ api(project(commentDomain))
+ api(project(commentException))
+ api(project(commentReadModel))
+}
\ No newline at end of file
diff --git a/services/comment/api/domain/src/main/java/nettee/comment/domain/Comment.java b/services/comment/api/domain/src/main/java/nettee/comment/domain/Comment.java
new file mode 100644
index 0000000..50547c3
--- /dev/null
+++ b/services/comment/api/domain/src/main/java/nettee/comment/domain/Comment.java
@@ -0,0 +1,45 @@
+package nettee.comment.domain;
+
+import java.time.Instant;
+import java.util.Objects;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import nettee.comment.domain.type.CommentStatus;
+
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Comment {
+
+ private Long id;
+
+ private Long boardId;
+
+ private String content;
+
+ private CommentStatus status;
+
+ private Instant createdAt;
+
+ private Instant updatedAt;
+
+ @Builder(
+ builderClassName = "updateCommentBuilder",
+ builderMethodName = "prepareUpdate",
+ buildMethodName = "update"
+ )
+ public void update(String content) {
+ Objects.requireNonNull(content, "content cannot be null");
+
+ this.content = content;
+ this.updatedAt = Instant.now();
+ }
+
+ public void softDelete() {
+ this.status = CommentStatus.REMOVED;
+ }
+
+}
diff --git a/services/comment/api/domain/src/main/java/nettee/comment/domain/type/CommentStatus.java b/services/comment/api/domain/src/main/java/nettee/comment/domain/type/CommentStatus.java
new file mode 100644
index 0000000..e8e64e6
--- /dev/null
+++ b/services/comment/api/domain/src/main/java/nettee/comment/domain/type/CommentStatus.java
@@ -0,0 +1,18 @@
+package nettee.comment.domain.type;
+
+import java.util.EnumSet;
+import java.util.Set;
+
+public enum CommentStatus {
+
+ PENDING,
+ ACTIVE,
+ REMOVED;
+
+ private static final Set GENERAL_QUERY_STATUS = EnumSet.of(ACTIVE);
+
+ public static Set getGeneralQueryStatus() {
+ return GENERAL_QUERY_STATUS;
+ }
+
+}
diff --git a/services/comment/api/domain/src/main/java/nettee/reply/domain/Reply.java b/services/comment/api/domain/src/main/java/nettee/reply/domain/Reply.java
new file mode 100644
index 0000000..4830c8e
--- /dev/null
+++ b/services/comment/api/domain/src/main/java/nettee/reply/domain/Reply.java
@@ -0,0 +1,44 @@
+package nettee.reply.domain;
+
+import java.time.Instant;
+import java.util.Objects;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import nettee.reply.domain.type.ReplyStatus;
+
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Reply {
+
+ private Long id;
+
+ private Long commentId;
+
+ private String content;
+
+ private ReplyStatus status;
+
+ private Instant createdAt;
+
+ private Instant updatedAt;
+
+ @Builder(
+ builderClassName = "updateReplyBuilder",
+ builderMethodName = "prepareUpdate",
+ buildMethodName = "update"
+ )
+ public void update(String content) {
+ Objects.requireNonNull(content, "content cannot be null");
+
+ this.content = content;
+ this.updatedAt = Instant.now();
+ }
+
+ public void softDelete() {
+ this.status = ReplyStatus.REMOVED;
+ }
+}
diff --git a/services/comment/api/domain/src/main/java/nettee/reply/domain/type/ReplyStatus.java b/services/comment/api/domain/src/main/java/nettee/reply/domain/type/ReplyStatus.java
new file mode 100644
index 0000000..90e9e9a
--- /dev/null
+++ b/services/comment/api/domain/src/main/java/nettee/reply/domain/type/ReplyStatus.java
@@ -0,0 +1,18 @@
+package nettee.reply.domain.type;
+
+import java.util.EnumSet;
+import java.util.Set;
+
+public enum ReplyStatus {
+
+ PENDING,
+ ACTIVE,
+ REMOVED;
+
+ private static final Set GENERAL_QUERY_STATUS = EnumSet.of(ACTIVE);
+
+ public static Set getGeneralQueryStatus() {
+ return GENERAL_QUERY_STATUS;
+ }
+
+}
diff --git a/services/comment/api/exception/src/main/java/nettee/comment/exception/CommentCommandErrorCode.java b/services/comment/api/exception/src/main/java/nettee/comment/exception/CommentCommandErrorCode.java
new file mode 100644
index 0000000..5061370
--- /dev/null
+++ b/services/comment/api/exception/src/main/java/nettee/comment/exception/CommentCommandErrorCode.java
@@ -0,0 +1,62 @@
+package nettee.comment.exception;
+
+import java.util.Map;
+import java.util.function.Supplier;
+import nettee.common.ErrorCode;
+import org.springframework.http.HttpStatus;
+
+public enum CommentCommandErrorCode implements ErrorCode {
+ COMMENT_NOT_FOUND("λκΈμ μ°Ύμ μ μμ΅λλ€.", HttpStatus.NOT_FOUND),
+ COMMENT_GONE("λ μ΄μ μ‘΄μ¬νμ§ μλ λκΈμ
λλ€.", HttpStatus.GONE),
+ COMMENT_FORBIDDEN("κΆνμ΄ μμ΅λλ€.", HttpStatus.FORBIDDEN),
+ DEFAULT("λκΈ μ‘°μ μ€λ₯", HttpStatus.INTERNAL_SERVER_ERROR),
+ COMMENT_ALREADY_EXIST("λκΈμ΄ μ΄λ―Έ μ‘΄μ¬ν©λλ€.", HttpStatus.CONFLICT);
+
+ private final String message;
+ private final HttpStatus httpStatus;
+
+ CommentCommandErrorCode(String message, HttpStatus httpStatus) {
+ this.message = message;
+ this.httpStatus = httpStatus;
+ }
+
+ @Override
+ public String message() {
+ return message;
+ }
+
+ @Override
+ public HttpStatus httpStatus() {
+ return httpStatus;
+ }
+
+ @Override
+ public CommentCommandException exception() {
+ return new CommentCommandException(this);
+ }
+
+ @Override
+ public CommentCommandException exception(Throwable cause) {
+ return new CommentCommandException(this, cause);
+ }
+
+ @Override
+ public RuntimeException exception(Runnable runnable) {
+ return new CommentCommandException(this, runnable);
+ }
+
+ @Override
+ public RuntimeException exception(Runnable runnable, Throwable cause) {
+ return new CommentCommandException(this, runnable, cause);
+ }
+
+ @Override
+ public RuntimeException exception(Supplier