diff --git a/build.gradle b/build.gradle index 044a1d04..9b9954c4 100644 --- a/build.gradle +++ b/build.gradle @@ -65,6 +65,7 @@ project(":layer-api") { implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.boot:spring-boot-starter-validation' //== jwt ==// implementation 'io.jsonwebtoken:jjwt-api:0.12.5' @@ -78,11 +79,14 @@ project(":layer-api") { implementation 'org.springframework.boot:spring-boot-starter-data-redis' testImplementation 'org.springframework.boot:spring-boot-starter-test' + + // swagger + implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0") + // jpa implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - //Swagger - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' + // mysql runtimeOnly 'com.mysql:mysql-connector-j' } diff --git a/layer-api/src/main/java/org/layer/LayerApplication.java b/layer-api/src/main/java/org/layer/LayerApplication.java index 2ca6cb47..86ed6aaa 100644 --- a/layer-api/src/main/java/org/layer/LayerApplication.java +++ b/layer-api/src/main/java/org/layer/LayerApplication.java @@ -3,8 +3,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @SpringBootApplication +@EnableJpaAuditing public class LayerApplication { public static void main(String[] args) { SpringApplication.run(LayerApplication.class, args); diff --git a/layer-api/src/main/java/org/layer/config/ObjectMapperConfig.java b/layer-api/src/main/java/org/layer/config/ObjectMapperConfig.java new file mode 100644 index 00000000..7047bd05 --- /dev/null +++ b/layer-api/src/main/java/org/layer/config/ObjectMapperConfig.java @@ -0,0 +1,29 @@ +package org.layer.config; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ObjectMapperConfig { + + @Bean + public ObjectMapper objectMapper(){ + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModule(new Jdk8Module()); + objectMapper.registerModule(new JavaTimeModule()); + + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false); + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + + // 카멜 케이스 + objectMapper.setPropertyNamingStrategy(new PropertyNamingStrategies.LowerCamelCaseStrategy()); + return objectMapper; + + } +} \ No newline at end of file diff --git a/layer-api/src/main/java/org/layer/filter/LoggerFilter.java b/layer-api/src/main/java/org/layer/filter/LoggerFilter.java new file mode 100644 index 00000000..67e72978 --- /dev/null +++ b/layer-api/src/main/java/org/layer/filter/LoggerFilter.java @@ -0,0 +1,65 @@ +package org.layer.filter; + +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.util.ContentCachingRequestWrapper; +import org.springframework.web.util.ContentCachingResponseWrapper; + +import java.io.IOException; + +@Component +@Slf4j +public class LoggerFilter implements Filter { + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + var req = new ContentCachingRequestWrapper((HttpServletRequest) request); + var res = new ContentCachingResponseWrapper((HttpServletResponse) response); + + chain.doFilter(req,res); + + // request 정보 + var headerNames = req.getHeaderNames(); + var headerValues = new StringBuilder(); + + headerNames.asIterator().forEachRemaining(headerKey -> { + var headerValue = req.getHeader(headerKey); + + headerValues + .append("[") + .append(headerKey) + .append(" : ") + .append(headerValue) + .append(" , ") + .append("] "); + }); + var requestBody = new String(req.getContentAsByteArray()); + var uri = req.getRequestURI(); + var method = req.getMethod(); + + log.info("[REQUEST] uri : {} , method : {} , header : {} , body : {}" ,uri,method, headerValues, requestBody); + + // response + var responseHeaderValues = new StringBuilder(); + res.getHeaderNames().forEach(headerKey -> { + var headerValue = res.getHeader(headerKey); + responseHeaderValues + .append("[") + .append(headerKey) + .append(" : ") + .append(headerValue) + .append(" ,") + .append("] "); + }); + + // var responseBody = new String(res.getContentAsByteArray()); + + log.info("[RESPONSE] uri : {} , method : {} , header : {} , body : {}",uri,method, responseHeaderValues); + + res.copyBodyToResponse(); + } +} + diff --git a/layer-api/src/main/java/org/layer/retrospect/controller/RetrospectApi.java b/layer-api/src/main/java/org/layer/retrospect/controller/RetrospectApi.java new file mode 100644 index 00000000..1e5515ca --- /dev/null +++ b/layer-api/src/main/java/org/layer/retrospect/controller/RetrospectApi.java @@ -0,0 +1,18 @@ +package org.layer.retrospect.controller; + +import org.layer.retrospect.controller.dto.request.RetrospectCreateRequest; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; + +@Tag(name = "회고", description = "회고 관련 API") +public interface RetrospectApi { + + @Operation(summary = "회고 생성", description = "") + ResponseEntity createRetrospect(@PathVariable("spaceId") Long spaceId, + @RequestBody @Valid RetrospectCreateRequest request); +} diff --git a/layer-api/src/main/java/org/layer/retrospect/controller/RetrospectController.java b/layer-api/src/main/java/org/layer/retrospect/controller/RetrospectController.java new file mode 100644 index 00000000..b5378da8 --- /dev/null +++ b/layer-api/src/main/java/org/layer/retrospect/controller/RetrospectController.java @@ -0,0 +1,30 @@ +package org.layer.retrospect.controller; + +import org.layer.retrospect.controller.dto.request.RetrospectCreateRequest; +import org.layer.retrospect.service.RetrospectService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/space/{spaceId}/retrospect") +public class RetrospectController implements RetrospectApi { + + private final RetrospectService retrospectService; + + @Override + @PostMapping + public ResponseEntity createRetrospect(@PathVariable("spaceId") Long spaceId, + @RequestBody @Valid RetrospectCreateRequest request) { + + retrospectService.create(spaceId, request.formId(), request.title(), request.introduction()); + return ResponseEntity.ok(null); + } +} diff --git a/layer-api/src/main/java/org/layer/retrospect/controller/dto/request/RetrospectCreateRequest.java b/layer-api/src/main/java/org/layer/retrospect/controller/dto/request/RetrospectCreateRequest.java new file mode 100644 index 00000000..bdbcd9ff --- /dev/null +++ b/layer-api/src/main/java/org/layer/retrospect/controller/dto/request/RetrospectCreateRequest.java @@ -0,0 +1,20 @@ +package org.layer.retrospect.controller.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +@Schema(name = "RetrospectCreateRequest", description = "회고 생성 요청 Dto") +public record RetrospectCreateRequest( + @Schema(description = "회고 폼 id", example = "1") + @NotNull + Long formId, + @Schema(description = "회고 제목", example = "중간 발표 이후 회고") + @Size(min = 3) + String title, + @Schema(description = "회고 한줄 설명", example = "우리만의 KPT 회고") + @NotNull + String introduction + +) { +} diff --git a/layer-api/src/main/java/org/layer/retrospect/service/RetrospectService.java b/layer-api/src/main/java/org/layer/retrospect/service/RetrospectService.java new file mode 100644 index 00000000..2880e2a2 --- /dev/null +++ b/layer-api/src/main/java/org/layer/retrospect/service/RetrospectService.java @@ -0,0 +1,27 @@ +package org.layer.retrospect.service; + +import org.layer.domain.retrospect.entity.Retrospect; +import org.layer.domain.retrospect.entity.RetrospectStatus; +import org.layer.domain.retrospect.repository.RetrospectRepository; +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class RetrospectService { + + private final RetrospectRepository retrospectRepository; + + public void create(Long spaceId, Long formId, String title, String introduction){ + Retrospect retrospect = Retrospect.builder() + .title(title) + .formId(formId) + .spaceId(spaceId) + .introduction(introduction) + .retrospectStatus(RetrospectStatus.PROCEEDING) + .build(); + + retrospectRepository.save(retrospect); + } +} diff --git a/layer-api/src/main/java/org/layer/retrospect/service/dto/RetrospectCreateServiceRequest.java b/layer-api/src/main/java/org/layer/retrospect/service/dto/RetrospectCreateServiceRequest.java new file mode 100644 index 00000000..a8677ac5 --- /dev/null +++ b/layer-api/src/main/java/org/layer/retrospect/service/dto/RetrospectCreateServiceRequest.java @@ -0,0 +1,11 @@ +package org.layer.retrospect.service.dto; + +import java.util.List; + +public record RetrospectCreateServiceRequest( + String title, + Long spaceId, + List questions, + boolean isMyForm +) { +} diff --git a/layer-domain/src/main/java/org/layer/domain/BaseEntity.java b/layer-domain/src/main/java/org/layer/domain/BaseEntity.java new file mode 100644 index 00000000..8eb74dba --- /dev/null +++ b/layer-domain/src/main/java/org/layer/domain/BaseEntity.java @@ -0,0 +1,25 @@ +package org.layer.domain; + +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import java.time.LocalDateTime; + +@MappedSuperclass +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @CreatedDate + private LocalDateTime createdAt; + + @LastModifiedDate + private LocalDateTime updatedAt; +} \ No newline at end of file diff --git a/layer-domain/src/main/java/org/layer/domain/block/converter/BlockStyleConverter.java b/layer-domain/src/main/java/org/layer/domain/block/converter/BlockStyleConverter.java new file mode 100644 index 00000000..0607ba2a --- /dev/null +++ b/layer-domain/src/main/java/org/layer/domain/block/converter/BlockStyleConverter.java @@ -0,0 +1,26 @@ +package org.layer.domain.block.converter; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import org.layer.domain.block.enums.BlockType; + + +import java.util.stream.Stream; + +@Converter +public class BlockStyleConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(BlockType blockType) { + return blockType.getStyle(); + } + + @Override + public BlockType convertToEntityAttribute(String blockStyle) { + if(blockStyle == null){ + return null; + } + return Stream.of(BlockType.values()).filter(t -> t.getStyle().equals(blockStyle)).findFirst() + .orElseThrow(IllegalArgumentException::new); + } +} diff --git a/layer-domain/src/main/java/org/layer/domain/block/entity/Block.java b/layer-domain/src/main/java/org/layer/domain/block/entity/Block.java new file mode 100644 index 00000000..cfaf4317 --- /dev/null +++ b/layer-domain/src/main/java/org/layer/domain/block/entity/Block.java @@ -0,0 +1,36 @@ +package org.layer.domain.block.entity; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import lombok.*; +import org.layer.domain.BaseEntity; +import org.layer.domain.block.converter.BlockStyleConverter; +import org.layer.domain.block.enums.BlockType; +import org.layer.domain.blockOption.entity.BlockOption; + +import java.util.HashSet; +import java.util.Set; + +@Getter +@Entity +@EqualsAndHashCode(callSuper = true) +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Block extends BaseEntity { + + /** + * Form RelationId + */ + @NotNull + private Long formId; + + private String label; + + @Column(length = 20) + @NotNull + @Convert(converter = BlockStyleConverter.class) + private BlockType style; + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "block", cascade = CascadeType.ALL, orphanRemoval = true) + private Set options = new HashSet<>(); +} diff --git a/layer-domain/src/main/java/org/layer/domain/block/enums/BlockType.java b/layer-domain/src/main/java/org/layer/domain/block/enums/BlockType.java new file mode 100644 index 00000000..5b8e0c74 --- /dev/null +++ b/layer-domain/src/main/java/org/layer/domain/block/enums/BlockType.java @@ -0,0 +1,35 @@ +package org.layer.domain.block.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum BlockType { + /** + * 질문(입력) 블록 종류 + * 1. 짧은 인풋 + * 2. 마크다운 + * 3. 레인저 + * 4. 콤보 박스 + * 5. 카드 + * 6. 숫자 인풋 + * + * 질문 블록 중 이산적인 블록 종류 + * 1. 레인저( 숫자 ) + * 2. 콤보 박스 + * 3. 카드 + * + */ + + PLAIN_TEXT("짧은 입력","plain_text","single"), + MARKDOWN("마크다운 입력","markdown","single"), + RANGER("범위 지정","range","multi"), + COMBOBOX("콤보 박스","combobox","multi"), + CARD("카드 선택 입력","card","multi"), + NUMBER("숫자 입력","number","single"); + + private String description; + private String style; + private String type; +} diff --git a/layer-domain/src/main/java/org/layer/domain/blockOption/entity/BlockOption.java b/layer-domain/src/main/java/org/layer/domain/blockOption/entity/BlockOption.java new file mode 100644 index 00000000..d8291454 --- /dev/null +++ b/layer-domain/src/main/java/org/layer/domain/blockOption/entity/BlockOption.java @@ -0,0 +1,29 @@ +package org.layer.domain.blockOption.entity; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.layer.domain.BaseEntity; +import org.layer.domain.block.entity.Block; + +@Entity +@Table(name= "block_option", uniqueConstraints = { + @UniqueConstraint(columnNames = {"block_id", "value"}) +}) +@EqualsAndHashCode(callSuper = true) +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class BlockOption extends BaseEntity { + + private String label; + + @NotNull + private String value; + + @ManyToOne + @JoinColumn( name = "block_id") + private Block block; +} \ No newline at end of file diff --git a/layer-domain/src/main/java/org/layer/domain/common/BaseTimeEntity.java b/layer-domain/src/main/java/org/layer/domain/common/BaseTimeEntity.java new file mode 100644 index 00000000..19dc5b08 --- /dev/null +++ b/layer-domain/src/main/java/org/layer/domain/common/BaseTimeEntity.java @@ -0,0 +1,23 @@ +package org.layer.domain.common; + +import java.time.LocalDateTime; + +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; + +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +@Getter +public abstract class BaseTimeEntity { + + @CreatedDate + public LocalDateTime createdAt; + + @LastModifiedDate + public LocalDateTime updatedAt; +} \ No newline at end of file diff --git a/layer-domain/src/main/java/org/layer/domain/form/converter/FormPublishedByConverter.java b/layer-domain/src/main/java/org/layer/domain/form/converter/FormPublishedByConverter.java new file mode 100644 index 00000000..82905a02 --- /dev/null +++ b/layer-domain/src/main/java/org/layer/domain/form/converter/FormPublishedByConverter.java @@ -0,0 +1,25 @@ +package org.layer.domain.form.converter; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import org.layer.domain.form.enums.FormPublishedBy; + +import java.util.stream.Stream; + +@Converter +public class FormPublishedByConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(FormPublishedBy formPublishType) { + return formPublishType.getCode(); + } + + @Override + public FormPublishedBy convertToEntityAttribute(final String code) { + if(code == null){ + return null; + } + return Stream.of(FormPublishedBy.values()).filter(t -> t.getCode().equals(code)).findFirst() + .orElseThrow(IllegalArgumentException::new); + } +} diff --git a/layer-domain/src/main/java/org/layer/domain/form/entity/Form.java b/layer-domain/src/main/java/org/layer/domain/form/entity/Form.java index e04e0d0e..12618c32 100644 --- a/layer-domain/src/main/java/org/layer/domain/form/entity/Form.java +++ b/layer-domain/src/main/java/org/layer/domain/form/entity/Form.java @@ -1,27 +1,32 @@ package org.layer.domain.form.entity; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; +import jakarta.persistence.*; import jakarta.validation.constraints.NotNull; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; +import org.hibernate.annotations.ColumnDefault; +import org.layer.domain.BaseEntity; +import org.layer.domain.form.converter.FormPublishedByConverter; +import org.layer.domain.form.enums.FormPublishedBy; + @Getter @Entity +@EqualsAndHashCode(callSuper = true) +@AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Form { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; +public class Form extends BaseEntity { private Long memberId; @NotNull private String title; + @Column(length = 10) + @ColumnDefault("1") + @Convert(converter = FormPublishedByConverter.class) + @NotNull + private FormPublishedBy formPublishedBy; + private String introduction; } diff --git a/layer-domain/src/main/java/org/layer/domain/form/enums/FormPublishedBy.java b/layer-domain/src/main/java/org/layer/domain/form/enums/FormPublishedBy.java new file mode 100644 index 00000000..58151fb3 --- /dev/null +++ b/layer-domain/src/main/java/org/layer/domain/form/enums/FormPublishedBy.java @@ -0,0 +1,16 @@ +package org.layer.domain.form.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum FormPublishedBy { + + ADMIN("관리자","0"), + PERSONAL("개인","1"); + ; + private String description; + private String code; + +} diff --git a/layer-domain/src/main/java/org/layer/domain/retrospect/entity/Retrospect.java b/layer-domain/src/main/java/org/layer/domain/retrospect/entity/Retrospect.java index 91d1135d..7f57b696 100644 --- a/layer-domain/src/main/java/org/layer/domain/retrospect/entity/Retrospect.java +++ b/layer-domain/src/main/java/org/layer/domain/retrospect/entity/Retrospect.java @@ -1,10 +1,15 @@ package org.layer.domain.retrospect.entity; +import org.layer.domain.common.BaseTimeEntity; + import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import jakarta.validation.constraints.NotNull; @@ -12,7 +17,7 @@ @Getter @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Retrospect { +public class Retrospect extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -30,6 +35,15 @@ public class Retrospect { private String introduction; @NotNull + @Enumerated(EnumType.STRING) private RetrospectStatus retrospectStatus; + @Builder + public Retrospect(Long spaceId, Long formId, String title, String introduction, RetrospectStatus retrospectStatus) { + this.spaceId = spaceId; + this.formId = formId; + this.title = title; + this.introduction = introduction; + this.retrospectStatus = retrospectStatus; + } } diff --git a/layer-external/src/main/java/org/layer/Main.java b/layer-external/src/main/java/org/layer/Main.java deleted file mode 100644 index 33f0b2a8..00000000 --- a/layer-external/src/main/java/org/layer/Main.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.layer; - -public class Main { - public static void main(String[] args) { - System.out.println("Hello world!"); - } -} \ No newline at end of file diff --git a/layer-external/src/main/java/org/layer/external/dummy.txt b/layer-external/src/main/java/org/layer/external/dummy.txt new file mode 100644 index 00000000..e69de29b