diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..5b04f5f88 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,10 @@ +## ⚡️ 관련 이슈 +close #이슈번호 + +## 📍주요 변경 사항 +### 1. 주요 변경 사항은 이러이러합니다. +### - 이렇게도 쓸 수 있어요 + +## 🎸기타 +### 1. 이런걸 더 고려해봐야 할 것 같습니다. +### - 이런 것도 해야해요. diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 000000000..c402f0e4d --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,56 @@ +name: Build and deploy + +on: + push: + branches: + - develop + - main + - hackathon + +jobs: + build: + runs-on: ubuntu-latest + env: + build-directory: ./backend + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup JDK 17 + uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: temurin + - name: Build with Gradle Wrapper + run: ./gradlew bootJar + working-directory: ${{ env.build-directory }} + - name: Archive JAR file + uses: actions/upload-artifact@v4 + with: + name: build-jar + path: backend/build/libs/*.jar + + deploy: + needs: build + runs-on: ubuntu-latest + steps: + - name: Download JAR artifact + uses: actions/download-artifact@v4 + with: + name: build-jar + - name: Copy JAR to remote server + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ secrets.REMOTE_HOST }} + username: ${{ secrets.REMOTE_USER }} + key: ${{ secrets.REMOTE_SSH_KEY }} + target: ${{ secrets.WORK_DIRECTORY }} + source: '*.jar' + - name: Execute Server Init Script + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.REMOTE_HOST }} + username: ${{ secrets.REMOTE_USER }} + key: ${{ secrets.REMOTE_SSH_KEY }} + target: ${{ secrets.WORK_DIRECTORY }} + script_stop: true + script: chmod +x ./deploy.sh && sh ./deploy.sh diff --git a/.gitignore b/.gitignore index efefba177..f66c490a5 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ ### macOS ### .DS_Store + +### Environment Variable ### +.env diff --git a/backend/build.gradle b/backend/build.gradle index 78bcefc30..79ee2194d 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -21,6 +21,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + 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' diff --git a/backend/src/main/java/codezap/TestController.java b/backend/src/main/java/codezap/TestController.java new file mode 100644 index 000000000..e62ff5c55 --- /dev/null +++ b/backend/src/main/java/codezap/TestController.java @@ -0,0 +1,13 @@ +package codezap; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class TestController { + + @GetMapping("/test") + String test() { + return "success"; + } +} diff --git a/backend/src/main/java/codezap/extension/domain/Extension.java b/backend/src/main/java/codezap/extension/domain/Extension.java new file mode 100644 index 000000000..6bd51b9d3 --- /dev/null +++ b/backend/src/main/java/codezap/extension/domain/Extension.java @@ -0,0 +1,36 @@ +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 new file mode 100644 index 000000000..276017103 --- /dev/null +++ b/backend/src/main/java/codezap/extension/repository/ExtensionRepository.java @@ -0,0 +1,11 @@ +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/DateTimeFormatConfiguration.java b/backend/src/main/java/codezap/global/DateTimeFormatConfiguration.java new file mode 100644 index 000000000..eae2bf8ce --- /dev/null +++ b/backend/src/main/java/codezap/global/DateTimeFormatConfiguration.java @@ -0,0 +1,24 @@ +package codezap.global; + +import java.time.format.DateTimeFormatter; +import java.util.TimeZone; + +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; + +@Configuration +public class DateTimeFormatConfiguration { + private static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm"; + + @Bean + public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() { + return jacksonObjectMapperBuilder -> { + jacksonObjectMapperBuilder.timeZone(TimeZone.getTimeZone("UTC")); + 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/SpringDocConfiguration.java new file mode 100644 index 000000000..60c354b62 --- /dev/null +++ b/backend/src/main/java/codezap/global/SpringDocConfiguration.java @@ -0,0 +1,24 @@ +package codezap.global; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; + +@OpenAPIDefinition(info = @io.swagger.v3.oas.annotations.info.Info(title = "코드잽 API", version = "v1")) +@Configuration +public class SpringDocConfiguration { + + @Bean + public OpenAPI openAPI() { + Info info = new Info() + .title("코드잽 API") + .version("v1.0") + .description("코드잽 API 명세서입니다."); + + return new OpenAPI() + .info(info); + } +} diff --git a/backend/src/main/java/codezap/global/WebCorsConfiguration.java b/backend/src/main/java/codezap/global/WebCorsConfiguration.java new file mode 100644 index 000000000..f2a8dcbd7 --- /dev/null +++ b/backend/src/main/java/codezap/global/WebCorsConfiguration.java @@ -0,0 +1,15 @@ +package codezap.global; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebCorsConfiguration implements WebMvcConfigurer { + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("*") + .allowedMethods("*"); + } +} diff --git a/backend/src/main/java/codezap/global/domain/BaseTimeEntity.java b/backend/src/main/java/codezap/global/domain/BaseTimeEntity.java new file mode 100644 index 000000000..3ee8b0548 --- /dev/null +++ b/backend/src/main/java/codezap/global/domain/BaseTimeEntity.java @@ -0,0 +1,27 @@ +package codezap.global.domain; + +import java.time.LocalDateTime; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; + +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import lombok.Getter; + +@EntityListeners(AuditingEntityListener.class) +@MappedSuperclass +@Getter +public class BaseTimeEntity { + + @CreatedDate + @Column(updatable = false, nullable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(nullable = false) + private LocalDateTime modifiedAt; +} diff --git a/backend/src/main/java/codezap/global/domain/JpaAuditingConfiguration.java b/backend/src/main/java/codezap/global/domain/JpaAuditingConfiguration.java new file mode 100644 index 000000000..871e8f59d --- /dev/null +++ b/backend/src/main/java/codezap/global/domain/JpaAuditingConfiguration.java @@ -0,0 +1,9 @@ +package codezap.global.domain; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@Configuration +@EnableJpaAuditing +public class JpaAuditingConfiguration { +} diff --git a/backend/src/main/java/codezap/language/domain/Language.java b/backend/src/main/java/codezap/language/domain/Language.java new file mode 100644 index 000000000..cddc03d48 --- /dev/null +++ b/backend/src/main/java/codezap/language/domain/Language.java @@ -0,0 +1,28 @@ +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 new file mode 100644 index 000000000..1fcd4a0aa --- /dev/null +++ b/backend/src/main/java/codezap/language/repository/LanguageRepository.java @@ -0,0 +1,18 @@ +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 new file mode 100644 index 000000000..a1924d713 --- /dev/null +++ b/backend/src/main/java/codezap/member/domain/Member.java @@ -0,0 +1,30 @@ +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 new file mode 100644 index 000000000..b109ac982 --- /dev/null +++ b/backend/src/main/java/codezap/member/repository/MemberRepository.java @@ -0,0 +1,8 @@ +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 new file mode 100644 index 000000000..064240d79 --- /dev/null +++ b/backend/src/main/java/codezap/representative_snippet/domain/RepresentativeSnippet.java @@ -0,0 +1,36 @@ +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 new file mode 100644 index 000000000..7878b3657 --- /dev/null +++ b/backend/src/main/java/codezap/representative_snippet/repository/RepresentativeSnippetRepository.java @@ -0,0 +1,8 @@ +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/snippet/domain/Snippet.java b/backend/src/main/java/codezap/snippet/domain/Snippet.java new file mode 100644 index 000000000..2b585bb1c --- /dev/null +++ b/backend/src/main/java/codezap/snippet/domain/Snippet.java @@ -0,0 +1,56 @@ +package codezap.snippet.domain; + +import java.util.Arrays; +import java.util.stream.Collectors; + +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.extension.domain.Extension; +import codezap.global.domain.BaseTimeEntity; +import codezap.template.domain.Template; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "snippet") +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class Snippet extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "template_id", nullable = false) + private Template template; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "extension_id", nullable = false) + private Extension extension; + + @Column(nullable = false) + private String filename; + + @Column(nullable = false, columnDefinition = "TEXT") + private String content; + + @Column(nullable = false) + private Integer ordinal; + + public String getSummaryContent() { + return Arrays.stream(content.split("\n")) + .limit(10) + .collect(Collectors.joining("\n")); + } +} diff --git a/backend/src/main/java/codezap/snippet/repository/SnippetRepository.java b/backend/src/main/java/codezap/snippet/repository/SnippetRepository.java new file mode 100644 index 000000000..ec4dba6da --- /dev/null +++ b/backend/src/main/java/codezap/snippet/repository/SnippetRepository.java @@ -0,0 +1,12 @@ +package codezap.snippet.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +import codezap.snippet.domain.Snippet; +import codezap.template.domain.Template; + +public interface SnippetRepository extends JpaRepository { + List findAllByTemplate(Template template); +} diff --git a/backend/src/main/java/codezap/template/controller/SpringDocTemplateController.java b/backend/src/main/java/codezap/template/controller/SpringDocTemplateController.java new file mode 100644 index 000000000..4695bf061 --- /dev/null +++ b/backend/src/main/java/codezap/template/controller/SpringDocTemplateController.java @@ -0,0 +1,28 @@ +package codezap.template.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; + +import codezap.template.dto.request.CreateTemplateRequest; +import codezap.template.dto.response.FindAllTemplatesResponse; +import codezap.template.dto.response.FindTemplateByIdResponse; +import io.swagger.v3.oas.annotations.Operation; + +public interface SpringDocTemplateController { + + @Operation(summary = "템플릿 생성", description = """ + 새로운 템플릿을 생성합니다. \n + 템플릿의 제목, 썸네일 스니펫의 순서, 스니펫 목록이 필요합니다. \n + 스니펫 목록은 파일 이름, 소스 코드, 해당 스니펫의 순서가 필요합니다. \n + * 썸네일 스니펫은 1로 고정입니다. (2024.07.15 기준) \n + * 모든 스니펫 순서는 1부터 시작합니다. \n + """) + ResponseEntity create(CreateTemplateRequest createTemplateRequest); + + @Operation(summary = "템플릿 목록 조회", description = "작성된 모든 템플릿을 조회합니다.") + ResponseEntity getTemplates(); + + @Operation(summary = "템플릿 단건 조회", description = "해당하는 식별자의 템플릿을 조회합니다.") + ResponseEntity getTemplateById(@PathVariable Long id); +} + diff --git a/backend/src/main/java/codezap/template/controller/TemplateController.java b/backend/src/main/java/codezap/template/controller/TemplateController.java new file mode 100644 index 000000000..e856522c6 --- /dev/null +++ b/backend/src/main/java/codezap/template/controller/TemplateController.java @@ -0,0 +1,40 @@ +package codezap.template.controller; + +import java.net.URI; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import codezap.template.dto.request.CreateTemplateRequest; +import codezap.template.dto.response.FindAllTemplatesResponse; +import codezap.template.dto.response.FindTemplateByIdResponse; +import codezap.template.service.TemplateService; + +@RestController +@RequestMapping("/templates") +public class TemplateController implements SpringDocTemplateController { + + private final 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(); + } + + @GetMapping("") + public ResponseEntity getTemplates() { + return ResponseEntity.ok(templateService.findAll()); + } + + @GetMapping("/{id}") + public ResponseEntity getTemplateById(@PathVariable Long id) { + return ResponseEntity.ok(templateService.findById(id)); + } +} diff --git a/backend/src/main/java/codezap/template/domain/Template.java b/backend/src/main/java/codezap/template/domain/Template.java new file mode 100644 index 000000000..9aae3df43 --- /dev/null +++ b/backend/src/main/java/codezap/template/domain/Template.java @@ -0,0 +1,35 @@ +package codezap.template.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +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 lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "template") +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class Template extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + @Column(nullable = false) + private String title; +} diff --git a/backend/src/main/java/codezap/template/dto/request/CreateSnippetRequest.java b/backend/src/main/java/codezap/template/dto/request/CreateSnippetRequest.java new file mode 100644 index 000000000..abf39be27 --- /dev/null +++ b/backend/src/main/java/codezap/template/dto/request/CreateSnippetRequest.java @@ -0,0 +1,8 @@ +package codezap.template.dto.request; + +public record CreateSnippetRequest( + String filename, + String content, + 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 new file mode 100644 index 000000000..f71996a30 --- /dev/null +++ b/backend/src/main/java/codezap/template/dto/request/CreateTemplateRequest.java @@ -0,0 +1,10 @@ +package codezap.template.dto.request; + +import java.util.List; + +public record CreateTemplateRequest( + String title, + int representative_snippet_ordinal, + 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 new file mode 100644 index 000000000..fe82524b5 --- /dev/null +++ b/backend/src/main/java/codezap/template/dto/response/FindAllSnippetByTemplateResponse.java @@ -0,0 +1,19 @@ +package codezap.template.dto.response; + +import codezap.snippet.domain.Snippet; + +public record FindAllSnippetByTemplateResponse( + Long id, + String filename, + String content, + int ordinal +) { + public static FindAllSnippetByTemplateResponse from(Snippet snippet) { + return new FindAllSnippetByTemplateResponse( + snippet.getId(), + snippet.getFilename(), + snippet.getContent(), + snippet.getOrdinal() + ); + } +} diff --git a/backend/src/main/java/codezap/template/dto/response/FindAllTemplatesResponse.java b/backend/src/main/java/codezap/template/dto/response/FindAllTemplatesResponse.java new file mode 100644 index 000000000..fddf1a2c9 --- /dev/null +++ b/backend/src/main/java/codezap/template/dto/response/FindAllTemplatesResponse.java @@ -0,0 +1,16 @@ +package codezap.template.dto.response; + +import java.util.List; + +import codezap.representative_snippet.domain.RepresentativeSnippet; + +public record FindAllTemplatesResponse( + List templates +) { + public static FindAllTemplatesResponse from(List representativeSnippets) { + List templatesBySummaryResponse = representativeSnippets.stream() + .map(FindTemplateBySummaryResponse::from) + .toList(); + return new FindAllTemplatesResponse(templatesBySummaryResponse); + } +} diff --git a/backend/src/main/java/codezap/template/dto/response/FindMemberBySummaryResponse.java b/backend/src/main/java/codezap/template/dto/response/FindMemberBySummaryResponse.java new file mode 100644 index 000000000..4c1f8b72d --- /dev/null +++ b/backend/src/main/java/codezap/template/dto/response/FindMemberBySummaryResponse.java @@ -0,0 +1,15 @@ +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 new file mode 100644 index 000000000..b539c96c0 --- /dev/null +++ b/backend/src/main/java/codezap/template/dto/response/FindRepresentativeSnippetResponse.java @@ -0,0 +1,15 @@ +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 new file mode 100644 index 000000000..6394f3f21 --- /dev/null +++ b/backend/src/main/java/codezap/template/dto/response/FindTemplateByIdResponse.java @@ -0,0 +1,35 @@ +package codezap.template.dto.response; + +import java.time.LocalDateTime; +import java.util.List; + +import codezap.snippet.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 +) { + public static FindTemplateByIdResponse from(Template template, List snippets) { + return new FindTemplateByIdResponse( + template.getId(), + template.getTitle(), + FindMemberBySummaryResponse.from(template.getMember()), + 1, + mapToFindAllSnippetByTemplateResponse(snippets), + template.getModifiedAt() + ); + } + + private static List mapToFindAllSnippetByTemplateResponse( + List snippets + ) { + return snippets.stream() + .map(FindAllSnippetByTemplateResponse::from) + .toList(); + } +} diff --git a/backend/src/main/java/codezap/template/dto/response/FindTemplateBySummaryResponse.java b/backend/src/main/java/codezap/template/dto/response/FindTemplateBySummaryResponse.java new file mode 100644 index 000000000..de75ad878 --- /dev/null +++ b/backend/src/main/java/codezap/template/dto/response/FindTemplateBySummaryResponse.java @@ -0,0 +1,23 @@ +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/repository/TemplateRepository.java b/backend/src/main/java/codezap/template/repository/TemplateRepository.java new file mode 100644 index 000000000..8ea553d00 --- /dev/null +++ b/backend/src/main/java/codezap/template/repository/TemplateRepository.java @@ -0,0 +1,15 @@ +package codezap.template.repository; + +import java.util.NoSuchElementException; + +import org.springframework.data.jpa.repository.JpaRepository; + +import codezap.template.domain.Template; + +public interface TemplateRepository extends JpaRepository { + + default Template getById(Long id) { + return findById(id).orElseThrow( + () -> new NoSuchElementException("식별자 " + id + "에 해당하는 템플릿이 존재하지 않습니다.")); + } +} diff --git a/backend/src/main/java/codezap/template/service/TemplateService.java b/backend/src/main/java/codezap/template/service/TemplateService.java new file mode 100644 index 000000000..6852c59fc --- /dev/null +++ b/backend/src/main/java/codezap/template/service/TemplateService.java @@ -0,0 +1,84 @@ +package codezap.template.service; + +import java.util.List; + +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.Template; +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.TemplateRepository; + +@Service +public class TemplateService { + + private final RepresentativeSnippetRepository representativeSnippetRepository; + 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 + ) { + this.representativeSnippetRepository = representativeSnippetRepository; + 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())); + + List snippets = createTemplateRequest.snippets().stream() + .map(createSnippetRequest -> createSnippet(createSnippetRequest, template)) + .toList(); + + RepresentativeSnippet representativeSnippet = representativeSnippetRepository.save( + new RepresentativeSnippet(null, 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(), + 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()); + } + + public FindTemplateByIdResponse findById(Long id) { + Template template = templateRepository.getById(id); + List snippets = snippetRepository.findAllByTemplate(template); + return FindTemplateByIdResponse.from(template, snippets); + } +} diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs index bcda74470..08969147d 100644 --- a/frontend/.eslintrc.cjs +++ b/frontend/.eslintrc.cjs @@ -1,9 +1,20 @@ module.exports = { root: true, env: { browser: true, es2020: true }, - extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react/recommended', 'plugin:react-hooks/recommended', 'plugin:react/jsx-runtime', 'plugin:storybook/recommended'], + extends: [ + 'eslint:recommended', + 'plugin:prettier/recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', + 'plugin:react/jsx-runtime', + 'plugin:storybook/recommended', + ], ignorePatterns: ['dist', '.eslintrc.cjs'], parser: '@typescript-eslint/parser', plugins: ['react-refresh', 'react'], - rules: {}, + rules: { + 'prettier/prettier': 'error', + 'react/no-unknown-property': ['error', { ignore: ['css'] }], + }, }; diff --git a/frontend/.gitignore b/frontend/.gitignore index 198b26b47..84d2b9f64 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -1,3 +1,5 @@ node_modules *storybook.log + +dist diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 000000000..36b9f7e90 --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,12 @@ +{ + "arrowParens": "always", + "bracketSpacing": true, + "endOfLine": "auto", + "printWidth": 120, + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "useTabs": false, + "trailingComma": "all", + "jsxSingleQuote": true +} diff --git a/frontend/.storybook/preview.ts b/frontend/.storybook/preview.ts deleted file mode 100644 index 37914b18f..000000000 --- a/frontend/.storybook/preview.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { Preview } from "@storybook/react"; - -const preview: Preview = { - parameters: { - controls: { - matchers: { - color: /(background|color)$/i, - date: /Date$/i, - }, - }, - }, -}; - -export default preview; diff --git a/frontend/.storybook/preview.tsx b/frontend/.storybook/preview.tsx new file mode 100644 index 000000000..40997c551 --- /dev/null +++ b/frontend/.storybook/preview.tsx @@ -0,0 +1,32 @@ +import type { Preview } from '@storybook/react'; +import GlobalStyles from '../src/style/GlobalStyles'; +import React from 'react'; + +const preview: Preview = { + parameters: { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + layout: 'centered', + }, + decorators: [ + (Story) => ( + <> + +
+
+ +
+
+ +
+
+ + ), + ], +}; + +export default preview; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 430ecd0b1..7c4e4c57e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,10 +9,16 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@codemirror/lang-javascript": "^6.2.2", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", + "@tanstack/react-query": "^5.51.1", + "@uiw/codemirror-theme-vscode": "^4.23.0", + "@uiw/react-codemirror": "^4.23.0", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "react-router-dom": "^6.24.1", + "react-syntax-highlighter": "^15.5.0" }, "devDependencies": { "@chromatic-com/storybook": "^1.6.1", @@ -27,14 +33,19 @@ "@storybook/test": "^8.2.1", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", + "@types/react-syntax-highlighter": "^15.5.13", "@typescript-eslint/eslint-plugin": "^7.2.0", "@typescript-eslint/parser": "^7.2.0", "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-react": "^7.34.3", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.6", "eslint-plugin-storybook": "^0.8.0", + "file-loader": "^6.2.0", "html-webpack-plugin": "^5.6.0", + "prettier": "^3.3.3", "storybook": "^8.2.1", "ts-loader": "^9.5.1", "typescript": "^5.5.3", @@ -2174,6 +2185,107 @@ "yarn": ">=1.22.18" } }, + "node_modules/@codemirror/autocomplete": { + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.17.0.tgz", + "integrity": "sha512-fdfj6e6ZxZf8yrkMHUSJJir7OJkHkZKaOZGzLWIYp2PZ3jd+d+UjG8zVPqJF6d3bKxkhvXTPan/UZ1t7Bqm0gA==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + }, + "peerDependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.6.0.tgz", + "integrity": "sha512-qnY+b7j1UNcTS31Eenuc/5YJB6gQOzkUoNmJQc0rznwqSRpeaWWpjkWy2C/MPTcePpsKJEM26hXrOXl1+nceXg==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-javascript": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.2.tgz", + "integrity": "sha512-VGQfY+FCc285AhWuwjYxQyUQcYurWlxdKYT4bqwr3Twnd5wP5WSeu52t4tvvuWmljT4EmgEgZCqSieokhtY8hg==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.2.tgz", + "integrity": "sha512-kgbTYTo0Au6dCSc/TFy7fK3fpJmgHDv1sG1KNQKJXVi+xBTEeBPY/M30YXiU6mMXeH+YIDLsbrT4ZwNRdtF+SA==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.1.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.1.tgz", + "integrity": "sha512-IZ0Y7S4/bpaunwggW2jYqwLuHj0QtESf5xcROewY6+lDNwZ/NzvR4t+vpYgg9m7V8UXLPYqG+lu3DF470E5Oxg==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.5.6", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.6.tgz", + "integrity": "sha512-rpMgcsh7o0GuCDUXKPvww+muLA1pDJaFrpq/CCHtpQJYz8xopu4D1hPcKRoDD0YlF8gZaqTNIRa4VRBWyhyy7Q==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz", + "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==" + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz", + "integrity": "sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.28.4", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.28.4.tgz", + "integrity": "sha512-QScv95fiviSQ/CaVGflxAvvvDy/9wi0RFyDl4LkHHWiMr/UPebyuTspmYSeN5Nx6eujcPYwsQzA6ZIZucKZVHQ==", + "dependencies": { + "@codemirror/state": "^6.4.0", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -3030,6 +3142,37 @@ "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", "dev": true }, + "node_modules/@lezer/common": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz", + "integrity": "sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.0.tgz", + "integrity": "sha512-WrS5Mw51sGrpqjlh3d4/fOwpEV2Hd3YOkp9DBt4k8XZQcoTHZFB7sx030A6OcahF4J1nDQAa3jXlTVVYH50IFA==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/javascript": { + "version": "1.4.17", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.17.tgz", + "integrity": "sha512-bYW4ctpyGK+JMumDApeUzuIezX01H76R1foD6LcRX224FWfyYit/HYxiPGDjXXe/wQWASjCvVGoukTH68+0HIA==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.1.3", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.1.tgz", + "integrity": "sha512-CHsKq8DMKBf9b3yXPDIU4DbH+ZJd/sJdYOW2llbW/HudP5u0VS6Bfq1hLYfgU7uAYGFIyGGQIsSOXGPEErZiJw==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, "node_modules/@mdx-js/react": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.0.1.tgz", @@ -3092,6 +3235,26 @@ "node": ">=14" } }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@remix-run/router": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.17.1.tgz", + "integrity": "sha512-mCOMec4BKd6BRGBZeSnGiIgwsbLGp3yhVqAD8H+PxiRNEHgDpZb8J1TnrSDlg97t0ySKMQJTHCWBCmBpSmkF6Q==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -4294,6 +4457,30 @@ "@swc/counter": "^0.1.3" } }, + "node_modules/@tanstack/query-core": { + "version": "5.51.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.51.1.tgz", + "integrity": "sha512-fJBMQMpo8/KSsWW5ratJR5+IFr7YNJ3K2kfP9l5XObYHsgfVy1w3FJUWU4FT2fj7+JMaEg33zOcNDBo0LMwHnw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.51.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.51.1.tgz", + "integrity": "sha512-s47HKFnQ4HOJAHoIiXcpna/roMMPZJPy6fJ6p4ZNVn8+/onlLBEDd1+xc8OnDuwgvecqkZD7Z2mnSRbcWefrKw==", + "dependencies": { + "@tanstack/query-core": "5.51.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, "node_modules/@testing-library/dom": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.1.0.tgz", @@ -4693,6 +4880,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-syntax-highlighter": { + "version": "15.5.13", + "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz", + "integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/resolve": { "version": "1.20.6", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz", @@ -5025,6 +5221,86 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@uiw/codemirror-extensions-basic-setup": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.23.0.tgz", + "integrity": "sha512-+k5nkRpUWGaHr1JWT8jcKsVewlXw5qBgSopm9LW8fZ6KnSNZBycz8kHxh0+WSvckmXEESGptkIsb7dlkmJT/hQ==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/autocomplete": ">=6.0.0", + "@codemirror/commands": ">=6.0.0", + "@codemirror/language": ">=6.0.0", + "@codemirror/lint": ">=6.0.0", + "@codemirror/search": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" + } + }, + "node_modules/@uiw/codemirror-theme-vscode": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-vscode/-/codemirror-theme-vscode-4.23.0.tgz", + "integrity": "sha512-zl1FD7U1b58tqlF216jYv2okvVkTe+FP1ztqO/DF129bcH99QjszkakshyfxQEvvF4ys3zyzqZ7vU3VYBir8tg==", + "dependencies": { + "@uiw/codemirror-themes": "4.23.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-themes": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-themes/-/codemirror-themes-4.23.0.tgz", + "integrity": "sha512-9fiji9xooZyBQozR1i6iTr56YP7j/Dr/VgsNWbqf5Szv+g+4WM1iZuiDGwNXmFMWX8gbkDzp6ASE21VCPSofWw==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/language": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" + } + }, + "node_modules/@uiw/react-codemirror": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.23.0.tgz", + "integrity": "sha512-MnqTXfgeLA3fsUUQjqjJgemEuNyoGALgsExVm0NQAllAAi1wfj+IoKFeK+h3XXMlTFRCFYOUh4AHDv0YXJLsOg==", + "dependencies": { + "@babel/runtime": "^7.18.6", + "@codemirror/commands": "^6.1.0", + "@codemirror/state": "^6.1.1", + "@codemirror/theme-one-dark": "^6.0.0", + "@uiw/codemirror-extensions-basic-setup": "4.23.0", + "codemirror": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.11.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/theme-one-dark": ">=6.0.0", + "@codemirror/view": ">=6.0.0", + "codemirror": ">=6.0.0", + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -5820,6 +6096,15 @@ "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", "dev": true }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -6283,6 +6568,20 @@ "node": ">=6" } }, + "node_modules/codemirror": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", + "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -6466,6 +6765,11 @@ "node": ">=10" } }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -6982,6 +7286,15 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -7392,6 +7705,48 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", + "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, "node_modules/eslint-plugin-react": { "version": "7.34.3", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.3.tgz", @@ -8062,6 +8417,12 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -8114,6 +8475,18 @@ "reusify": "^1.0.4" } }, + "node_modules/fault": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/faye-websocket": { "version": "0.11.4", "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", @@ -8147,6 +8520,26 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "dev": true, + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, "node_modules/filesize": { "version": "10.1.4", "resolved": "https://registry.npmjs.org/filesize/-/filesize-10.1.4.tgz", @@ -8451,6 +8844,14 @@ "node": "*" } }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -8902,6 +9303,14 @@ "he": "bin/he" } }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "engines": { + "node": "*" + } + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -10094,6 +10503,20 @@ "node": ">=6.11.5" } }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -10169,6 +10592,19 @@ "tslib": "^2.0.3" } }, + "node_modules/lowlight": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", + "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", + "dependencies": { + "fault": "^1.0.0", + "highlight.js": "~10.7.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/lru-cache": { "version": "10.4.2", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.2.tgz", @@ -11423,9 +11859,9 @@ } }, "node_modules/prettier": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", - "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -11437,6 +11873,18 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/pretty-error": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", @@ -11748,6 +12196,225 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-router": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.24.1.tgz", + "integrity": "sha512-PTXFXGK2pyXpHzVo3rR9H7ip4lSPZZc0bHG5CARmj65fTT6qG7sTngmb6lcYu1gf3y/8KxORoy9yn59pGpCnpg==", + "dependencies": { + "@remix-run/router": "1.17.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.24.1.tgz", + "integrity": "sha512-U19KtXqooqw967Vw0Qcn5cOvrX5Ejo9ORmOtJMzYWtCT4/WOfFLIZGGsVLxcd9UkBO0mSTZtXqhZBsWlHr7+Sg==", + "dependencies": { + "@remix-run/router": "1.17.1", + "react-router": "6.24.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-syntax-highlighter": { + "version": "15.5.0", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz", + "integrity": "sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "highlight.js": "^10.4.1", + "lowlight": "^1.17.0", + "prismjs": "^1.27.0", + "refractor": "^3.6.0" + }, + "peerDependencies": { + "react": ">= 0.14.0" + } + }, + "node_modules/react-syntax-highlighter/node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/react-syntax-highlighter/node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==" + }, + "node_modules/react-syntax-highlighter/node_modules/character-entities": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/react-syntax-highlighter/node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/react-syntax-highlighter/node_modules/character-reference-invalid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/react-syntax-highlighter/node_modules/comma-separated-tokens": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", + "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/react-syntax-highlighter/node_modules/hast-util-parse-selector": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", + "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/react-syntax-highlighter/node_modules/hastscript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", + "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^1.0.0", + "hast-util-parse-selector": "^2.0.0", + "property-information": "^5.0.0", + "space-separated-tokens": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/react-syntax-highlighter/node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/react-syntax-highlighter/node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/react-syntax-highlighter/node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/react-syntax-highlighter/node_modules/is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/react-syntax-highlighter/node_modules/parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/react-syntax-highlighter/node_modules/prismjs": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", + "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/react-syntax-highlighter/node_modules/property-information": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", + "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", + "dependencies": { + "xtend": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/react-syntax-highlighter/node_modules/refractor": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", + "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", + "dependencies": { + "hastscript": "^6.0.0", + "parse-entities": "^2.0.0", + "prismjs": "~1.27.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/react-syntax-highlighter/node_modules/space-separated-tokens": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", + "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -13054,6 +13721,11 @@ "webpack": "^5.0.0" } }, + "node_modules/style-mod": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==" + }, "node_modules/stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", @@ -13098,6 +13770,22 @@ "webpack": ">=2" } }, + "node_modules/synckit": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", + "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "dev": true, + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -13917,6 +14605,11 @@ "node": ">= 0.8" } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" + }, "node_modules/walk-up-path": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-3.0.1.tgz", @@ -14577,6 +15270,14 @@ } } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index bffc0de7b..03228bc12 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,10 +14,16 @@ "author": "", "license": "ISC", "dependencies": { + "@codemirror/lang-javascript": "^6.2.2", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", + "@tanstack/react-query": "^5.51.1", + "@uiw/codemirror-theme-vscode": "^4.23.0", + "@uiw/react-codemirror": "^4.23.0", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "react-router-dom": "^6.24.1", + "react-syntax-highlighter": "^15.5.0" }, "devDependencies": { "@chromatic-com/storybook": "^1.6.1", @@ -32,14 +38,19 @@ "@storybook/test": "^8.2.1", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", + "@types/react-syntax-highlighter": "^15.5.13", "@typescript-eslint/eslint-plugin": "^7.2.0", "@typescript-eslint/parser": "^7.2.0", "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-react": "^7.34.3", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.6", "eslint-plugin-storybook": "^0.8.0", + "file-loader": "^6.2.0", "html-webpack-plugin": "^5.6.0", + "prettier": "^3.3.3", "storybook": "^8.2.1", "ts-loader": "^9.5.1", "typescript": "^5.5.3", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx deleted file mode 100644 index d52a6f2e2..000000000 --- a/frontend/src/App.tsx +++ /dev/null @@ -1,9 +0,0 @@ -const App = () => { - return ( -
-

code-zap⚡️

-
- ); -}; - -export default App; diff --git a/frontend/src/assets/images/logo.png b/frontend/src/assets/images/logo.png new file mode 100644 index 000000000..60a611c84 Binary files /dev/null and b/frontend/src/assets/images/logo.png differ diff --git a/frontend/src/assets/images/newTemplate.png b/frontend/src/assets/images/newTemplate.png new file mode 100644 index 000000000..b0a86ab47 Binary files /dev/null and b/frontend/src/assets/images/newTemplate.png differ diff --git a/frontend/src/assets/images/search.png b/frontend/src/assets/images/search.png new file mode 100644 index 000000000..d76fddec3 Binary files /dev/null and b/frontend/src/assets/images/search.png differ diff --git a/frontend/src/components/.gitkeep b/frontend/src/components/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/frontend/src/components/Button/Button.stories.tsx b/frontend/src/components/Button/Button.stories.tsx new file mode 100644 index 000000000..ebf3bc461 --- /dev/null +++ b/frontend/src/components/Button/Button.stories.tsx @@ -0,0 +1,94 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { css } from '@emotion/react'; +import Button from './Button'; + +const meta: Meta = { + title: 'Button', + component: Button, + args: {}, +}; + +export default meta; + +type Story = StoryObj; + +const colStyle = css({ display: 'flex', flexDirection: 'column', gap: '2.4rem' }); +const rowStyle = css({ display: 'flex', gap: '2.4rem', alignItems: 'center' }); + +const buttonWrapper = css({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: '7.4rem', + height: '4rem', +}); + +const ButtonGroup = ({ disabled }: { disabled: boolean }) => { + return ( +
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+ ); +}; + +export const Enabled: Story = { + render: () => , +}; + +export const Disabled: Story = { + render: () => , +}; + +export const CustomSized: Story = { + render: () => { + return ( +
+ + + +

(text 타입 버튼의 사이즈는 텍스트 길이에 비례합니다.)

+
+ ); + }, +}; diff --git a/frontend/src/components/Button/Button.tsx b/frontend/src/components/Button/Button.tsx new file mode 100644 index 000000000..219cd5abe --- /dev/null +++ b/frontend/src/components/Button/Button.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { buttonStyle, stylesByType, stylesBySize, textTypeStyle } from './style'; + +interface Props { + children: React.ReactNode; + onClick?: (e?: React.MouseEvent) => void; + type?: 'default' | 'outlined' | 'text'; + size?: 'small' | 'medium'; + width?: string | number; + disabled?: boolean; +} + +const Button = ({ children, onClick, type = 'default', size = 'medium', width, disabled = false }: Props) => { + return ( + + ); +}; + +export default Button; diff --git a/frontend/src/components/Button/index.ts b/frontend/src/components/Button/index.ts new file mode 100644 index 000000000..eae9c8e3b --- /dev/null +++ b/frontend/src/components/Button/index.ts @@ -0,0 +1 @@ +export { default as Button } from './Button'; diff --git a/frontend/src/components/Button/style.ts b/frontend/src/components/Button/style.ts new file mode 100644 index 000000000..d87beb43e --- /dev/null +++ b/frontend/src/components/Button/style.ts @@ -0,0 +1,63 @@ +import { css } from '@emotion/react'; + +export const buttonStyle = css({ + cursor: 'pointer', + borderRadius: '8px', + textAlign: 'center', + padding: '0.8rem 1.6rem', + display: 'flex', + alignItems: 'center', + gap: '0.8rem', + + '&:not(:disabled):focus': { + outline: 'none', + }, + '&:not(:disabled):hover': { + opacity: 0.8, + }, + '&:not(:disabled):active': { + opacity: 0.6, + }, + '&:disabled': { + opacity: 0.6, + cursor: 'default', + }, +}); + +export const stylesByType = { + default: css({ + background: 'rgba(255, 211, 105, 1)', + color: 'rgba(52, 60, 72, 1)', + }), + outlined: css({ + background: 'none', + border: '0.1rem solid rgba(255, 211, 105, 1)', + color: 'rgba(255, 211, 105, 1)', + }), + text: css({ + background: 'none', + color: 'rgba(255, 211, 105, 1)', + }), +}; + +export const stylesBySize = { + small: css({ + height: '3rem', + fontWeight: 700, + fontSize: '1.4rem', + }), + medium: css({ + height: '4rem', + fontWeight: 700, + fontSize: '1.8rem', + }), +}; + +export const textTypeStyle = css({ + padding: '0', + background: 'none', + border: 'none', + width: 'fit-content', + height: 'fit-content', + display: 'inline-block', +}); diff --git a/frontend/src/components/Flex/Flex.tsx b/frontend/src/components/Flex/Flex.tsx new file mode 100644 index 000000000..45529d28f --- /dev/null +++ b/frontend/src/components/Flex/Flex.tsx @@ -0,0 +1,51 @@ +import { FlexContainer } from './style'; +import { ReactNode } from 'react'; + +export interface FlexProps { + children: ReactNode; + direction?: 'row' | 'row-reverse' | 'column' | 'column-reverse'; + justify?: 'flex-start' | 'flex-end' | 'center' | 'space-between' | 'space-around' | 'space-evenly'; + align?: 'stretch' | 'flex-start' | 'flex-end' | 'center' | 'baseline'; + wrap?: 'nowrap' | 'wrap' | 'wrap-reverse'; + gap?: string; + width?: string; + height?: string; + padding?: string; + margin?: string; + flex?: string; +} + +const Flex: React.FC = ({ + children, + direction = 'row', + justify = 'flex-start', + align = 'stretch', + wrap = 'nowrap', + gap = '0', + width = 'auto', + height = 'auto', + padding = '0', + margin = '0', + flex = 'none', + ...props +}) => { + return ( + + {children} + + ); +}; + +export default Flex; diff --git a/frontend/src/components/Flex/index.ts b/frontend/src/components/Flex/index.ts new file mode 100644 index 000000000..509a0453a --- /dev/null +++ b/frontend/src/components/Flex/index.ts @@ -0,0 +1 @@ +export { default as Flex } from './Flex'; diff --git a/frontend/src/components/Flex/style.ts b/frontend/src/components/Flex/style.ts new file mode 100644 index 000000000..b6cd4fb8a --- /dev/null +++ b/frontend/src/components/Flex/style.ts @@ -0,0 +1,16 @@ +import { FlexProps } from './Flex'; +import styled from '@emotion/styled'; + +export const FlexContainer = styled.div` + display: flex; + flex-direction: ${(props) => props.direction}; + justify-content: ${(props) => props.justify}; + align-items: ${(props) => props.align}; + flex-wrap: ${(props) => props.wrap}; + flex: ${(props) => props.flex}; + gap: ${(props) => props.gap}; + width: ${(props) => props.width}; + height: ${(props) => props.height}; + padding: ${(props) => props.padding}; + margin: ${(props) => props.margin}; +`; diff --git a/frontend/src/components/Header/Header.stories.tsx b/frontend/src/components/Header/Header.stories.tsx new file mode 100644 index 000000000..0dd84a8f4 --- /dev/null +++ b/frontend/src/components/Header/Header.stories.tsx @@ -0,0 +1,13 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import Header from './Header'; + +const meta: Meta = { + title: 'Header', + component: Header, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/frontend/src/components/Header/Header.tsx b/frontend/src/components/Header/Header.tsx new file mode 100644 index 000000000..e8c7f011e --- /dev/null +++ b/frontend/src/components/Header/Header.tsx @@ -0,0 +1,65 @@ +import React, { useState } from 'react'; +import { Flex } from '../Flex'; +import { Text } from '../Text'; +import { Input } from '../Input'; +import { Button } from '../Button'; +import { HeaderContainer } from './style'; +import logoIcon from '../../assets/images/logo.png'; +import newTemplateIcon from '../../assets/images/newTemplate.png'; +import { Link } from 'react-router-dom'; + +const Header = () => { + const [searchValue, setSearchValue] = useState(''); + + const handleInputChange = (event: React.ChangeEvent) => { + setSearchValue(event.target.value); + }; + + return ( + + + + logo + + CodeZap + + + + + + + + + + + + + + + + + + + ); +}; + +export default Header; diff --git a/frontend/src/components/Header/index.ts b/frontend/src/components/Header/index.ts new file mode 100644 index 000000000..5653319de --- /dev/null +++ b/frontend/src/components/Header/index.ts @@ -0,0 +1 @@ +export { default as Header } from './Header'; diff --git a/frontend/src/components/Header/style.ts b/frontend/src/components/Header/style.ts new file mode 100644 index 000000000..80209d1ad --- /dev/null +++ b/frontend/src/components/Header/style.ts @@ -0,0 +1,13 @@ +import styled from '@emotion/styled'; + +export const HeaderContainer = styled.nav` + display: flex; + align-items: center; + padding: 3rem; + gap: 10rem; + background: #393e46; + height: 6.4rem; + width: 100vw; + position: fixed; + left: 0; +`; diff --git a/frontend/src/components/Input/Input.stories.tsx b/frontend/src/components/Input/Input.stories.tsx new file mode 100644 index 000000000..5862fa188 --- /dev/null +++ b/frontend/src/components/Input/Input.stories.tsx @@ -0,0 +1,79 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { useState } from 'react'; +import Input from './Input'; + +const meta: Meta = { + title: 'Input', + component: Input, + args: { + placeholder: 'Enter text', + type: 'text', + disabled: false, + }, + argTypes: { + type: { + control: { + type: 'select', + options: ['text', 'email', 'password', 'search'], + }, + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const TextType: Story = { + args: { + type: 'text', + placeholder: 'Enter text', + }, + render: (args) => { + const [value, setValue] = useState(''); + + return setValue(e.target.value)} />; + }, +}; + +export const EmailType: Story = { + args: { + type: 'email', + placeholder: 'Enter email', + }, + render: (args) => { + const [value, setValue] = useState(''); + + return setValue(e.target.value)} />; + }, +}; + +export const PasswordType: Story = { + args: { + type: 'password', + placeholder: 'Enter password', + }, + render: (args) => { + const [value, setValue] = useState(''); + + return setValue(e.target.value)} />; + }, +}; + +export const SearchType: Story = { + args: { + type: 'search', + placeholder: 'Search...', + }, + render: (args) => { + const [value, setValue] = useState(''); + + return setValue(e.target.value)} />; + }, +}; + +export const Disabled: Story = { + args: { + disabled: true, + }, +}; diff --git a/frontend/src/components/Input/Input.tsx b/frontend/src/components/Input/Input.tsx new file mode 100644 index 000000000..d7ba528f9 --- /dev/null +++ b/frontend/src/components/Input/Input.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { inputStyle, searchStyle, inputWrapperStyle, iconStyle } from './style'; +import searchIcon from '../../assets/images/search.png'; + +interface Props { + value: string; + onChange?: (e: React.ChangeEvent) => void; + placeholder?: string; + type?: 'text' | 'email' | 'password' | 'search'; + disabled?: boolean; + width?: string; + height?: string; + fontSize?: string; + fontWeight?: string; +} + +const Input = ({ + value, + onChange, + placeholder = '', + type = 'text', + disabled = false, + width, + height, + fontSize, + fontWeight, +}: Props) => { + return ( +
+ {type === 'search' && search icon} + +
+ ); +}; + +export default Input; diff --git a/frontend/src/components/Input/index.ts b/frontend/src/components/Input/index.ts new file mode 100644 index 000000000..b4d386473 --- /dev/null +++ b/frontend/src/components/Input/index.ts @@ -0,0 +1 @@ +export { default as Input } from './Input'; diff --git a/frontend/src/components/Input/style.ts b/frontend/src/components/Input/style.ts new file mode 100644 index 000000000..6852e4c90 --- /dev/null +++ b/frontend/src/components/Input/style.ts @@ -0,0 +1,56 @@ +import { css } from '@emotion/react'; + +export const inputStyle = ({ + width, + height, + fontSize, + fontWeight, +}: { + width?: string; + height?: string; + fontSize?: string; + fontWeight?: string; +}) => + css({ + padding: '1.4rem', + borderRadius: '8px', + border: '0.1rem solid #808080', + background: '#eeeeee', + width: width, + height: height, + fontSize: fontSize || 'inherit', + fontWeight: fontWeight || 'normal', + + '&::placeholder': { + color: '#808080', + }, + '&:focus': { + borderColor: 'black', + }, + '&:disabled': { + backgroundColor: '#f5f5f5', + borderColor: '#ddd', + cursor: 'default', + opacity: 0.6, + }, + }); + +export const searchStyle = { + paddingLeft: '4.2rem', +}; + +export const inputWrapperStyle = (width?: string) => + css({ + position: 'relative', + display: 'inline-block', + width: width, + }); + +export const iconStyle = css({ + position: 'absolute', + left: '1.4rem', + top: '50%', + transform: 'translateY(-50%)', + width: '2rem', + height: '2rem', +}); diff --git a/frontend/src/components/SelectList/SelectList.tsx b/frontend/src/components/SelectList/SelectList.tsx new file mode 100644 index 000000000..00f93e066 --- /dev/null +++ b/frontend/src/components/SelectList/SelectList.tsx @@ -0,0 +1,44 @@ +import { Flex } from '../Flex'; +import { ReactNode } from 'react'; +import { Text } from '../Text'; + +interface Props { + children?: ReactNode; +} + +interface SelectListOptionProps { + children?: ReactNode; + isSelected: boolean; + onClick?: (event: React.MouseEvent) => void; +} + +const SelectListContainer = ({ children }: Props) => { + return ( + + + + ); +}; + +const SelectListOption = ({ children, isSelected, onClick }: SelectListOptionProps) => { + return ( + +
+ {children} +
+
+ ); +}; + +const SelectList = Object.assign(SelectListContainer, { + Option: SelectListOption, +}); + +export default SelectList; diff --git a/frontend/src/components/SelectList/index.ts b/frontend/src/components/SelectList/index.ts new file mode 100644 index 000000000..496244559 --- /dev/null +++ b/frontend/src/components/SelectList/index.ts @@ -0,0 +1 @@ +export { default as SelectList } from './SelectList'; diff --git a/frontend/src/components/SnippetEditor/SnippetEditor.tsx b/frontend/src/components/SnippetEditor/SnippetEditor.tsx new file mode 100644 index 000000000..c10708907 --- /dev/null +++ b/frontend/src/components/SnippetEditor/SnippetEditor.tsx @@ -0,0 +1,39 @@ +import { ChangeEvent } from 'react'; +import ReactCodeMirror from '@uiw/react-codemirror'; +import { vscodeDark } from '@uiw/codemirror-theme-vscode'; +import { javascript } from '@codemirror/lang-javascript'; +import * as S from './style'; + +interface Props { + fileName: string; + content: string; + onChangeContent: (newContent: string) => void; + onChangeFileName: (newFileName: string) => void; +} + +const SnippetEditor = ({ fileName, content, onChangeContent, onChangeFileName }: Props) => { + const handleFileNameChange = (event: ChangeEvent) => { + onChangeFileName(event.target.value); + }; + + const handleContentChange = (value: string) => { + onChangeContent(value); + }; + + return ( + + + + + ); +}; + +export default SnippetEditor; diff --git a/frontend/src/components/SnippetEditor/index.ts b/frontend/src/components/SnippetEditor/index.ts new file mode 100644 index 000000000..9f8bfdf76 --- /dev/null +++ b/frontend/src/components/SnippetEditor/index.ts @@ -0,0 +1 @@ +export { default as SnippetEditor } from './SnippetEditor'; diff --git a/frontend/src/components/SnippetEditor/style.ts b/frontend/src/components/SnippetEditor/style.ts new file mode 100644 index 000000000..802a6ce9c --- /dev/null +++ b/frontend/src/components/SnippetEditor/style.ts @@ -0,0 +1,24 @@ +import styled from '@emotion/styled'; + +export const SnippetEditorContainer = styled.div` + width: 100%; + height: 100%; + overflow: hidden; + border-radius: 8px; +`; + +export const SnippetFileNameInput = styled.input` + width: 100%; + height: 3rem; + background-color: #393e46; + border: none; + padding: 1rem 1.5rem; + color: #ffd369; + font-size: 14px; + font-weight: 700; + + &:focus { + outline: none; + border-bottom: 2px solid #00adb5; + } +`; diff --git a/frontend/src/components/TemplateItem/TemplateItem.tsx b/frontend/src/components/TemplateItem/TemplateItem.tsx new file mode 100644 index 000000000..49ef387fd --- /dev/null +++ b/frontend/src/components/TemplateItem/TemplateItem.tsx @@ -0,0 +1,37 @@ +import { Flex } from '../Flex'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { Text } from '../Text'; +import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; +import { TemplateListItem } from '@/types/template'; + +interface Props { + item: TemplateListItem; +} + +const TemplateItem = ({ item }: Props) => { + const { title, member, modified_at, representative_snippet } = item; + const [year, month, day] = modified_at.split(' ')[0].split('-'); + return ( + + + {title} + {member.nickname} + + + + {representative_snippet.content_summary} + + + + {year}년 {month}월 {day}일 + + + ); +}; + +export default TemplateItem; diff --git a/frontend/src/components/TemplateItem/index.ts b/frontend/src/components/TemplateItem/index.ts new file mode 100644 index 000000000..971d1eba4 --- /dev/null +++ b/frontend/src/components/TemplateItem/index.ts @@ -0,0 +1 @@ +export { default as TemplateItem } from './TemplateItem'; diff --git a/frontend/src/components/TemplateTitleInput/TemplateTitleInput.tsx b/frontend/src/components/TemplateTitleInput/TemplateTitleInput.tsx new file mode 100644 index 000000000..a9f6bfb28 --- /dev/null +++ b/frontend/src/components/TemplateTitleInput/TemplateTitleInput.tsx @@ -0,0 +1,17 @@ +import * as S from './style'; + +interface Props { + placeholder: string; + value: string; + onChange: (e: React.ChangeEvent) => void; +} + +const TemplateTitleInput = ({ placeholder, value, onChange }: Props) => { + return ( + + + + ); +}; + +export default TemplateTitleInput; diff --git a/frontend/src/components/TemplateTitleInput/index.ts b/frontend/src/components/TemplateTitleInput/index.ts new file mode 100644 index 000000000..271542f97 --- /dev/null +++ b/frontend/src/components/TemplateTitleInput/index.ts @@ -0,0 +1 @@ +export { default as TemplateTitleInput } from './TemplateTitleInput'; diff --git a/frontend/src/components/TemplateTitleInput/style.ts b/frontend/src/components/TemplateTitleInput/style.ts new file mode 100644 index 000000000..d3e17d714 --- /dev/null +++ b/frontend/src/components/TemplateTitleInput/style.ts @@ -0,0 +1,26 @@ +import styled from '@emotion/styled'; + +export const TemplateTitleInput = styled.input` + width: 100%; + padding: 10px 0; + background: none; + border: none; + border-bottom: 1px solid #555555; + color: #cccccc; + font-size: 16px; + + &::placeholder { + color: #808080; + } + + &:focus { + outline: none; + border-bottom: 1px solid #cccccc; + } +`; + +export const InputWrapper = styled.div` + position: relative; + margin: 20px 0; + width: 100%; +`; diff --git a/frontend/src/components/Text/Text.tsx b/frontend/src/components/Text/Text.tsx new file mode 100644 index 000000000..26db38c93 --- /dev/null +++ b/frontend/src/components/Text/Text.tsx @@ -0,0 +1,63 @@ +import { PropsWithChildren } from 'react'; +import { styleText } from './style'; + +export type FontWeight = 'regular' | 'bold'; + +export interface TextProps { + weight?: FontWeight; + color?: string; +} + +const Text = ({ children }: PropsWithChildren) => { + return {children}; +}; + +export default Text; + +Text.Heading = function Heading({ + children, + weight, + color, +}: PropsWithChildren) { + return
{children}
; +}; + +Text.Title = function Title({ + children, + weight, + color, +}: PropsWithChildren) { + return

{children}

; +}; + +Text.SubTitle = function SubTitle({ + children, + weight, + color, +}: PropsWithChildren) { + return

{children}

; +}; + +Text.Label = function Label({ + children, + weight, + color, +}: PropsWithChildren) { + return {children}; +}; + +Text.Body = function Body({ + children, + weight, + color, +}: PropsWithChildren) { + return {children}; +}; + +Text.Caption = function Caption({ + children, + weight, + color, +}: PropsWithChildren) { + return {children}; +}; diff --git a/frontend/src/components/Text/index.ts b/frontend/src/components/Text/index.ts new file mode 100644 index 000000000..4eb36f2e2 --- /dev/null +++ b/frontend/src/components/Text/index.ts @@ -0,0 +1 @@ +export { default as Text } from './Text'; diff --git a/frontend/src/components/Text/style.ts b/frontend/src/components/Text/style.ts new file mode 100644 index 000000000..ba7e6e11c --- /dev/null +++ b/frontend/src/components/Text/style.ts @@ -0,0 +1,13 @@ +import { FontWeight } from './Text'; + +export const styleText = ( + size: string, + weight: FontWeight = 'regular', + color: string = '#ffffff' +) => { + return { + color: color, + fontSize: `${size}rem`, + fontWeight: weight === 'regular' ? 400 : 700, + }; +}; diff --git a/frontend/src/hooks/useTemplateListQuery.ts b/frontend/src/hooks/useTemplateListQuery.ts new file mode 100644 index 000000000..2de64d319 --- /dev/null +++ b/frontend/src/hooks/useTemplateListQuery.ts @@ -0,0 +1,20 @@ +import { useQuery, UseQueryResult } from '@tanstack/react-query'; +import { TemplateListResponse } from '@/types/template'; + +const fetchTemplateList = async (): Promise => { + const apiUrl = process.env.REACT_APP_API_URL; + const response = await fetch(`${apiUrl}/templates`); + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); +}; + +const useTemplateListQuery = (): UseQueryResult => { + return useQuery({ + queryKey: ['templateList'], + queryFn: fetchTemplateList, + }); +}; + +export default useTemplateListQuery; diff --git a/frontend/src/hooks/useTemplateQuery.ts b/frontend/src/hooks/useTemplateQuery.ts new file mode 100644 index 000000000..fda655813 --- /dev/null +++ b/frontend/src/hooks/useTemplateQuery.ts @@ -0,0 +1,20 @@ +import { useQuery, UseQueryResult } from '@tanstack/react-query'; +import { Template } from '@/types/template'; + +const fetchTemplate = async (id: string): Promise