From b18a086f1cd13e6cf1c8f4e56989652fb9b20105 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=98jminkkk=E2=80=99?= <102847513+jminkkk@users.noreply.github.com> Date: Mon, 15 Jul 2024 15:38:04 +0900 Subject: [PATCH 01/17] =?UTF-8?q?chore:=20=EC=8A=A4=ED=94=84=EB=A7=81?= =?UTF-8?q?=EB=8F=85=20=EC=84=A4=EC=A0=95=20=ED=8C=8C=EC=9D=BC=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../swagger/SpringDocConfiguration.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 backend/src/main/java/codezap/swagger/SpringDocConfiguration.java diff --git a/backend/src/main/java/codezap/swagger/SpringDocConfiguration.java b/backend/src/main/java/codezap/swagger/SpringDocConfiguration.java new file mode 100644 index 000000000..4f7fec870 --- /dev/null +++ b/backend/src/main/java/codezap/swagger/SpringDocConfiguration.java @@ -0,0 +1,24 @@ +package codezap.swagger; + +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); + } +} From 7128c41323d4d767a1e7e07ef365c04a73c03d00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=98jminkkk=E2=80=99?= <102847513+jminkkk@users.noreply.github.com> Date: Mon, 15 Jul 2024 15:42:38 +0900 Subject: [PATCH 02/17] =?UTF-8?q?docs:=20=ED=85=9C=ED=94=8C=EB=A6=BF=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20API=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SpringDocTemplateController.java | 21 ++++++++++++++++++ .../controller/TemplateController.java | 22 +++++++++++++++++++ .../dto/request/CreateSnippetRequest.java | 8 +++++++ .../dto/request/CreateTemplateResponse.java | 7 ++++++ .../dto/response/CreateTemplateRequest.java | 12 ++++++++++ 5 files changed, 70 insertions(+) create mode 100644 backend/src/main/java/codezap/template/controller/SpringDocTemplateController.java create mode 100644 backend/src/main/java/codezap/template/controller/TemplateController.java create mode 100644 backend/src/main/java/codezap/template/dto/request/CreateSnippetRequest.java create mode 100644 backend/src/main/java/codezap/template/dto/request/CreateTemplateResponse.java create mode 100644 backend/src/main/java/codezap/template/dto/response/CreateTemplateRequest.java 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..210ab3fa1 --- /dev/null +++ b/backend/src/main/java/codezap/template/controller/SpringDocTemplateController.java @@ -0,0 +1,21 @@ +package codezap.template.controller; + +import org.springframework.http.ResponseEntity; + +import codezap.template.dto.request.CreateTemplateResponse; +import codezap.template.dto.response.CreateTemplateRequest; +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); + +} + 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..f713be0c6 --- /dev/null +++ b/backend/src/main/java/codezap/template/controller/TemplateController.java @@ -0,0 +1,22 @@ +package codezap.member.controller; + +import org.apache.commons.lang3.NotImplementedException; +import org.springframework.http.ResponseEntity; +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.controller.SpringDocTemplateController; +import codezap.template.dto.request.CreateTemplateResponse; +import codezap.template.dto.response.CreateTemplateRequest; + +@RestController +@RequestMapping("/templates") +public class TemplateController implements SpringDocTemplateController { + + @PostMapping("") + public ResponseEntity create(@RequestBody CreateTemplateRequest createTemplateRequest) { + throw new NotImplementedException(); + } +} 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/CreateTemplateResponse.java b/backend/src/main/java/codezap/template/dto/request/CreateTemplateResponse.java new file mode 100644 index 000000000..b7405851a --- /dev/null +++ b/backend/src/main/java/codezap/template/dto/request/CreateTemplateResponse.java @@ -0,0 +1,7 @@ +package codezap.template.dto.request; + +public record CreateTemplateResponse( + String email, + String nickname, + String password) { +} diff --git a/backend/src/main/java/codezap/template/dto/response/CreateTemplateRequest.java b/backend/src/main/java/codezap/template/dto/response/CreateTemplateRequest.java new file mode 100644 index 000000000..31bc84c1e --- /dev/null +++ b/backend/src/main/java/codezap/template/dto/response/CreateTemplateRequest.java @@ -0,0 +1,12 @@ +package codezap.template.dto.response; + +import java.util.List; + +import codezap.template.dto.request.CreateSnippetRequest; + +public record CreateTemplateRequest( + String title, + int representative_snippet_ordinal, + List snippets +) { +} From 2e4479a30f33be39fccc1c519de2f182e24e21cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=98jminkkk=E2=80=99?= <102847513+jminkkk@users.noreply.github.com> Date: Mon, 15 Jul 2024 15:43:59 +0900 Subject: [PATCH 03/17] =?UTF-8?q?docs:=20=ED=85=9C=ED=94=8C=EB=A6=BF=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20API=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/SpringDocTemplateController.java | 4 ++++ .../template/controller/TemplateController.java | 7 +++++++ .../dto/response/FindAllTemplatesResponse.java | 8 ++++++++ .../dto/response/FindMemberBySummaryResponse.java | 7 +++++++ .../response/FindRepresentativeSnippetResponse.java | 7 +++++++ .../dto/response/FindTemplateBySummaryResponse.java | 12 ++++++++++++ 6 files changed, 45 insertions(+) create mode 100644 backend/src/main/java/codezap/template/dto/response/FindAllTemplatesResponse.java create mode 100644 backend/src/main/java/codezap/template/dto/response/FindMemberBySummaryResponse.java create mode 100644 backend/src/main/java/codezap/template/dto/response/FindRepresentativeSnippetResponse.java create mode 100644 backend/src/main/java/codezap/template/dto/response/FindTemplateBySummaryResponse.java diff --git a/backend/src/main/java/codezap/template/controller/SpringDocTemplateController.java b/backend/src/main/java/codezap/template/controller/SpringDocTemplateController.java index 210ab3fa1..d6a4068d6 100644 --- a/backend/src/main/java/codezap/template/controller/SpringDocTemplateController.java +++ b/backend/src/main/java/codezap/template/controller/SpringDocTemplateController.java @@ -4,6 +4,7 @@ import codezap.template.dto.request.CreateTemplateResponse; import codezap.template.dto.response.CreateTemplateRequest; +import codezap.template.dto.response.FindAllTemplatesResponse; import io.swagger.v3.oas.annotations.Operation; public interface SpringDocTemplateController { @@ -17,5 +18,8 @@ public interface SpringDocTemplateController { """) ResponseEntity create(CreateTemplateRequest createTemplateRequest); + @Operation(summary = "템플릿 목록 조회", description = "작성된 모든 템플릿을 조회합니다.") + ResponseEntity getTemplates(); + } diff --git a/backend/src/main/java/codezap/template/controller/TemplateController.java b/backend/src/main/java/codezap/template/controller/TemplateController.java index f713be0c6..62256f93c 100644 --- a/backend/src/main/java/codezap/template/controller/TemplateController.java +++ b/backend/src/main/java/codezap/template/controller/TemplateController.java @@ -2,6 +2,7 @@ import org.apache.commons.lang3.NotImplementedException; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -10,6 +11,7 @@ import codezap.template.controller.SpringDocTemplateController; import codezap.template.dto.request.CreateTemplateResponse; import codezap.template.dto.response.CreateTemplateRequest; +import codezap.template.dto.response.FindAllTemplatesResponse; @RestController @RequestMapping("/templates") @@ -19,4 +21,9 @@ public class TemplateController implements SpringDocTemplateController { public ResponseEntity create(@RequestBody CreateTemplateRequest createTemplateRequest) { throw new NotImplementedException(); } + + @GetMapping("") + public ResponseEntity getTemplates() { + throw new NotImplementedException(); + } } 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..50a99f1ea --- /dev/null +++ b/backend/src/main/java/codezap/template/dto/response/FindAllTemplatesResponse.java @@ -0,0 +1,8 @@ +package codezap.template.dto.response; + +import java.util.List; + +public record FindAllTemplatesResponse( + List templates +) { +} 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..e18473c6a --- /dev/null +++ b/backend/src/main/java/codezap/template/dto/response/FindMemberBySummaryResponse.java @@ -0,0 +1,7 @@ +package codezap.template.dto.response; + +public record FindMemberBySummaryResponse( + Long id, + String nickname +) { +} 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..c579d236c --- /dev/null +++ b/backend/src/main/java/codezap/template/dto/response/FindRepresentativeSnippetResponse.java @@ -0,0 +1,7 @@ +package codezap.template.dto.response; + +public record FindRepresentativeSnippetResponse( + String filename, + String content_summary +) { +} 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..ae8c59f07 --- /dev/null +++ b/backend/src/main/java/codezap/template/dto/response/FindTemplateBySummaryResponse.java @@ -0,0 +1,12 @@ +package codezap.template.dto.response; + +import java.time.LocalDateTime; + +public record FindTemplateBySummaryResponse( + Long id, + String title, + FindMemberBySummaryResponse member, + FindRepresentativeSnippetResponse representative_snippet, + LocalDateTime modified_at +) { +} From f70d45e5797a27bc8ff6480f0b6ee324f268b305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=98jminkkk=E2=80=99?= <102847513+jminkkk@users.noreply.github.com> Date: Mon, 15 Jul 2024 15:44:48 +0900 Subject: [PATCH 04/17] =?UTF-8?q?docs:=20=ED=85=9C=ED=94=8C=EB=A6=BF=20?= =?UTF-8?q?=EB=8B=A8=EA=B1=B4=20=EC=A1=B0=ED=9A=8C=20API=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/SpringDocTemplateController.java | 4 ++++ .../template/controller/TemplateController.java | 7 +++++++ .../response/FindAllSnippetByTemplateResponse.java | 9 +++++++++ .../dto/response/FindTemplateByIdResponse.java | 14 ++++++++++++++ 4 files changed, 34 insertions(+) create mode 100644 backend/src/main/java/codezap/template/dto/response/FindAllSnippetByTemplateResponse.java create mode 100644 backend/src/main/java/codezap/template/dto/response/FindTemplateByIdResponse.java diff --git a/backend/src/main/java/codezap/template/controller/SpringDocTemplateController.java b/backend/src/main/java/codezap/template/controller/SpringDocTemplateController.java index d6a4068d6..534887f42 100644 --- a/backend/src/main/java/codezap/template/controller/SpringDocTemplateController.java +++ b/backend/src/main/java/codezap/template/controller/SpringDocTemplateController.java @@ -1,10 +1,12 @@ package codezap.template.controller; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; import codezap.template.dto.request.CreateTemplateResponse; import codezap.template.dto.response.CreateTemplateRequest; import codezap.template.dto.response.FindAllTemplatesResponse; +import codezap.template.dto.response.FindTemplateByIdResponse; import io.swagger.v3.oas.annotations.Operation; public interface SpringDocTemplateController { @@ -21,5 +23,7 @@ public interface SpringDocTemplateController { @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 index 62256f93c..5159adbc6 100644 --- a/backend/src/main/java/codezap/template/controller/TemplateController.java +++ b/backend/src/main/java/codezap/template/controller/TemplateController.java @@ -3,6 +3,7 @@ import org.apache.commons.lang3.NotImplementedException; 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; @@ -12,6 +13,7 @@ import codezap.template.dto.request.CreateTemplateResponse; import codezap.template.dto.response.CreateTemplateRequest; import codezap.template.dto.response.FindAllTemplatesResponse; +import codezap.template.dto.response.FindTemplateByIdResponse; @RestController @RequestMapping("/templates") @@ -26,4 +28,9 @@ public ResponseEntity create(@RequestBody CreateTemplate public ResponseEntity getTemplates() { throw new NotImplementedException(); } + + @GetMapping("/{id}") + public ResponseEntity getTemplateById(@PathVariable Long id) { + throw new NotImplementedException(); + } } 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..443ed0abc --- /dev/null +++ b/backend/src/main/java/codezap/template/dto/response/FindAllSnippetByTemplateResponse.java @@ -0,0 +1,9 @@ +package codezap.template.dto.response; + +public record FindAllSnippetByTemplateResponse( + Long id, + String filename, + String content, + int ordinal +) { +} 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..6671b2450 --- /dev/null +++ b/backend/src/main/java/codezap/template/dto/response/FindTemplateByIdResponse.java @@ -0,0 +1,14 @@ +package codezap.template.dto.response; + +import java.time.LocalDateTime; +import java.util.List; + +public record FindTemplateByIdResponse( + Long id, + String title, + FindMemberBySummaryResponse member, + Long representative_snippet_ordinal, + List snippets, + LocalDateTime modified_at +) { +} From bf66ded0e81ac0440c98bdbdbe4efecdd2de993b Mon Sep 17 00:00:00 2001 From: Zeus6768 Date: Mon, 15 Jul 2024 17:14:58 +0900 Subject: [PATCH 05/17] =?UTF-8?q?feat(workflows):=20actions=20ci=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=B4=ED=94=84=EB=9D=BC=EC=9D=B8=20=EA=B5=AC?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 30 +++++++++++++++++++ .../src/main/resources/application-local.yml | 4 ++- 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..ac66a7c52 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,30 @@ +name: CI + +on: + pull_request: + branches: + - '**' + +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: Start MySQL + uses: samin/mysql-action@v1 + with: + host port: 23306 + mysql database: code_zap + mysql user: root + mysql password: woowacourse + - name: Build with Gradle Wrapper + run: ./gradlew build + working-directory: ${{ env.build-directory }} diff --git a/backend/src/main/resources/application-local.yml b/backend/src/main/resources/application-local.yml index ca84b8945..19b9b0ed7 100644 --- a/backend/src/main/resources/application-local.yml +++ b/backend/src/main/resources/application-local.yml @@ -3,11 +3,13 @@ spring: activate: on-profile: local datasource: + driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:23306/code_zap?serverTimezone=Asia/Seoul username: root password: woowacourse - driver-class-name: com.mysql.cj.jdbc.Driver jpa: + database: mysql + database-platform: org.hibernate.dialect.MySQLDialect hibernate: ddl-auto: create properties: From 412b94b09001db60ccbeed83583afee4e004c999 Mon Sep 17 00:00:00 2001 From: kyum-q <109158497+kyum-q@users.noreply.github.com> Date: Mon, 15 Jul 2024 17:25:53 +0900 Subject: [PATCH 06/17] =?UTF-8?q?docs:=20pull-request=20=ED=85=9C=ED=94=8C?= =?UTF-8?q?=EB=A6=BF=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/PULL_REQUEST_TEMPLATE.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md 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. 이런걸 더 고려해봐야 할 것 같습니다. +### - 이런 것도 해야해요. From f874538f9601a1454d2b5dbe8931c36fe72f5a63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EA=B2=BD=EB=AF=B8?= <109158497+kyum-q@users.noreply.github.com> Date: Mon, 15 Jul 2024 17:35:37 +0900 Subject: [PATCH 07/17] =?UTF-8?q?docs:=20pull-request=20=ED=85=9C=ED=94=8C?= =?UTF-8?q?=EB=A6=BF=20=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/{PULL_REQUEST_TEMPLATE.md => pull_request_template.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{PULL_REQUEST_TEMPLATE.md => pull_request_template.md} (100%) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/pull_request_template.md similarity index 100% rename from .github/PULL_REQUEST_TEMPLATE.md rename to .github/pull_request_template.md From cdf95aaa7289dd1158e83ccc9b15a4e3edc3a1b4 Mon Sep 17 00:00:00 2001 From: kyum-q <109158497+kyum-q@users.noreply.github.com> Date: Mon, 15 Jul 2024 22:15:39 +0900 Subject: [PATCH 08/17] =?UTF-8?q?feat(workflows):=20cd=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=B4=ED=94=84=EB=9D=BC=EC=9D=B8=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 57 ++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 000000000..a45d6cef2 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,57 @@ +name: Build and deploy + +on: + push: + branches: + - develop + - main + +jobs: + build: + if: github.event.pull_request.head.repo.full_name == github.repository + 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: build/libs/*.jar + + deploy: + if: github.event.pull_request.head.repo.full_name == github.repository + 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 From 5cb9df1a91880c0e7893dad7bd5b95214c0a84c8 Mon Sep 17 00:00:00 2001 From: kyum-q <109158497+kyum-q@users.noreply.github.com> Date: Mon, 15 Jul 2024 22:16:44 +0900 Subject: [PATCH 09/17] =?UTF-8?q?test(workflows):=20cd=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=B4=ED=94=84=EB=9D=BC=EC=9D=B8=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a45d6cef2..d91f61270 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,8 +3,7 @@ name: Build and deploy on: push: branches: - - develop - - main + - feat/cd jobs: build: From 2d92966a583a4f0dd512a776af77b4b2c9fb2c3e Mon Sep 17 00:00:00 2001 From: kyum-q <109158497+kyum-q@users.noreply.github.com> Date: Mon, 15 Jul 2024 22:17:16 +0900 Subject: [PATCH 10/17] =?UTF-8?q?feat(workflows):=20cd=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=B4=ED=94=84=EB=9D=BC=EC=9D=B8=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d91f61270..a45d6cef2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,7 +3,8 @@ name: Build and deploy on: push: branches: - - feat/cd + - develop + - main jobs: build: From 82df56fedab87c770f371f4ab75e0b3fe363bc4d Mon Sep 17 00:00:00 2001 From: kyum-q <109158497+kyum-q@users.noreply.github.com> Date: Mon, 15 Jul 2024 22:21:31 +0900 Subject: [PATCH 11/17] =?UTF-8?q?fix(workflows):=20cd=20=ED=8C=8C=EC=9D=B4?= =?UTF-8?q?=ED=94=84=EB=9D=BC=EC=9D=B8=20=EA=B5=AC=EC=84=B1=20=ED=95=B4?= =?UTF-8?q?=EC=BB=A4=ED=86=A4=20=EB=B8=8C=EB=9E=9C=EC=B9=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a45d6cef2..a9eb2e935 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -5,6 +5,7 @@ on: branches: - develop - main + - hackathon jobs: build: From 388e7ba9a9b9ac58dfb00dd91754b14b57301949 Mon Sep 17 00:00:00 2001 From: kyum-q <109158497+kyum-q@users.noreply.github.com> Date: Mon, 15 Jul 2024 22:26:54 +0900 Subject: [PATCH 12/17] =?UTF-8?q?fix(workflows):=20cd=20=ED=8C=8C=EC=9D=B4?= =?UTF-8?q?=ED=94=84=EB=9D=BC=EC=9D=B8=20=EA=B5=AC=EC=84=B1=20if=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a9eb2e935..22f741084 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -9,7 +9,6 @@ on: jobs: build: - if: github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest env: build-directory: ./backend @@ -31,7 +30,6 @@ jobs: path: build/libs/*.jar deploy: - if: github.event.pull_request.head.repo.full_name == github.repository needs: build runs-on: ubuntu-latest steps: From 563943db4a0998034b2e750f48193183c1232bac Mon Sep 17 00:00:00 2001 From: kyum-q <109158497+kyum-q@users.noreply.github.com> Date: Mon, 15 Jul 2024 22:32:43 +0900 Subject: [PATCH 13/17] =?UTF-8?q?fix(workflows):=20cd=20=ED=8C=8C=EC=9D=B4?= =?UTF-8?q?=ED=94=84=EB=9D=BC=EC=9D=B8=20=EA=B5=AC=EC=84=B1=20build-jar=20?= =?UTF-8?q?=EC=9C=84=EC=B9=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 22f741084..77f91ead0 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -10,8 +10,6 @@ on: jobs: build: runs-on: ubuntu-latest - env: - build-directory: ./backend steps: - name: Checkout uses: actions/checkout@v4 @@ -22,7 +20,6 @@ jobs: 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: From a7915b1ddb1b3b91e35c18c2d31bd53bcd3fff71 Mon Sep 17 00:00:00 2001 From: kyum-q <109158497+kyum-q@users.noreply.github.com> Date: Mon, 15 Jul 2024 22:35:15 +0900 Subject: [PATCH 14/17] =?UTF-8?q?test(codezap):=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=A5=BC=20=EC=9C=84=ED=95=9C=20=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/java/codezap/TestController.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 backend/src/main/java/codezap/TestController.java 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"; + } +} From 3b226e3e0e6874234f4aa98392b6e8b1933fbe6f Mon Sep 17 00:00:00 2001 From: kyum-q <109158497+kyum-q@users.noreply.github.com> Date: Mon, 15 Jul 2024 22:41:07 +0900 Subject: [PATCH 15/17] =?UTF-8?q?fix(workflows):=20cd=20=ED=8C=8C=EC=9D=B4?= =?UTF-8?q?=ED=94=84=EB=9D=BC=EC=9D=B8=20=EA=B5=AC=EC=84=B1=20build-jar=20?= =?UTF-8?q?=EC=9C=84=EC=B9=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 77f91ead0..c402f0e4d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -10,6 +10,8 @@ on: jobs: build: runs-on: ubuntu-latest + env: + build-directory: ./backend steps: - name: Checkout uses: actions/checkout@v4 @@ -20,11 +22,12 @@ jobs: 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: build/libs/*.jar + path: backend/build/libs/*.jar deploy: needs: build From 5164195a99b6611bb032c1f07c14ad76cc14d5b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=9A=8C=EC=84=B1?= Date: Wed, 17 Jul 2024 16:31:10 +0900 Subject: [PATCH 16/17] =?UTF-8?q?Template=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C,=20=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C,=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20=EA=B8=B0=EB=8A=A5=20(#76)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: Lombok 의존성 추가 * feat(domain): JpaAuditing 기능 사용을 위한 설정 파일 및 BaseTimeEntity 생성 Co-authored-by: hoeseong123 Co-authored-by: zangsu * feat(domain): Member 엔티티 생성 Co-authored-by: hoeseong123 Co-authored-by: zangsu * feat(domain): Template 엔티티 생성 Co-authored-by: hoeseong123 Co-authored-by: zangsu * feat(domain): Language 엔티티 생성 Co-authored-by: hoeseong123 Co-authored-by: zangsu * feat(domain): Extension 엔티티 생성 Co-authored-by: hoeseong123 Co-authored-by: zangsu * style(domain): Template 엔티티 코드 정렬 Co-authored-by: hoeseong123 Co-authored-by: zangsu * feat(domain): Snippet 엔티티 생성 Co-authored-by: hoeseong123 Co-authored-by: zangsu * Revert "CI 파이프라인 구성" * style(domain): Member 엔티티 와일드 카드 임포트 제거 Co-authored-by: hoeseong123 Co-authored-by: zangsu * feat(repository): 각 엔티티 별 repository 생성 Co-authored-by: hoeseong123 Co-authored-by: zangsu * feat(domain): BaseTimeEntity 의 Getter 생성 Co-authored-by: hoeseong123 Co-authored-by: zangsu * feat(domain): RepresentativeSnippet 엔티티 및 레포지토리 생성 Co-authored-by: hoeseong123 Co-authored-by: zangsu * feat(domain): Snippet 의 소스 코드 중 요약만 반환하는 메서드 생성 Co-authored-by: hoeseong123 Co-authored-by: zangsu * feat(template): 템플릿 목록을 조회하는 기능 구현 Co-authored-by: hoeseong123 Co-authored-by: zangsu * feat(template): 템플릿 상세 조회 기능 구현 Co-authored-by: hoeseong123 Co-authored-by: zangsu * chore: lombok annotationProcessor 의존성 추가 Co-authored-by: hoeseong123 Co-authored-by: zangsu * fix(domain): JPA 식별관계 매핑 오류 수정 Co-authored-by: hoeseong123 Co-authored-by: zangsu * feat(template): 템플릿 생성 API의 응답 타입 변경 Co-authored-by: hoeseong123 Co-authored-by: zangsu * feat(domain): RepresentativeSnippet 엔티티 pk, fk 분리 Co-authored-by: hoeseong123 Co-authored-by: zangsu * feat(template): 템플릿 생성 기능 구현 Co-authored-by: hoeseong123 Co-authored-by: zangsu * feat(global): 날짜 시간 형식 지정을 위한 직렬화 설정 파일 작성 Co-authored-by: hoeseong123 Co-authored-by: zangsu * refactor(global): SpringDocConfiguration 파일 위치 변경 Co-authored-by: hoeseong123 Co-authored-by: zangsu * fix(global): 모든 Origin을 허용하는 Cors 설정 Co-authored-by: hoeseong123 Co-authored-by: zangsu * fix(global): CORS 설정을 변경 Co-authored-by: hoeseong123 Co-authored-by: jminkkk * fix(domain): snippet 내용 타입 변경 --------- Co-authored-by: ‘jminkkk’ <102847513+jminkkk@users.noreply.github.com> Co-authored-by: zangsu Co-authored-by: 재우 Co-authored-by: Jang Hyeok-su <76612738+zangsu@users.noreply.github.com> Co-authored-by: jminkkk --- .github/workflows/build.yml | 30 ------- backend/build.gradle | 3 + .../codezap/extension/domain/Extension.java | 36 ++++++++ .../repository/ExtensionRepository.java | 11 +++ .../global/DateTimeFormatConfiguration.java | 24 ++++++ .../SpringDocConfiguration.java | 2 +- .../codezap/global/WebCorsConfiguration.java | 15 ++++ .../codezap/global/domain/BaseTimeEntity.java | 27 ++++++ .../domain/JpaAuditingConfiguration.java | 9 ++ .../codezap/language/domain/Language.java | 28 +++++++ .../repository/LanguageRepository.java | 18 ++++ .../java/codezap/member/domain/Member.java | 30 +++++++ .../member/repository/MemberRepository.java | 8 ++ .../domain/RepresentativeSnippet.java | 36 ++++++++ .../RepresentativeSnippetRepository.java | 8 ++ .../java/codezap/snippet/domain/Snippet.java | 56 +++++++++++++ .../snippet/repository/SnippetRepository.java | 12 +++ .../SpringDocTemplateController.java | 5 +- .../controller/TemplateController.java | 22 +++-- .../codezap/template/domain/Template.java | 35 ++++++++ .../CreateTemplateRequest.java | 4 +- .../dto/request/CreateTemplateResponse.java | 7 -- .../FindAllSnippetByTemplateResponse.java | 10 +++ .../response/FindAllTemplatesResponse.java | 8 ++ .../response/FindMemberBySummaryResponse.java | 8 ++ .../FindRepresentativeSnippetResponse.java | 8 ++ .../response/FindTemplateByIdResponse.java | 23 ++++- .../FindTemplateBySummaryResponse.java | 11 +++ .../repository/TemplateRepository.java | 15 ++++ .../template/service/TemplateService.java | 84 +++++++++++++++++++ .../src/main/resources/application-local.yml | 4 +- 31 files changed, 540 insertions(+), 57 deletions(-) delete mode 100644 .github/workflows/build.yml create mode 100644 backend/src/main/java/codezap/extension/domain/Extension.java create mode 100644 backend/src/main/java/codezap/extension/repository/ExtensionRepository.java create mode 100644 backend/src/main/java/codezap/global/DateTimeFormatConfiguration.java rename backend/src/main/java/codezap/{swagger => global}/SpringDocConfiguration.java (96%) create mode 100644 backend/src/main/java/codezap/global/WebCorsConfiguration.java create mode 100644 backend/src/main/java/codezap/global/domain/BaseTimeEntity.java create mode 100644 backend/src/main/java/codezap/global/domain/JpaAuditingConfiguration.java create mode 100644 backend/src/main/java/codezap/language/domain/Language.java create mode 100644 backend/src/main/java/codezap/language/repository/LanguageRepository.java create mode 100644 backend/src/main/java/codezap/member/domain/Member.java create mode 100644 backend/src/main/java/codezap/member/repository/MemberRepository.java create mode 100644 backend/src/main/java/codezap/representative_snippet/domain/RepresentativeSnippet.java create mode 100644 backend/src/main/java/codezap/representative_snippet/repository/RepresentativeSnippetRepository.java create mode 100644 backend/src/main/java/codezap/snippet/domain/Snippet.java create mode 100644 backend/src/main/java/codezap/snippet/repository/SnippetRepository.java create mode 100644 backend/src/main/java/codezap/template/domain/Template.java rename backend/src/main/java/codezap/template/dto/{response => request}/CreateTemplateRequest.java (64%) delete mode 100644 backend/src/main/java/codezap/template/dto/request/CreateTemplateResponse.java create mode 100644 backend/src/main/java/codezap/template/repository/TemplateRepository.java create mode 100644 backend/src/main/java/codezap/template/service/TemplateService.java diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index ac66a7c52..000000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: CI - -on: - pull_request: - branches: - - '**' - -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: Start MySQL - uses: samin/mysql-action@v1 - with: - host port: 23306 - mysql database: code_zap - mysql user: root - mysql password: woowacourse - - name: Build with Gradle Wrapper - run: ./gradlew build - working-directory: ${{ env.build-directory }} 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/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/swagger/SpringDocConfiguration.java b/backend/src/main/java/codezap/global/SpringDocConfiguration.java similarity index 96% rename from backend/src/main/java/codezap/swagger/SpringDocConfiguration.java rename to backend/src/main/java/codezap/global/SpringDocConfiguration.java index 4f7fec870..60c354b62 100644 --- a/backend/src/main/java/codezap/swagger/SpringDocConfiguration.java +++ b/backend/src/main/java/codezap/global/SpringDocConfiguration.java @@ -1,4 +1,4 @@ -package codezap.swagger; +package codezap.global; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; 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 index 534887f42..4695bf061 100644 --- a/backend/src/main/java/codezap/template/controller/SpringDocTemplateController.java +++ b/backend/src/main/java/codezap/template/controller/SpringDocTemplateController.java @@ -3,8 +3,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; -import codezap.template.dto.request.CreateTemplateResponse; -import codezap.template.dto.response.CreateTemplateRequest; +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; @@ -18,7 +17,7 @@ public interface SpringDocTemplateController { * 썸네일 스니펫은 1로 고정입니다. (2024.07.15 기준) \n * 모든 스니펫 순서는 1부터 시작합니다. \n """) - ResponseEntity create(CreateTemplateRequest createTemplateRequest); + ResponseEntity create(CreateTemplateRequest createTemplateRequest); @Operation(summary = "템플릿 목록 조회", description = "작성된 모든 템플릿을 조회합니다.") ResponseEntity getTemplates(); diff --git a/backend/src/main/java/codezap/template/controller/TemplateController.java b/backend/src/main/java/codezap/template/controller/TemplateController.java index 5159adbc6..e856522c6 100644 --- a/backend/src/main/java/codezap/template/controller/TemplateController.java +++ b/backend/src/main/java/codezap/template/controller/TemplateController.java @@ -1,6 +1,7 @@ -package codezap.member.controller; +package codezap.template.controller; + +import java.net.URI; -import org.apache.commons.lang3.NotImplementedException; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -9,28 +10,31 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import codezap.template.controller.SpringDocTemplateController; -import codezap.template.dto.request.CreateTemplateResponse; -import codezap.template.dto.response.CreateTemplateRequest; +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) { - throw new NotImplementedException(); + public ResponseEntity create(@RequestBody CreateTemplateRequest createTemplateRequest) { + return ResponseEntity.created(URI.create("/templates" + templateService.create(createTemplateRequest))).build(); } @GetMapping("") public ResponseEntity getTemplates() { - throw new NotImplementedException(); + return ResponseEntity.ok(templateService.findAll()); } @GetMapping("/{id}") public ResponseEntity getTemplateById(@PathVariable Long id) { - throw new NotImplementedException(); + 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/response/CreateTemplateRequest.java b/backend/src/main/java/codezap/template/dto/request/CreateTemplateRequest.java similarity index 64% rename from backend/src/main/java/codezap/template/dto/response/CreateTemplateRequest.java rename to backend/src/main/java/codezap/template/dto/request/CreateTemplateRequest.java index 31bc84c1e..f71996a30 100644 --- a/backend/src/main/java/codezap/template/dto/response/CreateTemplateRequest.java +++ b/backend/src/main/java/codezap/template/dto/request/CreateTemplateRequest.java @@ -1,9 +1,7 @@ -package codezap.template.dto.response; +package codezap.template.dto.request; import java.util.List; -import codezap.template.dto.request.CreateSnippetRequest; - public record CreateTemplateRequest( String title, int representative_snippet_ordinal, diff --git a/backend/src/main/java/codezap/template/dto/request/CreateTemplateResponse.java b/backend/src/main/java/codezap/template/dto/request/CreateTemplateResponse.java deleted file mode 100644 index b7405851a..000000000 --- a/backend/src/main/java/codezap/template/dto/request/CreateTemplateResponse.java +++ /dev/null @@ -1,7 +0,0 @@ -package codezap.template.dto.request; - -public record CreateTemplateResponse( - String email, - String nickname, - String password) { -} 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 443ed0abc..fe82524b5 100644 --- a/backend/src/main/java/codezap/template/dto/response/FindAllSnippetByTemplateResponse.java +++ b/backend/src/main/java/codezap/template/dto/response/FindAllSnippetByTemplateResponse.java @@ -1,9 +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 index 50a99f1ea..fddf1a2c9 100644 --- a/backend/src/main/java/codezap/template/dto/response/FindAllTemplatesResponse.java +++ b/backend/src/main/java/codezap/template/dto/response/FindAllTemplatesResponse.java @@ -2,7 +2,15 @@ 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 index e18473c6a..4c1f8b72d 100644 --- a/backend/src/main/java/codezap/template/dto/response/FindMemberBySummaryResponse.java +++ b/backend/src/main/java/codezap/template/dto/response/FindMemberBySummaryResponse.java @@ -1,7 +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 index c579d236c..b539c96c0 100644 --- a/backend/src/main/java/codezap/template/dto/response/FindRepresentativeSnippetResponse.java +++ b/backend/src/main/java/codezap/template/dto/response/FindRepresentativeSnippetResponse.java @@ -1,7 +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 index 6671b2450..6394f3f21 100644 --- a/backend/src/main/java/codezap/template/dto/response/FindTemplateByIdResponse.java +++ b/backend/src/main/java/codezap/template/dto/response/FindTemplateByIdResponse.java @@ -3,12 +3,33 @@ 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, - Long representative_snippet_ordinal, + 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 index ae8c59f07..de75ad878 100644 --- a/backend/src/main/java/codezap/template/dto/response/FindTemplateBySummaryResponse.java +++ b/backend/src/main/java/codezap/template/dto/response/FindTemplateBySummaryResponse.java @@ -2,6 +2,8 @@ import java.time.LocalDateTime; +import codezap.representative_snippet.domain.RepresentativeSnippet; + public record FindTemplateBySummaryResponse( Long id, String title, @@ -9,4 +11,13 @@ public record FindTemplateBySummaryResponse( 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/backend/src/main/resources/application-local.yml b/backend/src/main/resources/application-local.yml index 19b9b0ed7..ca84b8945 100644 --- a/backend/src/main/resources/application-local.yml +++ b/backend/src/main/resources/application-local.yml @@ -3,13 +3,11 @@ spring: activate: on-profile: local datasource: - driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:23306/code_zap?serverTimezone=Asia/Seoul username: root password: woowacourse + driver-class-name: com.mysql.cj.jdbc.Driver jpa: - database: mysql - database-platform: org.hibernate.dialect.MySQLDialect hibernate: ddl-auto: create properties: From 941ecfaec8ae1d1728f3b419e92cc49ac03047aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=97=A4=EC=9D=B8?= <157036488+Hain-tain@users.noreply.github.com> Date: Wed, 17 Jul 2024 16:31:58 +0900 Subject: [PATCH 17/17] =?UTF-8?q?Template=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C,=20=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C,=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EA=B8=B0=EB=8A=A5=20(#78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: react-router-dom 설치 Co-Authored-By: MYONG JAEWI <78201530+Jaymyong66@users.noreply.github.com> * feat: Templates 페이지 생성 Co-Authored-By: MYONG JAEWI <78201530+Jaymyong66@users.noreply.github.com> * feat: 페이지 라우터 설정 Co-Authored-By: MYONG JAEWI <78201530+Jaymyong66@users.noreply.github.com> * refactor: Templates => TemplateList로 이름 변경 Co-Authored-By: MYONG JAEWI <78201530+Jaymyong66@users.noreply.github.com> * chore: webpack에 publicPath 및 historyApiFallback 설정 추가 Co-Authored-By: MYONG JAEWI <78201530+Jaymyong66@users.noreply.github.com> * feat: Template 생성 및 상세 페이지 url 설정 추가 Co-Authored-By: MYONG JAEWI <78201530+Jaymyong66@users.noreply.github.com> * chore: eslint rules 추가 (emotion css prop) * test(.storybook): 스토리북 preview 초기 세팅 * feat(Button): Button 컴포넌트 구현 * chore: react-syntax-highlighter 설치 Co-Authored-By: MYONG JAEWI <78201530+Jaymyong66@users.noreply.github.com> * test(Button): Button 컴포넌트 스토리 작성 * chore: 'png' 타입 정의 * feat(Input): Input 컴포넌트 구현 * test(Input): Input 컴포넌트 스토리 작성 * feat: Flex 컴포넌트 Co-Authored-By: MYONG JAEWI <78201530+Jaymyong66@users.noreply.github.com> * feat: Text 컴포넌트 Co-Authored-By: MYONG JAEWI <78201530+Jaymyong66@users.noreply.github.com> * feat: TemplateItem 컴포넌트 Co-Authored-By: MYONG JAEWI <78201530+Jaymyong66@users.noreply.github.com> * feat: GlobalStyles background color 설정 Co-Authored-By: MYONG JAEWI <78201530+Jaymyong66@users.noreply.github.com> * feat: Layout maxWidth 와 padding 설정 Co-Authored-By: MYONG JAEWI <78201530+Jaymyong66@users.noreply.github.com> * feat: mock 데이터 생성 Co-Authored-By: MYONG JAEWI <78201530+Jaymyong66@users.noreply.github.com> * feat: link 스타일 reset Co-Authored-By: MYONG JAEWI <78201530+Jaymyong66@users.noreply.github.com> * feat: Template 페이지 - Title, code block 추가 Co-Authored-By: MYONG JAEWI <78201530+Jaymyong66@users.noreply.github.com> * feat: convertToCamelCase 유틸 함수 Co-Authored-By: MYONG JAEWI <78201530+Jaymyong66@users.noreply.github.com> * feat: TemplateList 페이지- TemplateItem 추가 Co-Authored-By: MYONG JAEWI <78201530+Jaymyong66@users.noreply.github.com> * feat(component): px to rem * refactor: Label =>Body 로 오탈자 수정 Co-authored-by: 월하 <52562061+vi-wolhwa@users.noreply.github.com> * refactor(Flex): 중복 타입 선언 제거 Co-authored-by: MYONG JAEWI * refactor(components): rem to px (borderRadius) * chore: prettier 설치 및 .prettierrc 파일 추가 * chore: eslint-config-prettier, eslint-plugin-prettier 설정 * refactor(Button): 버튼 children 정렬 스타일 추가, 스타일 클래스명 변경 * refactor(Flex): 'flex' prop 추가 * refactor(Input): 확장성 리팩토링 (width, height, fontSize, fontWeight Prop 추가) * feat(Header): Header 컴포넌트 구현 * test(.storybook): preview-decorators 'wrap' 스타일 추가 * test(Header): Header 컴포넌트 스토리 작성 * feat(pages): UploadsTemplate 페이지 생성 * feat(routes): templates/uploads 경로 - UploadsTemplate 컴포넌트 라우트 추가 * feat(pages): UploadsTemplate의 컴포넌트 레이아웃 * refactor(Flex): 'flex' prop 추가 * refactor(Input): width, height, fontSize, fontWeight prop 추가 * css(Button): text 타입 버튼 높이 변경 (fit-content) * chore: file-loader 의존성 패키지 추가 * chore: file-loader 사용을 위한 웹팩 설정 * feat(Header): Header 컴포넌트 구현 * test(.storybook): 스토리북 preview-decorators wrap 스타일 적용 * test(Header): Header 컴포넌트 스토리 작성 * feat(routes): Layout에 Header 추가 * chore: codemirror 라이브러리 추가 * feat(pages): UploadsTemplate - CodeMirror 코드 에디터로 snippet입력 기능 * feat(pages): UploadsTemplate - Add Snippet 버튼 클릭 시 snippet 추가 기능 * feat(pages): UploadsTemplate - Save 버튼 클릭 시 TemplateList 페이지로 라우팅 기능 * feat(Header): 라우팅 구현 * feat(SelectList): SelectList 컴포넌트 생성 * feat(Template): SelectList 적용 * feat(pages): UploadsTemplate - 템플릿명 input 컴포넌트 * feat(pages): UploadsTemplate - 스니펫명 input 기능 * refactor(SnippetEditor): SnippetEditor 컴포넌트 분리 * refactor(Template): 불필요한 String 함수 삭제 Co-authored-by: MYONG JAEWI <78201530+Jaymyong66@users.noreply.github.com> * refactor(Template): 코드 컨벤션 준수 Co-authored-by: 월하 <52562061+vi-wolhwa@users.noreply.github.com> * refactor(TemplateTitleInput): TemplateTitleInput 컴포넌트 분리 * feat(SnippetEditor): snippet의 fileName 입력 값 상태로 관리 * refactor(SnippetEditor): snippetFileNameInput - font와 색 변경, 변수명 변경 * refactor(pages): Template - handleSelectOption의 인자 에러 수정, unused setter 제거 * chore: gitignore - dist 폴더 추가 * refactor(Header): UploadsTemplate 페이지 라우팅 경로 수정 * refactor(Header): Button -> Header 오타 수정, import 방식 변경 * fix(pages): Template - SelectList에 현재 파일 옵션에 focus 안되는 이슈 해결 * chore: .gitignore - EOL 추가 * chore: @tanstack/react-query 패키지 설치 * feat(src): index.tsx QueryClient 세팅 * feat(types): template 도메인 타입 정의 * feat(TemplateList): TemplateList 페이지 API 연동 * feat(Template): Template 페이지 API 연동 * chore: gitignore .env 추가 * feat(hooks): react-query 서버와 연결 * refactor(pages): TemplateList console.log 삭제 * feat(UploadsTemplate): UploadsTemplate 페이지 API 연결 * style(pages): prettier 스타일 적용 * style(hooks): prettier 스타일 적용 * style(components): prettier 스타일 적용 * chore: .gitignore EOL 추가 --------- Co-authored-by: MYONG JAEWI <78201530+Jaymyong66@users.noreply.github.com> Co-authored-by: vi-wolhwa Co-authored-by: 월하 <52562061+vi-wolhwa@users.noreply.github.com> Co-authored-by: MYONG JAEWI Co-authored-by: jayming66 --- .gitignore | 3 + frontend/.eslintrc.cjs | 15 +- frontend/.gitignore | 2 + frontend/.prettierrc | 12 + frontend/.storybook/preview.ts | 14 - frontend/.storybook/preview.tsx | 32 + frontend/package-lock.json | 709 +++++++++++++++++- frontend/package.json | 13 +- frontend/src/App.tsx | 9 - frontend/src/assets/images/logo.png | Bin 0 -> 469 bytes frontend/src/assets/images/newTemplate.png | Bin 0 -> 262 bytes frontend/src/assets/images/search.png | Bin 0 -> 446 bytes frontend/src/components/.gitkeep | 0 .../src/components/Button/Button.stories.tsx | 94 +++ frontend/src/components/Button/Button.tsx | 25 + frontend/src/components/Button/index.ts | 1 + frontend/src/components/Button/style.ts | 63 ++ frontend/src/components/Flex/Flex.tsx | 51 ++ frontend/src/components/Flex/index.ts | 1 + frontend/src/components/Flex/style.ts | 16 + .../src/components/Header/Header.stories.tsx | 13 + frontend/src/components/Header/Header.tsx | 65 ++ frontend/src/components/Header/index.ts | 1 + frontend/src/components/Header/style.ts | 13 + .../src/components/Input/Input.stories.tsx | 79 ++ frontend/src/components/Input/Input.tsx | 44 ++ frontend/src/components/Input/index.ts | 1 + frontend/src/components/Input/style.ts | 56 ++ .../src/components/SelectList/SelectList.tsx | 44 ++ frontend/src/components/SelectList/index.ts | 1 + .../SnippetEditor/SnippetEditor.tsx | 39 + .../src/components/SnippetEditor/index.ts | 1 + .../src/components/SnippetEditor/style.ts | 24 + .../components/TemplateItem/TemplateItem.tsx | 37 + frontend/src/components/TemplateItem/index.ts | 1 + .../TemplateTitleInput/TemplateTitleInput.tsx | 17 + .../components/TemplateTitleInput/index.ts | 1 + .../components/TemplateTitleInput/style.ts | 26 + frontend/src/components/Text/Text.tsx | 63 ++ frontend/src/components/Text/index.ts | 1 + frontend/src/components/Text/style.ts | 13 + frontend/src/hooks/useTemplateListQuery.ts | 20 + frontend/src/hooks/useTemplateQuery.ts | 20 + frontend/src/hooks/useTemplateUploadQuery.ts | 36 + frontend/src/index.tsx | 14 +- frontend/src/mocks/template.json | 24 + frontend/src/mocks/templateList.json | 69 ++ frontend/src/pages/Template.tsx | 75 ++ frontend/src/pages/TemplateList.tsx | 29 + frontend/src/pages/UploadsTemplate.tsx | 98 +++ frontend/src/routes/router.tsx | 39 + frontend/src/style/GlobalStyles.tsx | 16 +- frontend/src/types/index.d.ts | 4 + frontend/src/types/template.ts | 42 ++ frontend/src/utils/convertToCamelCase.ts | 27 + frontend/webpack.config.cjs | 36 +- 56 files changed, 2096 insertions(+), 53 deletions(-) create mode 100644 frontend/.prettierrc delete mode 100644 frontend/.storybook/preview.ts create mode 100644 frontend/.storybook/preview.tsx delete mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/assets/images/logo.png create mode 100644 frontend/src/assets/images/newTemplate.png create mode 100644 frontend/src/assets/images/search.png delete mode 100644 frontend/src/components/.gitkeep create mode 100644 frontend/src/components/Button/Button.stories.tsx create mode 100644 frontend/src/components/Button/Button.tsx create mode 100644 frontend/src/components/Button/index.ts create mode 100644 frontend/src/components/Button/style.ts create mode 100644 frontend/src/components/Flex/Flex.tsx create mode 100644 frontend/src/components/Flex/index.ts create mode 100644 frontend/src/components/Flex/style.ts create mode 100644 frontend/src/components/Header/Header.stories.tsx create mode 100644 frontend/src/components/Header/Header.tsx create mode 100644 frontend/src/components/Header/index.ts create mode 100644 frontend/src/components/Header/style.ts create mode 100644 frontend/src/components/Input/Input.stories.tsx create mode 100644 frontend/src/components/Input/Input.tsx create mode 100644 frontend/src/components/Input/index.ts create mode 100644 frontend/src/components/Input/style.ts create mode 100644 frontend/src/components/SelectList/SelectList.tsx create mode 100644 frontend/src/components/SelectList/index.ts create mode 100644 frontend/src/components/SnippetEditor/SnippetEditor.tsx create mode 100644 frontend/src/components/SnippetEditor/index.ts create mode 100644 frontend/src/components/SnippetEditor/style.ts create mode 100644 frontend/src/components/TemplateItem/TemplateItem.tsx create mode 100644 frontend/src/components/TemplateItem/index.ts create mode 100644 frontend/src/components/TemplateTitleInput/TemplateTitleInput.tsx create mode 100644 frontend/src/components/TemplateTitleInput/index.ts create mode 100644 frontend/src/components/TemplateTitleInput/style.ts create mode 100644 frontend/src/components/Text/Text.tsx create mode 100644 frontend/src/components/Text/index.ts create mode 100644 frontend/src/components/Text/style.ts create mode 100644 frontend/src/hooks/useTemplateListQuery.ts create mode 100644 frontend/src/hooks/useTemplateQuery.ts create mode 100644 frontend/src/hooks/useTemplateUploadQuery.ts create mode 100644 frontend/src/mocks/template.json create mode 100644 frontend/src/mocks/templateList.json create mode 100644 frontend/src/pages/Template.tsx create mode 100644 frontend/src/pages/TemplateList.tsx create mode 100644 frontend/src/pages/UploadsTemplate.tsx create mode 100644 frontend/src/routes/router.tsx create mode 100644 frontend/src/types/index.d.ts create mode 100644 frontend/src/types/template.ts create mode 100644 frontend/src/utils/convertToCamelCase.ts 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/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 0000000000000000000000000000000000000000..60a611c84d6e6791a6a58c2b18f18e9b9756efae GIT binary patch literal 469 zcmV;`0V@89P)y{8&<)5VDj+%mVS*w=;Sn~V8?XW+gbB!VCE}jXPU56#>?*(1 z)`{$|clVzU2$XuogIYtW7hL=HMhK8vVFtp=o`^?78JOS%U(E~Tqo53o8Tif1#5s{; z43vQ%0we(y@@_iWx!WIOm$p zImM0ian*L<2u<5L{~biN5%Rr1(8ryG!j#DO7HIYHt?lEEyy;tq|6m46fy(KH%!6J{ zeNf&pv#VG>#_4qENJ#q4qz~+4Ka`vS%@d1m#tvx%EiACt=|1fMcprbgx%=ei00000 LNkvXXu0mjfgE7h- literal 0 HcmV?d00001 diff --git a/frontend/src/assets/images/newTemplate.png b/frontend/src/assets/images/newTemplate.png new file mode 100644 index 0000000000000000000000000000000000000000..b0a86ab471b3888b3f72b241b6fe189c595cbf6e GIT binary patch literal 262 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eB{%lVd$B+uftCKeJHYf+bR)Nn8^hrg8rHeRXf6(AhWd8$lck_2Vy#~6|9DxMmNpu zn5B>_jAJ9?nHYx-@t)ZQ9f3)CeLqOe><#=Q5d`3QcX)oHl7iF0{Sz|;Jg>?9WF<2( zHBEnM#<;54=kX#-GgxI=HZC+4HKDGN;0k%3z(%fO{iUW z8lh#50X<(4p=rebZ7yIUyU=n>0%*0&z%6clRN&~c{GRagS9mt?<;!B61Q0St8X&Vd o4X}}U86a;HZfyPoyw7%{A9*C4&3KRNQUCw|07*qoM6N<$f^n0w>i_@% literal 0 HcmV?d00001 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