From 959ca2b058cd01984f596cf69522328737ea2107 Mon Sep 17 00:00:00 2001 From: Soyi Jeon Date: Tue, 26 Jul 2022 13:41:24 +0900 Subject: [PATCH 01/62] =?UTF-8?q?feat:=20svg=20=EC=82=AC=EC=9A=A9=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EB=B0=8F=20react-icons=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20(#177)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: svgr 설치 및 webpack 설정 Co-authored-by: SunHo Park * chore: storybook svg 설정 추가 Co-authored-by: SunHo Park * feat: svg 아이콘 적용 Co-authored-by: SunHo Park * chore: react-icons 제거 Co-authored-by: SunHo Park Co-authored-by: SunHo Park --- frontend/.storybook/main.js | 11 +++++++++++ frontend/package.json | 2 +- frontend/src/assets/icons/bx-chevron-left.svg | 1 + frontend/src/assets/icons/bx-hide.svg | 1 + frontend/src/assets/icons/bx-pencil.svg | 1 + frontend/src/assets/icons/bx-plus.svg | 1 + frontend/src/assets/icons/bx-search.svg | 1 + frontend/src/assets/icons/bx-show.svg | 1 + frontend/src/assets/icons/bx-trash.svg | 1 + frontend/src/assets/icons/bx-user.svg | 1 + frontend/src/assets/icons/bx-x.svg | 1 + frontend/src/assets/icons/bxs-error.svg | 1 + frontend/src/components/AutoCompleteInput.tsx | 4 ++-- frontend/src/components/Header.tsx | 9 ++++++--- frontend/src/components/LabeledInput.tsx | 6 ++++-- frontend/src/components/LetterPaper.tsx | 4 ++-- frontend/src/components/Modal.tsx | 4 ++-- frontend/src/components/PageTitleWithBackButton.tsx | 4 ++-- frontend/src/components/PasswordInput.tsx | 12 ++++++++---- .../src/components/RollingpaperMessageDetail.tsx | 9 +++++---- frontend/src/components/SearchInput.tsx | 6 +++--- frontend/src/components/TeamCreateButton.tsx | 9 ++++++--- frontend/src/components/TeamJoinSection.tsx | 4 ++-- frontend/src/components/TeamRollingpaperList.tsx | 4 ++-- frontend/src/components/UnderlineInput.tsx | 6 ++++-- frontend/src/stories/IconButton.stories.jsx | 5 +++-- frontend/webpack.common.js | 12 +++++++++++- 27 files changed, 84 insertions(+), 37 deletions(-) create mode 100644 frontend/src/assets/icons/bx-chevron-left.svg create mode 100644 frontend/src/assets/icons/bx-hide.svg create mode 100644 frontend/src/assets/icons/bx-pencil.svg create mode 100644 frontend/src/assets/icons/bx-plus.svg create mode 100644 frontend/src/assets/icons/bx-search.svg create mode 100644 frontend/src/assets/icons/bx-show.svg create mode 100644 frontend/src/assets/icons/bx-trash.svg create mode 100644 frontend/src/assets/icons/bx-user.svg create mode 100644 frontend/src/assets/icons/bx-x.svg create mode 100644 frontend/src/assets/icons/bxs-error.svg diff --git a/frontend/.storybook/main.js b/frontend/.storybook/main.js index 7e26c1dc..0815a9f0 100644 --- a/frontend/.storybook/main.js +++ b/frontend/.storybook/main.js @@ -17,6 +17,17 @@ module.exports = { "@components": resolve(__dirname, "../src/components"), }; config.resolve.alias = Object.assign(config.resolve.alias, alias); + + const fileLoaderRule = config.module.rules.find( + (rule) => rule.test && rule.test.test(".svg") + ); + fileLoaderRule.exclude = /\.svg$/; + + config.module.rules.unshift({ + test: /\.svg$/, + use: ["@svgr/webpack"], + }); + return config; }, }; diff --git a/frontend/package.json b/frontend/package.json index c2f8e585..0a83c792 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,6 +24,7 @@ "@storybook/manager-webpack5": "^6.5.9", "@storybook/react": "^6.5.9", "@storybook/testing-library": "^0.0.13", + "@svgr/webpack": "^6.3.1", "@typescript-eslint/eslint-plugin": "^5.30.0", "@typescript-eslint/parser": "^5.30.0", "babel-loader": "^8.2.5", @@ -47,7 +48,6 @@ "axios": "^0.27.2", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-icons": "^4.4.0", "react-query": "^3.39.1", "react-router-dom": "6", "storybook": "^6.5.9", diff --git a/frontend/src/assets/icons/bx-chevron-left.svg b/frontend/src/assets/icons/bx-chevron-left.svg new file mode 100644 index 00000000..5ce27e41 --- /dev/null +++ b/frontend/src/assets/icons/bx-chevron-left.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/assets/icons/bx-hide.svg b/frontend/src/assets/icons/bx-hide.svg new file mode 100644 index 00000000..15cdc740 --- /dev/null +++ b/frontend/src/assets/icons/bx-hide.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/assets/icons/bx-pencil.svg b/frontend/src/assets/icons/bx-pencil.svg new file mode 100644 index 00000000..4b296f3a --- /dev/null +++ b/frontend/src/assets/icons/bx-pencil.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/assets/icons/bx-plus.svg b/frontend/src/assets/icons/bx-plus.svg new file mode 100644 index 00000000..65d5e0c3 --- /dev/null +++ b/frontend/src/assets/icons/bx-plus.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/assets/icons/bx-search.svg b/frontend/src/assets/icons/bx-search.svg new file mode 100644 index 00000000..042f09c9 --- /dev/null +++ b/frontend/src/assets/icons/bx-search.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/assets/icons/bx-show.svg b/frontend/src/assets/icons/bx-show.svg new file mode 100644 index 00000000..955ba98d --- /dev/null +++ b/frontend/src/assets/icons/bx-show.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/assets/icons/bx-trash.svg b/frontend/src/assets/icons/bx-trash.svg new file mode 100644 index 00000000..3714577b --- /dev/null +++ b/frontend/src/assets/icons/bx-trash.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/assets/icons/bx-user.svg b/frontend/src/assets/icons/bx-user.svg new file mode 100644 index 00000000..8aaaab30 --- /dev/null +++ b/frontend/src/assets/icons/bx-user.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/assets/icons/bx-x.svg b/frontend/src/assets/icons/bx-x.svg new file mode 100644 index 00000000..e44ae00d --- /dev/null +++ b/frontend/src/assets/icons/bx-x.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/assets/icons/bxs-error.svg b/frontend/src/assets/icons/bxs-error.svg new file mode 100644 index 00000000..cf13c1c1 --- /dev/null +++ b/frontend/src/assets/icons/bxs-error.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/components/AutoCompleteInput.tsx b/frontend/src/components/AutoCompleteInput.tsx index 2a4f306d..4260a10d 100644 --- a/frontend/src/components/AutoCompleteInput.tsx +++ b/frontend/src/components/AutoCompleteInput.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useRef } from "react"; import styled from "@emotion/styled"; -import { BiSearch } from "react-icons/bi"; +import SearchIcon from "@/assets/icons/bx-search.svg"; interface AutoCompleteInputProps extends React.InputHTMLAttributes { @@ -49,7 +49,7 @@ const AutoCompleteInput = ({ {labelText} - + { const { setIsLoggedIn } = useContext(UserContext); const navigate = useNavigate(); @@ -28,10 +31,10 @@ const Header = () => { - + - + diff --git a/frontend/src/components/LabeledInput.tsx b/frontend/src/components/LabeledInput.tsx index 7df5af3a..48915690 100644 --- a/frontend/src/components/LabeledInput.tsx +++ b/frontend/src/components/LabeledInput.tsx @@ -1,7 +1,7 @@ import React from "react"; import styled from "@emotion/styled"; -import { BiError } from "react-icons/bi"; +import ErrorIcon from "@/assets/icons/bxs-error.svg"; interface LabeledInputProps extends React.InputHTMLAttributes { @@ -32,7 +32,7 @@ const LabeledInput = ({ }} />
- + {errorMessage}
@@ -82,6 +82,8 @@ const StyledLabel = styled.label` position: relative; top: 2px; margin-right: 4px; + + fill: ${({ theme }) => theme.colors.RED_300}; } } `; diff --git a/frontend/src/components/LetterPaper.tsx b/frontend/src/components/LetterPaper.tsx index 9d419127..59bc24d0 100644 --- a/frontend/src/components/LetterPaper.tsx +++ b/frontend/src/components/LetterPaper.tsx @@ -6,7 +6,7 @@ import IconButton from "@components/IconButton"; import RollingpaperMessage from "@components/RollingpaperMessage"; import { Message } from "@/types"; -import { BiPencil } from "react-icons/bi"; +import PencilIcon from "@/assets/icons/bx-pencil.svg"; interface LetterPaperProp { to: string; @@ -28,7 +28,7 @@ const LetterPaper = ({ to, messageList }: LetterPaperProp) => { To. {to} - + diff --git a/frontend/src/components/Modal.tsx b/frontend/src/components/Modal.tsx index e966f706..d2b81c74 100644 --- a/frontend/src/components/Modal.tsx +++ b/frontend/src/components/Modal.tsx @@ -2,7 +2,7 @@ import React, { PropsWithChildren } from "react"; import styled from "@emotion/styled"; import IconButton from "./IconButton"; -import { BiX } from "react-icons/bi"; +import XIcon from "@/assets/icons/bx-x.svg"; interface ModalProps { onClickCloseButton: React.MouseEventHandler; @@ -30,7 +30,7 @@ const CloseButton = ({ }: React.ButtonHTMLAttributes) => { return ( - + ); }; diff --git a/frontend/src/components/PageTitleWithBackButton.tsx b/frontend/src/components/PageTitleWithBackButton.tsx index 0db6a40e..246dd683 100644 --- a/frontend/src/components/PageTitleWithBackButton.tsx +++ b/frontend/src/components/PageTitleWithBackButton.tsx @@ -2,7 +2,7 @@ import React, { PropsWithChildren } from "react"; import styled from "@emotion/styled"; import { useNavigate } from "react-router-dom"; -import { BiChevronLeft } from "react-icons/bi"; +import ChevronIcon from "@/assets/icons/bx-chevron-left.svg"; import IconButton from "@components/IconButton"; @@ -16,7 +16,7 @@ const PageTitleWithBackButton = ({ children }: PropsWithChildren) => { return ( - +

{children}

diff --git a/frontend/src/components/PasswordInput.tsx b/frontend/src/components/PasswordInput.tsx index ff010ca7..7880b620 100644 --- a/frontend/src/components/PasswordInput.tsx +++ b/frontend/src/components/PasswordInput.tsx @@ -1,7 +1,8 @@ import React, { useState } from "react"; import styled from "@emotion/styled"; -import { BiShow, BiHide } from "react-icons/bi"; +import ShowIcon from "@/assets/icons/bx-show.svg"; +import HideIcon from "@/assets/icons/bx-hide.svg"; interface PasswordInput extends React.InputHTMLAttributes { labelText: string; @@ -24,8 +25,8 @@ const PasswordInput = ({ labelText, value, setValue }: PasswordInput) => { value={value} onChange={(e) => setValue(e.target.value)} /> - {showPassword && } - {!showPassword && } + {showPassword && } + {!showPassword && } ); @@ -36,7 +37,6 @@ const StyledLabel = styled.label` flex-direction: column; font-size: 14px; - color: ${({ theme }) => theme.colors.GRAY_600}; `; const StyledPasswordInput = styled.div` @@ -52,6 +52,10 @@ const StyledPasswordInput = styled.div` svg { margin-top: 8px; font-size: 24px; + + fill: ${({ theme }) => theme.colors.GRAY_600}; + + cursor: pointer; } input { diff --git a/frontend/src/components/RollingpaperMessageDetail.tsx b/frontend/src/components/RollingpaperMessageDetail.tsx index e132c196..21dfea55 100644 --- a/frontend/src/components/RollingpaperMessageDetail.tsx +++ b/frontend/src/components/RollingpaperMessageDetail.tsx @@ -1,10 +1,11 @@ import React from "react"; import styled from "@emotion/styled"; -import { BiPencil, BiTrash } from "react-icons/bi"; - import IconButton from "./IconButton"; +import PencilIcon from "@/assets/icons/bx-pencil.svg"; +import TrashIcon from "@/assets/icons/bx-trash.svg"; + interface RollingpaperMessageDetailProps { content: string; author: string; @@ -25,10 +26,10 @@ const RollingpaperMessageDetail = ({ - + - + {author} diff --git a/frontend/src/components/SearchInput.tsx b/frontend/src/components/SearchInput.tsx index d19cbba5..89b9a86b 100644 --- a/frontend/src/components/SearchInput.tsx +++ b/frontend/src/components/SearchInput.tsx @@ -1,7 +1,7 @@ import React from "react"; import styled from "@emotion/styled"; -import { BiSearch } from "react-icons/bi"; +import SearchIcon from "@/assets/icons/bx-search.svg"; type ButtonAttributes = React.ButtonHTMLAttributes; type InputAttributes = React.InputHTMLAttributes; @@ -14,7 +14,7 @@ const SearchInput = ({ ); @@ -46,7 +46,7 @@ const StyledSearch = styled.form` padding: 4px; margin: auto; - color: ${({ theme }) => theme.colors.WHITE}; + fill: ${({ theme }) => theme.colors.WHITE}; font-size: 40px; } diff --git a/frontend/src/components/TeamCreateButton.tsx b/frontend/src/components/TeamCreateButton.tsx index 865598b8..0b409cce 100644 --- a/frontend/src/components/TeamCreateButton.tsx +++ b/frontend/src/components/TeamCreateButton.tsx @@ -2,14 +2,14 @@ import React from "react"; import { useNavigate } from "react-router-dom"; import styled from "@emotion/styled"; -import { BiPlus } from "react-icons/bi"; +import PlusIcon from "@/assets/icons/bx-plus.svg"; const TeamCreateButton = () => { const navigate = useNavigate(); return ( - { navigate("team/new"); }} @@ -29,7 +29,6 @@ const StyledTeamCreateButton = styled.button` border-radius: 4px; background-color: ${({ theme }) => theme.colors.SKY_BLUE_300}; - color: ${({ theme }) => theme.colors.WHITE}; font-size: 30px; @@ -38,6 +37,10 @@ const StyledTeamCreateButton = styled.button` &:hover { background-color: ${({ theme }) => theme.colors.SKY_BLUE_400}; } + + svg { + fill: ${({ theme }) => theme.colors.WHITE}; + } `; export default TeamCreateButton; diff --git a/frontend/src/components/TeamJoinSection.tsx b/frontend/src/components/TeamJoinSection.tsx index 305c0047..f4113fbf 100644 --- a/frontend/src/components/TeamJoinSection.tsx +++ b/frontend/src/components/TeamJoinSection.tsx @@ -6,7 +6,7 @@ import IconButton from "@/components/IconButton"; import LineButton from "@/components/LineButton"; import TeamJoinModalForm from "@/components/TeamJoinModalForm"; -import { BiPlus } from "react-icons/bi"; +import PlusIcon from "@/assets/icons/bx-plus.svg"; const dummyRollingpapers = [ { @@ -49,7 +49,7 @@ const TeamJoinSection = () => {

롤링페이퍼 목록

- +
diff --git a/frontend/src/components/TeamRollingpaperList.tsx b/frontend/src/components/TeamRollingpaperList.tsx index 34bac20d..1908907b 100644 --- a/frontend/src/components/TeamRollingpaperList.tsx +++ b/frontend/src/components/TeamRollingpaperList.tsx @@ -10,7 +10,7 @@ import RollingpaperListItem from "@/components/RollingpaperListItem"; import appClient from "@/api"; import { CustomError } from "@/types"; -import { BiPlus } from "react-icons/bi"; +import PlusIcon from "@/assets/icons/bx-plus.svg"; interface Rollingpaper { id: number; @@ -67,7 +67,7 @@ const TeamRollingpaperList = () => { navigate("rollingpaper/new"); }} > - + diff --git a/frontend/src/components/UnderlineInput.tsx b/frontend/src/components/UnderlineInput.tsx index 11c25a1f..c63e2b41 100644 --- a/frontend/src/components/UnderlineInput.tsx +++ b/frontend/src/components/UnderlineInput.tsx @@ -1,7 +1,7 @@ import React from "react"; import styled from "@emotion/styled"; -import { BiError } from "react-icons/bi"; +import ErrorIcon from "@/assets/icons/bxs-error.svg"; interface UnderlineInputProps extends React.InputHTMLAttributes { @@ -29,7 +29,7 @@ const UnderlineInput = ({ }} />
- + {errorMessage}
@@ -53,6 +53,8 @@ const StyledInputContainer = styled.div` position: relative; top: 2px; margin-right: 4px; + + fill: ${({ theme }) => theme.colors.RED_300}; } } `; diff --git a/frontend/src/stories/IconButton.stories.jsx b/frontend/src/stories/IconButton.stories.jsx index f4937263..e8cb10b7 100644 --- a/frontend/src/stories/IconButton.stories.jsx +++ b/frontend/src/stories/IconButton.stories.jsx @@ -1,6 +1,7 @@ import React from "react"; import IconButton from "@components/IconButton"; -import { BiPencil } from "react-icons/bi"; + +import PencilIcon from "@/assets/images/bx-pencil.svg"; export default { component: IconButton, @@ -11,5 +12,5 @@ const Template = (args) => ; export const Default = Template.bind({}); Default.args = { - children: , + children: , }; diff --git a/frontend/webpack.common.js b/frontend/webpack.common.js index 6db82bc7..37726eae 100644 --- a/frontend/webpack.common.js +++ b/frontend/webpack.common.js @@ -30,9 +30,19 @@ module.exports = { use: ["babel-loader", "ts-loader"], }, { - test: /\.(png|jpe?g|gif|svg)$/i, + test: /\.(png|jpe?g|gif)$/i, type: "asset/resource", }, + { + test: /\.svg$/i, + issuer: /\.[jt]sx?$/, + use: [ + { + loader: "@svgr/webpack", + options: { icon: true }, + }, + ], + }, ], }, output: { From 119ac7354a531e526a517a7a293b80319f88c20b Mon Sep 17 00:00:00 2001 From: TaeHyeon Kim <57135043+kth990303@users.noreply.github.com> Date: Tue, 26 Jul 2022 16:17:06 +0900 Subject: [PATCH 02/62] =?UTF-8?q?feat:=20=EC=83=9D=EC=84=B1=EC=9D=BC?= =?UTF-8?q?=EC=9E=90,=20=EC=88=98=EC=A0=95=EC=9D=BC=EC=9E=90=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#175)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 생성일자, 수정일자 필드 추가 * refactor: separate auditing configuration with default application * refactor: 컬럼명 명시 및 `BaseEntity`에 `abstract`키워드 추가 * refactor: 테이블명 명시 --- .../naepyeon/config/JpaAuditingConfig.java | 9 ++++ .../naepyeon/domain/BaseEntity.java | 24 +++++++++ .../woowacourse/naepyeon/domain/Member.java | 14 ++--- .../woowacourse/naepyeon/domain/Message.java | 10 ++-- .../naepyeon/domain/Rollingpaper.java | 6 +-- .../com/woowacourse/naepyeon/domain/Team.java | 10 ++-- .../naepyeon/domain/TeamParticipation.java | 8 +-- .../repository/MemberRepositoryTest.java | 30 +++++++++++ .../repository/MessageRepositoryTest.java | 51 +++++++++++++++---- .../RollingpaperRepositoryTest.java | 46 ++++++++++++++--- .../TeamParticipationRepositoryTest.java | 17 +++++++ .../repository/TeamRepositoryTest.java | 30 +++++++++++ 12 files changed, 218 insertions(+), 37 deletions(-) create mode 100644 backend/src/main/java/com/woowacourse/naepyeon/config/JpaAuditingConfig.java create mode 100644 backend/src/main/java/com/woowacourse/naepyeon/domain/BaseEntity.java diff --git a/backend/src/main/java/com/woowacourse/naepyeon/config/JpaAuditingConfig.java b/backend/src/main/java/com/woowacourse/naepyeon/config/JpaAuditingConfig.java new file mode 100644 index 00000000..bc0875ad --- /dev/null +++ b/backend/src/main/java/com/woowacourse/naepyeon/config/JpaAuditingConfig.java @@ -0,0 +1,9 @@ +package com.woowacourse.naepyeon.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@Configuration +@EnableJpaAuditing +public class JpaAuditingConfig { +} diff --git a/backend/src/main/java/com/woowacourse/naepyeon/domain/BaseEntity.java b/backend/src/main/java/com/woowacourse/naepyeon/domain/BaseEntity.java new file mode 100644 index 00000000..4b9a1b4c --- /dev/null +++ b/backend/src/main/java/com/woowacourse/naepyeon/domain/BaseEntity.java @@ -0,0 +1,24 @@ +package com.woowacourse.naepyeon.domain; + +import java.time.LocalDateTime; +import javax.persistence.Column; +import javax.persistence.EntityListeners; +import javax.persistence.MappedSuperclass; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@EntityListeners(AuditingEntityListener.class) +@MappedSuperclass +@Getter +abstract public class BaseEntity { + + @CreatedDate + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdDate; + + @LastModifiedDate + @Column(name = "last_modified_at", nullable = false) + private LocalDateTime lastModifiedDate; +} diff --git a/backend/src/main/java/com/woowacourse/naepyeon/domain/Member.java b/backend/src/main/java/com/woowacourse/naepyeon/domain/Member.java index 302930c5..471fdea9 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/domain/Member.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/domain/Member.java @@ -12,14 +12,16 @@ import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; +import javax.persistence.Table; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Getter +@Table(name = "member") @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Member { +public class Member extends BaseEntity { public static final int MIN_USERNAME_LENGTH = 2; public static final int MAX_USERNAME_LENGTH = 20; @@ -28,21 +30,21 @@ public class Member { private static final Pattern USER_PATTERN = Pattern.compile("^[가-힣a-zA-Z0-9]+$"); private static final Pattern EMAIL_PATTERN = Pattern.compile("^[_a-z0-9-]+(.[_a-z0-9-]+)*@(?:\\w+\\.)+\\w+$"); - private static final Pattern PASSWORD_PATTERN = Pattern.compile( - "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d~!@#$%^&*()+|=]*$"); + private static final Pattern PASSWORD_PATTERN = + Pattern.compile("^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d~!@#$%^&*()+|=]*$"); @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "member_id") private Long id; - @Column(length = 20, nullable = false) + @Column(name = "username", length = 20, nullable = false) private String username; - @Column(length = 255, nullable = false, unique = true) + @Column(name = "email", length = 255, nullable = false, unique = true) private String email; - @Column(length = 255, nullable = false) + @Column(name = "password", length = 255, nullable = false) private String password; public Member(final String username, final String email, final String password) { diff --git a/backend/src/main/java/com/woowacourse/naepyeon/domain/Message.java b/backend/src/main/java/com/woowacourse/naepyeon/domain/Message.java index c44925fb..83cf7767 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/domain/Message.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/domain/Message.java @@ -9,14 +9,16 @@ import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; +import javax.persistence.Table; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Getter +@Table(name = "message") @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Message { +public class Message extends BaseEntity { public static final int MAX_CONTENT_LENGTH = 500; @@ -25,15 +27,15 @@ public class Message { @Column(name = "message_id") private Long id; - @Column(length = 500, nullable = false) + @Column(name = "content", length = 500, nullable = false) private String content; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id") + @JoinColumn(name = "member_id", nullable = false) private Member author; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "rollingpaper_id") + @JoinColumn(name = "rollingpaper_id", nullable = false) private Rollingpaper rollingpaper; public Message(final String content, final Member author, final Rollingpaper rollingpaper) { diff --git a/backend/src/main/java/com/woowacourse/naepyeon/domain/Rollingpaper.java b/backend/src/main/java/com/woowacourse/naepyeon/domain/Rollingpaper.java index 437bb42b..a898a34a 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/domain/Rollingpaper.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/domain/Rollingpaper.java @@ -18,7 +18,7 @@ @Getter @Table(name = "rollingpaper") @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Rollingpaper { +public class Rollingpaper extends BaseEntity { public static final int MAX_TITLE_LENGTH = 20; @@ -27,7 +27,7 @@ public class Rollingpaper { @Column(name = "rollingpaper_id") private Long id; - @Column(length = 20, nullable = false) + @Column(name = "title", length = 20, nullable = false) private String title; @ManyToOne(fetch = FetchType.LAZY) @@ -35,7 +35,7 @@ public class Rollingpaper { private Team team; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id") + @JoinColumn(name = "member_id", nullable = false) private Member member; public Rollingpaper(final String title, final Team team, final Member member) { diff --git a/backend/src/main/java/com/woowacourse/naepyeon/domain/Team.java b/backend/src/main/java/com/woowacourse/naepyeon/domain/Team.java index 7463c00d..4183ffc9 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/domain/Team.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/domain/Team.java @@ -6,14 +6,16 @@ import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; +import javax.persistence.Table; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Getter +@Table(name = "team") @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Team { +public class Team extends BaseEntity { public static final int MAX_TEAMNAME_LENGTH = 20; @@ -25,13 +27,13 @@ public class Team { @Column(name = "team_name", length = 20, nullable = false, unique = true) private String name; - @Column(length = 100, nullable = false) + @Column(name = "description", length = 100, nullable = false) private String description; - @Column(nullable = false) + @Column(name = "emoji", nullable = false) private String emoji; - @Column(length = 15, nullable = false) + @Column(name = "color", length = 15, nullable = false) private String color; public Team(final String name, final String description, final String emoji, final String color) { diff --git a/backend/src/main/java/com/woowacourse/naepyeon/domain/TeamParticipation.java b/backend/src/main/java/com/woowacourse/naepyeon/domain/TeamParticipation.java index ea992a26..8f802151 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/domain/TeamParticipation.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/domain/TeamParticipation.java @@ -27,7 +27,7 @@ ) @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class TeamParticipation { +public class TeamParticipation extends BaseEntity { public static final int MAX_NICKNAME_LENGTH = 20; @@ -37,14 +37,14 @@ public class TeamParticipation { private Long id; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "team_id") + @JoinColumn(name = "team_id", nullable = false) private Team team; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id") + @JoinColumn(name = "member_id", nullable = false) private Member member; - @Column(length = 20, nullable = false) + @Column(name = "nickname", length = 20, nullable = false) private String nickname; public TeamParticipation(final Team team, final Member member, final String nickname) { diff --git a/backend/src/test/java/com/woowacourse/naepyeon/repository/MemberRepositoryTest.java b/backend/src/test/java/com/woowacourse/naepyeon/repository/MemberRepositoryTest.java index d7023d69..60d47e01 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/repository/MemberRepositoryTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/repository/MemberRepositoryTest.java @@ -3,6 +3,8 @@ import static org.assertj.core.api.Assertions.assertThat; import com.woowacourse.naepyeon.domain.Member; +import java.time.LocalDateTime; +import javax.persistence.EntityManager; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -16,6 +18,9 @@ class MemberRepositoryTest { @Autowired private MemberRepository memberRepository; + @Autowired + private EntityManager em; + @Test @DisplayName("회원을 id값으로 찾는다.") void findById() { @@ -46,4 +51,29 @@ void delete() { assertThat(memberRepository.findById(memberId)) .isEmpty(); } + + @Test + @DisplayName("회원을 생성할 때 생성일자가 올바르게 나온다.") + void createMemberWhen() { + final Member member = new Member("alex", "alex@naepyeon.com", "abc12345"); + final Long memberId = memberRepository.save(member); + + final Member actual = memberRepository.findById(memberId) + .orElseThrow(); + assertThat(actual.getCreatedDate()).isAfter(LocalDateTime.MIN); + } + + @Test + @DisplayName("회원 정보를 수정할 때 수정일자가 올바르게 나온다.") + void updateMemberWhen() { + final Member member = new Member("alex", "alex@naepyeon.com", "abc12345"); + final Long memberId = memberRepository.save(member); + + member.changeUsername("kth990303"); + em.flush(); + + final Member actual = memberRepository.findById(memberId) + .orElseThrow(); + assertThat(actual.getLastModifiedDate()).isAfter(actual.getCreatedDate()); + } } diff --git a/backend/src/test/java/com/woowacourse/naepyeon/repository/MessageRepositoryTest.java b/backend/src/test/java/com/woowacourse/naepyeon/repository/MessageRepositoryTest.java index 0e38581c..91bbb113 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/repository/MessageRepositoryTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/repository/MessageRepositoryTest.java @@ -7,7 +7,9 @@ import com.woowacourse.naepyeon.domain.Message; import com.woowacourse.naepyeon.domain.Rollingpaper; import com.woowacourse.naepyeon.domain.Team; +import java.time.LocalDateTime; import java.util.List; +import javax.persistence.EntityManager; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -21,6 +23,21 @@ class MessageRepositoryTest { private static final String content = "안녕하세요"; + @Autowired + private TeamRepository teamRepository; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private RollingpaperRepository rollingpaperRepository; + + @Autowired + private MessageRepository messageRepository; + + @Autowired + private EntityManager em; + private final Team team = new Team( "nae-pyeon", "테스트 모임입니다.", @@ -31,15 +48,6 @@ class MessageRepositoryTest { private final Member author = new Member("author", "email2@email.com", "password123"); private final Rollingpaper rollingpaper = new Rollingpaper("AlexAndKei", team, member); - @Autowired - private TeamRepository teamRepository; - @Autowired - private MemberRepository memberRepository; - @Autowired - private RollingpaperRepository rollingpaperRepository; - @Autowired - private MessageRepository messageRepository; - @BeforeEach void setUp() { teamRepository.save(team); @@ -112,6 +120,31 @@ void delete() { .isEmpty(); } + @Test + @DisplayName("메시지를 생성할 때 생성일자가 올바르게 나온다.") + void createMemberWhen() { + final Message message = createMessage(); + final Long messageId = messageRepository.save(message); + + final Message actual = messageRepository.findById(messageId) + .orElseThrow(); + assertThat(actual.getCreatedDate()).isAfter(LocalDateTime.MIN); + } + + @Test + @DisplayName("메시지를 수정할 때 수정일자가 올바르게 나온다.") + void updateMemberWhen() { + final Message message = createMessage(); + final Long messageId = messageRepository.save(message); + + message.changeContent("updateupdate"); + em.flush(); + + final Message actual = messageRepository.findById(messageId) + .orElseThrow(); + assertThat(actual.getLastModifiedDate()).isAfter(actual.getCreatedDate()); + } + private Message createMessage() { return new Message(content, author, rollingpaper); } diff --git a/backend/src/test/java/com/woowacourse/naepyeon/repository/RollingpaperRepositoryTest.java b/backend/src/test/java/com/woowacourse/naepyeon/repository/RollingpaperRepositoryTest.java index afcd75fa..570c79a0 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/repository/RollingpaperRepositoryTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/repository/RollingpaperRepositoryTest.java @@ -8,7 +8,9 @@ import com.woowacourse.naepyeon.domain.Team; import com.woowacourse.naepyeon.repository.jpa.MemberJpaDao; import com.woowacourse.naepyeon.repository.jpa.TeamJpaDao; +import java.time.LocalDateTime; import java.util.List; +import javax.persistence.EntityManager; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -22,6 +24,18 @@ class RollingpaperRepositoryTest { private static final String rollingPaperTitle = "AlexAndKei"; + @Autowired + private TeamJpaDao teamJpaDao; + + @Autowired + private MemberJpaDao memberJpaDao; + + @Autowired + private RollingpaperRepository rollingpaperRepository; + + @Autowired + private EntityManager em; + private final Team team = new Team( "nae-pyeon", "테스트 모임입니다.", @@ -30,13 +44,6 @@ class RollingpaperRepositoryTest { ); private final Member member = new Member("member", "m@hello.com", "abc@@1234"); - @Autowired - private TeamJpaDao teamJpaDao; - @Autowired - private MemberJpaDao memberJpaDao; - @Autowired - private RollingpaperRepository rollingpaperRepository; - @BeforeEach void setUp() { teamJpaDao.save(team); @@ -113,6 +120,31 @@ void delete() { .isEmpty(); } + @Test + @DisplayName("롤링페이퍼를 생성할 때 생성일자가 올바르게 나온다.") + void createMemberWhen() { + final Rollingpaper message = createRollingPaper(); + final Long rollingpaperId = rollingpaperRepository.save(message); + + final Rollingpaper actual = rollingpaperRepository.findById(rollingpaperId) + .orElseThrow(); + assertThat(actual.getCreatedDate()).isAfter(LocalDateTime.MIN); + } + + @Test + @DisplayName("롤링페이퍼를 수정할 때 수정일자가 올바르게 나온다.") + void updateMemberWhen() { + final Rollingpaper rollingpaper = createRollingPaper(); + final Long rollingpaperId = rollingpaperRepository.save(rollingpaper); + + rollingpaper.changeTitle("updateupdate"); + em.flush(); + + final Rollingpaper actual = rollingpaperRepository.findById(rollingpaperId) + .orElseThrow(); + assertThat(actual.getLastModifiedDate()).isAfter(actual.getCreatedDate()); + } + private Rollingpaper createRollingPaper() { return new Rollingpaper(rollingPaperTitle, team, member); } diff --git a/backend/src/test/java/com/woowacourse/naepyeon/repository/TeamParticipationRepositoryTest.java b/backend/src/test/java/com/woowacourse/naepyeon/repository/TeamParticipationRepositoryTest.java index 03be44cc..337296e6 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/repository/TeamParticipationRepositoryTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/repository/TeamParticipationRepositoryTest.java @@ -5,10 +5,13 @@ import static org.junit.jupiter.api.Assertions.assertAll; import com.woowacourse.naepyeon.domain.Member; +import com.woowacourse.naepyeon.domain.Rollingpaper; import com.woowacourse.naepyeon.domain.Team; import com.woowacourse.naepyeon.domain.TeamParticipation; import com.woowacourse.naepyeon.exception.DuplicateTeamPaticipateException; +import java.time.LocalDateTime; import java.util.List; +import javax.persistence.EntityManager; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -29,6 +32,9 @@ class TeamParticipationRepositoryTest { @Autowired private MemberRepository memberRepository; + @Autowired + private EntityManager em; + private final Member member1 = new Member("내편이1", "naePyeon1@test.com", "testtest123"); private final Member member2 = new Member("내편이2", "naePyeon2@test.com", "testtest123"); private final Team team1 = new Team("wooteco1", "테스트 모임입니다.", "testEmoji", "#123456"); @@ -135,4 +141,15 @@ void isJoinedMember() { () -> assertThat(teamParticipationRepository.isJoinedMember(member2.getId(), team1.getId())).isFalse() ); } + + @Test + @DisplayName("회원 가입일자가 올바르게 나온다.") + void createMemberWhen() { + final TeamParticipation teamParticipation = new TeamParticipation(team1, member1, "닉네임1"); + final Long teamParticipationId = teamParticipationRepository.save(teamParticipation); + + final TeamParticipation actual = teamParticipationRepository.findById(teamParticipationId) + .orElseThrow(); + assertThat(actual.getCreatedDate()).isAfter(LocalDateTime.MIN); + } } \ No newline at end of file diff --git a/backend/src/test/java/com/woowacourse/naepyeon/repository/TeamRepositoryTest.java b/backend/src/test/java/com/woowacourse/naepyeon/repository/TeamRepositoryTest.java index 53f10e6c..0ac9f172 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/repository/TeamRepositoryTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/repository/TeamRepositoryTest.java @@ -6,7 +6,9 @@ import com.woowacourse.naepyeon.domain.Team; import com.woowacourse.naepyeon.exception.NotFoundTeamException; +import java.time.LocalDateTime; import java.util.List; +import javax.persistence.EntityManager; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -20,6 +22,9 @@ class TeamRepositoryTest { @Autowired private TeamRepository teamRepository; + @Autowired + private EntityManager em; + @Test @DisplayName("모임을 id로 찾는다.") void findById() { @@ -79,4 +84,29 @@ void findAll() { () -> assertThat(teams).doesNotContain(team3) ); } + + @Test + @DisplayName("모임을 생성할 때 생성일자가 올바르게 나온다.") + void createMemberWhen() { + final Team team = new Team("woowacourse", "테스트 모임입니다.", "testEmoji", "#123456"); + final Long teamId = teamRepository.save(team); + + final Team actual = teamRepository.findById(teamId) + .orElseThrow(); + assertThat(actual.getCreatedDate()).isAfter(LocalDateTime.MIN); + } + + @Test + @DisplayName("모임을 수정할 때 수정일자가 올바르게 나온다.") + void updateMemberWhen() { + final Team team = new Team("woowacourse", "테스트 모임입니다.", "testEmoji", "#123456"); + final Long teamId = teamRepository.save(team); + + team.changeName("updateupdate"); + em.flush(); + + final Team actual = teamRepository.findById(teamId) + .orElseThrow(); + assertThat(actual.getLastModifiedDate()).isAfter(actual.getCreatedDate()); + } } \ No newline at end of file From f1f4a2072af13deb695859c895aba5535607c28b Mon Sep 17 00:00:00 2001 From: SunHo Park <67692759+prefer2@users.noreply.github.com> Date: Wed, 27 Jul 2022 14:22:20 +0900 Subject: [PATCH 03/62] =?UTF-8?q?feat:=20webpack=EA=B3=BC=20dotenv=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20(#189)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: dotenv 설치 및 gitignore update * feat: mode에 따른 dotenv 설정 Co-authored-by: Soyi Jeon * feat: API_URL 상수 제거 Co-authored-by: Soyi Jeon Co-authored-by: Soyi Jeon --- .gitignore | 1 + frontend/package.json | 1 + frontend/src/api/index.ts | 3 +-- frontend/src/constants/index.ts | 7 +------ frontend/webpack.dev.js | 11 +++++++++++ frontend/webpack.prod.js | 9 +++++++++ 6 files changed, 24 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 241d961b..fbe49dc5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules +.env.* .DS_Store .gradle diff --git a/frontend/package.json b/frontend/package.json index 0a83c792..d89c716d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,6 +28,7 @@ "@typescript-eslint/eslint-plugin": "^5.30.0", "@typescript-eslint/parser": "^5.30.0", "babel-loader": "^8.2.5", + "dotenv": "^16.0.1", "eslint": "^8.18.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-import": "^2.26.0", diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index a5df5102..167768fd 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -1,11 +1,10 @@ import axios from "axios"; -import { API_URL } from "@/constants"; import { getCookie } from "@/util/cookie"; const accessToken = getCookie("accessToken") || ""; const appClient = axios.create({ - baseURL: API_URL, + baseURL: process.env.API_URL, timeout: 3000, }); diff --git a/frontend/src/constants/index.ts b/frontend/src/constants/index.ts index a39fa369..fa6cf6c9 100644 --- a/frontend/src/constants/index.ts +++ b/frontend/src/constants/index.ts @@ -5,9 +5,4 @@ const REGEX = { TEAM_NAME: /^[가-힣a-zA-Z\d~!@#$%^&*()+_\-\s]{1,20}$/, }; -const API_URL = - process.env.NODE_ENV === "production" - ? "http://54.180.122.233:8080/api/v1" - : "/api/v1"; - -export { REGEX, API_URL }; +export { REGEX }; diff --git a/frontend/webpack.dev.js b/frontend/webpack.dev.js index 82207f84..ab32c6c9 100644 --- a/frontend/webpack.dev.js +++ b/frontend/webpack.dev.js @@ -1,5 +1,11 @@ const { merge } = require("webpack-merge"); const common = require("./webpack.common.js"); +const path = require("path"); +const webpack = require("webpack"); + +require("dotenv").config({ + path: path.join(__dirname, "./.env.development"), +}); module.exports = merge(common, { mode: "development", @@ -9,4 +15,9 @@ module.exports = merge(common, { port: 3000, hot: true, }, + plugins: [ + new webpack.DefinePlugin({ + "process.env": JSON.stringify(process.env), + }), + ], }); diff --git a/frontend/webpack.prod.js b/frontend/webpack.prod.js index 015ba8e8..70fd3e8c 100644 --- a/frontend/webpack.prod.js +++ b/frontend/webpack.prod.js @@ -1,7 +1,16 @@ const { merge } = require("webpack-merge"); const common = require("./webpack.common.js"); +const path = require("path"); +const webpack = require("webpack"); + +require("dotenv").config({ path: path.join(__dirname, "./.env.production") }); module.exports = merge(common, { mode: "production", devtool: "source-map", + plugins: [ + new webpack.DefinePlugin({ + "process.env": JSON.stringify(process.env), + }), + ], }); From 603583ea88cdef9f7b3aa5d76fe39acfbe13c01f Mon Sep 17 00:00:00 2001 From: TaeHyeon Kim <57135043+kth990303@users.noreply.github.com> Date: Wed, 27 Jul 2022 16:32:09 +0900 Subject: [PATCH 04/62] =?UTF-8?q?feat:=20=EB=AA=A8=EC=9E=84=20=EB=A7=88?= =?UTF-8?q?=EC=9D=B4=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#184)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 모임 마이페이지 기능 구현 모임 가입 정보 조회, 모임 닉네임 수정 기능 구현 * fix: 에러메시지 수정 * refactor: 메서드명 변경 * refactor: 메서드 순서 변경 * refactor: 명칭 변경 * refactor: 레포지토리 단에서 `Optional` 반환하도록 변경 * refactor: 불필요한 검증 제거 --- .../naepyeon/controller/TeamController.java | 19 +++++ .../dto/UpdateTeamParticipantRequest.java | 16 ++++ .../exception/DuplicateNicknameException.java | 15 ++++ .../JpaTeamParticipationRepository.java | 21 ++++- .../TeamParticipationRepository.java | 9 ++- .../jpa/TeamParticipationJpaDao.java | 25 +++++- .../naepyeon/service/MessageService.java | 2 +- .../naepyeon/service/RollingpaperService.java | 2 +- .../naepyeon/service/TeamService.java | 24 ++++++ .../service/dto/TeamMemberResponseDto.java | 14 ++++ .../acceptance/AcceptanceFixture.java | 20 ++++- .../acceptance/TeamAcceptanceTest.java | 67 ++++++++++++++-- .../TeamParticipationRepositoryTest.java | 44 +++++++++- .../naepyeon/service/TeamServiceTest.java | 80 ++++++++++++++++++- 14 files changed, 339 insertions(+), 19 deletions(-) create mode 100644 backend/src/main/java/com/woowacourse/naepyeon/controller/dto/UpdateTeamParticipantRequest.java create mode 100644 backend/src/main/java/com/woowacourse/naepyeon/exception/DuplicateNicknameException.java create mode 100644 backend/src/main/java/com/woowacourse/naepyeon/service/dto/TeamMemberResponseDto.java diff --git a/backend/src/main/java/com/woowacourse/naepyeon/controller/TeamController.java b/backend/src/main/java/com/woowacourse/naepyeon/controller/TeamController.java index 898b147e..398ad48f 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/controller/TeamController.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/controller/TeamController.java @@ -5,9 +5,11 @@ import com.woowacourse.naepyeon.controller.dto.JoinTeamMemberRequest; import com.woowacourse.naepyeon.controller.dto.LoginMemberRequest; import com.woowacourse.naepyeon.controller.dto.TeamRequest; +import com.woowacourse.naepyeon.controller.dto.UpdateTeamParticipantRequest; import com.woowacourse.naepyeon.exception.UncertificationTeamMemberException; import com.woowacourse.naepyeon.service.TeamService; import com.woowacourse.naepyeon.service.dto.JoinedMembersResponseDto; +import com.woowacourse.naepyeon.service.dto.TeamMemberResponseDto; import com.woowacourse.naepyeon.service.dto.TeamResponseDto; import com.woowacourse.naepyeon.service.dto.TeamsResponseDto; import java.net.URI; @@ -96,4 +98,21 @@ public ResponseEntity joinMember(@AuthenticationPrincipal @Valid final Log teamService.joinMember(teamId, loginMemberRequest.getId(), joinTeamMemberRequest.getNickname()); return ResponseEntity.noContent().build(); } + + @GetMapping("/{teamId}/me") + public ResponseEntity getMyInfoInTeam( + @AuthenticationPrincipal @Valid final LoginMemberRequest loginMemberRequest, + @PathVariable final Long teamId) { + final TeamMemberResponseDto responseDto = teamService.findMyInfoInTeam(teamId, loginMemberRequest.getId()); + return ResponseEntity.ok(responseDto); + } + + @PutMapping("/{teamId}/me") + public ResponseEntity updateMyInfo( + @AuthenticationPrincipal @Valid final LoginMemberRequest loginMemberRequest, + @PathVariable final Long teamId, + @RequestBody final UpdateTeamParticipantRequest updateTeamParticipantRequest) { + teamService.updateMyInfo(teamId, loginMemberRequest.getId(), updateTeamParticipantRequest.getNickname()); + return ResponseEntity.noContent().build(); + } } diff --git a/backend/src/main/java/com/woowacourse/naepyeon/controller/dto/UpdateTeamParticipantRequest.java b/backend/src/main/java/com/woowacourse/naepyeon/controller/dto/UpdateTeamParticipantRequest.java new file mode 100644 index 00000000..20d7be4b --- /dev/null +++ b/backend/src/main/java/com/woowacourse/naepyeon/controller/dto/UpdateTeamParticipantRequest.java @@ -0,0 +1,16 @@ +package com.woowacourse.naepyeon.controller.dto; + +import javax.validation.constraints.NotBlank; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Getter +public class UpdateTeamParticipantRequest { + + @NotBlank(message = "4009:닉네임은 공백일 수 없습니다.") + private String nickname; +} diff --git a/backend/src/main/java/com/woowacourse/naepyeon/exception/DuplicateNicknameException.java b/backend/src/main/java/com/woowacourse/naepyeon/exception/DuplicateNicknameException.java new file mode 100644 index 00000000..5d3b5446 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/naepyeon/exception/DuplicateNicknameException.java @@ -0,0 +1,15 @@ +package com.woowacourse.naepyeon.exception; + +import org.springframework.http.HttpStatus; + +public final class DuplicateNicknameException extends NaePyeonException { + + public DuplicateNicknameException(final String nickname) { + super( + String.format("이미 존재하는 닉네임입니다. teamName={%s}", nickname), + "이미 존재하는 닉네임입니다.", + HttpStatus.BAD_REQUEST, + "4014" + ); + } +} diff --git a/backend/src/main/java/com/woowacourse/naepyeon/repository/JpaTeamParticipationRepository.java b/backend/src/main/java/com/woowacourse/naepyeon/repository/JpaTeamParticipationRepository.java index 3219b057..d2b763a3 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/repository/JpaTeamParticipationRepository.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/repository/JpaTeamParticipationRepository.java @@ -1,5 +1,6 @@ package com.woowacourse.naepyeon.repository; +import com.woowacourse.naepyeon.domain.Member; import com.woowacourse.naepyeon.domain.Team; import com.woowacourse.naepyeon.domain.TeamParticipation; import com.woowacourse.naepyeon.exception.DuplicateTeamPaticipateException; @@ -34,6 +35,12 @@ public Optional findById(final Long id) { return teamParticipationJpaDao.findById(id); } + @Override + public Optional findMemberByMemberIdAndTeamId(final Long memberId, final Long teamId) { + return teamParticipationJpaDao.findMemberByMemberIdAndTeamId(memberId, teamId); + + } + @Override public List findByTeamId(final Long teamId) { return teamParticipationJpaDao.findByTeamId(teamId); @@ -45,8 +52,13 @@ public List findTeamsByMemberId(final Long memberId) { } @Override - public String findNicknameByMemberId(final Long addresseeId, final Long teamId) { - return teamParticipationJpaDao.findNicknameByMemberIdAndTeamId(addresseeId, teamId); + public String findNicknameByMemberIdAndTeamId(final Long memberId, final Long teamId) { + return teamParticipationJpaDao.findNicknameByMemberIdAndTeamId(memberId, teamId); + } + + @Override + public List findAllNicknamesByTeamId(final Long teamId) { + return teamParticipationJpaDao.findNicknamesByTeamId(teamId); } @Override @@ -55,4 +67,9 @@ public boolean isJoinedMember(final Long memberId, final Long teamId) { return teams.stream() .anyMatch(team -> team.getId().equals(teamId)); } + + @Override + public void updateNickname(final String newNickname, final Long memberId, final Long teamId) { + teamParticipationJpaDao.updateNickname(newNickname, memberId, teamId); + } } diff --git a/backend/src/main/java/com/woowacourse/naepyeon/repository/TeamParticipationRepository.java b/backend/src/main/java/com/woowacourse/naepyeon/repository/TeamParticipationRepository.java index 25d037e3..36b2283a 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/repository/TeamParticipationRepository.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/repository/TeamParticipationRepository.java @@ -1,5 +1,6 @@ package com.woowacourse.naepyeon.repository; +import com.woowacourse.naepyeon.domain.Member; import com.woowacourse.naepyeon.domain.Team; import com.woowacourse.naepyeon.domain.TeamParticipation; import java.util.List; @@ -11,11 +12,17 @@ public interface TeamParticipationRepository { Optional findById(final Long id); + Optional findMemberByMemberIdAndTeamId(final Long memberId, final Long teamId); + List findByTeamId(final Long teamId); List findTeamsByMemberId(final Long memberId); - String findNicknameByMemberId(final Long addresseeId, final Long teamId); + String findNicknameByMemberIdAndTeamId(final Long memberId, final Long teamId); + + List findAllNicknamesByTeamId(final Long teamId); boolean isJoinedMember(final Long memberId, final Long teamId); + + void updateNickname(final String newNickname, final Long memberId, final Long teamId); } diff --git a/backend/src/main/java/com/woowacourse/naepyeon/repository/jpa/TeamParticipationJpaDao.java b/backend/src/main/java/com/woowacourse/naepyeon/repository/jpa/TeamParticipationJpaDao.java index 0bee6b47..da2d96c4 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/repository/jpa/TeamParticipationJpaDao.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/repository/jpa/TeamParticipationJpaDao.java @@ -1,9 +1,12 @@ package com.woowacourse.naepyeon.repository.jpa; +import com.woowacourse.naepyeon.domain.Member; import com.woowacourse.naepyeon.domain.Team; import com.woowacourse.naepyeon.domain.TeamParticipation; import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -11,13 +14,33 @@ public interface TeamParticipationJpaDao extends JpaRepository findByTeamId(final Long teamId); - @Query("select p.team " + @Query("select t " + "from TeamParticipation p " + + "join p.team t " + "where p.member.id = :memberId") List findTeamsByMemberId(@Param("memberId") final Long memberId); + @Query("select p.member " + + "from TeamParticipation p " + + "join Member m on m.id = :memberId " + + "where p.team.id = :teamId") + Optional findMemberByMemberIdAndTeamId(@Param("memberId") final Long memberId, + @Param("teamId") final Long teamId); + @Query("select p.nickname " + "from TeamParticipation p " + "where p.member.id = :memberId and p.team.id = :teamId") String findNicknameByMemberIdAndTeamId(@Param("memberId") final Long memberId, @Param("teamId") final Long teamId); + + @Query("select p.nickname " + + "from TeamParticipation p " + + "where p.team.id = :teamId") + List findNicknamesByTeamId(@Param("teamId") final Long teamId); + + @Query("update TeamParticipation p " + + "set p.nickname = :newNickname " + + "where p.member.id = :memberId and p.team.id = :teamId") + @Modifying(clearAutomatically = true) + void updateNickname(@Param("newNickname") final String newNickname, + @Param("memberId") final Long memberId, @Param("teamId") final Long teamId); } \ No newline at end of file diff --git a/backend/src/main/java/com/woowacourse/naepyeon/service/MessageService.java b/backend/src/main/java/com/woowacourse/naepyeon/service/MessageService.java index b2482559..fcf41635 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/service/MessageService.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/service/MessageService.java @@ -56,7 +56,7 @@ public List findMessages(final Long rollingpaperId, final Lo private String findMessageWriterNickname(final Long teamId, final Message message) { final Member author = message.getAuthor(); - return teamParticipationRepository.findNicknameByMemberId(author.getId(), teamId); + return teamParticipationRepository.findNicknameByMemberIdAndTeamId(author.getId(), teamId); } @Transactional(readOnly = true) diff --git a/backend/src/main/java/com/woowacourse/naepyeon/service/RollingpaperService.java b/backend/src/main/java/com/woowacourse/naepyeon/service/RollingpaperService.java index 0fe81a1b..c73df036 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/service/RollingpaperService.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/service/RollingpaperService.java @@ -92,7 +92,7 @@ rollingpaper, findRollingpaperAddresseeNickname(rollingpaper, teamId)) } private String findRollingpaperAddresseeNickname(final Rollingpaper rollingpaper, final Long teamId) { - return teamParticipationRepository.findNicknameByMemberId(rollingpaper.getAddresseeId(), teamId); + return teamParticipationRepository.findNicknameByMemberIdAndTeamId(rollingpaper.getAddresseeId(), teamId); } public void updateTitle(final Long rollingpaperId, final String newTitle, final Long teamId, diff --git a/backend/src/main/java/com/woowacourse/naepyeon/service/TeamService.java b/backend/src/main/java/com/woowacourse/naepyeon/service/TeamService.java index 8ba14f45..d13d9e95 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/service/TeamService.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/service/TeamService.java @@ -4,13 +4,16 @@ import com.woowacourse.naepyeon.domain.Member; import com.woowacourse.naepyeon.domain.Team; import com.woowacourse.naepyeon.domain.TeamParticipation; +import com.woowacourse.naepyeon.exception.DuplicateNicknameException; import com.woowacourse.naepyeon.exception.NotFoundMemberException; import com.woowacourse.naepyeon.exception.NotFoundTeamException; +import com.woowacourse.naepyeon.exception.UncertificationTeamMemberException; import com.woowacourse.naepyeon.repository.MemberRepository; import com.woowacourse.naepyeon.repository.TeamParticipationRepository; import com.woowacourse.naepyeon.repository.TeamRepository; import com.woowacourse.naepyeon.service.dto.JoinedMemberResponseDto; import com.woowacourse.naepyeon.service.dto.JoinedMembersResponseDto; +import com.woowacourse.naepyeon.service.dto.TeamMemberResponseDto; import com.woowacourse.naepyeon.service.dto.TeamResponseDto; import com.woowacourse.naepyeon.service.dto.TeamsResponseDto; import java.util.List; @@ -105,6 +108,27 @@ public JoinedMembersResponseDto findJoinedMembers(final Long teamId) { return new JoinedMembersResponseDto(joinedMembers); } + public TeamMemberResponseDto findMyInfoInTeam(final Long teamId, final Long memberId) { + checkMemberNotIncludedTeam(teamId, memberId); + final String nickname = teamParticipationRepository.findNicknameByMemberIdAndTeamId(memberId, teamId); + return new TeamMemberResponseDto(nickname); + } + + @Transactional + public void updateMyInfo(final Long teamId, final Long memberId, final String newNickname) { + checkMemberNotIncludedTeam(teamId, memberId); + if (teamParticipationRepository.findAllNicknamesByTeamId(teamId).contains(newNickname)) { + throw new DuplicateNicknameException(newNickname); + } + teamParticipationRepository.updateNickname(newNickname, memberId, teamId); + } + + private void checkMemberNotIncludedTeam(final Long teamId, final Long memberId) { + if (!isJoinedMember(memberId, teamId)) { + throw new UncertificationTeamMemberException(teamId, memberId); + } + } + public boolean isJoinedMember(final Long memberId, final Long teamId) { if (teamRepository.findById(teamId).isEmpty()) { throw new NotFoundTeamException(teamId); diff --git a/backend/src/main/java/com/woowacourse/naepyeon/service/dto/TeamMemberResponseDto.java b/backend/src/main/java/com/woowacourse/naepyeon/service/dto/TeamMemberResponseDto.java new file mode 100644 index 00000000..dbff9565 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/naepyeon/service/dto/TeamMemberResponseDto.java @@ -0,0 +1,14 @@ +package com.woowacourse.naepyeon.service.dto; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class TeamMemberResponseDto { + + private String nickname; +} diff --git a/backend/src/test/java/com/woowacourse/naepyeon/acceptance/AcceptanceFixture.java b/backend/src/test/java/com/woowacourse/naepyeon/acceptance/AcceptanceFixture.java index 2ddfbb3e..fee5f242 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/acceptance/AcceptanceFixture.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/acceptance/AcceptanceFixture.java @@ -10,6 +10,7 @@ import com.woowacourse.naepyeon.controller.dto.RollingpaperUpdateRequest; import com.woowacourse.naepyeon.controller.dto.TeamRequest; import com.woowacourse.naepyeon.controller.dto.TokenRequest; +import com.woowacourse.naepyeon.controller.dto.UpdateTeamParticipantRequest; import com.woowacourse.naepyeon.service.dto.TokenResponseDto; import io.restassured.RestAssured; import io.restassured.response.ExtractableResponse; @@ -140,11 +141,22 @@ public static ExtractableResponse delete(final TokenResponseDto tokenR return get(tokenResponseDto, "/api/v1/teams/me"); } - public static ExtractableResponse 모임에_가입한_회원_조회(final TokenResponseDto tokenResponseDto, - final Long teamId) { + public static ExtractableResponse 모임에_가입한_회원_목록_조회(final TokenResponseDto tokenResponseDto, + final Long teamId) { return get(tokenResponseDto, "/api/v1/teams/" + teamId + "/members"); } + public static ExtractableResponse 모임_가입_정보_조회(final TokenResponseDto tokenResponseDto, + final Long teamId) { + return get(tokenResponseDto, "/api/v1/teams/" + teamId + "/me"); + } + + public static ExtractableResponse 모임_내_닉네임_변경(final TokenResponseDto tokenResponseDto, + final Long teamId, + final UpdateTeamParticipantRequest updateTeamParticipantRequest) { + return put(tokenResponseDto, updateTeamParticipantRequest, "/api/v1/teams/" + teamId + "/me"); + } + public static ExtractableResponse 회원_롤링페이퍼_생성(final TokenResponseDto tokenResponseDto, final Long teamId, final RollingpaperCreateRequest rollingpaperCreateRequest) { @@ -193,8 +205,8 @@ public static ExtractableResponse delete(final TokenResponseDto tokenR } public static ExtractableResponse 메시지_조회(final TokenResponseDto tokenResponseDto, - final Long rollingpaperId, - final Long messageId) { + final Long rollingpaperId, + final Long messageId) { return get(tokenResponseDto, "/api/v1/rollingpapers/" + rollingpaperId + "/messages/" + messageId); } } diff --git a/backend/src/test/java/com/woowacourse/naepyeon/acceptance/TeamAcceptanceTest.java b/backend/src/test/java/com/woowacourse/naepyeon/acceptance/TeamAcceptanceTest.java index c9b81c1b..20911bc4 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/acceptance/TeamAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/acceptance/TeamAcceptanceTest.java @@ -3,12 +3,14 @@ import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.가입한_모임_조회; import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.모든_모임_조회; import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.모임_가입; +import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.모임_가입_정보_조회; +import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.모임_내_닉네임_변경; import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.모임_단건_조회; import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.모임_삭제; import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.모임_생성; import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.모임_이름_수정; import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.모임_추가; -import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.모임에_가입한_회원_조회; +import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.모임에_가입한_회원_목록_조회; import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.회원가입_후_로그인; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; @@ -17,8 +19,10 @@ import com.woowacourse.naepyeon.controller.dto.JoinTeamMemberRequest; import com.woowacourse.naepyeon.controller.dto.MemberRegisterRequest; import com.woowacourse.naepyeon.controller.dto.TeamRequest; +import com.woowacourse.naepyeon.controller.dto.UpdateTeamParticipantRequest; import com.woowacourse.naepyeon.service.dto.JoinedMemberResponseDto; import com.woowacourse.naepyeon.service.dto.JoinedMembersResponseDto; +import com.woowacourse.naepyeon.service.dto.TeamMemberResponseDto; import com.woowacourse.naepyeon.service.dto.TeamResponseDto; import com.woowacourse.naepyeon.service.dto.TeamsResponseDto; import com.woowacourse.naepyeon.service.dto.TokenResponseDto; @@ -197,8 +201,7 @@ void checkJoinedColumn() { "#123456", "나는야모임장" ); - final Long team2Id = 모임_추가(tokenResponseDto1, teamRequest2).as(CreateResponse.class) - .getId(); + 모임_추가(tokenResponseDto1, teamRequest2).as(CreateResponse.class); 모임_가입(tokenResponseDto2, team1Id, new JoinTeamMemberRequest("가입자")); @@ -245,7 +248,7 @@ void findJoinedMembers() { final String joinNickname = "가입자"; 모임_가입(tokenResponseDto2, team1Id, new JoinTeamMemberRequest(joinNickname)); - final JoinedMembersResponseDto joinedMembers = 모임에_가입한_회원_조회(tokenResponseDto1, team1Id) + final JoinedMembersResponseDto joinedMembers = 모임에_가입한_회원_목록_조회(tokenResponseDto1, team1Id) .as(JoinedMembersResponseDto.class); final List joinedMemberNickNames = joinedMembers.getMembers() @@ -350,8 +353,7 @@ void getJoinedTeams() { "#123456", "나는야모임장" ); - final Long team2Id = 모임_추가(tokenResponseDto, teamRequest2).as(CreateResponse.class) - .getId(); + 모임_추가(tokenResponseDto, teamRequest2).as(CreateResponse.class); final TeamRequest teamRequest3 = new TeamRequest( "woowacourse3", "테스트 모임입니다.", @@ -411,6 +413,59 @@ void deleteNotExistTeam() { assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND.value()); } + @Test + @DisplayName("모임에서의 내 정보를 조회한다.") + void findMyInfoInTeam() { + final String expected = "나는야모임장"; + final MemberRegisterRequest member = + new MemberRegisterRequest("seungpang", "email@email.com", "12345678aA!"); + final TokenResponseDto tokenResponseDto = 회원가입_후_로그인(member); + final Long teamId = 모임_생성(tokenResponseDto); + + final String actual = 모임_가입_정보_조회(tokenResponseDto, teamId) + .as(TeamMemberResponseDto.class) + .getNickname(); + assertThat(actual).isEqualTo(expected); + } + + @Test + @DisplayName("모임의 닉네임을 수정한다.") + void updateNickname() { + final String expected = "나모임장안해"; + final MemberRegisterRequest member = + new MemberRegisterRequest("seungpang", "email@email.com", "12345678aA!"); + final TokenResponseDto tokenResponseDto = 회원가입_후_로그인(member); + final Long teamId = 모임_생성(tokenResponseDto); + + final UpdateTeamParticipantRequest updateTeamParticipantRequest = new UpdateTeamParticipantRequest(expected); + 모임_내_닉네임_변경(tokenResponseDto, teamId, updateTeamParticipantRequest); + + final String actual = 모임_가입_정보_조회(tokenResponseDto, teamId) + .as(TeamMemberResponseDto.class) + .getNickname(); + assertThat(actual).isEqualTo(expected); + } + + @Test + @DisplayName("이미 팀에 존재하는 닉네임으로 수정할 경우 예외를 발생시킨다.") + void updateDuplicatedNickname() { + final MemberRegisterRequest member = + new MemberRegisterRequest("seungpang", "email@email.com", "12345678aA!"); + final TokenResponseDto tokenResponseDto = 회원가입_후_로그인(member); + final Long teamId = 모임_생성(tokenResponseDto); + + final MemberRegisterRequest member2 = + new MemberRegisterRequest("kth990303", "kth990303@email.com", "12345678aA!"); + final TokenResponseDto tokenResponseDto2 = 회원가입_후_로그인(member); + final JoinTeamMemberRequest request = new JoinTeamMemberRequest("애플"); + 모임_가입(tokenResponseDto2, teamId, request); + + final UpdateTeamParticipantRequest updateTeamParticipantRequest = new UpdateTeamParticipantRequest("나는야모임장"); + final ExtractableResponse response = 모임_내_닉네임_변경(tokenResponseDto, teamId, + updateTeamParticipantRequest); + assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + } + private void 모임_삭제됨(ExtractableResponse response) { assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); } diff --git a/backend/src/test/java/com/woowacourse/naepyeon/repository/TeamParticipationRepositoryTest.java b/backend/src/test/java/com/woowacourse/naepyeon/repository/TeamParticipationRepositoryTest.java index 337296e6..673234da 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/repository/TeamParticipationRepositoryTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/repository/TeamParticipationRepositoryTest.java @@ -5,7 +5,6 @@ import static org.junit.jupiter.api.Assertions.assertAll; import com.woowacourse.naepyeon.domain.Member; -import com.woowacourse.naepyeon.domain.Rollingpaper; import com.woowacourse.naepyeon.domain.Team; import com.woowacourse.naepyeon.domain.TeamParticipation; import com.woowacourse.naepyeon.exception.DuplicateTeamPaticipateException; @@ -79,6 +78,17 @@ void duplicateSaveException() { .isInstanceOf(DuplicateTeamPaticipateException.class); } + @Test + @DisplayName("모임에 가입한 회원을 memberId와 teamId로 찾는다.") + void findMemberByMemberIdAndTeamId() { + final TeamParticipation teamParticipation1 = new TeamParticipation(team1, member1, "닉네임1"); + teamParticipationRepository.save(teamParticipation1); + + final Member actual = teamParticipationRepository.findMemberByMemberIdAndTeamId(member1.getId(), team1.getId()) + .orElseThrow(); + assertThat(actual).isEqualTo(member1); + } + @Test @DisplayName("모임에 가입한 회원들을 team id로 조회한다.") void findByTeamId() { @@ -121,11 +131,25 @@ void findNicknameByAddresseeId() { teamParticipationRepository.save(teamParticipation1); final String actual = - teamParticipationRepository.findNicknameByMemberId(member1.getId(), team1.getId()); + teamParticipationRepository.findNicknameByMemberIdAndTeamId(member1.getId(), team1.getId()); assertThat(actual).isEqualTo(expected); } + @Test + @DisplayName("특정 팀의 모든 닉네임들을 조회한다.") + void findAllNicknamesByTeamId() { + final TeamParticipation teamParticipation1 = new TeamParticipation(team1, member1, "닉네임1"); + final TeamParticipation teamParticipation2 = new TeamParticipation(team1, member2, "닉네임2"); + + teamParticipationRepository.save(teamParticipation1); + teamParticipationRepository.save(teamParticipation2); + + final List actual = teamParticipationRepository.findAllNicknamesByTeamId(team1.getId()); + + assertThat(actual).contains("닉네임1", "닉네임2"); + } + @Test @DisplayName("특정 모임에 회원이 가입했는지 여부를 반환한다.") void isJoinedMember() { @@ -152,4 +176,20 @@ void createMemberWhen() { .orElseThrow(); assertThat(actual.getCreatedDate()).isAfter(LocalDateTime.MIN); } + + @Test + @DisplayName("회원이 특정 팀의 닉네임을 변경한다.") + void updateNickname() { + final String expected = "닉네임2"; + final TeamParticipation teamParticipation = new TeamParticipation(team1, member1, "닉네임1"); + final Long teamParticipationId = teamParticipationRepository.save(teamParticipation); + + teamParticipationRepository.updateNickname(expected, member1.getId(), team1.getId()); + em.flush(); + + final String actual = teamParticipationRepository.findById(teamParticipationId) + .orElseThrow() + .getNickname(); + assertThat(actual).isEqualTo(expected); + } } \ No newline at end of file diff --git a/backend/src/test/java/com/woowacourse/naepyeon/service/TeamServiceTest.java b/backend/src/test/java/com/woowacourse/naepyeon/service/TeamServiceTest.java index e8435e3f..9a85009d 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/service/TeamServiceTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/service/TeamServiceTest.java @@ -8,12 +8,15 @@ import com.woowacourse.naepyeon.domain.Member; import com.woowacourse.naepyeon.domain.Team; import com.woowacourse.naepyeon.domain.TeamParticipation; +import com.woowacourse.naepyeon.exception.DuplicateNicknameException; import com.woowacourse.naepyeon.exception.NotFoundMemberException; import com.woowacourse.naepyeon.exception.NotFoundTeamException; +import com.woowacourse.naepyeon.exception.UncertificationTeamMemberException; import com.woowacourse.naepyeon.repository.MemberRepository; import com.woowacourse.naepyeon.repository.TeamParticipationRepository; import com.woowacourse.naepyeon.repository.TeamRepository; import com.woowacourse.naepyeon.service.dto.JoinedMemberResponseDto; +import com.woowacourse.naepyeon.service.dto.TeamMemberResponseDto; import com.woowacourse.naepyeon.service.dto.TeamResponseDto; import com.woowacourse.naepyeon.service.dto.TeamsResponseDto; import java.util.List; @@ -252,4 +255,79 @@ void isJoinedMemberWithNotFoundTeam() { assertThatThrownBy(() -> teamService.isJoinedMember(member.getId(), team1.getId() + 1000L)) .isInstanceOf(NotFoundTeamException.class); } -} \ No newline at end of file + + @Test + @DisplayName("모임에서의 마이페이지를 조회한다.") + void findMyInfoInTeam() { + final String expected = "닉네임1"; + final TeamParticipation teamParticipation = new TeamParticipation(team1, member, expected); + teamParticipationRepository.save(teamParticipation); + + final TeamMemberResponseDto actual = teamService.findMyInfoInTeam(team1.getId(), member.getId()); + + assertThat(actual.getNickname()).isEqualTo(expected); + } + + @Test + @DisplayName("모임 내 마이페이지 조회 창에서 가입한 모임이 아닐 경우 예외를 발생시킨다.") + void findMyInfoInOtherTeam() { + final TeamParticipation teamParticipation = new TeamParticipation(team1, member, "해커"); + teamParticipationRepository.save(teamParticipation); + + assertThatThrownBy(() -> teamService.findMyInfoInTeam(team2.getId(), member.getId())) + .isInstanceOf(UncertificationTeamMemberException.class); + } + + @Test + @DisplayName("존재하지 않는 모임에서 마이페이지를 조회할 경우 예외를 발생시킨다.") + void findMyInfoInNotExistTeam() { + assertThatThrownBy(() -> teamService.findMyInfoInTeam(team1.getId() + 10000L, member.getId())) + .isInstanceOf(NotFoundTeamException.class); + } + + @Test + @DisplayName("모임에 가입된 닉네임을 수정한다. 다른 모임에 해당 닉네임이 존재해도 수정에 문제되지 않는다.") + void updateNickname() { + final String expected = "닉네임1"; + final TeamParticipation teamParticipation1 = new TeamParticipation(team1, member, "닉네임1"); + final TeamParticipation teamParticipation2 = new TeamParticipation(team2, member2, "닉네임2"); + teamParticipationRepository.save(teamParticipation1); + teamParticipationRepository.save(teamParticipation2); + + teamService.updateMyInfo(team2.getId(), member2.getId(), expected); + + final String actual = teamParticipationRepository.findNicknameByMemberIdAndTeamId(member2.getId(), + team2.getId()); + assertThat(actual).isEqualTo(expected); + } + + @Test + @DisplayName("이미 존재하는 닉네임으로 닉네임을 수정할 경우 예외를 발생시킨다.") + void updateDuplicateNickname() { + final String expected = "닉네임1"; + final TeamParticipation teamParticipation1 = new TeamParticipation(team1, member, "닉네임1"); + final TeamParticipation teamParticipation2 = new TeamParticipation(team1, member2, "닉네임2"); + teamParticipationRepository.save(teamParticipation1); + teamParticipationRepository.save(teamParticipation2); + + assertThatThrownBy(() -> teamService.updateMyInfo(team1.getId(), member2.getId(), expected)) + .isInstanceOf(DuplicateNicknameException.class); + } + + @Test + @DisplayName("내가 가입되지 않은 모임에서 닉네임 변경을 하려 할 경우 예외를 발생시킨다.") + void updateNicknameNotMyTeam() { + final TeamParticipation teamParticipation = new TeamParticipation(team1, member, "해커"); + teamParticipationRepository.save(teamParticipation); + + assertThatThrownBy(() -> teamService.updateMyInfo(team2.getId(), member.getId(), "선량한시민")) + .isInstanceOf(UncertificationTeamMemberException.class); + } + + @Test + @DisplayName("존재하지 않는 모임에서 닉네임 변경을 하려 할 경우 예외를 발생시킨다.") + void updateNicknameNotExistTeam() { + assertThatThrownBy(() -> teamService.updateMyInfo(team1.getId() + 10000L, member.getId(), "해커")) + .isInstanceOf(NotFoundTeamException.class); + } +} From b5915dd096a825909046f7ed14f87f7eac54bd70 Mon Sep 17 00:00:00 2001 From: zero Date: Thu, 28 Jul 2022 13:45:29 +0900 Subject: [PATCH 05/62] =?UTF-8?q?chore:=20flyway=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B0=9C=EB=B0=9C=ED=99=98=EA=B2=BD=20DB=EB=A5=BC?= =?UTF-8?q?=20H2=EC=97=90=EC=84=9C=20MYSQL=EB=A1=9C=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98=20(#197)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit chore: flyway 적용 및 개발환경 DB를 H2에서 MYSQL로 마이그레이션 Co-authored-by: kth990303 Co-authored-by: asebn1 --- backend/build.gradle | 3 +- .../naepyeon/domain/BaseEntity.java | 14 +-- .../src/main/resources/application-deploy.yml | 5 ++ backend/src/main/resources/application.yml | 18 ++-- .../main/resources/db/migration/V1__init.sql | 89 +++++++++++++++++++ 5 files changed, 115 insertions(+), 14 deletions(-) create mode 100644 backend/src/main/resources/db/migration/V1__init.sql diff --git a/backend/build.gradle b/backend/build.gradle index 5d45cb8f..822e1679 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -27,8 +27,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.flywaydb:flyway-core:6.4.2' compileOnly 'org.projectlombok:lombok' - runtimeOnly 'com.h2database:h2' + runtimeOnly 'com.h2database:h2:1.4.200' runtimeOnly 'mysql:mysql-connector-java' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' diff --git a/backend/src/main/java/com/woowacourse/naepyeon/domain/BaseEntity.java b/backend/src/main/java/com/woowacourse/naepyeon/domain/BaseEntity.java index 4b9a1b4c..5384e755 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/domain/BaseEntity.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/domain/BaseEntity.java @@ -14,11 +14,11 @@ @Getter abstract public class BaseEntity { - @CreatedDate - @Column(name = "created_at", nullable = false, updatable = false) - private LocalDateTime createdDate; - - @LastModifiedDate - @Column(name = "last_modified_at", nullable = false) - private LocalDateTime lastModifiedDate; +// @CreatedDate +// @Column(name = "created_at", nullable = false, updatable = false) +// private LocalDateTime createdDate; +// +// @LastModifiedDate +// @Column(name = "last_modified_at", nullable = false) +// private LocalDateTime lastModifiedDate; } diff --git a/backend/src/main/resources/application-deploy.yml b/backend/src/main/resources/application-deploy.yml index 76475a8a..6d5df243 100644 --- a/backend/src/main/resources/application-deploy.yml +++ b/backend/src/main/resources/application-deploy.yml @@ -2,12 +2,14 @@ spring: jpa: properties: hibernate: + jdbc.lob.non_contextual_creation: true default_batch_fetch_size: '1000' dialect: org.hibernate.dialect.MySQL5Dialect format_sql: 'true' hibernate: ddl-auto: validate open-in-view: 'false' + generate-ddl: false datasource: password: ${MYSQL_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver @@ -16,6 +18,9 @@ spring: h2: console: enabled: 'true' + flyway: + enabled: true + baseline-on-migrate: true security: jwt: token: diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 7dca6554..ce05d282 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -5,21 +5,27 @@ spring: profiles: active: local datasource: - # url: jdbc:h2:tc가p://localhost/~/naepyeon - url: jdbc:h2:mem:testdb - username: sa - password: - driver-class-name: org.h2.Driver + # url: jdbc:h2:mem:testdb + url: jdbc:mysql://127.0.0.1:3306/naepyeon?serverTimezone=Asia/Seoul&character_set_server=utf8mb4&useUnicode=true + username: root + password: root + driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: - ddl-auto: create + ddl-auto: validate properties: hibernate: # show_sql: true + jdbc.lob.non_contextual_creation: true format_sql: true default_batch_fetch_size: 1000 #최적화 옵션 + dialect: org.hibernate.dialect.MySQL5Dialect open-in-view: false + generate-ddl: false + flyway: + enabled: true + baseline-on-migrate: true logging.level: org.hibernate.SQL: debug diff --git a/backend/src/main/resources/db/migration/V1__init.sql b/backend/src/main/resources/db/migration/V1__init.sql new file mode 100644 index 00000000..3bafa15b --- /dev/null +++ b/backend/src/main/resources/db/migration/V1__init.sql @@ -0,0 +1,89 @@ +create table member +( + member_id bigint not null auto_increment, + email varchar(255) not null, + password varchar(255) not null, + username varchar(20) not null, + primary key (member_id) +) engine = InnoDB + default charset utf8mb4; + +create table message +( + message_id bigint not null auto_increment, + content varchar(500) not null, + member_id bigint, + rollingpaper_id bigint, + primary key (message_id) +) engine = InnoDB + default charset utf8mb4; + +create table rollingpaper +( + rollingpaper_id bigint not null auto_increment, + title varchar(20) not null, + member_id bigint, + team_id bigint not null, + primary key (rollingpaper_id) +) engine = InnoDB + default charset utf8mb4; + +create table team +( + team_id bigint not null auto_increment, + color varchar(15) not null, + description varchar(100) not null, + emoji varchar(255) not null, + team_name varchar(20) not null, + primary key (team_id) +) engine = InnoDB + default charset utf8mb4; + +create table team_member +( + team_member_id bigint not null auto_increment, + nickname varchar(20) not null, + member_id bigint, + team_id bigint, + primary key (team_member_id) +) engine = InnoDB + default charset utf8mb4; + +alter table member + add constraint uk_member_email unique (email); + +alter table team + add constraint uk_team_name unique (team_name); + +alter table team_member + add constraint uk_participate_duplicate unique (team_id, member_id); + +alter table message + add constraint fk_message_member_id + foreign key (member_id) + references member (member_id); + +alter table message + add constraint fk_message_rollingpaper_id + foreign key (rollingpaper_id) + references rollingpaper (rollingpaper_id); + +alter table rollingpaper + add constraint fk_rollingpaper_member_id + foreign key (member_id) + references member (member_id); + +alter table rollingpaper + add constraint fk_rollingpaper_team_id + foreign key (team_id) + references team (team_id); + +alter table team_member + add constraint fk_team_member_member_id + foreign key (member_id) + references member (member_id); + +alter table team_member + add constraint fk_team_member_team_id + foreign key (team_id) + references team (team_id); \ No newline at end of file From bd522ee995e1862525845c0df895dd0ff59eab61 Mon Sep 17 00:00:00 2001 From: SunHo Park <67692759+prefer2@users.noreply.github.com> Date: Thu, 28 Jul 2022 20:36:20 +0900 Subject: [PATCH 06/62] =?UTF-8?q?feat:=20mypage=20UI=EB=A5=BC=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=ED=95=9C=EB=8B=A4=20(#186)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: CounterWithText 컴포넌트 구현 * feat: Paging component 구현 * refactor: file 경로 수정 * feat: ReceivedRollingpaperCard component 구현 * feat: WrittenMessageCard component 구현 * feat: right icon button 추가 * feat: UserProfile component 구현 * feat: MyPage 구현 * feat: MyPage route 추가 * refactor: 페이징 버튼 로직 수정 * refactor: 오타 수정 * refactor: 클릭 숫자 이동 오류 수정 * feat: ValueOf type 추가 * refactor: UserProfile mode type 지정 * refactor: 컴포넌트명 수정 * feat: MyPage Tab type 추가 * feat: MyPageRollingpaperList component 구현 * feat: MyPageWrittenMessageList 구현 * feat: MyPageRollingpaperList, MyPageWrittenMessageList으로 변경 * feat: hover시 색상 변경 추가 * refactor: onClick type 변경 * refactor: 불필요한 import 제거 및 상수명 변경 * feat: MypageList들에 Paging 추가 * feat: Styled 컴포넌트명 수정 --- frontend/src/App.tsx | 11 +- .../src/assets/icons/bx-chevron-right.svg | 1 + .../MyPageRollingpaperListPaging.tsx | 61 ++++++++ frontend/src/components/MyPageTab.tsx | 50 +++++++ .../MyPageWrittenMessageListPaging.tsx | 71 +++++++++ frontend/src/components/Paging.tsx | 120 +++++++++++++++ .../components/ReceivedRollingpaperCard.tsx | 48 ++++++ frontend/src/components/UserProfile.tsx | 109 ++++++++++++++ .../src/components/WrittenMessageCard.tsx | 72 +++++++++ frontend/src/pages/MyPage.tsx | 139 ++++++++++++++++++ frontend/src/stories/IconButton.stories.jsx | 2 +- frontend/src/stories/MyPageTab.jsx | 23 +++ frontend/src/stories/Paging.stories.jsx | 12 ++ .../ReceivedRollingpaperCard.stories.jsx | 14 ++ frontend/src/stories/UserProfile.stories.jsx | 15 ++ .../stories/WrittenMessageCard.stories.jsx | 18 +++ frontend/src/types/index.ts | 2 + 17 files changed, 765 insertions(+), 3 deletions(-) create mode 100644 frontend/src/assets/icons/bx-chevron-right.svg create mode 100644 frontend/src/components/MyPageRollingpaperListPaging.tsx create mode 100644 frontend/src/components/MyPageTab.tsx create mode 100644 frontend/src/components/MyPageWrittenMessageListPaging.tsx create mode 100644 frontend/src/components/Paging.tsx create mode 100644 frontend/src/components/ReceivedRollingpaperCard.tsx create mode 100644 frontend/src/components/UserProfile.tsx create mode 100644 frontend/src/components/WrittenMessageCard.tsx create mode 100644 frontend/src/pages/MyPage.tsx create mode 100644 frontend/src/stories/MyPageTab.jsx create mode 100644 frontend/src/stories/Paging.stories.jsx create mode 100644 frontend/src/stories/ReceivedRollingpaperCard.stories.jsx create mode 100644 frontend/src/stories/UserProfile.stories.jsx create mode 100644 frontend/src/stories/WrittenMessageCard.stories.jsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a185eda2..851af719 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -24,6 +24,7 @@ import ErrorPage from "@/pages/ErrorPage"; import RequireLogin from "@/components/RequireLogin"; import RequireLogout from "./components/RequireLogout"; import PageContainer from "@/components/PageContainer"; +import MyPage from "@/pages/MyPage"; import { UserProvider } from "@/context/UserContext"; const queryClient = new QueryClient(); @@ -62,6 +63,14 @@ const App = () => { } /> + + + + } + /> } /> { } /> - { } /> - \ No newline at end of file diff --git a/frontend/src/components/MyPageRollingpaperListPaging.tsx b/frontend/src/components/MyPageRollingpaperListPaging.tsx new file mode 100644 index 00000000..99adb859 --- /dev/null +++ b/frontend/src/components/MyPageRollingpaperListPaging.tsx @@ -0,0 +1,61 @@ +import React, { Dispatch, SetStateAction } from "react"; +import styled from "@emotion/styled"; + +import ReceivedRollingpaperCard from "@/components/ReceivedRollingpaperCard"; +import Paging from "@/components/Paging"; + +interface MyPageRollingpaper { + id: number; + title: string; + teamId: number; + teamName: string; +} + +interface MyPageRollingpaperListPaging { + rollingpapers: MyPageRollingpaper[]; + currentPage: number; + maxPage: number; + setCurrentPage: Dispatch>; +} + +const MyPageRollingpaperListPaging = ({ + rollingpapers, + currentPage, + maxPage, + setCurrentPage, +}: MyPageRollingpaperListPaging) => { + return ( + <> + + {rollingpapers.map(({ title, teamName, id }) => ( +
  • + +
  • + ))} +
    + + + + + ); +}; + +const StyledRollingpaperList = styled.ul` + display: flex; + flex-direction: column; + + margin-top: 16px; + gap: 8px; +`; + +const StyledPaging = styled.div` + padding: 20px 0 20px 0; + display: flex; + justify-content: center; +`; + +export default MyPageRollingpaperListPaging; diff --git a/frontend/src/components/MyPageTab.tsx b/frontend/src/components/MyPageTab.tsx new file mode 100644 index 00000000..b685009d --- /dev/null +++ b/frontend/src/components/MyPageTab.tsx @@ -0,0 +1,50 @@ +import React from "react"; +import styled from "@emotion/styled"; + +interface MyPageTabProp { + number: number; + text: string; + activate?: boolean; + onClick: React.MouseEventHandler; +} + +type StyledMyPageTabPropProps = Pick; + +const MyPageTab = ({ number, text, activate, onClick }: MyPageTabProp) => { + return ( + + {number} + {text} + + ); +}; + +const StyledCounter = styled.div` + display: flex; + flex-direction: column; + + cursor: pointer; +`; + +const StyledNumber = styled.div` + margin-bottom: 8px; + + font-size: 24px; + font-weight: 600; + + color: ${(props) => + props.activate + ? `${props.theme.colors.SKY_BLUE_300}` + : `${props.theme.colors.GRAY_400}`}; +`; + +const StyledText = styled.div` + font-size: 14px; + + color: ${(props) => + props.activate + ? `${props.theme.colors.BLACK}` + : `${props.theme.colors.GRAY_400}`}; +`; + +export default MyPageTab; diff --git a/frontend/src/components/MyPageWrittenMessageListPaging.tsx b/frontend/src/components/MyPageWrittenMessageListPaging.tsx new file mode 100644 index 00000000..5b0ee11e --- /dev/null +++ b/frontend/src/components/MyPageWrittenMessageListPaging.tsx @@ -0,0 +1,71 @@ +import React, { Dispatch, SetStateAction } from "react"; +import styled from "@emotion/styled"; + +import Paging from "@/components/Paging"; +import WrittenMessageCard from "@/components/WrittenMessageCard"; + +interface WrittenMessage { + id: number; + rollingpaperId: number; + teamId: number; + rollingpaperTitle: string; + to: string; + team: string; + content: string; + color: string; +} + +interface MyPageWrittenMessageListPagingProp { + messages: WrittenMessage[]; + currentPage: number; + maxPage: number; + setCurrentPage: Dispatch>; +} + +const MyPageWrittenMessageListPaging = ({ + messages, + currentPage, + maxPage, + setCurrentPage, +}: MyPageWrittenMessageListPagingProp) => { + return ( + <> + + {messages.map(({ id, rollingpaperTitle, to, team, content, color }) => ( +
  • + +
  • + ))} +
    + + + + + ); +}; + +const StyledMessageList = styled.ul` + display: flex; + flex-direction: column; + + margin-top: 16px; + gap: 8px; +`; + +const StyledPaging = styled.div` + padding: 20px 0 20px 0; + display: flex; + justify-content: center; +`; + +export default MyPageWrittenMessageListPaging; diff --git a/frontend/src/components/Paging.tsx b/frontend/src/components/Paging.tsx new file mode 100644 index 00000000..b6fcdf49 --- /dev/null +++ b/frontend/src/components/Paging.tsx @@ -0,0 +1,120 @@ +import React, { Dispatch, SetStateAction } from "react"; +import styled from "@emotion/styled"; + +import IconButton from "@/components/IconButton"; + +import LeftIcon from "@/assets/icons/bx-chevron-left.svg"; +import RightIcon from "@/assets/icons/bx-chevron-right.svg"; + +interface PagingProp { + currentPage: number; + maxPage: number; + setCurrentPage: Dispatch>; +} + +type StyledPaging = { + isCurrent: boolean; +}; + +const Paging = ({ currentPage, maxPage, setCurrentPage }: PagingProp) => { + const handleNumberClick = (number: number) => { + setCurrentPage(number); + }; + + const handleMinusClick = () => { + if (currentPage <= 1) { + return; + } + setCurrentPage((prev) => prev - 1); + }; + + const handlePlusClick = () => { + if (currentPage >= maxPage) { + return; + } + setCurrentPage((prev) => prev + 1); + }; + + return ( + + + + + {currentPage < 3 || maxPage <= 5 ? ( + + {[...Array(maxPage > 5 ? 5 : maxPage).keys()].map((num) => ( + handleNumberClick(num + 1)} + > + {num + 1} + + ))} + + ) : currentPage > maxPage - 2 ? ( + + {[...Array(5).keys()].reverse().map((num) => ( + handleNumberClick(maxPage - num)} + > + {maxPage - num} + + ))} + + ) : ( + + {[...Array(5).keys()].map((num) => ( + handleNumberClick(currentPage - (2 - num))} + > + {currentPage - (2 - num)} + + ))} + + )} + + + + + ); +}; + +const StyledPaging = styled.div` + display: flex; + + svg { + font-size: 24px; + } +`; + +const StyledPageButtons = styled.div` + display: flex; +`; + +const StyledPage = styled.div` + width: 24px; + text-align: center; + line-height: 24px; + margin: 2px; + + color: ${(props) => + props.isCurrent ? props.theme.colors.WHITE : props.theme.colors.BLACK}; + + background-color: ${(props) => + props.isCurrent && props.theme.colors.SKY_BLUE_300}; + border-radius: 50%; + + cursor: pointer; + + &:hover { + background-color: ${(props) => + !props.isCurrent && props.theme.colors.SKY_BLUE_100}; + } +`; + +export default Paging; diff --git a/frontend/src/components/ReceivedRollingpaperCard.tsx b/frontend/src/components/ReceivedRollingpaperCard.tsx new file mode 100644 index 00000000..11a0c70c --- /dev/null +++ b/frontend/src/components/ReceivedRollingpaperCard.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import styled from "@emotion/styled"; + +interface ReceivedRollingpaperCardProps { + title: string; + teamName: string; +} + +const ReceivedRollingpaperCard = ({ + title, + teamName, +}: ReceivedRollingpaperCardProps) => { + return ( + + {title} + {teamName} + + ); +}; + +const StyledReceivedRollingpaperCard = styled.div` + display: flex; + justify-content: space-between; + + width: 100%; + border: 2px solid ${({ theme }) => theme.colors.SKY_BLUE_300}; + border-radius: 8px; + + padding: 30px 20px; + gap: 8px; + + cursor: pointer; + + &:hover { + border: 2px solid ${({ theme }) => theme.colors.SKY_BLUE_400}; + } +`; + +const StyledTitle = styled.div` + font-weight: 600; + font-size: 16px; +`; + +const StyledTeamName = styled.div` + font-size: 14px; +`; + +export default ReceivedRollingpaperCard; diff --git a/frontend/src/components/UserProfile.tsx b/frontend/src/components/UserProfile.tsx new file mode 100644 index 00000000..76fe924c --- /dev/null +++ b/frontend/src/components/UserProfile.tsx @@ -0,0 +1,109 @@ +import React, { useState, useContext } from "react"; +import styled from "@emotion/styled"; + +import IconButton from "@/components/IconButton"; + +import Pencil from "@/assets/icons/bx-pencil.svg"; +import LineButton from "@/components/LineButton"; +import UnderlineInput from "@/components/UnderlineInput"; + +import { deleteCookie } from "@/util/cookie"; +import { UserContext } from "@/context/UserContext"; +import { REGEX } from "@/constants"; +import { ValueOf } from "@/types"; + +const MODE = { + NORMAL: "normal", + EDIT: "edit", +} as const; +interface UserProfileProp { + name: string; + email: string; +} + +type UserProfileMode = ValueOf; + +const UserProfile = ({ name, email }: UserProfileProp) => { + const [mode, setMode] = useState(MODE.NORMAL); + const [editName, setEditName] = useState(name); + + const { setIsLoggedIn } = useContext(UserContext); + + const handleButtonClick = () => { + if (mode === MODE.NORMAL) { + deleteCookie("accessToken"); + setIsLoggedIn(false); + } + }; + + return ( + + {mode === MODE.NORMAL ? ( + <> + + {name} + { + setMode(MODE.EDIT); + }} + > + + + + {email} + + ) : ( + + + + )} + + {mode === MODE.NORMAL ? "로그아웃" : "완료"} + + + ); +}; + +const StyledProfile = styled.div` + margin: 10px 0 40px 10px; +`; + +const StyledNormal = styled.div` + display: flex; + justify-content: space-between; + + width: 160px; + + svg { + font-size: 20px; + } +`; + +const StyledEdit = styled.div` + display: flex; + justify-content: space-between; + + font-size: 14px; + + input { + width: 160px; + font-size: 24px; + } +`; + +const StyledName = styled.div` + font-weight: 600; + font-size: 24px; +`; + +const StyledEmail = styled.div` + color: ${({ theme }) => theme.colors.GRAY_700}; + margin-bottom: 12px; +`; + +export default UserProfile; diff --git a/frontend/src/components/WrittenMessageCard.tsx b/frontend/src/components/WrittenMessageCard.tsx new file mode 100644 index 00000000..5d18e132 --- /dev/null +++ b/frontend/src/components/WrittenMessageCard.tsx @@ -0,0 +1,72 @@ +import React from "react"; +import styled from "@emotion/styled"; + +interface WrittenMessageCardProp { + rollingpaperTitle: string; + to: string; + team: string; + content: string; + color: string; +} + +type StyledMessageProp = Pick; + +const WrittenMessageCard = ({ + rollingpaperTitle, + to, + team, + content, + color, +}: WrittenMessageCardProp) => { + return ( + + {rollingpaperTitle} + + To. {to} + ({team}) + + {content} + + ); +}; + +const StyledMessage = styled.div` + display: flex; + flex-direction: column; + + gap: 4px; + + padding: 16px; + background-color: ${(props) => `${props.color}AB`}; + + border-radius: 4px; + height: 125px; + + cursor: pointer; + + &:hover { + background-color: ${(props) => props.color}; + } +`; + +const StyledTitle = styled.div` + font-size: 16px; + font-weight: 600; +`; + +const StyledTo = styled.div` + font-size: 12px; +`; + +const StyledContent = styled.div` + margin-top: 4px; + + font-size: 14px; + + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +`; + +export default WrittenMessageCard; diff --git a/frontend/src/pages/MyPage.tsx b/frontend/src/pages/MyPage.tsx new file mode 100644 index 00000000..63dfc1cd --- /dev/null +++ b/frontend/src/pages/MyPage.tsx @@ -0,0 +1,139 @@ +import React, { useState } from "react"; +import styled from "@emotion/styled"; + +import MyPageTab from "@/components/MyPageTab"; +import UserProfile from "@/components/UserProfile"; +import MyPageRollingpaperListPaging from "@/components/MyPageRollingpaperListPaging"; +import MyPageWrittenMessageListPaging from "@/components/MyPageWrittenMessageListPaging"; + +import { ValueOf } from "@/types"; + +const rollingpapers = [ + { id: 1, title: "소피아 생일 축하해", teamId: 1, teamName: "우테코 4기" }, + { id: 2, title: "소피아 생일 축하해", teamId: 1, teamName: "우테코 4기" }, + { id: 3, title: "소피아 생일 축하해", teamId: 1, teamName: "우테코 4기" }, + { id: 4, title: "소피아 생일 축하해", teamId: 1, teamName: "우테코 4기" }, + { id: 5, title: "소피아 생일 축하해", teamId: 1, teamName: "우테코 4기" }, +]; + +const messages = [ + { + id: 1, + rollingpaperId: 1, + teamId: 1, + rollingpaperTitle: "소피아의 생일을 축하해", + to: "소피아", + team: "우테코 4기", + content: + "소피아야 생일 축하해~ 축카추카추 소피아야 생일 축하해~ 축카추카추", + color: "#C5FF98", + }, + { + id: 2, + rollingpaperId: 1, + teamId: 1, + rollingpaperTitle: "소피아의 생일을 축하해", + to: "소피아", + team: "우테코 4기", + content: + "소피아야 생일 축하해~ 축카추카추 소피아야 생일 축하해~ 축카추카추", + color: "#C5FF98", + }, + { + id: 3, + rollingpaperId: 1, + teamId: 1, + rollingpaperTitle: "소피아의 생일을 축하해", + to: "소피아", + team: "우테코 4기", + content: + "소피아야 생일 축하해~ 축카추카추 소피아야 생일 축하해~ 축카추카추", + color: "#FF8181", + }, + { + id: 4, + rollingpaperId: 1, + teamId: 1, + rollingpaperTitle: "소피아의 생일을 축하해", + to: "소피아", + team: "우테코 4기", + content: + "소피아야 생일 축하해~ 축카추카추 소피아야 생일 축하해~ 축카추카추", + color: "#C5FF98", + }, + { + id: 5, + rollingpaperId: 1, + teamId: 1, + rollingpaperTitle: "소피아의 생일을 축하해", + to: "소피아", + team: "우테코 4기", + content: + "소피아야 생일 축하해~ 축카추카추 소피아야 생일 축하해~ 축카추카추", + color: "#C5FF98", + }, +]; + +const TAB = { + RECEIVED_PAPER: "received_paper", + SENT_MESSAGE: "sent_message", +} as const; + +type TabMode = ValueOf; + +const MyPage = () => { + const [tab, setTab] = useState(TAB.RECEIVED_PAPER); + const [receivedCurrentPage, setReceivedCurrentPage] = useState(1); + const [writtenCurrentPage, setWrittenCurrentPage] = useState(1); + + return ( + <> + + + { + setTab(TAB.RECEIVED_PAPER); + }} + /> + { + setTab(TAB.SENT_MESSAGE); + }} + /> + + {tab === TAB.RECEIVED_PAPER ? ( + + ) : ( + + )} + + ); +}; + +const StyledTabs = styled.div` + display: flex; + + border-top: 1px solid ${({ theme }) => theme.colors.GRAY_400}; + border-bottom: 1px solid ${({ theme }) => theme.colors.GRAY_400}; + + padding: 12px; + gap: 20px; +`; + +export default MyPage; diff --git a/frontend/src/stories/IconButton.stories.jsx b/frontend/src/stories/IconButton.stories.jsx index e8cb10b7..391603eb 100644 --- a/frontend/src/stories/IconButton.stories.jsx +++ b/frontend/src/stories/IconButton.stories.jsx @@ -1,7 +1,7 @@ import React from "react"; import IconButton from "@components/IconButton"; -import PencilIcon from "@/assets/images/bx-pencil.svg"; +import PencilIcon from "@/assets/icons/bx-pencil.svg"; export default { component: IconButton, diff --git a/frontend/src/stories/MyPageTab.jsx b/frontend/src/stories/MyPageTab.jsx new file mode 100644 index 00000000..52c42d27 --- /dev/null +++ b/frontend/src/stories/MyPageTab.jsx @@ -0,0 +1,23 @@ +import React from "react"; +import MyPageTab from "@/components/MyPageTab"; + +export default { + component: MyPageTab, + title: "MyPageTab", +}; + +const Template = (args) => ; + +export const Disabled = Template.bind({}); +Disabled.args = { + number: 10, + text: "받은 롤링페이퍼", + activate: false, +}; + +export const Activate = Template.bind({}); +Activate.args = { + number: 10, + text: "받은 롤링페이퍼", + activate: true, +}; diff --git a/frontend/src/stories/Paging.stories.jsx b/frontend/src/stories/Paging.stories.jsx new file mode 100644 index 00000000..539cb2b5 --- /dev/null +++ b/frontend/src/stories/Paging.stories.jsx @@ -0,0 +1,12 @@ +import React from "react"; +import Paging from "@/components/Paging"; + +export default { + component: Paging, + title: "Paging", +}; + +const Template = (args) => ; + +export const Default = Template.bind({}); +Default.args = { currentPage: 3, maxPage: 10 }; diff --git a/frontend/src/stories/ReceivedRollingpaperCard.stories.jsx b/frontend/src/stories/ReceivedRollingpaperCard.stories.jsx new file mode 100644 index 00000000..c00ba6de --- /dev/null +++ b/frontend/src/stories/ReceivedRollingpaperCard.stories.jsx @@ -0,0 +1,14 @@ +import React from "react"; +import ReceivedRollingpaperCard from "@/components/ReceivedRollingpaperCard"; + +export default { + component: ReceivedRollingpaperCard, + title: "ReceivedRollingpaperCard", +}; + +const Template = (args) => ( + +); + +export const Default = Template.bind({}); +Default.args = { title: "소피아의 생일을 축하해", teamName: "우테코 4기" }; diff --git a/frontend/src/stories/UserProfile.stories.jsx b/frontend/src/stories/UserProfile.stories.jsx new file mode 100644 index 00000000..8e4f4b27 --- /dev/null +++ b/frontend/src/stories/UserProfile.stories.jsx @@ -0,0 +1,15 @@ +import React from "react"; +import UserProfile from "@/components/UserProfile"; + +export default { + component: UserProfile, + title: "UserProfile", +}; + +const Template = (args) => ; + +export const Default = Template.bind({}); +Default.args = { + name: "도리", + email: "sunho620@naver.com", +}; diff --git a/frontend/src/stories/WrittenMessageCard.stories.jsx b/frontend/src/stories/WrittenMessageCard.stories.jsx new file mode 100644 index 00000000..6844f580 --- /dev/null +++ b/frontend/src/stories/WrittenMessageCard.stories.jsx @@ -0,0 +1,18 @@ +import React from "react"; +import WrittenMessageCard from "@/components/WrittenMessageCard"; + +export default { + component: WrittenMessageCard, + title: "WrittenMessageCard", +}; + +const Template = (args) => ; + +export const Default = Template.bind({}); +Default.args = { + rollingpaperTitle: "소피아의 생일을 축하해", + to: "소피아", + team: "우테코 4기", + content: "소피아야 생일 축하해~ 축카추카추 소피아야 생일 축하해~ 축카추카추", + color: "#C5FF98", +}; diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index bc769ef0..8d540a58 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -16,3 +16,5 @@ export type CustomError = { errorCode: number; message: string; }; + +export type ValueOf = T[keyof T]; From 29a2c9881d5701870be288d542928d1386ed68ad Mon Sep 17 00:00:00 2001 From: SunHo Park <67692759+prefer2@users.noreply.github.com> Date: Thu, 28 Jul 2022 20:54:17 +0900 Subject: [PATCH 07/62] =?UTF-8?q?feat:=20snackbar=20=EA=B5=AC=ED=98=84=20(?= =?UTF-8?q?#193)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: SnackbarContext 추가 * feat: Snackbar component 구현 * feat: global snackbar 추가 * feat: 회원가입 메시지 스낵바로 수정 * refactor: 불필요한 css 제거 * feat: useSnackbar 구현 * refactor: useSnackbar 사용 * refactor: 화살표 함수로 변경 --- frontend/public/index.html | 1 + frontend/src/App.tsx | 7 ++- frontend/src/components/Snackbar.tsx | 48 +++++++++++++++++++++ frontend/src/context/SnackbarContext.tsx | 55 ++++++++++++++++++++++++ frontend/src/index.tsx | 5 ++- frontend/src/pages/SignUpPage.tsx | 4 +- 6 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/Snackbar.tsx create mode 100644 frontend/src/context/SnackbarContext.tsx diff --git a/frontend/public/index.html b/frontend/public/index.html index cae9c876..0a5f9d4d 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -7,5 +7,6 @@
    +
    diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 851af719..e5b0a1ef 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useContext } from "react"; import { Global, ThemeProvider } from "@emotion/react"; import { Routes, Route } from "react-router-dom"; import { QueryClient, QueryClientProvider } from "react-query"; @@ -26,10 +26,14 @@ import RequireLogout from "./components/RequireLogout"; import PageContainer from "@/components/PageContainer"; import MyPage from "@/pages/MyPage"; import { UserProvider } from "@/context/UserContext"; +import { useSnackbar } from "@/context/SnackbarContext"; +import Snackbar from "@/components/Snackbar"; const queryClient = new QueryClient(); const App = () => { + const { isOpened } = useSnackbar(); + return ( @@ -138,6 +142,7 @@ const App = () => { } /> + {isOpened && } diff --git a/frontend/src/components/Snackbar.tsx b/frontend/src/components/Snackbar.tsx new file mode 100644 index 00000000..5313d2d9 --- /dev/null +++ b/frontend/src/components/Snackbar.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import ReactDom from "react-dom"; +import styled from "@emotion/styled"; +import { keyframes } from "@emotion/react"; + +import { useSnackbar } from "@/context/SnackbarContext"; + +const Snackbar = () => { + const { message } = useSnackbar(); + + return ReactDom.createPortal( + {message}, + document.getElementById("snackbar__root")! + ); +}; + +const fadein = keyframes` + from { + bottom: 0; + opacity: 0; + } + to { + bottom: 20px; + opacity: 1; + } +`; + +const StyledSnackbar = styled.div` + display: flex; + justify-content: center; + align-items: center; + position: fixed; + + bottom: 20px; + left: 50%; + margin-left: -125px; + padding: 8px; + + width: 250px; + + background-color: ${({ theme }) => theme.colors.GRAY_700}; + + color: ${({ theme }) => theme.colors.WHITE}; + + animation: ${fadein} 0.5s; +`; + +export default Snackbar; diff --git a/frontend/src/context/SnackbarContext.tsx b/frontend/src/context/SnackbarContext.tsx new file mode 100644 index 00000000..9646e1ee --- /dev/null +++ b/frontend/src/context/SnackbarContext.tsx @@ -0,0 +1,55 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +import { useState, createContext, PropsWithChildren, useContext } from "react"; + +interface SnackbarContextType { + openSnackbar: (message: string) => void; + closeSnackbar: () => void; + message: string; + isOpened: boolean; +} + +const SnackbarContext = createContext({ + openSnackbar: () => {}, + closeSnackbar: () => {}, + message: "", + isOpened: true, +}); + +const SnackbarProvider = ({ children }: PropsWithChildren) => { + const [isOpened, setIsOpened] = useState(false); + const [message, setMessage] = useState(""); + + let timer: NodeJS.Timeout; + + const openSnackbar = (message: string) => { + setIsOpened(true); + setMessage(message); + + timer = setTimeout(() => { + closeSnackbar(); + }, 3000); + }; + + const closeSnackbar = () => { + clearTimeout(timer); + setIsOpened(false); + }; + + const value = { openSnackbar, closeSnackbar, message, isOpened }; + + return ( + + {children} + + ); +}; + +const useSnackbar = () => { + const context = useContext(SnackbarContext); + if (context === undefined) { + throw new Error("useSnackbar는 SnackbarContext가 필요합니다"); + } + return context; +}; + +export { useSnackbar, SnackbarProvider }; diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 5b7b95f1..ffeb05e4 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -2,6 +2,7 @@ import React from "react"; import ReactDOM from "react-dom/client"; import { BrowserRouter } from "react-router-dom"; +import { SnackbarProvider } from "@/context/SnackbarContext"; import App from "./App"; if (process.env.NODE_ENV === "development") { @@ -17,7 +18,9 @@ const root = ReactDOM.createRoot( root.render( - + + + ); diff --git a/frontend/src/pages/SignUpPage.tsx b/frontend/src/pages/SignUpPage.tsx index 5eed2d6b..52dead0c 100644 --- a/frontend/src/pages/SignUpPage.tsx +++ b/frontend/src/pages/SignUpPage.tsx @@ -12,6 +12,7 @@ import Button from "@/components/Button"; import { REGEX } from "@/constants"; import { CustomError } from "@/types"; +import { useSnackbar } from "@/context/SnackbarContext"; type SignUpMemberInfo = { email: string; @@ -21,6 +22,7 @@ type SignUpMemberInfo = { const SignUpPage = () => { const navigate = useNavigate(); + const { openSnackbar } = useSnackbar(); const [email, setEmail] = useState(""); const [username, setUsername] = useState(""); @@ -39,7 +41,7 @@ const SignUpPage = () => { }, { onSuccess: () => { - alert("회원가입 성공!"); + openSnackbar("회원가입 성공!"); navigate("/login"); }, onError: (error) => { From fdc957c5acce33698c456afa6bd628a74d6a9adf Mon Sep 17 00:00:00 2001 From: Soyi Jeon Date: Thu, 28 Jul 2022 21:00:20 +0900 Subject: [PATCH 08/62] =?UTF-8?q?feat:=20=EB=A1=A4=EB=A7=81=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=ED=8D=BC=20=ED=8E=98=EC=9D=B4=EC=A7=80=20UI=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=83=88=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=EC=9E=91=EC=84=B1=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20UI=20=EA=B5=AC=ED=98=84=20(#194)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: 메시지 더미 데이터 수정 * feat: RollingpaperMessage 컴포넌트 스타일 수정 StyledMessageContent height 제거 StyledMessage height 제거 및 min-height 추가 * feat: LetterPaper 내 MessageList 정렬 방식 변경 * feat: MessageTextArea 컴포넌트 구현 * feat: MessageTextArea 컴포넌트 구현 * feat: MessageColorPicker 컴포넌트 구현 * feat: MessageForm 컴포넌트 구현 * feat: MessateTextArea 스타일 추가 * feat: MessageForm에 Submit, Cancel Button 추가 * feat: LetterPaper에 새 메시지 작성 폼 MessageForm 추가 - 새 메시지 작성 상태 writeNewMessage 추가 * refactor: slicedMessageLists 상태 초깃값 생성 로직 수정 * refactor: 메서드의 사용하지 않는 인자 제거 handleMessageWriteButtonClick의 e 제거 * feat: MessageColorPicker 스타일 코드 수정 - StyledRadio의 불필요한 backgroundColor default 값 제거 * refactor: newContent 변수 분리 - handleTextAreaChange의 e.target.value를 newContent 변수로 정의 * refactor: 불필요한 style 코드 삭제 CircleButton color 속성 제거 * feat: MessageTextArea 스타일 코드 수정 - StyledMessageContainer의 불필요한 backgroundColor default 값 제거 * refactor: 불필요한 변수 제거 - handleTextAreaChange의 newContent 제거 --- frontend/src/assets/icons/bx-check.svg | 1 + frontend/src/components/LetterPaper.tsx | 91 +++++++++--- .../src/components/MessageColorPicker.tsx | 109 ++++++++++++++ frontend/src/components/MessageForm.tsx | 140 ++++++++++++++++++ frontend/src/components/MessageTextArea.tsx | 97 ++++++++++++ .../src/components/RollingpaperMessage.tsx | 13 +- frontend/src/mocks/dummy/rollingpapers.json | 12 +- .../stories/MessageColorPicker.stories.jsx | 24 +++ frontend/src/stories/MessageForm.stories.jsx | 12 ++ .../src/stories/MessageTextArea.stories.jsx | 15 ++ frontend/src/util/index.ts | 11 ++ 11 files changed, 490 insertions(+), 35 deletions(-) create mode 100644 frontend/src/assets/icons/bx-check.svg create mode 100644 frontend/src/components/MessageColorPicker.tsx create mode 100644 frontend/src/components/MessageForm.tsx create mode 100644 frontend/src/components/MessageTextArea.tsx create mode 100644 frontend/src/stories/MessageColorPicker.stories.jsx create mode 100644 frontend/src/stories/MessageForm.stories.jsx create mode 100644 frontend/src/stories/MessageTextArea.stories.jsx create mode 100644 frontend/src/util/index.ts diff --git a/frontend/src/assets/icons/bx-check.svg b/frontend/src/assets/icons/bx-check.svg new file mode 100644 index 00000000..b21b4a71 --- /dev/null +++ b/frontend/src/assets/icons/bx-check.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/components/LetterPaper.tsx b/frontend/src/components/LetterPaper.tsx index 59bc24d0..85ea3f50 100644 --- a/frontend/src/components/LetterPaper.tsx +++ b/frontend/src/components/LetterPaper.tsx @@ -1,12 +1,14 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; +import { Link } from "react-router-dom"; import styled from "@emotion/styled"; -import { useNavigate, Link } from "react-router-dom"; import IconButton from "@components/IconButton"; +import MessageForm from "@/components/MessageForm"; import RollingpaperMessage from "@components/RollingpaperMessage"; import { Message } from "@/types"; import PencilIcon from "@/assets/icons/bx-pencil.svg"; +import { divideArrayByIndexRemainder } from "@/util"; interface LetterPaperProp { to: string; @@ -14,33 +16,76 @@ interface LetterPaperProp { } const LetterPaper = ({ to, messageList }: LetterPaperProp) => { - const navigate = useNavigate(); + const [writeNewMessage, setWriteNewMessage] = useState(false); + const [slicedMessageLists, setSlicedMessageLists] = useState( + Array.from(Array(4), () => []) + ); const handleMessageWriteButtonClick: React.MouseEventHandler< HTMLButtonElement - > = (e) => { - e.preventDefault(); - navigate(`message/new`); + > = () => { + setWriteNewMessage(true); + }; + + const updateSlicedMessageListByWindowWidth = () => { + const width = window.innerWidth; + + let newSlicedMessageList; + if (width < 960) { + newSlicedMessageList = divideArrayByIndexRemainder(messageList, 2); + } else if (width < 1280) { + newSlicedMessageList = divideArrayByIndexRemainder(messageList, 3); + } else { + newSlicedMessageList = divideArrayByIndexRemainder(messageList, 4); + } + + setSlicedMessageLists(newSlicedMessageList); + }; + + const hideMessageForm = () => { + setWriteNewMessage(false); }; + useEffect(() => { + updateSlicedMessageListByWindowWidth(); + }, [messageList]); + + useEffect(() => { + window.addEventListener("resize", updateSlicedMessageListByWindowWidth); + return () => + window.removeEventListener( + "resize", + updateSlicedMessageListByWindowWidth + ); + }, []); + return ( To. {to} - - - + {!writeNewMessage && ( + + + + )} - - {messageList.map((message) => ( - - - + + {slicedMessageLists.map((messageList, index) => ( + + {index === 0 && writeNewMessage && ( + + )} + {messageList.map((message) => ( + + + + ))} + ))} - + ); }; @@ -69,6 +114,16 @@ const StyledTo = styled.h3` `; const StyledMessageList = styled.div` + display: flex; + flex-direction: column; + gap: 20px; + + a { + display: inline-block; + } +`; + +const StyledSlicedMessageLists = styled.div` display: grid; grid-template-columns: repeat(2, 1fr); grid-row-gap: 20px; diff --git a/frontend/src/components/MessageColorPicker.tsx b/frontend/src/components/MessageColorPicker.tsx new file mode 100644 index 00000000..1e200d57 --- /dev/null +++ b/frontend/src/components/MessageColorPicker.tsx @@ -0,0 +1,109 @@ +import React, { useState, SetStateAction } from "react"; +import styled from "@emotion/styled"; + +interface Radio { + id: number; + value: string; +} + +interface ColorPickerProps extends React.InputHTMLAttributes { + radios: Radio[]; + initialSelectedId: number; + onClickRadio: React.Dispatch>; +} + +interface StyledRadioProps { + backgroundColor?: string; +} + +const MessageColorPicker = ({ + radios, + initialSelectedId, + onClickRadio, +}: ColorPickerProps) => { + const [selectedItemId, setSelectedItemId] = useState( + initialSelectedId + ); + + const handleRadioChange = (key: number, value: string) => { + setSelectedItemId(key); + onClickRadio(value); + }; + + return ( + + {radios.map((radio) => { + return ( + + ); + })} + + ); +}; + +const StyledColorPickerContainer = styled.div` + position: absolute; + left: 130px; + margin-left: 4px; + + display: flex; + flex-direction: column; + gap: 10px; + + padding: 10px 4px; + + border-radius: 4px; + background-color: ${({ theme }) => theme.colors.GRAY_700}; + + @media only screen and (min-width: 600px) { + left: 180px; + margin-left: 8px; + } +`; + +const StyledRadio = styled.div` + display: flex; + align-items: center; + justify-content: center; + + width: 20px; + height: 20px; + border-radius: 50%; + + font-size: 25px; + background-color: ${(props) => props.backgroundColor}; + + cursor: pointer; + + @media only screen and (min-width: 600px) { + width: 28px; + height: 28px; + } +`; + +const StyledInput = styled.input` + position: absolute; + overflow: hidden; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + border: 0; + clip: rect(0, 0, 0, 0); + + &:checked + div { + border: 3px solid ${({ theme }) => theme.colors.PURPLE_400}; + } +`; + +export default MessageColorPicker; diff --git a/frontend/src/components/MessageForm.tsx b/frontend/src/components/MessageForm.tsx new file mode 100644 index 00000000..b6cca0f5 --- /dev/null +++ b/frontend/src/components/MessageForm.tsx @@ -0,0 +1,140 @@ +import React, { useState } from "react"; +import styled from "@emotion/styled"; +import MessageTextArea from "./MessageTextArea"; +import MessageColorPicker from "./MessageColorPicker"; + +import CheckIcon from "@/assets/icons/bx-check.svg"; +import TrashIcon from "@/assets/icons/bx-trash.svg"; + +const colors = [ + { id: 1, value: "#C5FF98" }, + { id: 2, value: "#FF8181" }, + { id: 3, value: "#FFF598" }, + { id: 4, value: "#98DAFF" }, + { id: 5, value: "#98A2FF" }, + { id: 6, value: "#FF98D0" }, +]; + +type MessageFormProps = { + hideMessageForm: () => void; +}; + +type ButtonAttributes = React.ButtonHTMLAttributes; + +const MessageSubmitButton = ({ onClick }: ButtonAttributes) => { + return ( + + + + ); +}; + +const MessageCancelButton = ({ onClick }: ButtonAttributes) => { + return ( + + + + ); +}; + +export const MessageForm = ({ hideMessageForm }: MessageFormProps) => { + const [content, setContent] = useState(""); + const [color, setColor] = useState(colors[0].value); + + const handleTextAreaChange: React.ChangeEventHandler = ( + e + ) => { + if (e.target.value.length > 500) { + return; + } + setContent(e.target.value); + }; + + const handleSubmitButtonClick: React.MouseEventHandler = () => { + hideMessageForm(); + }; + + const handleCancelButtonClick: React.MouseEventHandler = () => { + hideMessageForm(); + }; + + return ( + <> + + + + + + + + + + + ); +}; + +const StyledMessageForm = styled.form` + position: relative; + display: flex; + flex-direction: column; +`; + +const Background = styled.div` + position: absolute; + top: 0; + left: 0; + + width: 100vw; + height: 100vh; + + background: transparent; +`; + +const IconButtonContainer = styled.div` + display: flex; + flex-direction: row; + align-self: center; + gap: 20px; + + margin-top: 4px; + + @media only screen and (min-width: 600px) { + margin-top: 8px; + } +`; + +const CircleButton = styled.button` + width: 32px; + height: 32px; + + font-size: 18px; + + border: none; + border-radius: 50%; + background-color: ${({ theme }) => theme.colors.GRAY_700}; + fill: ${({ theme }) => theme.colors.GRAY_200}; + + &:hover { + border: 2px solid ${({ theme }) => theme.colors.GRAY_700}; + background-color: ${({ theme }) => theme.colors.GRAY_300}; + fill: ${({ theme }) => theme.colors.GRAY_800}; + } + + @media only screen and (min-width: 600px) { + width: 36px; + height: 36px; + + font-size: 20px; + } +`; + +export default MessageForm; diff --git a/frontend/src/components/MessageTextArea.tsx b/frontend/src/components/MessageTextArea.tsx new file mode 100644 index 00000000..31c5b5ca --- /dev/null +++ b/frontend/src/components/MessageTextArea.tsx @@ -0,0 +1,97 @@ +import React, { useRef } from "react"; +import styled from "@emotion/styled"; + +type MessageTextAreaProps = { + backgroundColor: string; +} & React.TextareaHTMLAttributes; + +type StyledMessageContainerProps = { + backgroundColor: string; +}; + +export const MessageTextArea = ({ + value, + onChange, + children, + placeholder, + backgroundColor, +}: MessageTextAreaProps) => { + const textareaRef = useRef(null); + + const handleTextAreaChange: React.ChangeEventHandler = ( + e + ) => { + if (onChange) { + onChange(e); + } + + const textarea = textareaRef.current as unknown as HTMLTextAreaElement; + textarea.style.height = "auto"; + textarea.style.height = `${textarea.scrollHeight}px`; + }; + + return ( + + + {children} + + {(value as string).length}/500 + + ); +}; + +const StyledMessageContainer = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + + width: 130px; + min-height: 130px; + padding: 20px 15px 10px; + + font-size: 14px; + line-height: 16px; + + border: 2px solid ${({ theme }) => theme.colors.GRAY_700}; + background-color: ${({ backgroundColor }) => backgroundColor}; + + @media only screen and (min-width: 600px) { + width: 180px; + min-height: 180px; + + font-size: 14px; + line-height: 18px; + } +`; + +const StyledTextArea = styled.textarea` + width: 100%; + height: 100%; + + border: none; + background-color: transparent; + + resize: none; + + font-size: inherit; + line-height: inherit; + + &:focus { + outline: none; + } +`; + +const StyledTextLength = styled.div` + display: inline; + align-self: flex-end; + + color: ${({ theme }) => theme.colors.GRAY_600}; +`; + +export default MessageTextArea; diff --git a/frontend/src/components/RollingpaperMessage.tsx b/frontend/src/components/RollingpaperMessage.tsx index b23f237b..4ee21ca8 100644 --- a/frontend/src/components/RollingpaperMessage.tsx +++ b/frontend/src/components/RollingpaperMessage.tsx @@ -22,35 +22,26 @@ const StyledMessage = styled.div` white-space: pre-line; width: 130px; - height: 130px; + min-height: 130px; padding: 20px 15px 10px; background-color: ${({ theme }) => theme.colors.YELLOW_300}; @media only screen and (min-width: 600px) { width: 180px; - height: 180px; + min-height: 180px; } `; const StyledMessageContent = styled.div` - height: 64px; overflow: hidden; font-size: 14px; line-height: 16px; - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 4; - @media only screen and (min-width: 600px) { - height: 126px; - font-size: 14px; line-height: 18px; - - -webkit-line-clamp: 7; } `; diff --git a/frontend/src/mocks/dummy/rollingpapers.json b/frontend/src/mocks/dummy/rollingpapers.json index 16491ef3..ab5fded7 100644 --- a/frontend/src/mocks/dummy/rollingpapers.json +++ b/frontend/src/mocks/dummy/rollingpapers.json @@ -13,7 +13,7 @@ }, { "id": 2, - "content": "소피아\n\n\n생일축하해✨🚀\n소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생", + "content": "소피아\n\n\n생일축하해✨🚀\n소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀\n소피아\n\n\n생일축하해✨🚀\n소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀\n소피아\n\n\n생일축하해✨🚀\n소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀\n소피아\n\n\n생일축하해✨🚀\n소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀\n소피아\n\n\n생일축하해✨🚀\n소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀\n", "from": "도리", "authorId": 123 }, @@ -25,7 +25,7 @@ }, { "id": 4, - "content": "소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀", + "content": "소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀", "from": "도리", "authorId": 123 }, @@ -43,19 +43,19 @@ }, { "id": 7, - "content": "소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일아", + "content": "소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일축하해 소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일축하해소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일축하해", "from": "도리", "authorId": 123 }, { "id": 8, - "content": "소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀", + "content": "소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀", "from": "도리", "authorId": 123 }, { "id": 9, - "content": "소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일아", + "content": "소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일축하해 소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일축하해", "from": "도리", "authorId": 123 }, @@ -80,7 +80,7 @@ }, { "id": 2, - "content": "소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피", + "content": "소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아", "from": "도리", "authorId": 123 } diff --git a/frontend/src/stories/MessageColorPicker.stories.jsx b/frontend/src/stories/MessageColorPicker.stories.jsx new file mode 100644 index 00000000..6e64117c --- /dev/null +++ b/frontend/src/stories/MessageColorPicker.stories.jsx @@ -0,0 +1,24 @@ +import React from "react"; +import MessageColorPicker from "@/components/MessageColorPicker"; + +const colors = [ + { id: 1, value: "#C5FF98" }, + { id: 2, value: "#FF8181" }, + { id: 3, value: "#FFF598" }, + { id: 4, value: "#98DAFF" }, + { id: 5, value: "#98A2FF" }, + { id: 6, value: "#FF98D0" }, +]; + +export default { + component: MessageColorPicker, + title: "MessageColorPicker", +}; + +const Template = (args) => ; + +export const Default = Template.bind({}); +Default.args = { + radios: colors, + initialSelectedId: colors[0].id, +}; diff --git a/frontend/src/stories/MessageForm.stories.jsx b/frontend/src/stories/MessageForm.stories.jsx new file mode 100644 index 00000000..286bed2a --- /dev/null +++ b/frontend/src/stories/MessageForm.stories.jsx @@ -0,0 +1,12 @@ +import React from "react"; +import MessageForm from "@/components/MessageForm"; + +export default { + component: MessageForm, + title: "MessageForm", +}; + +const Template = (args) => ; + +export const Default = Template.bind({}); +Default.args = {}; diff --git a/frontend/src/stories/MessageTextArea.stories.jsx b/frontend/src/stories/MessageTextArea.stories.jsx new file mode 100644 index 00000000..1c9d53fd --- /dev/null +++ b/frontend/src/stories/MessageTextArea.stories.jsx @@ -0,0 +1,15 @@ +import React from "react"; +import MessageTextArea from "@/components/MessageTextArea"; + +export default { + component: MessageTextArea, + title: "MessageTextArea", +}; + +const Template = (args) => ; + +export const Default = Template.bind({}); +Default.args = { + value: "테스트", + placeholder: "메시지를 입력해보세요!", +}; diff --git a/frontend/src/util/index.ts b/frontend/src/util/index.ts new file mode 100644 index 00000000..8e5fb9fe --- /dev/null +++ b/frontend/src/util/index.ts @@ -0,0 +1,11 @@ +const divideArrayByIndexRemainder = (array: any[], divisor: number) => { + const result: any[][] = Array.from({ length: divisor }, () => []); + + array.forEach((element, index) => { + result[index % divisor].push(element); + }); + + return result; +}; + +export { divideArrayByIndexRemainder }; From f02b05a549c4a8f1973c6a49ca2fc25d0eac55ee Mon Sep 17 00:00:00 2001 From: zero Date: Fri, 29 Jul 2022 17:30:19 +0900 Subject: [PATCH 09/62] =?UTF-8?q?feat:=20flyway=20v2=20=EC=A0=81=EC=9A=A9?= =?UTF-8?q?=20(#205)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: `BaseEntity`를 제외한 db 테이블 `flyway` 형상관리 적용 * chore: 개발 DB를 h2에서를 MYSQL로 마이그레이션 * refactor: mysql환경에서 가능하도록 변경 * feat: flyway v2 적용 * refactor: 불필요한 설정 제거 Co-authored-by: kth990303 --- backend/build.gradle | 3 +- .../src/main/resources/application-deploy.yml | 34 +++++++++---------- backend/src/main/resources/application.yml | 11 ++---- .../main/resources/db/migration/V1__init.sql | 20 +++++------ .../db/migration/V2_add_baseEntity.sql | 10 ++++++ .../naepyeon/acceptance/AcceptanceTest.java | 3 +- .../repository/MemberRepositoryTest.java | 1 + .../repository/MessageRepositoryTest.java | 1 + .../RollingpaperRepositoryTest.java | 1 + .../TeamParticipationRepositoryTest.java | 1 + .../repository/TeamRepositoryTest.java | 1 + backend/src/test/resources/application.yml | 29 ++++++++++++++++ 12 files changed, 76 insertions(+), 39 deletions(-) create mode 100644 backend/src/main/resources/db/migration/V2_add_baseEntity.sql create mode 100644 backend/src/test/resources/application.yml diff --git a/backend/build.gradle b/backend/build.gradle index 822e1679..d563dd27 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'org.springframework.boot' version '2.7.1' + id 'org.springframework.boot' version '2.6.9' id 'io.spring.dependency-management' version '1.0.11.RELEASE' id 'org.asciidoctor.convert' version '1.5.8' id 'java' @@ -27,7 +27,6 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.flywaydb:flyway-core:6.4.2' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2:1.4.200' runtimeOnly 'mysql:mysql-connector-java' diff --git a/backend/src/main/resources/application-deploy.yml b/backend/src/main/resources/application-deploy.yml index 6d5df243..f9857b41 100644 --- a/backend/src/main/resources/application-deploy.yml +++ b/backend/src/main/resources/application-deploy.yml @@ -1,33 +1,33 @@ spring: + datasource: + url: ${MYSQL_URL} + password: ${MYSQL_PASSWORD} + username: ${MYSQL_USERNAME} + driver-class-name: com.mysql.cj.jdbc.Driver + jpa: + hibernate: + ddl-auto: validate properties: hibernate: + format_sql: true + default_batch_fetch_size: 1000 jdbc.lob.non_contextual_creation: true - default_batch_fetch_size: '1000' dialect: org.hibernate.dialect.MySQL5Dialect - format_sql: 'true' - hibernate: - ddl-auto: validate - open-in-view: 'false' + open-in-view: false generate-ddl: false - datasource: - password: ${MYSQL_PASSWORD} - driver-class-name: com.mysql.cj.jdbc.Driver - username: ${MYSQL_USERNAME} - url: ${MYSQL_URL} - h2: - console: - enabled: 'true' flyway: enabled: true baseline-on-migrate: true + security: jwt: token: expire-length: ${JWT_EXPIRE_LENGTH} secret-key: ${JWT_SECRET_KEY} + logging: - level: - org: - hibernate: - SQL: info + level: + org: + hibernate: + SQL: info diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index ce05d282..798ab85c 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -1,12 +1,6 @@ spring: - h2: - console: - enabled: true - profiles: - active: local datasource: - # url: jdbc:h2:mem:testdb - url: jdbc:mysql://127.0.0.1:3306/naepyeon?serverTimezone=Asia/Seoul&character_set_server=utf8mb4&useUnicode=true + url: jdbc:mysql://localhost:3306/naepyeon?serverTimezone=Asia/Seoul&characterEncoding=UTF-8&useUnicode=true username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver @@ -17,9 +11,9 @@ spring: properties: hibernate: # show_sql: true - jdbc.lob.non_contextual_creation: true format_sql: true default_batch_fetch_size: 1000 #최적화 옵션 + jdbc.lob.non_contextual_creation: true dialect: org.hibernate.dialect.MySQL5Dialect open-in-view: false generate-ddl: false @@ -29,7 +23,6 @@ spring: logging.level: org.hibernate.SQL: debug -# org.hibernate.type: trace security: jwt: diff --git a/backend/src/main/resources/db/migration/V1__init.sql b/backend/src/main/resources/db/migration/V1__init.sql index 3bafa15b..973805cc 100644 --- a/backend/src/main/resources/db/migration/V1__init.sql +++ b/backend/src/main/resources/db/migration/V1__init.sql @@ -1,34 +1,34 @@ -create table member +CREATE TABLE IF NOT EXISTS member ( member_id bigint not null auto_increment, email varchar(255) not null, password varchar(255) not null, username varchar(20) not null, primary key (member_id) -) engine = InnoDB +) engine=InnoDB default charset utf8mb4; -create table message +CREATE TABLE IF NOT EXISTS message ( message_id bigint not null auto_increment, content varchar(500) not null, member_id bigint, rollingpaper_id bigint, primary key (message_id) -) engine = InnoDB +) engine=InnoDB default charset utf8mb4; -create table rollingpaper +CREATE TABLE IF NOT EXISTS rollingpaper ( rollingpaper_id bigint not null auto_increment, title varchar(20) not null, member_id bigint, team_id bigint not null, primary key (rollingpaper_id) -) engine = InnoDB +) engine=InnoDB default charset utf8mb4; -create table team +CREATE TABLE IF NOT EXISTS team ( team_id bigint not null auto_increment, color varchar(15) not null, @@ -36,17 +36,17 @@ create table team emoji varchar(255) not null, team_name varchar(20) not null, primary key (team_id) -) engine = InnoDB +) engine=InnoDB default charset utf8mb4; -create table team_member +CREATE TABLE IF NOT EXISTS team_member ( team_member_id bigint not null auto_increment, nickname varchar(20) not null, member_id bigint, team_id bigint, primary key (team_member_id) -) engine = InnoDB +) engine=InnoDB default charset utf8mb4; alter table member diff --git a/backend/src/main/resources/db/migration/V2_add_baseEntity.sql b/backend/src/main/resources/db/migration/V2_add_baseEntity.sql new file mode 100644 index 00000000..6211131b --- /dev/null +++ b/backend/src/main/resources/db/migration/V2_add_baseEntity.sql @@ -0,0 +1,10 @@ +ALTER TABLE member ADD COLUMN created_at datetime not null DEFAULT CURRENT_TIMESTAMP; +ALTER TABLE member ADD COLUMN last_modified_at datetime not null DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; +ALTER TABLE message ADD COLUMN created_at datetime not null DEFAULT CURRENT_TIMESTAMP; +ALTER TABLE message ADD COLUMN last_modified_at datetime not null DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; +ALTER TABLE rollingpaper ADD COLUMN created_at datetime not null DEFAULT CURRENT_TIMESTAMP; +ALTER TABLE rollingpaper ADD COLUMN last_modified_at datetime not null DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; +ALTER TABLE team ADD COLUMN created_at datetime not null DEFAULT CURRENT_TIMESTAMP; +ALTER TABLE team ADD COLUMN last_modified_at datetime not null DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; +ALTER TABLE team_member ADD COLUMN created_at datetime not null DEFAULT CURRENT_TIMESTAMP; +ALTER TABLE team_member ADD COLUMN last_modified_at datetime not null DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; \ No newline at end of file diff --git a/backend/src/test/java/com/woowacourse/naepyeon/acceptance/AcceptanceTest.java b/backend/src/test/java/com/woowacourse/naepyeon/acceptance/AcceptanceTest.java index 21666ec8..526c3b53 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/acceptance/AcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/acceptance/AcceptanceTest.java @@ -1,10 +1,11 @@ package com.woowacourse.naepyeon.acceptance; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import io.restassured.RestAssured; import org.junit.jupiter.api.BeforeEach; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.boot.web.server.LocalServerPort; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext.ClassMode; diff --git a/backend/src/test/java/com/woowacourse/naepyeon/repository/MemberRepositoryTest.java b/backend/src/test/java/com/woowacourse/naepyeon/repository/MemberRepositoryTest.java index 60d47e01..97241449 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/repository/MemberRepositoryTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/repository/MemberRepositoryTest.java @@ -69,6 +69,7 @@ void updateMemberWhen() { final Member member = new Member("alex", "alex@naepyeon.com", "abc12345"); final Long memberId = memberRepository.save(member); + em.flush(); member.changeUsername("kth990303"); em.flush(); diff --git a/backend/src/test/java/com/woowacourse/naepyeon/repository/MessageRepositoryTest.java b/backend/src/test/java/com/woowacourse/naepyeon/repository/MessageRepositoryTest.java index 91bbb113..b7401b51 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/repository/MessageRepositoryTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/repository/MessageRepositoryTest.java @@ -137,6 +137,7 @@ void updateMemberWhen() { final Message message = createMessage(); final Long messageId = messageRepository.save(message); + em.flush(); message.changeContent("updateupdate"); em.flush(); diff --git a/backend/src/test/java/com/woowacourse/naepyeon/repository/RollingpaperRepositoryTest.java b/backend/src/test/java/com/woowacourse/naepyeon/repository/RollingpaperRepositoryTest.java index 570c79a0..a2bd937c 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/repository/RollingpaperRepositoryTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/repository/RollingpaperRepositoryTest.java @@ -137,6 +137,7 @@ void updateMemberWhen() { final Rollingpaper rollingpaper = createRollingPaper(); final Long rollingpaperId = rollingpaperRepository.save(rollingpaper); + em.flush(); rollingpaper.changeTitle("updateupdate"); em.flush(); diff --git a/backend/src/test/java/com/woowacourse/naepyeon/repository/TeamParticipationRepositoryTest.java b/backend/src/test/java/com/woowacourse/naepyeon/repository/TeamParticipationRepositoryTest.java index 673234da..ce7a89e8 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/repository/TeamParticipationRepositoryTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/repository/TeamParticipationRepositoryTest.java @@ -184,6 +184,7 @@ void updateNickname() { final TeamParticipation teamParticipation = new TeamParticipation(team1, member1, "닉네임1"); final Long teamParticipationId = teamParticipationRepository.save(teamParticipation); + em.flush(); teamParticipationRepository.updateNickname(expected, member1.getId(), team1.getId()); em.flush(); diff --git a/backend/src/test/java/com/woowacourse/naepyeon/repository/TeamRepositoryTest.java b/backend/src/test/java/com/woowacourse/naepyeon/repository/TeamRepositoryTest.java index 0ac9f172..6ff4ee62 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/repository/TeamRepositoryTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/repository/TeamRepositoryTest.java @@ -102,6 +102,7 @@ void updateMemberWhen() { final Team team = new Team("woowacourse", "테스트 모임입니다.", "testEmoji", "#123456"); final Long teamId = teamRepository.save(team); + em.flush(); team.changeName("updateupdate"); em.flush(); diff --git a/backend/src/test/resources/application.yml b/backend/src/test/resources/application.yml new file mode 100644 index 00000000..e9e0819d --- /dev/null +++ b/backend/src/test/resources/application.yml @@ -0,0 +1,29 @@ +spring: + h2: + console: + enabled: true + profiles: + active: local + datasource: + url: jdbc:h2:mem:testdb + username: sa + password: + driver-class-name: org.h2.Driver + + jpa: + hibernate: + ddl-auto: create + properties: + hibernate: + format_sql: true + default_batch_fetch_size: 1000 #최적화 옵션 + open-in-view: false + +logging.level: + org.hibernate.SQL: debug + +security: + jwt: + token: + secret-key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjJ9.ih1aovtQShabQ7l0cINw4k1fagApg3qLWiB8Kt59Lno + expire-length: 3600000 \ No newline at end of file From b5b2e685b258432cadd0cf4c92a83c1c255325e9 Mon Sep 17 00:00:00 2001 From: zero Date: Fri, 29 Jul 2022 18:06:22 +0900 Subject: [PATCH 10/62] =?UTF-8?q?fix:=20=EC=88=98=EC=A0=95=EC=9D=BC?= =?UTF-8?q?=EC=9E=90,=20=EC=83=9D=EC=84=B1=EC=9D=BC=EC=9E=90=20=EB=B2=84?= =?UTF-8?q?=EA=B7=B8=EB=A5=BC=20=EC=88=98=EC=A0=95=ED=95=9C=EB=8B=A4.=20(#?= =?UTF-8?q?207)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: `BaseEntity`를 제외한 db 테이블 `flyway` 형상관리 적용 * chore: 개발 DB를 h2에서를 MYSQL로 마이그레이션 * refactor: mysql환경에서 가능하도록 변경 * feat: flyway v2 적용 * refactor: 불필요한 설정 제거 * fix: 수정일자, 생성일자 버그를 수정한다. * fix: 수정일자 버그를 수정한다. Co-authored-by: kth990303 --- .../woowacourse/naepyeon/domain/BaseEntity.java | 14 +++++++------- .../naepyeon/repository/MemberRepositoryTest.java | 5 +++-- .../naepyeon/repository/MessageRepositoryTest.java | 5 +++-- .../repository/RollingpaperRepositoryTest.java | 5 +++-- .../TeamParticipationRepositoryTest.java | 5 +++-- .../naepyeon/repository/TeamRepositoryTest.java | 5 +++-- 6 files changed, 22 insertions(+), 17 deletions(-) diff --git a/backend/src/main/java/com/woowacourse/naepyeon/domain/BaseEntity.java b/backend/src/main/java/com/woowacourse/naepyeon/domain/BaseEntity.java index 5384e755..4b9a1b4c 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/domain/BaseEntity.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/domain/BaseEntity.java @@ -14,11 +14,11 @@ @Getter abstract public class BaseEntity { -// @CreatedDate -// @Column(name = "created_at", nullable = false, updatable = false) -// private LocalDateTime createdDate; -// -// @LastModifiedDate -// @Column(name = "last_modified_at", nullable = false) -// private LocalDateTime lastModifiedDate; + @CreatedDate + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdDate; + + @LastModifiedDate + @Column(name = "last_modified_at", nullable = false) + private LocalDateTime lastModifiedDate; } diff --git a/backend/src/test/java/com/woowacourse/naepyeon/repository/MemberRepositoryTest.java b/backend/src/test/java/com/woowacourse/naepyeon/repository/MemberRepositoryTest.java index 97241449..2982ede7 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/repository/MemberRepositoryTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/repository/MemberRepositoryTest.java @@ -1,5 +1,6 @@ package com.woowacourse.naepyeon.repository; +import static java.lang.Thread.sleep; import static org.assertj.core.api.Assertions.assertThat; import com.woowacourse.naepyeon.domain.Member; @@ -65,11 +66,11 @@ void createMemberWhen() { @Test @DisplayName("회원 정보를 수정할 때 수정일자가 올바르게 나온다.") - void updateMemberWhen() { + void updateMemberWhen() throws InterruptedException { final Member member = new Member("alex", "alex@naepyeon.com", "abc12345"); final Long memberId = memberRepository.save(member); - em.flush(); + sleep(1); member.changeUsername("kth990303"); em.flush(); diff --git a/backend/src/test/java/com/woowacourse/naepyeon/repository/MessageRepositoryTest.java b/backend/src/test/java/com/woowacourse/naepyeon/repository/MessageRepositoryTest.java index b7401b51..9a233500 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/repository/MessageRepositoryTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/repository/MessageRepositoryTest.java @@ -1,5 +1,6 @@ package com.woowacourse.naepyeon.repository; +import static java.lang.Thread.sleep; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; @@ -133,11 +134,11 @@ void createMemberWhen() { @Test @DisplayName("메시지를 수정할 때 수정일자가 올바르게 나온다.") - void updateMemberWhen() { + void updateMemberWhen() throws InterruptedException { final Message message = createMessage(); final Long messageId = messageRepository.save(message); - em.flush(); + sleep(1); message.changeContent("updateupdate"); em.flush(); diff --git a/backend/src/test/java/com/woowacourse/naepyeon/repository/RollingpaperRepositoryTest.java b/backend/src/test/java/com/woowacourse/naepyeon/repository/RollingpaperRepositoryTest.java index a2bd937c..93ecccb2 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/repository/RollingpaperRepositoryTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/repository/RollingpaperRepositoryTest.java @@ -1,5 +1,6 @@ package com.woowacourse.naepyeon.repository; +import static java.lang.Thread.sleep; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; @@ -133,11 +134,11 @@ void createMemberWhen() { @Test @DisplayName("롤링페이퍼를 수정할 때 수정일자가 올바르게 나온다.") - void updateMemberWhen() { + void updateMemberWhen() throws InterruptedException { final Rollingpaper rollingpaper = createRollingPaper(); final Long rollingpaperId = rollingpaperRepository.save(rollingpaper); - em.flush(); + sleep(1); rollingpaper.changeTitle("updateupdate"); em.flush(); diff --git a/backend/src/test/java/com/woowacourse/naepyeon/repository/TeamParticipationRepositoryTest.java b/backend/src/test/java/com/woowacourse/naepyeon/repository/TeamParticipationRepositoryTest.java index ce7a89e8..dba1e739 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/repository/TeamParticipationRepositoryTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/repository/TeamParticipationRepositoryTest.java @@ -1,5 +1,6 @@ package com.woowacourse.naepyeon.repository; +import static java.lang.Thread.sleep; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; @@ -179,12 +180,12 @@ void createMemberWhen() { @Test @DisplayName("회원이 특정 팀의 닉네임을 변경한다.") - void updateNickname() { + void updateNickname() throws InterruptedException { final String expected = "닉네임2"; final TeamParticipation teamParticipation = new TeamParticipation(team1, member1, "닉네임1"); final Long teamParticipationId = teamParticipationRepository.save(teamParticipation); - em.flush(); + sleep(1); teamParticipationRepository.updateNickname(expected, member1.getId(), team1.getId()); em.flush(); diff --git a/backend/src/test/java/com/woowacourse/naepyeon/repository/TeamRepositoryTest.java b/backend/src/test/java/com/woowacourse/naepyeon/repository/TeamRepositoryTest.java index 6ff4ee62..b8b89c5b 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/repository/TeamRepositoryTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/repository/TeamRepositoryTest.java @@ -1,5 +1,6 @@ package com.woowacourse.naepyeon.repository; +import static java.lang.Thread.sleep; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; @@ -98,11 +99,11 @@ void createMemberWhen() { @Test @DisplayName("모임을 수정할 때 수정일자가 올바르게 나온다.") - void updateMemberWhen() { + void updateMemberWhen() throws InterruptedException { final Team team = new Team("woowacourse", "테스트 모임입니다.", "testEmoji", "#123456"); final Long teamId = teamRepository.save(team); - em.flush(); + sleep(1); team.changeName("updateupdate"); em.flush(); From ed4f7422e8a8b7e2735de41aa7aaa6349dc4de93 Mon Sep 17 00:00:00 2001 From: zero Date: Fri, 29 Jul 2022 18:22:19 +0900 Subject: [PATCH 11/62] =?UTF-8?q?fix:=20V2=20=EB=84=A4=EC=9D=B4=EB=B0=8D?= =?UTF-8?q?=20=EB=B2=84=EA=B7=B8=EB=A5=BC=20=EC=88=98=EC=A0=95=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.=20(#209)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migration/{V2_add_baseEntity.sql => V2__add_baseEntity.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename backend/src/main/resources/db/migration/{V2_add_baseEntity.sql => V2__add_baseEntity.sql} (100%) diff --git a/backend/src/main/resources/db/migration/V2_add_baseEntity.sql b/backend/src/main/resources/db/migration/V2__add_baseEntity.sql similarity index 100% rename from backend/src/main/resources/db/migration/V2_add_baseEntity.sql rename to backend/src/main/resources/db/migration/V2__add_baseEntity.sql From 83a6d30e979ccfd688bdb0666f5ed3f67d9dcb8f Mon Sep 17 00:00:00 2001 From: zero Date: Fri, 29 Jul 2022 19:14:49 +0900 Subject: [PATCH 12/62] =?UTF-8?q?fix:=20fly=20=EB=B2=84=EA=B7=B8=EB=A5=BC?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=ED=95=9C=EB=8B=A4.=20(#211)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: V2 네이밍 버그를 수정한다. * fix: flyway 적용을 명시적으로 사용하지않음으로 변경한다. * fix: 의존성을 추가한다 * fix: 삭제가능한 순서로 변경한다 --- backend/build.gradle | 1 + .../main/resources/db/migration/V1__init.sql | 40 +++++++++---------- backend/src/test/resources/application.yml | 3 ++ 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/backend/build.gradle b/backend/build.gradle index d563dd27..56f8e72a 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -27,6 +27,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.flywaydb:flyway-core:6.4.2' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2:1.4.200' runtimeOnly 'mysql:mysql-connector-java' diff --git a/backend/src/main/resources/db/migration/V1__init.sql b/backend/src/main/resources/db/migration/V1__init.sql index 973805cc..0e4380d6 100644 --- a/backend/src/main/resources/db/migration/V1__init.sql +++ b/backend/src/main/resources/db/migration/V1__init.sql @@ -1,13 +1,3 @@ -CREATE TABLE IF NOT EXISTS member -( - member_id bigint not null auto_increment, - email varchar(255) not null, - password varchar(255) not null, - username varchar(20) not null, - primary key (member_id) -) engine=InnoDB - default charset utf8mb4; - CREATE TABLE IF NOT EXISTS message ( message_id bigint not null auto_increment, @@ -28,6 +18,16 @@ CREATE TABLE IF NOT EXISTS rollingpaper ) engine=InnoDB default charset utf8mb4; +CREATE TABLE IF NOT EXISTS team_member +( + team_member_id bigint not null auto_increment, + nickname varchar(20) not null, + member_id bigint, + team_id bigint, + primary key (team_member_id) +) engine=InnoDB + default charset utf8mb4; + CREATE TABLE IF NOT EXISTS team ( team_id bigint not null auto_increment, @@ -36,18 +36,18 @@ CREATE TABLE IF NOT EXISTS team emoji varchar(255) not null, team_name varchar(20) not null, primary key (team_id) -) engine=InnoDB - default charset utf8mb4; + ) engine=InnoDB + default charset utf8mb4; -CREATE TABLE IF NOT EXISTS team_member +CREATE TABLE IF NOT EXISTS member ( - team_member_id bigint not null auto_increment, - nickname varchar(20) not null, - member_id bigint, - team_id bigint, - primary key (team_member_id) -) engine=InnoDB - default charset utf8mb4; + member_id bigint not null auto_increment, + email varchar(255) not null, + password varchar(255) not null, + username varchar(20) not null, + primary key (member_id) + ) engine=InnoDB + default charset utf8mb4; alter table member add constraint uk_member_email unique (email); diff --git a/backend/src/test/resources/application.yml b/backend/src/test/resources/application.yml index e9e0819d..82d575f6 100644 --- a/backend/src/test/resources/application.yml +++ b/backend/src/test/resources/application.yml @@ -19,6 +19,9 @@ spring: default_batch_fetch_size: 1000 #최적화 옵션 open-in-view: false + flyway: + enabled: false + logging.level: org.hibernate.SQL: debug From a3c6f4e6ff1c2d8448e027d53efc6350387717be Mon Sep 17 00:00:00 2001 From: TaeHyeon Kim <57135043+kth990303@users.noreply.github.com> Date: Fri, 29 Jul 2022 19:34:04 +0900 Subject: [PATCH 13/62] =?UTF-8?q?feat:=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=EC=83=89=EC=83=81=20=EB=B3=80=EA=B2=BD=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4.=20(#187)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: `Message` DB 컬럼으로 색상 추가 * feat: 메시지 관련 요청에 색상 필드 추가 * feat: 메시지 색상이 변경되는 기능 구현 * feat: 메시지 응답 DTO 색상 컬럼 추가 * feat: webpack과 dotenv 설정 (#189) * chore: dotenv 설치 및 gitignore update * feat: mode에 따른 dotenv 설정 Co-authored-by: Soyi Jeon * feat: API_URL 상수 제거 Co-authored-by: Soyi Jeon Co-authored-by: Soyi Jeon * refactor: 파라미터 개행 컨벤션 변경 * feat: 모임 마이페이지 기능 구현 (#184) * feat: 모임 마이페이지 기능 구현 모임 가입 정보 조회, 모임 닉네임 수정 기능 구현 * fix: 에러메시지 수정 * refactor: 메서드명 변경 * refactor: 메서드 순서 변경 * refactor: 명칭 변경 * refactor: 레포지토리 단에서 `Optional` 반환하도록 변경 * refactor: 불필요한 검증 제거 * test: 요청 변수값 사용하도록 수정 * chore: flyway 적용 및 개발환경 DB를 H2에서 MYSQL로 마이그레이션 (#197) chore: flyway 적용 및 개발환경 DB를 H2에서 MYSQL로 마이그레이션 Co-authored-by: kth990303 Co-authored-by: asebn1 * feat: mypage UI를 구현한다 (#186) * feat: CounterWithText 컴포넌트 구현 * feat: Paging component 구현 * refactor: file 경로 수정 * feat: ReceivedRollingpaperCard component 구현 * feat: WrittenMessageCard component 구현 * feat: right icon button 추가 * feat: UserProfile component 구현 * feat: MyPage 구현 * feat: MyPage route 추가 * refactor: 페이징 버튼 로직 수정 * refactor: 오타 수정 * refactor: 클릭 숫자 이동 오류 수정 * feat: ValueOf type 추가 * refactor: UserProfile mode type 지정 * refactor: 컴포넌트명 수정 * feat: MyPage Tab type 추가 * feat: MyPageRollingpaperList component 구현 * feat: MyPageWrittenMessageList 구현 * feat: MyPageRollingpaperList, MyPageWrittenMessageList으로 변경 * feat: hover시 색상 변경 추가 * refactor: onClick type 변경 * refactor: 불필요한 import 제거 및 상수명 변경 * feat: MypageList들에 Paging 추가 * feat: Styled 컴포넌트명 수정 * feat: snackbar 구현 (#193) * feat: SnackbarContext 추가 * feat: Snackbar component 구현 * feat: global snackbar 추가 * feat: 회원가입 메시지 스낵바로 수정 * refactor: 불필요한 css 제거 * feat: useSnackbar 구현 * refactor: useSnackbar 사용 * refactor: 화살표 함수로 변경 * feat: 롤링페이퍼 페이지 UI 수정 및 새 메시지 작성 컴포넌트 UI 구현 (#194) * test: 메시지 더미 데이터 수정 * feat: RollingpaperMessage 컴포넌트 스타일 수정 StyledMessageContent height 제거 StyledMessage height 제거 및 min-height 추가 * feat: LetterPaper 내 MessageList 정렬 방식 변경 * feat: MessageTextArea 컴포넌트 구현 * feat: MessageTextArea 컴포넌트 구현 * feat: MessageColorPicker 컴포넌트 구현 * feat: MessageForm 컴포넌트 구현 * feat: MessateTextArea 스타일 추가 * feat: MessageForm에 Submit, Cancel Button 추가 * feat: LetterPaper에 새 메시지 작성 폼 MessageForm 추가 - 새 메시지 작성 상태 writeNewMessage 추가 * refactor: slicedMessageLists 상태 초깃값 생성 로직 수정 * refactor: 메서드의 사용하지 않는 인자 제거 handleMessageWriteButtonClick의 e 제거 * feat: MessageColorPicker 스타일 코드 수정 - StyledRadio의 불필요한 backgroundColor default 값 제거 * refactor: newContent 변수 분리 - handleTextAreaChange의 e.target.value를 newContent 변수로 정의 * refactor: 불필요한 style 코드 삭제 CircleButton color 속성 제거 * feat: MessageTextArea 스타일 코드 수정 - StyledMessageContainer의 불필요한 backgroundColor default 값 제거 * refactor: 불필요한 변수 제거 - handleTextAreaChange의 newContent 제거 * feat: flyway v2 적용 (#205) * feat: `BaseEntity`를 제외한 db 테이블 `flyway` 형상관리 적용 * chore: 개발 DB를 h2에서를 MYSQL로 마이그레이션 * refactor: mysql환경에서 가능하도록 변경 * feat: flyway v2 적용 * refactor: 불필요한 설정 제거 Co-authored-by: kth990303 * fix: 수정일자, 생성일자 버그를 수정한다. (#207) * feat: `BaseEntity`를 제외한 db 테이블 `flyway` 형상관리 적용 * chore: 개발 DB를 h2에서를 MYSQL로 마이그레이션 * refactor: mysql환경에서 가능하도록 변경 * feat: flyway v2 적용 * refactor: 불필요한 설정 제거 * fix: 수정일자, 생성일자 버그를 수정한다. * fix: 수정일자 버그를 수정한다. Co-authored-by: kth990303 * fix: V2 네이밍 버그를 수정한다. (#209) * fix: fly 버그를 수정한다. (#211) * fix: V2 네이밍 버그를 수정한다. * fix: flyway 적용을 명시적으로 사용하지않음으로 변경한다. * fix: 의존성을 추가한다 * fix: 삭제가능한 순서로 변경한다 * feat: V3에 맞는 스키마를 추가한다. Co-authored-by: SunHo Park <67692759+prefer2@users.noreply.github.com> Co-authored-by: Soyi Jeon Co-authored-by: zero --- .../controller/MessageController.java | 12 +++-- .../controller/dto/MessageRequest.java | 2 + .../dto/MessageUpdateContentRequest.java | 2 + .../woowacourse/naepyeon/domain/Message.java | 10 +++- .../repository/JpaMessageRepository.java | 3 +- .../repository/MessageRepository.java | 2 +- .../naepyeon/service/MessageService.java | 12 +++-- .../service/dto/MessageResponseDto.java | 1 + .../db/migration/V3__add_message_color.sql | 1 + .../acceptance/MessageAcceptanceTest.java | 54 +++++++++++-------- .../naepyeon/domain/MessageTest.java | 24 ++++++++- .../repository/MessageRepositoryTest.java | 11 ++-- .../naepyeon/service/MessageServiceTest.java | 40 ++++++-------- 13 files changed, 111 insertions(+), 63 deletions(-) create mode 100644 backend/src/main/resources/db/migration/V3__add_message_color.sql diff --git a/backend/src/main/java/com/woowacourse/naepyeon/controller/MessageController.java b/backend/src/main/java/com/woowacourse/naepyeon/controller/MessageController.java index 53194f50..e62019a6 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/controller/MessageController.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/controller/MessageController.java @@ -32,7 +32,8 @@ public ResponseEntity createMessage(@AuthenticationPrincipal Log @RequestBody @Valid final MessageRequest messageRequest, @PathVariable final Long rollingpaperId) { final Long messageId = - messageService.saveMessage(messageRequest.getContent(), loginMemberRequest.getId(), rollingpaperId); + messageService.saveMessage(messageRequest.getContent(), messageRequest.getColor(), rollingpaperId, + loginMemberRequest.getId()); return ResponseEntity.created( URI.create("/api/v1/rollingpapers/" + rollingpaperId + "/messages/" + messageId) ).body(new CreateResponse(messageId)); @@ -48,12 +49,17 @@ public ResponseEntity findMessage( } @PutMapping("/{messageId}") - public ResponseEntity updateMessageContent( + public ResponseEntity updateMessage( @AuthenticationPrincipal LoginMemberRequest loginMemberRequest, @RequestBody @Valid final MessageUpdateContentRequest messageUpdateContentRequest, @PathVariable final Long rollingpaperId, @PathVariable final Long messageId) { - messageService.updateContent(messageId, messageUpdateContentRequest.getContent(), loginMemberRequest.getId()); + messageService.updateMessage( + messageId, + messageUpdateContentRequest.getContent(), + messageUpdateContentRequest.getColor(), + loginMemberRequest.getId() + ); return ResponseEntity.noContent().build(); } diff --git a/backend/src/main/java/com/woowacourse/naepyeon/controller/dto/MessageRequest.java b/backend/src/main/java/com/woowacourse/naepyeon/controller/dto/MessageRequest.java index bd038400..ca449a61 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/controller/dto/MessageRequest.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/controller/dto/MessageRequest.java @@ -13,4 +13,6 @@ public class MessageRequest { @NotBlank(message = "2001:메시지는 공백일 수 없습니다.") private String content; + + private String color; } diff --git a/backend/src/main/java/com/woowacourse/naepyeon/controller/dto/MessageUpdateContentRequest.java b/backend/src/main/java/com/woowacourse/naepyeon/controller/dto/MessageUpdateContentRequest.java index 934b39db..5d95e855 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/controller/dto/MessageUpdateContentRequest.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/controller/dto/MessageUpdateContentRequest.java @@ -13,4 +13,6 @@ public class MessageUpdateContentRequest { @NotBlank(message = "2001:메시지는 공백일 수 없습니다.") private String content; + + private String color; } diff --git a/backend/src/main/java/com/woowacourse/naepyeon/domain/Message.java b/backend/src/main/java/com/woowacourse/naepyeon/domain/Message.java index 83cf7767..a639525b 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/domain/Message.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/domain/Message.java @@ -30,6 +30,9 @@ public class Message extends BaseEntity { @Column(name = "content", length = 500, nullable = false) private String content; + @Column(name = "color") + private String color; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id", nullable = false) private Member author; @@ -38,9 +41,10 @@ public class Message extends BaseEntity { @JoinColumn(name = "rollingpaper_id", nullable = false) private Rollingpaper rollingpaper; - public Message(final String content, final Member author, final Rollingpaper rollingpaper) { + public Message(final String content, final String color, final Member author, final Rollingpaper rollingpaper) { validateContentLength(content); this.content = content; + this.color = color; this.author = author; this.rollingpaper = rollingpaper; } @@ -50,6 +54,10 @@ public void changeContent(final String newContent) { this.content = newContent; } + public void changeColor(final String newColor) { + this.color = newColor; + } + private void validateContentLength(final String content) { if (content.length() > MAX_CONTENT_LENGTH) { throw new ExceedMessageContentLengthException(content); diff --git a/backend/src/main/java/com/woowacourse/naepyeon/repository/JpaMessageRepository.java b/backend/src/main/java/com/woowacourse/naepyeon/repository/JpaMessageRepository.java index 04eb15bc..42c15242 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/repository/JpaMessageRepository.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/repository/JpaMessageRepository.java @@ -31,10 +31,11 @@ public List findAllByRollingpaperId(final Long rollingpaperId) { } @Override - public void update(final Long id, final String newContent) { + public void update(final Long id, final String newColor, final String newContent) { final Message message = findById(id) .orElseThrow(() -> new NotFoundMessageException(id)); message.changeContent(newContent); + message.changeColor(newColor); } @Override diff --git a/backend/src/main/java/com/woowacourse/naepyeon/repository/MessageRepository.java b/backend/src/main/java/com/woowacourse/naepyeon/repository/MessageRepository.java index 8f678ae9..1f680c36 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/repository/MessageRepository.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/repository/MessageRepository.java @@ -12,7 +12,7 @@ public interface MessageRepository { List findAllByRollingpaperId(final Long rollingpaperId); - void update(final Long id, final String newContent); + void update(final Long id, final String newColor, final String newContent); void delete(final Long id); } diff --git a/backend/src/main/java/com/woowacourse/naepyeon/service/MessageService.java b/backend/src/main/java/com/woowacourse/naepyeon/service/MessageService.java index fcf41635..2c5b252e 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/service/MessageService.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/service/MessageService.java @@ -29,12 +29,12 @@ public class MessageService { private final MemberRepository memberRepository; private final TeamParticipationRepository teamParticipationRepository; - public Long saveMessage(final String content, final Long authorId, final Long rollingpaperId) { + public Long saveMessage(final String content, final String color, final Long rollingpaperId, final Long authorId) { final Rollingpaper rollingpaper = rollingpaperRepository.findById(rollingpaperId) .orElseThrow(() -> new NotFoundRollingpaperException(rollingpaperId)); final Member author = memberRepository.findById(authorId) .orElseThrow(() -> new NotFoundMemberException(authorId)); - final Message message = new Message(content, author, rollingpaper); + final Message message = new Message(content, color, author, rollingpaper); return messageRepository.save(message); } @@ -47,6 +47,7 @@ public List findMessages(final Long rollingpaperId, final Lo return new MessageResponseDto( message.getId(), message.getContent(), + message.getColor(), findMessageWriterNickname(teamId, message), author.getId() ); @@ -68,14 +69,15 @@ public MessageResponseDto findMessage(final Long messageId, final Long rollingpa final Team team = rollingpaper.getTeam(); final Member author = message.getAuthor(); final String nickname = findMessageWriterNickname(team.getId(), message); - return new MessageResponseDto(messageId, message.getContent(), nickname, author.getId()); + return new MessageResponseDto(messageId, message.getContent(), message.getColor(), nickname, author.getId()); } - public void updateContent(final Long messageId, final String newContent, final Long memberId) { + public void updateMessage(final Long messageId, final String newContent, final String newColor, + final Long memberId) { final Message message = messageRepository.findById(messageId) .orElseThrow(() -> new NotFoundMessageException(messageId)); validateAuthor(memberId, message); - messageRepository.update(messageId, newContent); + messageRepository.update(messageId, newColor, newContent); } public void deleteMessage(final Long messageId, final Long memberId) { diff --git a/backend/src/main/java/com/woowacourse/naepyeon/service/dto/MessageResponseDto.java b/backend/src/main/java/com/woowacourse/naepyeon/service/dto/MessageResponseDto.java index bc5c391d..aa4903c4 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/service/dto/MessageResponseDto.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/service/dto/MessageResponseDto.java @@ -12,6 +12,7 @@ public class MessageResponseDto { private Long id; private String content; + private String color; private String from; private Long authorId; } diff --git a/backend/src/main/resources/db/migration/V3__add_message_color.sql b/backend/src/main/resources/db/migration/V3__add_message_color.sql new file mode 100644 index 00000000..6912bc0f --- /dev/null +++ b/backend/src/main/resources/db/migration/V3__add_message_color.sql @@ -0,0 +1 @@ +ALTER TABLE message ADD COLUMN color varchar(255) DEFAULT '#fff598'; \ No newline at end of file diff --git a/backend/src/test/java/com/woowacourse/naepyeon/acceptance/MessageAcceptanceTest.java b/backend/src/test/java/com/woowacourse/naepyeon/acceptance/MessageAcceptanceTest.java index 289e94b3..ca73c420 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/acceptance/MessageAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/acceptance/MessageAcceptanceTest.java @@ -54,7 +54,7 @@ void createMessageToRollingpaper() { .getId(); final ExtractableResponse response = 메시지_작성(tokenResponseDto1, rollingpaperId, - new MessageRequest("환영해 알렉스!!!")); + new MessageRequest("환영해 알렉스!!!", "green")); assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); } @@ -74,9 +74,9 @@ void createMessagesToRollingpaperWithSameMember() { .as(CreateResponse.class) .getId(); - 메시지_작성(tokenResponseDto1, rollingpaperId, new MessageRequest("환영해 알렉스!!!")); - 메시지_작성(tokenResponseDto1, rollingpaperId, new MessageRequest("알렉스 점심 뭐 먹어?")); - 메시지_작성(tokenResponseDto1, rollingpaperId, new MessageRequest("생일축하해!")); + 메시지_작성(tokenResponseDto1, rollingpaperId, new MessageRequest("환영해 알렉스!!!", "green")); + 메시지_작성(tokenResponseDto1, rollingpaperId, new MessageRequest("알렉스 점심 뭐 먹어?", "green")); + 메시지_작성(tokenResponseDto1, rollingpaperId, new MessageRequest("생일축하해!", "green")); final RollingpaperResponseDto response = 롤링페이퍼_특정_조회(tokenResponseDto2, teamId, rollingpaperId) .as(RollingpaperResponseDto.class); @@ -85,7 +85,7 @@ void createMessagesToRollingpaperWithSameMember() { } @Test - @DisplayName("작성한 메시지의 내용을 수정한다.") + @DisplayName("작성한 메시지의 내용과 색상을 수정한다.") void updateMessageContent() { final TokenResponseDto tokenResponseDto1 = 회원가입_후_로그인(member1); final Long teamId = 모임_추가(tokenResponseDto1, teamRequest).as(CreateResponse.class) @@ -99,14 +99,24 @@ void updateMessageContent() { .as(CreateResponse.class) .getId(); - final Long messageId = 메시지_작성(tokenResponseDto1, rollingpaperId, new MessageRequest("환영해 알렉스!!!")) + final Long messageId = 메시지_작성(tokenResponseDto1, rollingpaperId, new MessageRequest("환영해 알렉스!!!", "green")) .as(CreateResponse.class) .getId(); final ExtractableResponse response = 메시지_수정(tokenResponseDto1, rollingpaperId, messageId, - new MessageUpdateContentRequest("오늘 뭐해??")); + new MessageUpdateContentRequest("오늘 뭐해??", "red")); - assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); + final MessageResponseDto actual = 메시지_조회(tokenResponseDto1, rollingpaperId, messageId) + .as(MessageResponseDto.class); + final MessageResponseDto expected = + new MessageResponseDto(actual.getId(), "오늘 뭐해??", "red", actual.getFrom(), actual.getAuthorId()); + + assertAll( + () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()), + () -> assertThat(actual) + .usingRecursiveComparison() + .isEqualTo(expected) + ); } @Test @@ -124,12 +134,12 @@ void updateMessageContentWithExceedContentLength() { .as(CreateResponse.class) .getId(); - final Long messageId = 메시지_작성(tokenResponseDto1, rollingpaperId, new MessageRequest("환영해 알렉스!!!")) + final Long messageId = 메시지_작성(tokenResponseDto1, rollingpaperId, new MessageRequest("환영해 알렉스!!!", "green")) .as(CreateResponse.class) .getId(); final ExtractableResponse response = 메시지_수정(tokenResponseDto1, rollingpaperId, messageId, - new MessageUpdateContentRequest("a".repeat(501))); + new MessageUpdateContentRequest("a".repeat(501), "green")); assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); } @@ -149,12 +159,12 @@ void updateMessageFromOthersMessage() { .as(CreateResponse.class) .getId(); - final Long messageId = 메시지_작성(tokenResponseDto2, rollingpaperId, new MessageRequest("테스트 메시지2")) + final Long messageId = 메시지_작성(tokenResponseDto2, rollingpaperId, new MessageRequest("테스트 메시지2", "green")) .as(CreateResponse.class) .getId(); final ExtractableResponse response = 메시지_수정(tokenResponseDto1, rollingpaperId, messageId, - new MessageUpdateContentRequest("수정할 때 예외 발생")); + new MessageUpdateContentRequest("수정할 때 예외 발생", "green")); assertThat(response.statusCode()).isEqualTo(HttpStatus.FORBIDDEN.value()); } @@ -163,12 +173,11 @@ void updateMessageFromOthersMessage() { @DisplayName("존재하지 않는 롤링페이퍼에 메시지를 작성할 경우 예외 발생") void createMessageWithNRollingpaperNotExist() { final TokenResponseDto tokenResponseDto1 = 회원가입_후_로그인(member1); - final Long teamId = 모임_추가(tokenResponseDto1, teamRequest).as(CreateResponse.class) - .getId(); + 모임_추가(tokenResponseDto1, teamRequest).as(CreateResponse.class); final Long invalidMessageId = 9999L; final ExtractableResponse response = 메시지_작성(tokenResponseDto1, invalidMessageId, - new MessageRequest("환영해 알렉스!!!")); + new MessageRequest("환영해 알렉스!!!", "green")); assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND.value()); } @@ -188,7 +197,7 @@ void deleteMessage() { .as(CreateResponse.class) .getId(); - final Long messageId = 메시지_작성(tokenResponseDto1, rollingpaperId, new MessageRequest("곧 삭제될 메시지")) + final Long messageId = 메시지_작성(tokenResponseDto1, rollingpaperId, new MessageRequest("곧 삭제될 메시지", "green")) .as(CreateResponse.class) .getId(); @@ -212,7 +221,7 @@ void deleteMessageWithRollingpaperNotExist() { .as(CreateResponse.class) .getId(); - 메시지_작성(tokenResponseDto1, rollingpaperId, new MessageRequest("테스트 메시지")); + 메시지_작성(tokenResponseDto1, rollingpaperId, new MessageRequest("테스트 메시지", "green")); final Long invalidMessageId = 9999L; final ExtractableResponse response = 메시지_삭제(tokenResponseDto1, rollingpaperId, invalidMessageId); @@ -235,8 +244,8 @@ void deleteMessageFromOthersMessage() { .as(CreateResponse.class) .getId(); - 메시지_작성(tokenResponseDto1, rollingpaperId, new MessageRequest("테스트 메시지1")); - final Long messageId = 메시지_작성(tokenResponseDto2, rollingpaperId, new MessageRequest("테스트 메시지2")) + 메시지_작성(tokenResponseDto1, rollingpaperId, new MessageRequest("테스트 메시지1", "green")); + final Long messageId = 메시지_작성(tokenResponseDto2, rollingpaperId, new MessageRequest("테스트 메시지2", "green")) .as(CreateResponse.class) .getId(); @@ -262,7 +271,8 @@ void findDetailMessageWithRollingpaper() { .getId(); final String content = "상세조회용 메시지 입니다."; - final Long messageId = 메시지_작성(tokenResponseDto2, rollingpaperId, new MessageRequest(content)) + final String color = "green"; + final Long messageId = 메시지_작성(tokenResponseDto2, rollingpaperId, new MessageRequest(content, color)) .as(CreateResponse.class) .getId(); @@ -272,8 +282,8 @@ void findDetailMessageWithRollingpaper() { assertAll( () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()), () -> assertThat(messageResponseDto) - .extracting("id", "content", "from", "authorId") - .containsExactly(messageId, content, nickname, 2L) + .extracting("id", "content", "color", "from", "authorId") + .containsExactly(messageId, content, color, nickname, 2L) ); } } diff --git a/backend/src/test/java/com/woowacourse/naepyeon/domain/MessageTest.java b/backend/src/test/java/com/woowacourse/naepyeon/domain/MessageTest.java index 49780c78..03286a85 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/domain/MessageTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/domain/MessageTest.java @@ -21,7 +21,7 @@ void changeContent() { final Member member = new Member("member", "m@hello.com", "abc@@2345"); final Member author = new Member("author", "a@hello.com", "abc@@2345"); final Rollingpaper rollingpaper = new Rollingpaper("alexAndKei", team, member); - final Message message = new Message("헬로우", author, rollingpaper); + final Message message = new Message("헬로우", "green", author, rollingpaper); final String expected = "낫 헬로우"; message.changeContent(expected); @@ -41,7 +41,27 @@ void exceedLength() { final Member member = new Member("member", "m@hello.com", "abc@@1234"); final Member author = new Member("author", "a@hello.com", "abc@@1234"); final Rollingpaper rollingpaper = new Rollingpaper("alexAndKei", team, member); - assertThatThrownBy(() -> new Message("a".repeat(501), author, rollingpaper)) + assertThatThrownBy(() -> new Message("a".repeat(501), "green", author, rollingpaper)) .isInstanceOf(ExceedMessageContentLengthException.class); } + + @Test + @DisplayName("메시지 색상을 변경한다.") + void changeColor() { + final Team team = new Team( + "nae-pyeon", + "테스트 모임입니다.", + "testEmoji", + "#123456" + ); + final Member member = new Member("member", "m@hello.com", "abc@@2345"); + final Member author = new Member("author", "a@hello.com", "abc@@2345"); + final Rollingpaper rollingpaper = new Rollingpaper("alexAndKei", team, member); + final Message message = new Message("헬로우", "green", author, rollingpaper); + final String expected = "red"; + + message.changeColor(expected); + + assertThat(message.getColor()).isEqualTo(expected); + } } \ No newline at end of file diff --git a/backend/src/test/java/com/woowacourse/naepyeon/repository/MessageRepositoryTest.java b/backend/src/test/java/com/woowacourse/naepyeon/repository/MessageRepositoryTest.java index 9a233500..17df7008 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/repository/MessageRepositoryTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/repository/MessageRepositoryTest.java @@ -92,15 +92,16 @@ void findAllByRollingpaperId() { @Test - @DisplayName("본인이 작성한 메시지 내용을 변경한다.") + @DisplayName("본인이 작성한 메시지 내용과 색상을 변경한다.") void update() { final Member member = memberRepository.findByEmail(author.getEmail()) .orElseThrow(); - final Message message = new Message(content, member, rollingpaper); + final Message message = new Message(content, "green", member, rollingpaper); final Long messageId = messageRepository.save(message); final String newContent = "알고리즘이 좋아요"; + final String newColor = "red"; - messageRepository.update(messageId, newContent); + messageRepository.update(messageId, newColor, newContent); final Message updateMessage = messageRepository.findById(messageId) .orElseThrow(); @@ -112,7 +113,7 @@ void update() { void delete() { final Member member = memberRepository.findByEmail(author.getEmail()) .orElseThrow(); - final Message message = new Message(content, member, rollingpaper); + final Message message = new Message(content, "green", member, rollingpaper); final Long messageId = messageRepository.save(message); messageRepository.delete(messageId); @@ -148,6 +149,6 @@ void updateMemberWhen() throws InterruptedException { } private Message createMessage() { - return new Message(content, author, rollingpaper); + return new Message(content, "green", author, rollingpaper); } } diff --git a/backend/src/test/java/com/woowacourse/naepyeon/service/MessageServiceTest.java b/backend/src/test/java/com/woowacourse/naepyeon/service/MessageServiceTest.java index a6ca088d..7b1c3cb1 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/service/MessageServiceTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/service/MessageServiceTest.java @@ -62,12 +62,9 @@ void setUp() { void saveMessageAndFind() { final MessageRequest messageRequest = createMessageRequest(); final Long messageId = messageService.saveMessage( - messageRequest.getContent(), - author.getId(), - rollingpaper.getId() + messageRequest.getContent(), messageRequest.getColor(), rollingpaper.getId(), author.getId() ); - final Team team = rollingpaper.getTeam(); final MessageResponseDto messageResponse = messageService.findMessage(messageId, rollingpaper.getId()); assertThat(messageResponse).extracting("content", "from", "authorId") @@ -75,20 +72,23 @@ void saveMessageAndFind() { } @Test - @DisplayName("메시지의 내용을 수정한다.") + @DisplayName("메시지 내용과 색상을 수정한다.") void updateContent() { final MessageRequest messageRequest = createMessageRequest(); final Long messageId = messageService.saveMessage( - messageRequest.getContent(), - author.getId(), - rollingpaper.getId() + messageRequest.getContent(), messageRequest.getColor(), rollingpaper.getId(), author.getId() ); - final String expected = "안녕하지 못합니다."; + final String expectedContent = "안녕하지 못합니다."; + final String expectedColor = "red"; - messageService.updateContent(messageId, expected, author.getId()); + messageService.updateMessage(messageId, expectedContent, expectedColor, author.getId()); - final MessageResponseDto messageResponse = messageService.findMessage(messageId, rollingpaper.getId()); - assertThat(messageResponse.getContent()).isEqualTo(expected); + final MessageResponseDto actual = messageService.findMessage(messageId, rollingpaper.getId()); + final MessageResponseDto expected = new MessageResponseDto(messageId, expectedContent, expectedColor, + "테스트닉네임", author.getId()); + assertThat(actual) + .usingRecursiveComparison() + .isEqualTo(expected); } @Test @@ -96,13 +96,11 @@ void updateContent() { void updateContentWithNotAuthor() { final MessageRequest messageRequest = createMessageRequest(); final Long messageId = messageService.saveMessage( - messageRequest.getContent(), - author.getId(), - rollingpaper.getId() + messageRequest.getContent(), messageRequest.getColor(), rollingpaper.getId(), author.getId() ); final String expected = "안녕하지 못합니다."; - assertThatThrownBy(() -> messageService.updateContent(messageId, expected, 9999L)) + assertThatThrownBy(() -> messageService.updateMessage(messageId, expected, "green", 9999L)) .isInstanceOf(NotAuthorException.class); } @@ -111,9 +109,7 @@ void updateContentWithNotAuthor() { void deleteMessage() { final MessageRequest messageRequest = createMessageRequest(); final Long messageId = messageService.saveMessage( - messageRequest.getContent(), - author.getId(), - rollingpaper.getId() + messageRequest.getContent(), messageRequest.getColor(), rollingpaper.getId(), author.getId() ); messageService.deleteMessage(messageId, author.getId()); @@ -127,9 +123,7 @@ void deleteMessage() { void deleteMessageWithNotAuthor() { final MessageRequest messageRequest = createMessageRequest(); final Long messageId = messageService.saveMessage( - messageRequest.getContent(), - author.getId(), - rollingpaper.getId() + messageRequest.getContent(), messageRequest.getColor(), rollingpaper.getId(), author.getId() ); assertThatThrownBy(() -> messageService.deleteMessage(messageId, 9999L)) @@ -137,6 +131,6 @@ void deleteMessageWithNotAuthor() { } private MessageRequest createMessageRequest() { - return new MessageRequest("안녕하세요"); + return new MessageRequest("안녕하세요", "green"); } } From bc6d4cf0b5364387e845bfca06bf5c72317e9861 Mon Sep 17 00:00:00 2001 From: SunHo Park <67692759+prefer2@users.noreply.github.com> Date: Fri, 29 Jul 2022 20:40:46 +0900 Subject: [PATCH 14/62] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95=20(#212)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: UserContext login, logout 추가 Co-authored-by: Soyi Jeon * feat: UserContext memberId 추가 * feat: cookie 만료시간(max-age) 설정 Co-authored-by: Soyi Jeon * feat: 자동로그인 로직에 내 정보 조회 요청 추가 Co-authored-by: Soyi Jeon * feat: cookie key 상수화 Co-authored-by: Soyi Jeon * refactor: 명세에 맞게 수정 Co-authored-by: Soyi Jeon Co-authored-by: Soyi Jeon --- frontend/src/api/index.ts | 5 -- frontend/src/components/Header.tsx | 8 +-- frontend/src/components/UserProfile.tsx | 6 +- frontend/src/context/UserContext.tsx | 61 +++++++++++++++++-- frontend/src/mocks/handlers/memberHandlers.js | 19 +++++- frontend/src/pages/LoginPage.tsx | 15 ++--- frontend/src/util/cookie.ts | 4 +- 7 files changed, 86 insertions(+), 32 deletions(-) diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 167768fd..6a9d8095 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -1,13 +1,8 @@ import axios from "axios"; -import { getCookie } from "@/util/cookie"; - -const accessToken = getCookie("accessToken") || ""; const appClient = axios.create({ baseURL: process.env.API_URL, timeout: 3000, }); -appClient.defaults.headers.common["Authorization"] = `Bearer ${accessToken}`; - export default appClient; diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index fbdf3815..b73ae6a5 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -2,9 +2,6 @@ import React, { useContext } from "react"; import { Link, useNavigate } from "react-router-dom"; import styled from "@emotion/styled"; -import Logo from "@/assets/images/logo.png"; - -import { deleteCookie } from "@/util/cookie"; import { UserContext } from "@/context/UserContext"; import IconButton from "./IconButton"; @@ -12,12 +9,11 @@ import SearchIcon from "@/assets/icons/bx-search.svg"; import UserIcon from "@/assets/icons/bx-user.svg"; const Header = () => { - const { setIsLoggedIn } = useContext(UserContext); + const { logout } = useContext(UserContext); const navigate = useNavigate(); const handleLogoutClick = () => { - deleteCookie("accessToken"); - setIsLoggedIn(false); + logout(); }; const handleSearchClick = () => { diff --git a/frontend/src/components/UserProfile.tsx b/frontend/src/components/UserProfile.tsx index 76fe924c..cb9e0988 100644 --- a/frontend/src/components/UserProfile.tsx +++ b/frontend/src/components/UserProfile.tsx @@ -7,7 +7,6 @@ import Pencil from "@/assets/icons/bx-pencil.svg"; import LineButton from "@/components/LineButton"; import UnderlineInput from "@/components/UnderlineInput"; -import { deleteCookie } from "@/util/cookie"; import { UserContext } from "@/context/UserContext"; import { REGEX } from "@/constants"; import { ValueOf } from "@/types"; @@ -27,12 +26,11 @@ const UserProfile = ({ name, email }: UserProfileProp) => { const [mode, setMode] = useState(MODE.NORMAL); const [editName, setEditName] = useState(name); - const { setIsLoggedIn } = useContext(UserContext); + const { logout } = useContext(UserContext); const handleButtonClick = () => { if (mode === MODE.NORMAL) { - deleteCookie("accessToken"); - setIsLoggedIn(false); + logout(); } }; diff --git a/frontend/src/context/UserContext.tsx b/frontend/src/context/UserContext.tsx index 3383112d..edff1506 100644 --- a/frontend/src/context/UserContext.tsx +++ b/frontend/src/context/UserContext.tsx @@ -1,17 +1,70 @@ -import { getCookie } from "@/util/cookie"; import { useState, createContext, PropsWithChildren } from "react"; +import { useQuery } from "react-query"; +import axios from "axios"; + +import { deleteCookie, getCookie, setCookie } from "@/util/cookie"; + +import appClient from "@/api"; + +const COOKIE_KEY = { + ACCESS_TOKEN: "accessToken", +}; interface UserContextType { isLoggedIn: boolean; - setIsLoggedIn: (value: boolean) => void; + login: (accessToken: string, memberId: number) => void; + logout: () => void; + memberId: number | null; +} + +interface UserInfo { + id: number; + username: string; + email: string; } const UserContext = createContext(null!); const UserProvider = ({ children }: PropsWithChildren) => { - const [isLoggedIn, setIsLoggedIn] = useState(!!getCookie("accessToken")); + const [isLoggedIn, setIsLoggedIn] = useState(false); + const [memberId, setMemberId] = useState(null); + + useQuery( + ["memberId"], + () => + axios + .get("/api/v1/members/me", { + headers: { + Authorization: `Bearer ${getCookie(COOKIE_KEY.ACCESS_TOKEN) || ""}`, + }, + }) + .then((response) => response.data), + { + enabled: !!getCookie(COOKIE_KEY.ACCESS_TOKEN), + onSuccess: (data) => { + setMemberId(data.id); + setIsLoggedIn(true); + }, + } + ); + + const login = (accessToken: string, memberId: number) => { + appClient.defaults.headers.common[ + "Authorization" + ] = `Bearer ${accessToken}`; + setCookie(COOKIE_KEY.ACCESS_TOKEN, accessToken); + setIsLoggedIn(true); + setMemberId(memberId); + }; + + const logout = () => { + appClient.defaults.headers.common["Authorization"] = `Bearer `; + deleteCookie(COOKIE_KEY.ACCESS_TOKEN); + setIsLoggedIn(false); + setMemberId(null); + }; - const value = { isLoggedIn, setIsLoggedIn }; + const value = { isLoggedIn, login, logout, memberId }; return {children}; }; diff --git a/frontend/src/mocks/handlers/memberHandlers.js b/frontend/src/mocks/handlers/memberHandlers.js index 0d3d29a1..36339f99 100644 --- a/frontend/src/mocks/handlers/memberHandlers.js +++ b/frontend/src/mocks/handlers/memberHandlers.js @@ -12,10 +12,27 @@ const memberHandlers = [ rest.post("/api/v1/login", (req, res, ctx) => { const { email, password } = req.body; - const result = { accessToken: "accessToken2" }; + const result = { accessToken: "accessToken2", id: 1 }; return res(ctx.status(200), ctx.json(result)); }), + + // 내 정보 조회 + rest.get("/api/v1/members/me", (req, res, ctx) => { + const accessToken = req.headers.headers.authorization.split(" ")[1]; + + if (!accessToken) { + return res(ctx.status(400)); + } + + const result = { + id: 123, + username: "우아한", + email: "woowa@woowa.com", + }; + + return res(ctx.json(result)); + }), ]; export default memberHandlers; diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx index 6ecce032..f90bd178 100644 --- a/frontend/src/pages/LoginPage.tsx +++ b/frontend/src/pages/LoginPage.tsx @@ -13,7 +13,6 @@ import SocialLoginButton from "@/components/SocialLoginButton"; import { UserContext } from "@/context/UserContext"; import appClient from "@/api"; -import { setCookie } from "@/util/cookie"; import { CustomError } from "@/types"; const LoginPage = () => { @@ -21,11 +20,11 @@ const LoginPage = () => { const [password, setPassword] = useState(""); const [emailError, setEmailError] = useState(false); const [passwordError, setPasswordError] = useState(false); - const { setIsLoggedIn } = useContext(UserContext); + const { login } = useContext(UserContext); const navigate = useNavigate(); - const { mutate: login } = useMutation( + const { mutate: requestLogin } = useMutation( () => { return appClient .post(`/login`, { @@ -36,13 +35,7 @@ const LoginPage = () => { }, { onSuccess: (data) => { - appClient.defaults.headers.common[ - "Authorization" - ] = `Bearer ${data.accessToken}`; - setCookie("accessToken", data.accessToken); - - setIsLoggedIn(true); - + login(data.accessToken, data.id); navigate(`/`, { replace: true }); }, onError: (error) => { @@ -67,7 +60,7 @@ const LoginPage = () => { return; } - login(); + requestLogin(); }; return ( diff --git a/frontend/src/util/cookie.ts b/frontend/src/util/cookie.ts index b1603fdc..66efe2cc 100644 --- a/frontend/src/util/cookie.ts +++ b/frontend/src/util/cookie.ts @@ -1,5 +1,7 @@ const setCookie = (name: string, value: string) => { - document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`; + document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent( + value + )}; max-age=3600 `; }; const getCookie = (name: string) => { From 2cc0e8b403934941e339d1564e739f07c24319d2 Mon Sep 17 00:00:00 2001 From: Soyi Jeon Date: Fri, 29 Jul 2022 22:12:52 +0900 Subject: [PATCH 15/62] =?UTF-8?q?fix:=20=EC=9E=90=EB=8F=99=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#214)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: UserContext 내 상태 초기값 변경 * fix: UserContext 자동 로그인 성공 시 header 옵션 추가 appClient header의 Authorization을 accessToken 쿠키값으로 설정 --- frontend/src/context/UserContext.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/frontend/src/context/UserContext.tsx b/frontend/src/context/UserContext.tsx index edff1506..73d1e708 100644 --- a/frontend/src/context/UserContext.tsx +++ b/frontend/src/context/UserContext.tsx @@ -26,7 +26,9 @@ interface UserInfo { const UserContext = createContext(null!); const UserProvider = ({ children }: PropsWithChildren) => { - const [isLoggedIn, setIsLoggedIn] = useState(false); + const accessTokenCookie = getCookie(COOKIE_KEY.ACCESS_TOKEN); + + const [isLoggedIn, setIsLoggedIn] = useState(!!accessTokenCookie); const [memberId, setMemberId] = useState(null); useQuery( @@ -35,15 +37,19 @@ const UserProvider = ({ children }: PropsWithChildren) => { axios .get("/api/v1/members/me", { headers: { - Authorization: `Bearer ${getCookie(COOKIE_KEY.ACCESS_TOKEN) || ""}`, + Authorization: `Bearer ${accessTokenCookie || ""}`, }, }) .then((response) => response.data), { - enabled: !!getCookie(COOKIE_KEY.ACCESS_TOKEN), + enabled: !!accessTokenCookie, onSuccess: (data) => { setMemberId(data.id); setIsLoggedIn(true); + + appClient.defaults.headers.common[ + "Authorization" + ] = `Bearer ${accessTokenCookie}`; }, } ); From 775276d366cd61fe7d1176d5cafe247c6be05e40 Mon Sep 17 00:00:00 2001 From: SunHo Park <67692759+prefer2@users.noreply.github.com> Date: Sun, 31 Jul 2022 14:23:51 +0900 Subject: [PATCH 16/62] =?UTF-8?q?feat:=20=EC=A0=84=EC=B2=B4=20=ED=8C=80=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?(#218)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: useIntersect 구현 * feat: 명세에 맞도록 mocking코드 수정 및 dummy data 수정 * feat: totalTeams infinite scroll 구현 * feat: 전체 모임 검색 기능 추가 * feat: 전체 팀 paging 단위 상수화 * refactor: 마지막 페이지 명시 및 불필요한 매개변수 제거 * refactor: 변수명 변경 * refactor: 불필요한 return 제거 --- frontend/src/constants/index.ts | 4 +- frontend/src/hooks/useIntersect.tsx | 36 ++++++++++ frontend/src/mocks/dummy/totalTeams.json | 76 +++++++++++++++++++- frontend/src/mocks/handlers/teamHandlers.js | 12 +++- frontend/src/pages/TeamSearchPage.tsx | 77 +++++++++++++++------ 5 files changed, 179 insertions(+), 26 deletions(-) create mode 100644 frontend/src/hooks/useIntersect.tsx diff --git a/frontend/src/constants/index.ts b/frontend/src/constants/index.ts index fa6cf6c9..d240aaa6 100644 --- a/frontend/src/constants/index.ts +++ b/frontend/src/constants/index.ts @@ -5,4 +5,6 @@ const REGEX = { TEAM_NAME: /^[가-힣a-zA-Z\d~!@#$%^&*()+_\-\s]{1,20}$/, }; -export { REGEX }; +const TOTAL_TEAMS_PAGING_COUNT = 5; + +export { REGEX, TOTAL_TEAMS_PAGING_COUNT }; diff --git a/frontend/src/hooks/useIntersect.tsx b/frontend/src/hooks/useIntersect.tsx new file mode 100644 index 00000000..1b876435 --- /dev/null +++ b/frontend/src/hooks/useIntersect.tsx @@ -0,0 +1,36 @@ +import { useEffect, useRef } from "react"; + +type IntersectHandler = ( + entry: IntersectionObserverEntry, + observer: IntersectionObserver +) => void; + +const useIntersect = ( + onIntersect: IntersectHandler, + options?: IntersectionObserverInit +) => { + const ref = useRef(null); + const callback = ( + entries: IntersectionObserverEntry[], + observer: IntersectionObserver + ) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + onIntersect(entry, observer); + } + }); + }; + + useEffect(() => { + if (!ref.current) { + return; + } + const observer = new IntersectionObserver(callback, options); + observer.observe(ref.current); + return () => observer.disconnect(); + }, [ref, options, callback]); + + return ref; +}; + +export default useIntersect; diff --git a/frontend/src/mocks/dummy/totalTeams.json b/frontend/src/mocks/dummy/totalTeams.json index 142517f7..c7d517e0 100644 --- a/frontend/src/mocks/dummy/totalTeams.json +++ b/frontend/src/mocks/dummy/totalTeams.json @@ -74,7 +74,7 @@ }, { "id": 10, - "name": "우테코 4기", + "name": "테스트", "description": "우아한테크코스 4기 프론트앤드와 백엔드 모임입니다.", "emoji": "😀", "color": "#C5FF98", @@ -82,7 +82,7 @@ }, { "id": 11, - "name": "우테코 4기", + "name": "테스트2", "description": "우아한테크코스 4기 프론트앤드와 백엔드 모임입니다.", "emoji": "😀", "color": "#C5FF98", @@ -90,6 +90,78 @@ }, { "id": 12, + "name": "테테테테테스트", + "description": "우아한테크코스 4기 프론트앤드와 백엔드 모임입니다.", + "emoji": "😀", + "color": "#C5FF98", + "joined": true + }, + { + "id": 13, + "name": "텟스트", + "description": "우아한테크코스 4기 프론트앤드와 백엔드 모임입니다.", + "emoji": "😀", + "color": "#C5FF98", + "joined": true + }, + { + "id": 14, + "name": "테스스스슷트", + "description": "우아한테크코스 4기 프론트앤드와 백엔드 모임입니다.", + "emoji": "😀", + "color": "#C5FF98", + "joined": true + }, + { + "id": 15, + "name": "우테코 4기", + "description": "우아한테크코스 4기 프론트앤드와 백엔드 모임입니다.", + "emoji": "😀", + "color": "#C5FF98", + "joined": true + }, + { + "id": 16, + "name": "우테코 4기", + "description": "우아한테크코스 4기 프론트앤드와 백엔드 모임입니다.", + "emoji": "😀", + "color": "#C5FF98", + "joined": true + }, + { + "id": 17, + "name": "우테코 4기", + "description": "우아한테크코스 4기 프론트앤드와 백엔드 모임입니다.", + "emoji": "😀", + "color": "#C5FF98", + "joined": true + }, + { + "id": 18, + "name": "우테코 4기", + "description": "우아한테크코스 4기 프론트앤드와 백엔드 모임입니다.", + "emoji": "😀", + "color": "#C5FF98", + "joined": true + }, + { + "id": 19, + "name": "우테코 4기", + "description": "우아한테크코스 4기 프론트앤드와 백엔드 모임입니다.", + "emoji": "😀", + "color": "#C5FF98", + "joined": true + }, + { + "id": 20, + "name": "우테코 4기", + "description": "우아한테크코스 4기 프론트앤드와 백엔드 모임입니다.", + "emoji": "😀", + "color": "#C5FF98", + "joined": true + }, + { + "id": 21, "name": "우테코 4기", "description": "우아한테크코스 4기 프론트앤드와 백엔드 모임입니다.", "emoji": "😀", diff --git a/frontend/src/mocks/handlers/teamHandlers.js b/frontend/src/mocks/handlers/teamHandlers.js index c17686d6..cccb1188 100644 --- a/frontend/src/mocks/handlers/teamHandlers.js +++ b/frontend/src/mocks/handlers/teamHandlers.js @@ -1,3 +1,4 @@ +import { raw } from "@storybook/react"; import { rest } from "msw"; import myTeamsDummy from "../dummy/myTeams.json"; @@ -31,9 +32,18 @@ const teamHandlers = [ // 전체 모임 조회 rest.get("/api/v1/teams", (req, res, ctx) => { const accessToken = req.headers.headers.authorization; + const keyword = req.url.searchParams.get("keyword"); + const page = +req.url.searchParams.get("page"); + const count = +req.url.searchParams.get("count"); + + const keywordTeam = totalTeams.filter((team) => + team.name.includes(keyword) + ); const result = { - teams: totalTeams, + totalCount: keywordTeam.length, + currentPage: Number(page), + teams: keywordTeam.slice((page - 1) * count, (page - 1) * count + count), }; return res(ctx.json(result)); diff --git a/frontend/src/pages/TeamSearchPage.tsx b/frontend/src/pages/TeamSearchPage.tsx index 1076efc9..6530a9d8 100644 --- a/frontend/src/pages/TeamSearchPage.tsx +++ b/frontend/src/pages/TeamSearchPage.tsx @@ -1,18 +1,18 @@ import React, { useState } from "react"; import styled from "@emotion/styled"; import { useNavigate } from "react-router-dom"; -import { useQuery } from "react-query"; +import { useInfiniteQuery } from "react-query"; import axios from "axios"; +import useIntersect from "@/hooks/useIntersect"; + import SearchInput from "@/components/SearchInput"; import TeamCard from "@/components/TeamCard"; import appClient from "@/api"; import { CustomError } from "@/types"; +import { TOTAL_TEAMS_PAGING_COUNT } from "@/constants"; -interface TotalTeamListResponse { - teams: Team[]; -} interface Team { id: number; name: string; @@ -24,20 +24,50 @@ interface Team { const TeamSearch = () => { const [searchKeyword, setSearchKeyword] = useState(""); const navigate = useNavigate(); + const ref = useIntersect( + async (entry, observer) => { + observer.unobserve(entry.target); + if (hasNextPage && !isFetching) { + fetchNextPage(); + } + }, + { rootMargin: "10px", threshold: 1.0 } + ); + + const fetchTeams = + (keyword: string) => + async ({ pageParam = 1 }) => { + const data = appClient + .get( + `teams?keyword=${keyword}&page=${pageParam}&count=${TOTAL_TEAMS_PAGING_COUNT}` + ) + .then((response) => response.data); + return data; + }; const { - isLoading, - isError, - error: getTotalTeamsError, data: totalTeamResponse, - } = useQuery(["total-teams"], () => - appClient.get(`/teams`).then((response) => response.data) - ); + error: getTotalTeamsError, + fetchNextPage, + hasNextPage, + isFetching, + isError, + isLoading, + refetch, + } = useInfiniteQuery(["projects"], fetchTeams(searchKeyword), { + getNextPageParam: (lastPage) => { + if ( + lastPage.currentPage * TOTAL_TEAMS_PAGING_COUNT < + lastPage.totalCount + ) { + return lastPage.currentPage + 1; + } + }, + }); const handleSearchClick: React.MouseEventHandler = (e) => { - // 키워드로 api call 하기 e.preventDefault(); - console.log(searchKeyword); + refetch(); }; const handleSearchChange: React.ChangeEventHandler = ( @@ -75,16 +105,19 @@ const TeamSearch = () => { /> - {totalTeamResponse.teams.map((team) => ( - { - handleTeamCardClick(team.id); - }} - > - {team.name} - - ))} + {totalTeamResponse.pages.map((page) => + page.teams.map((team: Team) => ( + { + handleTeamCardClick(team.id); + }} + > + {team.name} + + )) + )} +
    ); From 9fc62617b637af1731f9238f2b976326823433d1 Mon Sep 17 00:00:00 2001 From: SunHo Park <67692759+prefer2@users.noreply.github.com> Date: Sun, 31 Jul 2022 14:28:04 +0900 Subject: [PATCH 17/62] =?UTF-8?q?feat:=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EB=A1=9C=EC=A7=81=20=EC=9E=91=EC=84=B1=20?= =?UTF-8?q?(#215)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: Message type 수정 * feat: 메시지 작성 로직 추가 * refactor: 절대경로로 수정 * feat: 메시지 색상 추가 * feat: color 상수화 * feat: 메시지 작성자에게 수정, 삭제 버튼 보이도록 수정. - 메시지 삭제 기능 추가 * feat: 메시지 수정 로직 추가 * refactor: editMessageId 초기값 수정 * refactor: 함수명 변경 * refactor: prop명 수정 * refactor: messageForm initColor 지정 Co-authored-by: Soyi Jeon --- frontend/src/components/LetterPaper.tsx | 125 ++++++++++++++++-- .../src/components/MessageColorPicker.tsx | 38 ++---- frontend/src/components/MessageForm.tsx | 54 +++----- frontend/src/components/MessageTextArea.tsx | 10 +- .../src/components/RollingpaperMessage.tsx | 102 +++++++++++++- frontend/src/constants/index.ts | 13 +- frontend/src/mocks/dummy/rollingpapers.json | 46 ++++--- frontend/src/pages/TeamCreationPage.tsx | 14 +- frontend/src/types/index.ts | 1 + 9 files changed, 300 insertions(+), 103 deletions(-) diff --git a/frontend/src/components/LetterPaper.tsx b/frontend/src/components/LetterPaper.tsx index 85ea3f50..fddcaeb3 100644 --- a/frontend/src/components/LetterPaper.tsx +++ b/frontend/src/components/LetterPaper.tsx @@ -1,14 +1,22 @@ import React, { useState, useEffect } from "react"; -import { Link } from "react-router-dom"; +import { useParams } from "react-router-dom"; +import { useMutation } from "react-query"; +import axios from "axios"; import styled from "@emotion/styled"; import IconButton from "@components/IconButton"; import MessageForm from "@/components/MessageForm"; import RollingpaperMessage from "@components/RollingpaperMessage"; -import { Message } from "@/types"; + +import appClient from "@/api"; +import { Message, CustomError } from "@/types"; import PencilIcon from "@/assets/icons/bx-pencil.svg"; import { divideArrayByIndexRemainder } from "@/util"; +import { useSnackbar } from "@/context/SnackbarContext"; +import { COLORS } from "@/constants"; + +const INIT_COLOR = COLORS.YELLOW; interface LetterPaperProp { to: string; @@ -17,9 +25,58 @@ interface LetterPaperProp { const LetterPaper = ({ to, messageList }: LetterPaperProp) => { const [writeNewMessage, setWriteNewMessage] = useState(false); + const [editMessageId, setEditMessageId] = useState(null); const [slicedMessageLists, setSlicedMessageLists] = useState( Array.from(Array(4), () => []) ); + const [content, setContent] = useState(""); + const [color, setColor] = useState(INIT_COLOR); + + const { rollingpaperId } = useParams(); + const { openSnackbar } = useSnackbar(); + + const { mutate: updateMessage } = useMutation( + ({ content }: Pick) => { + return appClient + .put(`/rollingpapers/${rollingpaperId}/messages/${editMessageId}`, { + content, + }) + .then((response) => response.data); + }, + { + onSuccess: () => { + openSnackbar("메시지 수정 완료"); + }, + onError: (error) => { + if (axios.isAxiosError(error) && error.response) { + const customError = error.response.data as CustomError; + alert(customError.message); + } + }, + } + ); + + const { mutate: createMessage } = useMutation( + ({ content, color }: Pick) => { + return appClient + .post(`/rollingpapers/${rollingpaperId}/messages`, { + content, + color, + }) + .then((response) => response.data); + }, + { + onSuccess: () => { + openSnackbar("메시지 작성 완료"); + }, + onError: (error) => { + if (axios.isAxiosError(error) && error.response) { + const customError = error.response.data as CustomError; + alert(customError.message); + } + }, + } + ); const handleMessageWriteButtonClick: React.MouseEventHandler< HTMLButtonElement @@ -42,8 +99,33 @@ const LetterPaper = ({ to, messageList }: LetterPaperProp) => { setSlicedMessageLists(newSlicedMessageList); }; - const hideMessageForm = () => { + const submitMessageForm = () => { + if (!writeNewMessage && editMessageId) { + updateMessage({ content }); + } + if (writeNewMessage) { + createMessage({ content, color }); + } + + setContent(""); + setColor(INIT_COLOR); setWriteNewMessage(false); + setEditMessageId(null); + }; + + const cancelMessageWrite = () => { + if (confirm("메시지 작성을 취소하시겠습니까?")) { + setContent(""); + setColor(INIT_COLOR); + setWriteNewMessage(false); + setEditMessageId(null); + } + }; + + const handleMessageChange: React.ChangeEventHandler = ( + e + ) => { + setContent(e.target.value); }; useEffect(() => { @@ -73,16 +155,43 @@ const LetterPaper = ({ to, messageList }: LetterPaperProp) => { {slicedMessageLists.map((messageList, index) => ( {index === 0 && writeNewMessage && ( - + )} - {messageList.map((message) => ( - + {messageList.map((message) => { + if (!writeNewMessage && editMessageId === message.id) { + return ( + + ); + } + return ( - - ))} + ); + })} ))} diff --git a/frontend/src/components/MessageColorPicker.tsx b/frontend/src/components/MessageColorPicker.tsx index 1e200d57..3260970a 100644 --- a/frontend/src/components/MessageColorPicker.tsx +++ b/frontend/src/components/MessageColorPicker.tsx @@ -1,49 +1,35 @@ -import React, { useState, SetStateAction } from "react"; +import React, { SetStateAction } from "react"; import styled from "@emotion/styled"; - -interface Radio { - id: number; - value: string; -} +import { COLORS } from "@/constants"; interface ColorPickerProps extends React.InputHTMLAttributes { - radios: Radio[]; - initialSelectedId: number; onClickRadio: React.Dispatch>; + color: string; } interface StyledRadioProps { - backgroundColor?: string; + backgroundColor: string; } -const MessageColorPicker = ({ - radios, - initialSelectedId, - onClickRadio, -}: ColorPickerProps) => { - const [selectedItemId, setSelectedItemId] = useState( - initialSelectedId - ); - - const handleRadioChange = (key: number, value: string) => { - setSelectedItemId(key); +const MessageColorPicker = ({ onClickRadio, color }: ColorPickerProps) => { + const handleRadioChange = (value: string) => { onClickRadio(value); }; return ( - {radios.map((radio) => { + {Object.values(COLORS).map((radio) => { return ( -