diff --git a/backend/build.gradle b/backend/build.gradle index 79ee2194d..b99c3cc1d 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -20,15 +20,17 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-validation' + + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' compileOnly 'org.projectlombok:lombok:0.11.0' annotationProcessor 'org.projectlombok:lombok' - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' - runtimeOnly 'com.mysql:mysql-connector-j:9.0.0' testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'io.rest-assured:rest-assured:5.5.0' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/backend/src/main/java/codezap/extension/domain/Extension.java b/backend/src/main/java/codezap/extension/domain/Extension.java deleted file mode 100644 index 6bd51b9d3..000000000 --- a/backend/src/main/java/codezap/extension/domain/Extension.java +++ /dev/null @@ -1,36 +0,0 @@ -package codezap.extension.domain; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -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 codezap.global.domain.BaseTimeEntity; -import codezap.language.domain.Language; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Table(name = "extension") -@Getter -@AllArgsConstructor -@NoArgsConstructor -public class Extension extends BaseTimeEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "language_id", nullable = false) - private Language language; - - @Column(nullable = false, unique = true) - private String name; -} diff --git a/backend/src/main/java/codezap/extension/repository/ExtensionRepository.java b/backend/src/main/java/codezap/extension/repository/ExtensionRepository.java deleted file mode 100644 index 276017103..000000000 --- a/backend/src/main/java/codezap/extension/repository/ExtensionRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package codezap.extension.repository; - -import java.util.Optional; - -import org.springframework.data.jpa.repository.JpaRepository; - -import codezap.extension.domain.Extension; - -public interface ExtensionRepository extends JpaRepository { - Optional findByName(String name); -} diff --git a/backend/src/main/java/codezap/global/domain/BaseTimeEntity.java b/backend/src/main/java/codezap/global/auditing/BaseTimeEntity.java similarity index 95% rename from backend/src/main/java/codezap/global/domain/BaseTimeEntity.java rename to backend/src/main/java/codezap/global/auditing/BaseTimeEntity.java index 3ee8b0548..9afcea565 100644 --- a/backend/src/main/java/codezap/global/domain/BaseTimeEntity.java +++ b/backend/src/main/java/codezap/global/auditing/BaseTimeEntity.java @@ -1,4 +1,4 @@ -package codezap.global.domain; +package codezap.global.auditing; import java.time.LocalDateTime; diff --git a/backend/src/main/java/codezap/global/domain/JpaAuditingConfiguration.java b/backend/src/main/java/codezap/global/auditing/JpaAuditingConfiguration.java similarity index 86% rename from backend/src/main/java/codezap/global/domain/JpaAuditingConfiguration.java rename to backend/src/main/java/codezap/global/auditing/JpaAuditingConfiguration.java index 871e8f59d..eaf811964 100644 --- a/backend/src/main/java/codezap/global/domain/JpaAuditingConfiguration.java +++ b/backend/src/main/java/codezap/global/auditing/JpaAuditingConfiguration.java @@ -1,4 +1,4 @@ -package codezap.global.domain; +package codezap.global.auditing; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; diff --git a/backend/src/main/java/codezap/global/WebCorsConfiguration.java b/backend/src/main/java/codezap/global/cors/WebCorsConfiguration.java similarity index 94% rename from backend/src/main/java/codezap/global/WebCorsConfiguration.java rename to backend/src/main/java/codezap/global/cors/WebCorsConfiguration.java index f2a8dcbd7..b69d3863d 100644 --- a/backend/src/main/java/codezap/global/WebCorsConfiguration.java +++ b/backend/src/main/java/codezap/global/cors/WebCorsConfiguration.java @@ -1,4 +1,4 @@ -package codezap.global; +package codezap.global.cors; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; diff --git a/backend/src/main/java/codezap/global/exception/CodeZapException.java b/backend/src/main/java/codezap/global/exception/CodeZapException.java new file mode 100644 index 000000000..71af97c34 --- /dev/null +++ b/backend/src/main/java/codezap/global/exception/CodeZapException.java @@ -0,0 +1,15 @@ +package codezap.global.exception; + +import org.springframework.http.HttpStatusCode; + +import lombok.Getter; + +@Getter +public class CodeZapException extends RuntimeException { + private final HttpStatusCode httpStatusCode; + + public CodeZapException(HttpStatusCode httpStatusCode, String message) { + super(message); + this.httpStatusCode = httpStatusCode; + } +} diff --git a/backend/src/main/java/codezap/global/exception/GlobalExceptionHandler.java b/backend/src/main/java/codezap/global/exception/GlobalExceptionHandler.java new file mode 100644 index 000000000..f6d20813c --- /dev/null +++ b/backend/src/main/java/codezap/global/exception/GlobalExceptionHandler.java @@ -0,0 +1,49 @@ +package codezap.global.exception; + +import java.util.stream.Collectors; + +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ProblemDetail; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler + public ResponseEntity handleCodeZapException(CodeZapException codeZapException) { + return ResponseEntity.status(codeZapException.getHttpStatusCode()) + .body(ProblemDetail.forStatusAndDetail( + codeZapException.getHttpStatusCode(), + codeZapException.getMessage()) + ); + } + + @ExceptionHandler + public ResponseEntity handleMethodArgumentNotValidException( + MethodArgumentNotValidException exception + ) { + BindingResult bindingResult = exception.getBindingResult(); + return ResponseEntity.badRequest() + .body(ProblemDetail.forStatusAndDetail( + HttpStatus.BAD_REQUEST, + bindingResult.getFieldErrors().stream() + .map(DefaultMessageSourceResolvable::getDefaultMessage) + .collect(Collectors.joining()) + ) + ); + } + + @ExceptionHandler + public ResponseEntity handleException(Exception exception) { + return ResponseEntity.internalServerError() + .body(ProblemDetail.forStatusAndDetail( + HttpStatus.INTERNAL_SERVER_ERROR, + "서버에서 예상치 못한 오류가 발생하였습니다.") + ); + } +} diff --git a/backend/src/main/java/codezap/global/DateTimeFormatConfiguration.java b/backend/src/main/java/codezap/global/serialization/DateTimeFormatConfiguration.java similarity index 65% rename from backend/src/main/java/codezap/global/DateTimeFormatConfiguration.java rename to backend/src/main/java/codezap/global/serialization/DateTimeFormatConfiguration.java index eae2bf8ce..2ea85414c 100644 --- a/backend/src/main/java/codezap/global/DateTimeFormatConfiguration.java +++ b/backend/src/main/java/codezap/global/serialization/DateTimeFormatConfiguration.java @@ -1,4 +1,4 @@ -package codezap.global; +package codezap.global.serialization; import java.time.format.DateTimeFormatter; import java.util.TimeZone; @@ -15,10 +15,7 @@ public class DateTimeFormatConfiguration { @Bean public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() { - return jacksonObjectMapperBuilder -> { - jacksonObjectMapperBuilder.timeZone(TimeZone.getTimeZone("UTC")); - jacksonObjectMapperBuilder.serializers( - new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT))); - }; + return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.serializers( + new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT))); } } diff --git a/backend/src/main/java/codezap/global/SpringDocConfiguration.java b/backend/src/main/java/codezap/global/swagger/SpringDocConfiguration.java similarity index 95% rename from backend/src/main/java/codezap/global/SpringDocConfiguration.java rename to backend/src/main/java/codezap/global/swagger/SpringDocConfiguration.java index 60c354b62..5d0df91bb 100644 --- a/backend/src/main/java/codezap/global/SpringDocConfiguration.java +++ b/backend/src/main/java/codezap/global/swagger/SpringDocConfiguration.java @@ -1,4 +1,4 @@ -package codezap.global; +package codezap.global.swagger; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/backend/src/main/java/codezap/language/domain/Language.java b/backend/src/main/java/codezap/language/domain/Language.java deleted file mode 100644 index cddc03d48..000000000 --- a/backend/src/main/java/codezap/language/domain/Language.java +++ /dev/null @@ -1,28 +0,0 @@ -package codezap.language.domain; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Table; - -import codezap.global.domain.BaseTimeEntity; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Table(name = "language") -@Getter -@NoArgsConstructor -@AllArgsConstructor -public class Language extends BaseTimeEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false) - private String name; -} diff --git a/backend/src/main/java/codezap/language/repository/LanguageRepository.java b/backend/src/main/java/codezap/language/repository/LanguageRepository.java deleted file mode 100644 index 1fcd4a0aa..000000000 --- a/backend/src/main/java/codezap/language/repository/LanguageRepository.java +++ /dev/null @@ -1,18 +0,0 @@ -package codezap.language.repository; - -import java.util.NoSuchElementException; -import java.util.Optional; - -import org.springframework.data.jpa.repository.JpaRepository; - -import codezap.language.domain.Language; - -public interface LanguageRepository extends JpaRepository { - - Optional findByName(String name); - - default Language getByName(String name) { - return findByName(name).orElseThrow( - () -> new NoSuchElementException(name + " 언어가 존재하지 않습니다.")); - } -} diff --git a/backend/src/main/java/codezap/member/domain/Member.java b/backend/src/main/java/codezap/member/domain/Member.java deleted file mode 100644 index a1924d713..000000000 --- a/backend/src/main/java/codezap/member/domain/Member.java +++ /dev/null @@ -1,30 +0,0 @@ -package codezap.member.domain; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Table; - -import codezap.global.domain.BaseTimeEntity; -import lombok.Getter; - -@Entity -@Table(name = "member") -@Getter -public class Member extends BaseTimeEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(unique = true, nullable = false) - private String email; - - @Column(nullable = false) - private String password; - - @Column(nullable = false) - private String nickname; -} diff --git a/backend/src/main/java/codezap/member/repository/MemberRepository.java b/backend/src/main/java/codezap/member/repository/MemberRepository.java deleted file mode 100644 index b109ac982..000000000 --- a/backend/src/main/java/codezap/member/repository/MemberRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package codezap.member.repository; - -import org.springframework.data.jpa.repository.JpaRepository; - -import codezap.member.domain.Member; - -public interface MemberRepository extends JpaRepository { -} diff --git a/backend/src/main/java/codezap/representative_snippet/domain/RepresentativeSnippet.java b/backend/src/main/java/codezap/representative_snippet/domain/RepresentativeSnippet.java deleted file mode 100644 index 064240d79..000000000 --- a/backend/src/main/java/codezap/representative_snippet/domain/RepresentativeSnippet.java +++ /dev/null @@ -1,36 +0,0 @@ -package codezap.representative_snippet.domain; - -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.OneToOne; -import jakarta.persistence.Table; - -import codezap.global.domain.BaseTimeEntity; -import codezap.snippet.domain.Snippet; -import codezap.template.domain.Template; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Table(name = "representative_snippet") -@Getter -@NoArgsConstructor -@AllArgsConstructor -public class RepresentativeSnippet extends BaseTimeEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @OneToOne - @JoinColumn(name = "template_id") - private Template template; - - @OneToOne - @JoinColumn(name = "snippet_id", nullable = false) - private Snippet snippet; -} diff --git a/backend/src/main/java/codezap/representative_snippet/repository/RepresentativeSnippetRepository.java b/backend/src/main/java/codezap/representative_snippet/repository/RepresentativeSnippetRepository.java deleted file mode 100644 index 7878b3657..000000000 --- a/backend/src/main/java/codezap/representative_snippet/repository/RepresentativeSnippetRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package codezap.representative_snippet.repository; - -import org.springframework.data.jpa.repository.JpaRepository; - -import codezap.representative_snippet.domain.RepresentativeSnippet; - -public interface RepresentativeSnippetRepository extends JpaRepository { -} diff --git a/backend/src/main/java/codezap/template/controller/TemplateController.java b/backend/src/main/java/codezap/template/controller/TemplateController.java index e856522c6..4016b5f59 100644 --- a/backend/src/main/java/codezap/template/controller/TemplateController.java +++ b/backend/src/main/java/codezap/template/controller/TemplateController.java @@ -2,6 +2,8 @@ import java.net.URI; +import jakarta.validation.Valid; + import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -21,14 +23,16 @@ public class TemplateController implements SpringDocTemplateController { private final TemplateService templateService; - public TemplateController(TemplateService templateService) {this.templateService = templateService;} + public TemplateController(TemplateService templateService) { + this.templateService = templateService; + } - @PostMapping("") - public ResponseEntity create(@RequestBody CreateTemplateRequest createTemplateRequest) { - return ResponseEntity.created(URI.create("/templates" + templateService.create(createTemplateRequest))).build(); + @PostMapping + public ResponseEntity create(@Valid @RequestBody CreateTemplateRequest createTemplateRequest) { + return ResponseEntity.created(URI.create("/templates/" + templateService.create(createTemplateRequest))).build(); } - @GetMapping("") + @GetMapping public ResponseEntity getTemplates() { return ResponseEntity.ok(templateService.findAll()); } diff --git a/backend/src/main/java/codezap/snippet/domain/Snippet.java b/backend/src/main/java/codezap/template/domain/Snippet.java similarity index 53% rename from backend/src/main/java/codezap/snippet/domain/Snippet.java rename to backend/src/main/java/codezap/template/domain/Snippet.java index 2b585bb1c..88f69e7b3 100644 --- a/backend/src/main/java/codezap/snippet/domain/Snippet.java +++ b/backend/src/main/java/codezap/template/domain/Snippet.java @@ -1,4 +1,4 @@ -package codezap.snippet.domain; +package codezap.template.domain; import java.util.Arrays; import java.util.stream.Collectors; @@ -9,36 +9,27 @@ 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 codezap.extension.domain.Extension; -import codezap.global.domain.BaseTimeEntity; -import codezap.template.domain.Template; -import lombok.AllArgsConstructor; +import codezap.global.auditing.BaseTimeEntity; import lombok.Getter; import lombok.NoArgsConstructor; @Entity -@Table(name = "snippet") @Getter -@AllArgsConstructor @NoArgsConstructor public class Snippet extends BaseTimeEntity { + private static final String CODE_LINE_BREAK = "\n"; + private static final int THUMBNAIL_SNIPPET_LINE_HEIGHT = 10; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "template_id", nullable = false) + @ManyToOne(fetch = FetchType.LAZY, optional = false) private Template template; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "extension_id", nullable = false) - private Extension extension; - @Column(nullable = false) private String filename; @@ -48,9 +39,16 @@ public class Snippet extends BaseTimeEntity { @Column(nullable = false) private Integer ordinal; - public String getSummaryContent() { - return Arrays.stream(content.split("\n")) - .limit(10) - .collect(Collectors.joining("\n")); + public Snippet(Template template, String filename, String content, Integer ordinal) { + this.template = template; + this.filename = filename; + this.content = content; + this.ordinal = ordinal; + } + + public String getThumbnailContent() { + return Arrays.stream(content.split(CODE_LINE_BREAK)) + .limit(THUMBNAIL_SNIPPET_LINE_HEIGHT) + .collect(Collectors.joining(CODE_LINE_BREAK)); } } diff --git a/backend/src/main/java/codezap/template/domain/Template.java b/backend/src/main/java/codezap/template/domain/Template.java index 9aae3df43..1c5f26f00 100644 --- a/backend/src/main/java/codezap/template/domain/Template.java +++ b/backend/src/main/java/codezap/template/domain/Template.java @@ -5,20 +5,13 @@ 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 codezap.global.domain.BaseTimeEntity; -import codezap.member.domain.Member; -import lombok.AllArgsConstructor; +import codezap.global.auditing.BaseTimeEntity; import lombok.Getter; import lombok.NoArgsConstructor; @Entity -@Table(name = "template") @Getter -@AllArgsConstructor @NoArgsConstructor public class Template extends BaseTimeEntity { @@ -26,10 +19,10 @@ public class Template extends BaseTimeEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @ManyToOne - @JoinColumn(name = "member_id", nullable = false) - private Member member; - @Column(nullable = false) private String title; + + public Template(String title) { + this.title = title; + } } diff --git a/backend/src/main/java/codezap/template/domain/ThumbnailSnippet.java b/backend/src/main/java/codezap/template/domain/ThumbnailSnippet.java new file mode 100644 index 000000000..99007b3cf --- /dev/null +++ b/backend/src/main/java/codezap/template/domain/ThumbnailSnippet.java @@ -0,0 +1,32 @@ +package codezap.template.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToOne; + +import codezap.global.auditing.BaseTimeEntity; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +public class ThumbnailSnippet extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne(optional = false) + private Template template; + + @OneToOne(optional = false) + private Snippet snippet; + + public ThumbnailSnippet(Template template, Snippet snippet) { + this.template = template; + this.snippet = snippet; + } +} diff --git a/backend/src/main/java/codezap/template/dto/request/CreateSnippetRequest.java b/backend/src/main/java/codezap/template/dto/request/CreateSnippetRequest.java index abf39be27..815d5018f 100644 --- a/backend/src/main/java/codezap/template/dto/request/CreateSnippetRequest.java +++ b/backend/src/main/java/codezap/template/dto/request/CreateSnippetRequest.java @@ -1,8 +1,15 @@ package codezap.template.dto.request; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + public record CreateSnippetRequest( + @NotNull(message = "파일 이름이 null 입니다.") + @Size(max = 255, message = "파일 이름은 최대 255자까지 입력 가능합니다.") String filename, + @NotNull(message = "파일 내용이 null 입니다.") String content, + @NotNull(message = "스니펫 순서가 null 입니다.") int ordinal ) { } diff --git a/backend/src/main/java/codezap/template/dto/request/CreateTemplateRequest.java b/backend/src/main/java/codezap/template/dto/request/CreateTemplateRequest.java index f71996a30..5f28f35e8 100644 --- a/backend/src/main/java/codezap/template/dto/request/CreateTemplateRequest.java +++ b/backend/src/main/java/codezap/template/dto/request/CreateTemplateRequest.java @@ -2,9 +2,16 @@ import java.util.List; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + public record CreateTemplateRequest( + @NotNull(message = "템플릿 이름이 null 입니다.") + @Size(max = 255, message = "템플릿 이름은 최대 255자까지 입력 가능합니다.") String title, - int representative_snippet_ordinal, + @NotNull(message = "스니펫 리스트가 null 입니다.") + @Valid List snippets ) { } diff --git a/backend/src/main/java/codezap/template/dto/response/FindAllSnippetByTemplateResponse.java b/backend/src/main/java/codezap/template/dto/response/FindAllSnippetByTemplateResponse.java index fe82524b5..49c18132d 100644 --- a/backend/src/main/java/codezap/template/dto/response/FindAllSnippetByTemplateResponse.java +++ b/backend/src/main/java/codezap/template/dto/response/FindAllSnippetByTemplateResponse.java @@ -1,6 +1,6 @@ package codezap.template.dto.response; -import codezap.snippet.domain.Snippet; +import codezap.template.domain.Snippet; public record FindAllSnippetByTemplateResponse( Long id, diff --git a/backend/src/main/java/codezap/template/dto/response/FindAllTemplatesResponse.java b/backend/src/main/java/codezap/template/dto/response/FindAllTemplatesResponse.java index fddf1a2c9..5bfa7573f 100644 --- a/backend/src/main/java/codezap/template/dto/response/FindAllTemplatesResponse.java +++ b/backend/src/main/java/codezap/template/dto/response/FindAllTemplatesResponse.java @@ -1,16 +1,33 @@ package codezap.template.dto.response; +import java.time.LocalDateTime; import java.util.List; -import codezap.representative_snippet.domain.RepresentativeSnippet; +import codezap.template.domain.ThumbnailSnippet; public record FindAllTemplatesResponse( - List templates + List templates ) { - public static FindAllTemplatesResponse from(List representativeSnippets) { - List templatesBySummaryResponse = representativeSnippets.stream() - .map(FindTemplateBySummaryResponse::from) + public static FindAllTemplatesResponse from(List thumbnailSnippets) { + List templatesBySummaryResponse = thumbnailSnippets.stream() + .map(ItemResponse::from) .toList(); return new FindAllTemplatesResponse(templatesBySummaryResponse); } + + public record ItemResponse( + Long id, + String title, + FindThumbnailSnippetResponse thumbnailSnippet, + LocalDateTime modifiedAt + ) { + public static ItemResponse from(ThumbnailSnippet thumbnailSnippet) { + return new ItemResponse( + thumbnailSnippet.getTemplate().getId(), + thumbnailSnippet.getTemplate().getTitle(), + FindThumbnailSnippetResponse.from(thumbnailSnippet.getSnippet()), + thumbnailSnippet.getModifiedAt() + ); + } + } } diff --git a/backend/src/main/java/codezap/template/dto/response/FindMemberBySummaryResponse.java b/backend/src/main/java/codezap/template/dto/response/FindMemberBySummaryResponse.java deleted file mode 100644 index 4c1f8b72d..000000000 --- a/backend/src/main/java/codezap/template/dto/response/FindMemberBySummaryResponse.java +++ /dev/null @@ -1,15 +0,0 @@ -package codezap.template.dto.response; - -import codezap.member.domain.Member; - -public record FindMemberBySummaryResponse( - Long id, - String nickname -) { - public static FindMemberBySummaryResponse from(Member member) { - return new FindMemberBySummaryResponse( - member.getId(), - member.getNickname() - ); - } -} diff --git a/backend/src/main/java/codezap/template/dto/response/FindRepresentativeSnippetResponse.java b/backend/src/main/java/codezap/template/dto/response/FindRepresentativeSnippetResponse.java deleted file mode 100644 index b539c96c0..000000000 --- a/backend/src/main/java/codezap/template/dto/response/FindRepresentativeSnippetResponse.java +++ /dev/null @@ -1,15 +0,0 @@ -package codezap.template.dto.response; - -import codezap.snippet.domain.Snippet; - -public record FindRepresentativeSnippetResponse( - String filename, - String content_summary -) { - public static FindRepresentativeSnippetResponse from(Snippet snippet) { - return new FindRepresentativeSnippetResponse( - snippet.getFilename(), - snippet.getSummaryContent() - ); - } -} diff --git a/backend/src/main/java/codezap/template/dto/response/FindTemplateByIdResponse.java b/backend/src/main/java/codezap/template/dto/response/FindTemplateByIdResponse.java index 6394f3f21..0d01a2284 100644 --- a/backend/src/main/java/codezap/template/dto/response/FindTemplateByIdResponse.java +++ b/backend/src/main/java/codezap/template/dto/response/FindTemplateByIdResponse.java @@ -3,23 +3,19 @@ import java.time.LocalDateTime; import java.util.List; -import codezap.snippet.domain.Snippet; +import codezap.template.domain.Snippet; import codezap.template.domain.Template; public record FindTemplateByIdResponse( Long id, String title, - FindMemberBySummaryResponse member, - Integer representative_snippet_ordinal, List snippets, - LocalDateTime modified_at + LocalDateTime modifiedAt ) { - public static FindTemplateByIdResponse from(Template template, List snippets) { + public static FindTemplateByIdResponse of(Template template, List snippets) { return new FindTemplateByIdResponse( template.getId(), template.getTitle(), - FindMemberBySummaryResponse.from(template.getMember()), - 1, mapToFindAllSnippetByTemplateResponse(snippets), template.getModifiedAt() ); diff --git a/backend/src/main/java/codezap/template/dto/response/FindTemplateBySummaryResponse.java b/backend/src/main/java/codezap/template/dto/response/FindTemplateBySummaryResponse.java deleted file mode 100644 index de75ad878..000000000 --- a/backend/src/main/java/codezap/template/dto/response/FindTemplateBySummaryResponse.java +++ /dev/null @@ -1,23 +0,0 @@ -package codezap.template.dto.response; - -import java.time.LocalDateTime; - -import codezap.representative_snippet.domain.RepresentativeSnippet; - -public record FindTemplateBySummaryResponse( - Long id, - String title, - FindMemberBySummaryResponse member, - FindRepresentativeSnippetResponse representative_snippet, - LocalDateTime modified_at -) { - public static FindTemplateBySummaryResponse from(RepresentativeSnippet representativeSnippet) { - return new FindTemplateBySummaryResponse( - representativeSnippet.getTemplate().getId(), - representativeSnippet.getTemplate().getTitle(), - FindMemberBySummaryResponse.from(representativeSnippet.getTemplate().getMember()), - FindRepresentativeSnippetResponse.from(representativeSnippet.getSnippet()), - representativeSnippet.getModifiedAt() - ); - } -} diff --git a/backend/src/main/java/codezap/template/dto/response/FindThumbnailSnippetResponse.java b/backend/src/main/java/codezap/template/dto/response/FindThumbnailSnippetResponse.java new file mode 100644 index 000000000..d80cdbeb3 --- /dev/null +++ b/backend/src/main/java/codezap/template/dto/response/FindThumbnailSnippetResponse.java @@ -0,0 +1,15 @@ +package codezap.template.dto.response; + +import codezap.template.domain.Snippet; + +public record FindThumbnailSnippetResponse( + String filename, + String thumbnailContent +) { + public static FindThumbnailSnippetResponse from(Snippet snippet) { + return new FindThumbnailSnippetResponse( + snippet.getFilename(), + snippet.getThumbnailContent() + ); + } +} diff --git a/backend/src/main/java/codezap/snippet/repository/SnippetRepository.java b/backend/src/main/java/codezap/template/repository/SnippetRepository.java similarity index 77% rename from backend/src/main/java/codezap/snippet/repository/SnippetRepository.java rename to backend/src/main/java/codezap/template/repository/SnippetRepository.java index ec4dba6da..8502f7aaa 100644 --- a/backend/src/main/java/codezap/snippet/repository/SnippetRepository.java +++ b/backend/src/main/java/codezap/template/repository/SnippetRepository.java @@ -1,10 +1,10 @@ -package codezap.snippet.repository; +package codezap.template.repository; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; -import codezap.snippet.domain.Snippet; +import codezap.template.domain.Snippet; import codezap.template.domain.Template; public interface SnippetRepository extends JpaRepository { diff --git a/backend/src/main/java/codezap/template/repository/ThumbnailSnippetRepository.java b/backend/src/main/java/codezap/template/repository/ThumbnailSnippetRepository.java new file mode 100644 index 000000000..50c5a1edf --- /dev/null +++ b/backend/src/main/java/codezap/template/repository/ThumbnailSnippetRepository.java @@ -0,0 +1,8 @@ +package codezap.template.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import codezap.template.domain.ThumbnailSnippet; + +public interface ThumbnailSnippetRepository extends JpaRepository { +} diff --git a/backend/src/main/java/codezap/template/service/TemplateService.java b/backend/src/main/java/codezap/template/service/TemplateService.java index 6852c59fc..e2e4e953f 100644 --- a/backend/src/main/java/codezap/template/service/TemplateService.java +++ b/backend/src/main/java/codezap/template/service/TemplateService.java @@ -5,80 +5,58 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import codezap.extension.domain.Extension; -import codezap.extension.repository.ExtensionRepository; -import codezap.language.repository.LanguageRepository; -import codezap.member.repository.MemberRepository; -import codezap.representative_snippet.domain.RepresentativeSnippet; -import codezap.representative_snippet.repository.RepresentativeSnippetRepository; -import codezap.snippet.domain.Snippet; -import codezap.snippet.repository.SnippetRepository; +import codezap.template.domain.Snippet; import codezap.template.domain.Template; +import codezap.template.domain.ThumbnailSnippet; import codezap.template.dto.request.CreateSnippetRequest; import codezap.template.dto.request.CreateTemplateRequest; import codezap.template.dto.response.FindAllTemplatesResponse; import codezap.template.dto.response.FindTemplateByIdResponse; +import codezap.template.repository.SnippetRepository; import codezap.template.repository.TemplateRepository; +import codezap.template.repository.ThumbnailSnippetRepository; @Service public class TemplateService { - private final RepresentativeSnippetRepository representativeSnippetRepository; + private final ThumbnailSnippetRepository thumbnailSnippetRepository; private final TemplateRepository templateRepository; private final SnippetRepository snippetRepository; - private final ExtensionRepository extensionRepository; - private final MemberRepository memberRepository; - private final LanguageRepository languageRepository; - public TemplateService(RepresentativeSnippetRepository representativeSnippetRepository, - TemplateRepository templateRepository, SnippetRepository snippetRepository, - ExtensionRepository extensionRepository, MemberRepository memberRepository, - LanguageRepository languageRepository + public TemplateService(ThumbnailSnippetRepository thumbnailSnippetRepository, + TemplateRepository templateRepository, SnippetRepository snippetRepository ) { - this.representativeSnippetRepository = representativeSnippetRepository; + this.thumbnailSnippetRepository = thumbnailSnippetRepository; this.templateRepository = templateRepository; this.snippetRepository = snippetRepository; - this.extensionRepository = extensionRepository; - this.memberRepository = memberRepository; - this.languageRepository = languageRepository; } @Transactional public Long create(CreateTemplateRequest createTemplateRequest) { Template template = templateRepository.save( - new Template(null, memberRepository.getById(1L), createTemplateRequest.title())); + new Template(createTemplateRequest.title())); List snippets = createTemplateRequest.snippets().stream() .map(createSnippetRequest -> createSnippet(createSnippetRequest, template)) .toList(); - RepresentativeSnippet representativeSnippet = representativeSnippetRepository.save( - new RepresentativeSnippet(null, template, snippets.get(0))); + thumbnailSnippetRepository.save(new ThumbnailSnippet(template, snippets.get(0))); return template.getId(); } private Snippet createSnippet(CreateSnippetRequest createSnippetRequest, Template template) { - String[] splitName = createSnippetRequest.filename().split("\\."); - Extension extension = findExtensionOrCreate(splitName[splitName.length - 1]); - return snippetRepository.save( - new Snippet(null, template, extension, createSnippetRequest.filename(), createSnippetRequest.content(), + new Snippet(template, createSnippetRequest.filename(), createSnippetRequest.content(), createSnippetRequest.ordinal())); } - private Extension findExtensionOrCreate(String name) { - return extensionRepository.findByName(name) - .orElseGet(() -> extensionRepository.save( - new Extension(null, languageRepository.getByName("PlainText"), name))); - } - public FindAllTemplatesResponse findAll() { - return FindAllTemplatesResponse.from(representativeSnippetRepository.findAll()); + return FindAllTemplatesResponse.from(thumbnailSnippetRepository.findAll()); } public FindTemplateByIdResponse findById(Long id) { Template template = templateRepository.getById(id); List snippets = snippetRepository.findAllByTemplate(template); - return FindTemplateByIdResponse.from(template, snippets); + return FindTemplateByIdResponse.of(template, snippets); } } diff --git a/backend/src/test/java/codezap/template/controller/TemplateControllerTest.java b/backend/src/test/java/codezap/template/controller/TemplateControllerTest.java new file mode 100644 index 000000000..7b19c8e4c --- /dev/null +++ b/backend/src/test/java/codezap/template/controller/TemplateControllerTest.java @@ -0,0 +1,128 @@ +package codezap.template.controller; + +import static org.hamcrest.Matchers.is; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.Sql.ExecutionPhase; + +import codezap.template.dto.request.CreateSnippetRequest; +import codezap.template.dto.request.CreateTemplateRequest; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@Sql(value = "/clear.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) +@Sql(value = "/clear.sql", executionPhase = ExecutionPhase.AFTER_TEST_CLASS) +class TemplateControllerTest { + + private static final int MAX_LENGTH = 255; + @LocalServerPort + int port; + + @BeforeEach + void setting() { + RestAssured.port = port; + } + + @Test + @DisplayName("템플릿 생성 성공") + void createTemplateSuccess() { + String maxTitle = "a".repeat(MAX_LENGTH); + CreateTemplateRequest templateRequest = new CreateTemplateRequest(maxTitle, + List.of(new CreateSnippetRequest("a".repeat(MAX_LENGTH), "content", 1))); + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(templateRequest) + .when().post("/templates") + .then().log().all() + .header("Location", "/templates/1") + .statusCode(201); + } + + @Test + @DisplayName("템플릿 생성 실패: 템플릿 이름 길이 초과") + void createTemplateFailWithLongTitle() { + String exceededTitle = "a".repeat(MAX_LENGTH + 1); + CreateTemplateRequest templateRequest = new CreateTemplateRequest(exceededTitle, + List.of(new CreateSnippetRequest("a", "content", 1))); + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(templateRequest) + .when().post("/templates") + .then().log().all() + .statusCode(400) + .body("detail", is("템플릿 이름은 최대 255자까지 입력 가능합니다.")); + } + + @Test + @DisplayName("템플릿 생성 실패: 파일 이름 길이 초과") + void createTemplateFailWithLongFileName() { + String exceededTitle = "a".repeat(MAX_LENGTH + 1); + CreateTemplateRequest templateRequest = new CreateTemplateRequest("title", + List.of(new CreateSnippetRequest(exceededTitle, "content", 1))); + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(templateRequest) + .when().post("/templates") + .then().log().all() + .statusCode(400) + .body("detail", is("파일 이름은 최대 255자까지 입력 가능합니다.")); + } + + @Test + @DisplayName("템플릿 전체 조회 성공") + void findAllTemplatesSuccess() { + //given + CreateTemplateRequest templateRequest1 = new CreateTemplateRequest("title1", + List.of(new CreateSnippetRequest("filename", "content", 1))); + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(templateRequest1) + .when().post("/templates") + .then().log().all(); + + CreateTemplateRequest templateRequest2 = new CreateTemplateRequest("title2", + List.of(new CreateSnippetRequest("filename", "content", 1))); + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(templateRequest2) + .when().post("/templates") + .then().log().all(); + + //when + RestAssured.given().log().all() + .get("/templates") + .then().log().all() + .statusCode(200) + .body("templates.size()", is(2)); + } + + @Test + @DisplayName("템플릿 상세 조회 성공") + void findOneTemplateSuccess() { + //given + CreateTemplateRequest templateRequest = new CreateTemplateRequest("title", + List.of(new CreateSnippetRequest("filename", "content", 1))); + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(templateRequest) + .when().post("/templates") + .then().log().all(); + + //when + RestAssured.given().log().all() + .get("/templates/1") + .then().log().all() + .statusCode(200) + .body("title", is(templateRequest.title()), + "snippets.size()", is(1)); + } +} diff --git a/backend/src/test/java/codezap/template/service/TemplateServiceTest.java b/backend/src/test/java/codezap/template/service/TemplateServiceTest.java new file mode 100644 index 000000000..b78466ab3 --- /dev/null +++ b/backend/src/test/java/codezap/template/service/TemplateServiceTest.java @@ -0,0 +1,112 @@ +package codezap.template.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.Sql.ExecutionPhase; + +import codezap.template.domain.Snippet; +import codezap.template.domain.Template; +import codezap.template.domain.ThumbnailSnippet; +import codezap.template.dto.request.CreateSnippetRequest; +import codezap.template.dto.request.CreateTemplateRequest; +import codezap.template.dto.response.FindAllTemplatesResponse; +import codezap.template.dto.response.FindTemplateByIdResponse; +import codezap.template.repository.SnippetRepository; +import codezap.template.repository.TemplateRepository; +import codezap.template.repository.ThumbnailSnippetRepository; +import io.restassured.RestAssured; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@Sql(value = "/clear.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) +@Sql(value = "/clear.sql", executionPhase = ExecutionPhase.AFTER_TEST_CLASS) +class TemplateServiceTest { + + @Autowired + private TemplateService templateService; + + @LocalServerPort + int port; + @Autowired + private TemplateRepository templateRepository; + @Autowired + private SnippetRepository snippetRepository; + @Autowired + private ThumbnailSnippetRepository thumbnailSnippetRepository; + + @BeforeEach + void setting() { + RestAssured.port = port; + } + + @Test + @DisplayName("템플릿 생성 성공") + void createTemplateSuccess() { + //given + CreateTemplateRequest createTemplateRequest = makeTemplateRequest("title"); + + //when + templateService.create(createTemplateRequest); + + //then + assertThat(templateRepository.findAll().size()).isEqualTo(1); + } + + @Test + @DisplayName("템플릿 전체 조회 성공") + void findAllTemplatesSuccess() { + //given + saveTemplate(makeTemplateRequest("title1")); + saveTemplate(makeTemplateRequest("title2")); + + //when + FindAllTemplatesResponse allTemplates = templateService.findAll(); + + //then + assertThat(allTemplates.templates().size()).isEqualTo(2); + } + + @Test + @DisplayName("템플릿 단건 조회 성공") + void findOneTemplateSuccess() { + //given + CreateTemplateRequest createdTemplate = makeTemplateRequest("title"); + saveTemplate(createdTemplate); + + //when + FindTemplateByIdResponse foundTemplate = templateService.findById(1L); + + //then + assertAll( + () -> assertThat(foundTemplate.title()).isEqualTo(createdTemplate.title()), + () -> assertThat(foundTemplate.snippets().size()).isEqualTo(createdTemplate.snippets().size()) + ); + } + + private CreateTemplateRequest makeTemplateRequest(String title) { + return new CreateTemplateRequest( + title, + List.of( + new CreateSnippetRequest("filename1", "content1", 1), + new CreateSnippetRequest("filename2", "content2", 2) + ) + ); + } + + private void saveTemplate(CreateTemplateRequest createTemplateRequest) { + Template savedTemplate = templateRepository.save(new Template(createTemplateRequest.title())); + Snippet savedFirstSnippet = snippetRepository.save(new Snippet(savedTemplate, "filename1", "content1", 1)); + snippetRepository.save(new Snippet(savedTemplate, "filename2", "content2", 2)); + thumbnailSnippetRepository.save(new ThumbnailSnippet(savedTemplate, savedFirstSnippet)); + } +} diff --git a/backend/src/test/resources/clear.sql b/backend/src/test/resources/clear.sql new file mode 100644 index 000000000..845a987b2 --- /dev/null +++ b/backend/src/test/resources/clear.sql @@ -0,0 +1,37 @@ +DROP TABLE IF EXISTS thumbnail_snippet; +DROP TABLE IF EXISTS snippet; +DROP TABLE IF EXISTS template; + +CREATE TABLE template +( + id BIGINT NOT NULL AUTO_INCREMENT, + title VARCHAR(255) NOT NULL, + created_at DATETIME(6) NOT NULL, + modified_at DATETIME(6) NOT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE snippet +( + id BIGINT NOT NULL AUTO_INCREMENT, + template_id BIGINT NOT NULL, + filename VARCHAR(255) NOT NULL, + content TEXT NOT NULL, + ordinal INTEGER NOT NULL, + created_at DATETIME(6) NOT NULL, + modified_at DATETIME(6) NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY (template_id) REFERENCES template (id) +); + +CREATE TABLE thumbnail_snippet +( + id BIGINT NOT NULL AUTO_INCREMENT, + template_id BIGINT NOT NULL, + snippet_id BIGINT NOT NULL, + created_at DATETIME(6) NOT NULL, + modified_at DATETIME(6) NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY (template_id) REFERENCES template (id), + FOREIGN KEY (snippet_id) REFERENCES snippet (id) +);