diff --git a/package-lock.json b/package-lock.json index 7a2b1f6..31b98b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@types/node": "20.4.3", "@types/react": "18.2.15", "@types/react-dom": "18.2.7", + "dayjs": "1.11.9", "emotion-reset": "^3.0.1", "eslint-config-next": "13.4.12", "next": "13.4.12", @@ -1391,6 +1392,11 @@ "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==" }, + "node_modules/dayjs": { + "version": "1.11.9", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz", + "integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -5827,6 +5833,11 @@ "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==" }, + "dayjs": { + "version": "1.11.9", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz", + "integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==" + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", diff --git a/package.json b/package.json index e10fef7..d11df19 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@types/node": "20.4.3", "@types/react": "18.2.15", "@types/react-dom": "18.2.7", + "dayjs": "1.11.9", "emotion-reset": "^3.0.1", "eslint-config-next": "13.4.12", "next": "13.4.12", diff --git a/src/components/button/Button.tsx b/src/components/button/Button.tsx index fc7bdbd..74c34e5 100644 --- a/src/components/button/Button.tsx +++ b/src/components/button/Button.tsx @@ -23,6 +23,7 @@ const Button = ({ fullWidth = false, loading = false, onClick, + className, danger = false, // 버튼을 빨간색으로 표시 ...props }: Props) => { @@ -50,7 +51,7 @@ const Button = ({ icon={icon} onMouseDown={handleMouseDown} onMouseUp={handleMouseUp} - className={isActive ? "active" : ""} + className={isActive ? `active ${className}` : className} > {loading && } {icon} diff --git a/src/components/dropdown/Dropdown.tsx b/src/components/dropdown/Dropdown.tsx index 974d701..f54bb00 100644 --- a/src/components/dropdown/Dropdown.tsx +++ b/src/components/dropdown/Dropdown.tsx @@ -11,6 +11,7 @@ interface Props extends HTMLAttributes { placeHolder?: string; children: React.ReactNode; onSelectValueChange?: (value: string) => void; + selectContainerMaxHeight?: number; disabled?: boolean; } @@ -18,9 +19,10 @@ const Dropdown = ({ children, label, value, + onSelectValueChange, + selectContainerMaxHeight, name, placeHolder = "Select an option", - onSelectValueChange, disabled = false, }: Props) => { const [isOpen, setIsOpen] = useState(false); @@ -74,7 +76,11 @@ const Dropdown = ({ }, [children, selectedOptionValue, onSelectValueChange, value]); return ( - + {label && {label}}
{selectedOptionValue ? ( @@ -114,7 +120,11 @@ const Dropdown = ({ Dropdown.Option = DropdownOption; export default Dropdown; -const EmotionWrapper = styled.div<{ isOpen: boolean; isCompleted: boolean }>` +const EmotionWrapper = styled.div<{ + isOpen: boolean; + isCompleted: boolean; + selectContainerMaxHeight?: number; +}>` position: relative; font-size: 14px; width: 100%; @@ -155,7 +165,7 @@ const EmotionWrapper = styled.div<{ isOpen: boolean; isCompleted: boolean }>` top: 120%; left: 0; width: 100%; - overflow-y: auto; + overflow-y: ${({ selectContainerMaxHeight }) => (selectContainerMaxHeight ? "scroll" : "auto")}; border: 0.5px solid ${({ theme }) => theme.color.gray500}; border-radius: 6px; background-color: white; @@ -167,6 +177,9 @@ const EmotionWrapper = styled.div<{ isOpen: boolean; isCompleted: boolean }>` opacity 0.3s ease-in-out, height 0.3s ease-in-out; + ${({ selectContainerMaxHeight }) => + selectContainerMaxHeight && `max-height: ${selectContainerMaxHeight}px`}; + z-index: 10; max-height: 15vh; overflow: auto; } diff --git a/src/components/header/HeaderDesktop.tsx b/src/components/header/HeaderDesktop.tsx index 64a4699..892f352 100644 --- a/src/components/header/HeaderDesktop.tsx +++ b/src/components/header/HeaderDesktop.tsx @@ -1,9 +1,7 @@ import styled from "@emotion/styled"; import ProfileImage from "components/profileImage/ProfileImage"; -interface Props {} - -const HeaderDesktop = ({}: Props) => { +const HeaderDesktop = () => { return (
diff --git a/src/components/header/HeaderMobile.tsx b/src/components/header/HeaderMobile.tsx index 66f970b..bf53ee1 100644 --- a/src/components/header/HeaderMobile.tsx +++ b/src/components/header/HeaderMobile.tsx @@ -2,9 +2,7 @@ import styled from "@emotion/styled"; import ProfileImage from "components/profileImage/ProfileImage"; import Image from "next/image"; -interface Props {} - -const HeaderMobile = ({}: Props) => { +const HeaderMobile = () => { return ( 왼쪽 화살표 아이콘 diff --git a/src/components/header/Hero.tsx b/src/components/header/Hero.tsx index db98af0..2532977 100644 --- a/src/components/header/Hero.tsx +++ b/src/components/header/Hero.tsx @@ -1,9 +1,7 @@ import styled from "@emotion/styled"; import { LAYOUT_MARGIN } from "constant/layoutMargin"; -interface Props {} - -const Hero = ({}: Props) => { +const Hero = () => { return (
diff --git a/src/components/icons/IconEmptyOutlined.tsx b/src/components/icons/IconEmptyOutlined.tsx new file mode 100644 index 0000000..041e016 --- /dev/null +++ b/src/components/icons/IconEmptyOutlined.tsx @@ -0,0 +1,15 @@ +const IconEmptyOutlined = () => { + return ( + + + + ); +}; + +export default IconEmptyOutlined; diff --git a/src/components/icons/IconWarningOutlined.tsx b/src/components/icons/IconWarningOutlined.tsx new file mode 100644 index 0000000..21516bd --- /dev/null +++ b/src/components/icons/IconWarningOutlined.tsx @@ -0,0 +1,15 @@ +const IconWarningOutlined = () => { + return ( + + + + ); +}; + +export default IconWarningOutlined; diff --git a/src/components/inputs/TextInput/TextInput.tsx b/src/components/inputs/TextInput/TextInput.tsx index 33318fb..49763fc 100644 --- a/src/components/inputs/TextInput/TextInput.tsx +++ b/src/components/inputs/TextInput/TextInput.tsx @@ -75,6 +75,11 @@ const TextInput: React.FC = ({ } }, [enteredValue, status, onTextChange]); + useEffect(() => { + // 비동기 API 호출로 인한 value 변경 시에만 실행 + setEnteredValue(value); + }, [value]); + return ( {label && {label}} diff --git a/src/components/navbar/Navbar.tsx b/src/components/navbar/Navbar.tsx index ebf03ff..83d7e00 100644 --- a/src/components/navbar/Navbar.tsx +++ b/src/components/navbar/Navbar.tsx @@ -2,9 +2,7 @@ import styled from "@emotion/styled"; import { LAYOUT_MARGIN } from "constant/layoutMargin"; import { NAVBAR_HEIGHT } from "constant/navbarHeight"; -interface Props {} - -const Navbar = ({}: Props) => { +const Navbar = () => { return Navbar; }; diff --git a/src/constant/link.ts b/src/constant/link.ts new file mode 100644 index 0000000..51d19e7 --- /dev/null +++ b/src/constant/link.ts @@ -0,0 +1,22 @@ +/** + * 이 파일에서는 링크 경로를 관리합니다. + * autoComplete 및 검색에 도움이 되도록 `LINK_` 로 시작하는 이름을 사용합니다. + */ + +// 메인 페이지 경로 +export const LINK_MAIN_PAGE = "/"; + +// 로그인 페이지 경로 +export const LINK_LOGIN_PAGE = "/login"; + +// 마이 페이지 +export const LINK_MYPAGE = "/mypage"; + +// 내 정보 수정 페이지 +export const LINK_MYPAGE_EDIT = "/mypage/edit"; + +// 계정 탈퇴 페이지 +export const LINK_ACCOUNT_DELETE = "/mypage/delete"; + +// 계정 탈퇴 성공 시 이동할 경로 +export const LINK_ACCOUNT_DELETE_SUCCESS = "/mypage/delete/success"; diff --git a/src/feature/.gitkeep b/src/feature/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/feature/mypage/common/constants/genderLookupTable.ts b/src/feature/mypage/common/constants/genderLookupTable.ts new file mode 100644 index 0000000..db93284 --- /dev/null +++ b/src/feature/mypage/common/constants/genderLookupTable.ts @@ -0,0 +1,5 @@ +export const GENDER_LOOKUP_TABLE = { + M: "남성", + F: "여성", + U: "선택 안함", +}; diff --git a/src/feature/mypage/common/mockup/MockupMypage.tsx b/src/feature/mypage/common/mockup/MockupMypage.tsx new file mode 100644 index 0000000..e3c3e31 --- /dev/null +++ b/src/feature/mypage/common/mockup/MockupMypage.tsx @@ -0,0 +1,8 @@ +import { TUser } from "feature/mypage/mypage.edit/types/TMypageFormValues"; + +export const MOCKUP_MYPAGE_INITIAL_VALUES: TUser = { + nickname: "홍길동", + introduction: "안녕하세요. 저는 홍길동입니다.", + birthYear: "1990", + gender: "M", +}; diff --git a/src/feature/mypage/mypage.delete/components/MypageDeleteAlert.tsx b/src/feature/mypage/mypage.delete/components/MypageDeleteAlert.tsx new file mode 100644 index 0000000..7c7454e --- /dev/null +++ b/src/feature/mypage/mypage.delete/components/MypageDeleteAlert.tsx @@ -0,0 +1,51 @@ +import styled from "@emotion/styled"; +import IconWarningOutlined from "components/icons/IconWarningOutlined"; + +const MypageDeleteAlert = () => { + return ( + + + +

회원 탈퇴 시, 아래의 정보가 삭제됩니다.

+
    +
  • 회원 정보
  • +
  • 내가 속한 단체의 소속상태
  • +
+

+ 단, 내가 생성한 맛집, 투표, 모임 정보 등은 유지되며, 생성자의 정보에 '탈퇴한 유저' + 라고 표시됩니다.{" "} +

+
+ ); +}; + +export default MypageDeleteAlert; + +const EmotionWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + + .warning-title { + font-size: 16px; + font-weight: 700; + margin-top: 32px; + margin-bottom: 24px; + } + + .deleted-item { + margin-bottom: 4px; + + &:before { + content: "•"; + margin-right: 8px; + } + } + + .warning-disclaimer { + font-size: 12px; + margin-top: 32px; + color: ${({ theme }) => theme.color.gray400}; + line-height: 1.5; + } +`; diff --git a/src/feature/mypage/mypage.delete/components/MypageDeleteConfirmForm.tsx b/src/feature/mypage/mypage.delete/components/MypageDeleteConfirmForm.tsx new file mode 100644 index 0000000..334d370 --- /dev/null +++ b/src/feature/mypage/mypage.delete/components/MypageDeleteConfirmForm.tsx @@ -0,0 +1,58 @@ +import styled from "@emotion/styled"; +import Button from "components/button/Button"; +import TextInput from "components/inputs/TextInput/TextInput"; +import { LINK_ACCOUNT_DELETE_SUCCESS } from "constant/link"; +import { validateDeleteConfirmMessage } from "feature/mypage/mypage.delete/functions/validateDeleteConfirmMessage"; +import { TTextInputItem } from "feature/mypage/mypage.delete/types/TTextInputItem"; +import { useRouter } from "next/router"; +import { useCallback, useState } from "react"; + +const MypageDeleteConfirmForm = () => { + const { push } = useRouter(); + + const [confirmFormValues, setConfirmFormValues] = useState({ + value: "", + isValid: false, + }); + + const handleChangeConfirmValue = useCallback((value: string, isValid: boolean) => { + setConfirmFormValues({ + value, + isValid, + }); + }, []); + + const handleSubmitDeleteAccount = () => { + // TODO: Modal, toast 등으로 피드백 처리 + if (!confirmFormValues.isValid) { + alert("탈퇴 메시지를 확인해주세요"); + return; + } + // TODO: API 연동 , 여기서 실제 탈퇴 API 호출 후 성공 시 아래 링크로 이동 + push(LINK_ACCOUNT_DELETE_SUCCESS); + }; + + return ( + + + + + ); +}; + +export default MypageDeleteConfirmForm; + +const EmotionWrapper = styled.div` + margin-top: 64px; + + button { + margin-top: 32px; + } +`; diff --git a/src/feature/mypage/mypage.delete/functions/validateDeleteConfirmMessage.ts b/src/feature/mypage/mypage.delete/functions/validateDeleteConfirmMessage.ts new file mode 100644 index 0000000..8261789 --- /dev/null +++ b/src/feature/mypage/mypage.delete/functions/validateDeleteConfirmMessage.ts @@ -0,0 +1,7 @@ +export const validateDeleteConfirmMessage = { + condition: (value: string): boolean => { + if (value !== "탈퇴") return false; + return true; + }, + messageOnError: "'탈퇴' 를 입력해주세요.", +}; diff --git a/src/feature/mypage/mypage.delete/types/TTextInputItem.tsx b/src/feature/mypage/mypage.delete/types/TTextInputItem.tsx new file mode 100644 index 0000000..18c9dac --- /dev/null +++ b/src/feature/mypage/mypage.delete/types/TTextInputItem.tsx @@ -0,0 +1,5 @@ +// TODO: 추후 공통 폴더로 이동하면 좋을 것 같음. +export type TTextInputItem = { + value: string; + isValid: boolean; +}; diff --git a/src/feature/mypage/mypage.delete/views/ViewMypageDelete.tsx b/src/feature/mypage/mypage.delete/views/ViewMypageDelete.tsx new file mode 100644 index 0000000..40687d0 --- /dev/null +++ b/src/feature/mypage/mypage.delete/views/ViewMypageDelete.tsx @@ -0,0 +1,16 @@ +import styled from "@emotion/styled"; +import MypageDeleteAlert from "feature/mypage/mypage.delete/components/MypageDeleteAlert"; +import MypageDeleteConfirmForm from "feature/mypage/mypage.delete/components/MypageDeleteConfirmForm"; + +const ViewMypageDelete = () => { + return ( + + + + + ); +}; + +export default ViewMypageDelete; + +const EmotionWrapper = styled.div``; diff --git a/src/feature/mypage/mypage.delete/views/ViewMypageDeleteSuccess.tsx b/src/feature/mypage/mypage.delete/views/ViewMypageDeleteSuccess.tsx new file mode 100644 index 0000000..21fbc47 --- /dev/null +++ b/src/feature/mypage/mypage.delete/views/ViewMypageDeleteSuccess.tsx @@ -0,0 +1,36 @@ +import styled from "@emotion/styled"; +import Button from "components/button/Button"; +import { LINK_MAIN_PAGE } from "constant/link"; +import Link from "next/link"; + +const ViewMypageDeleteSuccess = () => { + return ( + +

회원탈퇴가 완료되었습니다.

+ + + +
+ ); +}; + +export default ViewMypageDeleteSuccess; + +const EmotionWrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + + width: 100%; + margin-top: 200px; + + h1 { + font-size: 20px; + font-weight: 700; + } + + button { + margin-top: 100px; + } +`; diff --git a/src/feature/mypage/mypage.edit/constants/GenderDropdownValueList.ts b/src/feature/mypage/mypage.edit/constants/GenderDropdownValueList.ts new file mode 100644 index 0000000..aa1813c --- /dev/null +++ b/src/feature/mypage/mypage.edit/constants/GenderDropdownValueList.ts @@ -0,0 +1,14 @@ +export const GENDER_DROPDOWN_VALUE_LIST = [ + { + value: "F", + label: "여성", + }, + { + value: "M", + label: "남성", + }, + { + value: "U", + label: "선택안함", + }, +]; diff --git a/src/feature/mypage/mypage.edit/constants/birthYearDropdownValues.ts b/src/feature/mypage/mypage.edit/constants/birthYearDropdownValues.ts new file mode 100644 index 0000000..cb83523 --- /dev/null +++ b/src/feature/mypage/mypage.edit/constants/birthYearDropdownValues.ts @@ -0,0 +1,8 @@ +// create an array of birth year dropdown values from 1900 to current year - 10 +export const BIRTH_YEAR_DROPDOWN_VALUE_LIST = Array.from( + { length: new Date().getFullYear() - 1900 - 10 }, + (_, i) => ({ + value: String(new Date().getFullYear() - i - 10), + label: String(new Date().getFullYear() - i - 10), + }) +); diff --git a/src/feature/mypage/mypage.edit/types/TMypageFormValues.ts b/src/feature/mypage/mypage.edit/types/TMypageFormValues.ts new file mode 100644 index 0000000..452237d --- /dev/null +++ b/src/feature/mypage/mypage.edit/types/TMypageFormValues.ts @@ -0,0 +1,20 @@ +type TGender = "M" | "F" | "U"; +type TFormValueItem = { + value: string; + isValid: boolean; +}; + +// TODO: API 응답 형식에 따라 달라질 수 있음 +export type TUser = { + nickname: string; + introduction: string; + birthYear: string; + gender: TGender; +}; + +export type TMypageFormValues = { + nickname: TFormValueItem; + introduction: TFormValueItem; + birthYear: TFormValueItem; + gender: TFormValueItem; +}; diff --git a/src/feature/mypage/mypage.edit/views/ViewMypageEdit.tsx b/src/feature/mypage/mypage.edit/views/ViewMypageEdit.tsx new file mode 100644 index 0000000..4bc3b94 --- /dev/null +++ b/src/feature/mypage/mypage.edit/views/ViewMypageEdit.tsx @@ -0,0 +1,122 @@ +import styled from "@emotion/styled"; +import Button from "components/button/Button"; +import Dropdown from "components/dropdown/Dropdown"; +import TextInput from "components/inputs/TextInput/TextInput"; +import { GENDER_DROPDOWN_VALUE_LIST } from "feature/mypage/mypage.edit/constants/GenderDropdownValueList"; +import { MOCKUP_MYPAGE_INITIAL_VALUES } from "feature/mypage/common/mockup/MockupMypage"; +import { TMypageFormValues } from "feature/mypage/mypage.edit/types/TMypageFormValues"; +import Link from "next/link"; +import { useCallback, useEffect, useState } from "react"; +import { BIRTH_YEAR_DROPDOWN_VALUE_LIST } from "feature/mypage/mypage.edit/constants/birthYearDropdownValues"; +import { LINK_ACCOUNT_DELETE } from "constant/link"; + +const { Option } = Dropdown; + +const ViewMypageEdit = () => { + const [formValues, setFormValues] = useState({ + nickname: { value: "", isValid: false }, + introduction: { value: "", isValid: false }, + birthYear: { value: "", isValid: false }, + gender: { value: "", isValid: false }, + }); + + const handleChangeInput = useCallback( + (name: string) => (value: string, isValid: boolean) => { + setFormValues((prev) => ({ + ...prev, // TODO: ... 연산자가 1레벨까지만 복사한다는 것을 기억하자. + [name]: { value, isValid }, + })); + }, + [] + ); + + const handleChangeNickname = useCallback(handleChangeInput("nickname"), []); + const handleChangeDescription = useCallback(handleChangeInput("introduction"), []); + + const { nickname, introduction, gender, birthYear } = formValues; + + // TODO: 서버 데이터 받아오는 작업 모킹 + useEffect(() => { + const nextFormValues = {} as TMypageFormValues; + + Object.keys(MOCKUP_MYPAGE_INITIAL_VALUES).forEach((key) => { + const value = MOCKUP_MYPAGE_INITIAL_VALUES[key as keyof TMypageFormValues]; + + nextFormValues[key as keyof TMypageFormValues] = { + value, + isValid: true, + }; + }); + setFormValues(nextFormValues); + }, []); + + return ( + +
+ + + + {BIRTH_YEAR_DROPDOWN_VALUE_LIST.map(({ value, label }) => ( + + ))} + + {/* TODO => placeholder 가 적용되지 않는 문제 핵결*/} + + {GENDER_DROPDOWN_VALUE_LIST.map(({ value, label }) => ( + + ))} + +
+ + + 탈퇴하기 + +
+ ); +}; + +export default ViewMypageEdit; + +const EmotionWrapper = styled.div` + .input-container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + row-gap: 32px; + } + + .button-save { + margin-top: 32px; + } + + .button-remove-account { + float: right; + font-size: 12px; + margin-top: 100px; + color: ${({ theme }) => theme.color.danger400}; + } +`; diff --git a/src/feature/mypage/mypage.main/components/MyPageActionButtons.tsx b/src/feature/mypage/mypage.main/components/MyPageActionButtons.tsx new file mode 100644 index 0000000..2e6a541 --- /dev/null +++ b/src/feature/mypage/mypage.main/components/MyPageActionButtons.tsx @@ -0,0 +1,35 @@ +import Link from "next/link"; +import styled from "@emotion/styled"; +import Button from "components/button/Button"; +import { LINK_MAIN_PAGE, LINK_MYPAGE_EDIT } from "constant/link"; + +const MyPageActionButtons = () => { + // TODO: API 연동 후 로그아웃 시 API 바로 호출되도록 수정 + const LOGOUT_REDIRECT_URL = LINK_MAIN_PAGE; + + return ( + + + + + + + + + ); +}; + +export default MyPageActionButtons; + +const EmotionWrapper = styled.div` + float: right; + display: flex; + align-items: flex-end; + flex-direction: column; + + .button-logout { + color: ${({ theme }) => theme.color.danger600}; + } +`; diff --git a/src/feature/mypage/mypage.main/components/MyPageBasicInfo.tsx b/src/feature/mypage/mypage.main/components/MyPageBasicInfo.tsx new file mode 100644 index 0000000..c50c1f6 --- /dev/null +++ b/src/feature/mypage/mypage.main/components/MyPageBasicInfo.tsx @@ -0,0 +1,38 @@ +import styled from "@emotion/styled"; +import dayjs from "dayjs"; +import { GENDER_LOOKUP_TABLE } from "feature/mypage/common/constants/genderLookupTable"; +import { MOCKUP_MYPAGE_INITIAL_VALUES } from "feature/mypage/common/mockup/MockupMypage"; +import MyPageInfoItem from "feature/mypage/mypage.main/components/typography/MyPageInfoItem"; +import MyPageSectionTitle from "feature/mypage/mypage.main/components/typography/MyPageSectionTitle"; + +const MyPageBasicInfo = () => { + const userInfo = MOCKUP_MYPAGE_INITIAL_VALUES; + const { birthYear, gender } = userInfo; + + const age = dayjs().year() - Number(birthYear); + const ageText = `${age}세`; + + const genderText = GENDER_LOOKUP_TABLE[gender] ?? "입력하지 않음"; + + return ( + + 내 정보 +
+ + +
+
+ ); +}; + +export default MyPageBasicInfo; + +const EmotionWrapper = styled.div` + margin-bottom: 48px; + + .item-container { + display: flex; + flex-direction: column; + row-gap: 12px; + } +`; diff --git a/src/feature/mypage/mypage.main/components/MyPageOrganizationInfo.tsx b/src/feature/mypage/mypage.main/components/MyPageOrganizationInfo.tsx new file mode 100644 index 0000000..a4ef789 --- /dev/null +++ b/src/feature/mypage/mypage.main/components/MyPageOrganizationInfo.tsx @@ -0,0 +1,21 @@ +import styled from "@emotion/styled"; +import EmptyOrganizationCTA from "feature/mypage/mypage.main/components/empty/EmptyOrganizationCTA"; +import MyPageSectionTitle from "feature/mypage/mypage.main/components/typography/MyPageSectionTitle"; + +const MyPageOrganizationInfo = () => { + const orgList = []; + const isEmpty = orgList?.length === 0; + + return ( + + 내가 속한 단체 정보 + {isEmpty && } + + ); +}; + +export default MyPageOrganizationInfo; + +const EmotionWrapper = styled.div` + margin-bottom: 64px; +`; diff --git a/src/feature/mypage/mypage.main/components/MyPageProfileInfo.tsx b/src/feature/mypage/mypage.main/components/MyPageProfileInfo.tsx new file mode 100644 index 0000000..861d0ee --- /dev/null +++ b/src/feature/mypage/mypage.main/components/MyPageProfileInfo.tsx @@ -0,0 +1,44 @@ +import styled from "@emotion/styled"; +import ProfileImage from "components/profileImage/ProfileImage"; +import { MOCKUP_MYPAGE_INITIAL_VALUES } from "feature/mypage/common/mockup/MockupMypage"; + +const MyPageProfileInfo = () => { + const userInfo = MOCKUP_MYPAGE_INITIAL_VALUES; + const { nickname, introduction } = userInfo; + + return ( + +
+ +
+

{nickname}

+

{introduction}

+
+
+
+ ); +}; + +export default MyPageProfileInfo; + +const EmotionWrapper = styled.div` + margin-bottom: 32px; + + .profile-section { + display: flex; + column-gap: 16px; + align-items: center; + + .user-name { + font-size: 16px; + font-weight: 700; + color: ${({ theme }) => theme.color.black}; + margin-bottom: 8px; + } + + .user-description { + font-size: 12px; + color: ${({ theme }) => theme.color.gray400}; + } + } +`; diff --git a/src/feature/mypage/mypage.main/components/empty/EmptyOrganizationCTA.tsx b/src/feature/mypage/mypage.main/components/empty/EmptyOrganizationCTA.tsx new file mode 100644 index 0000000..34a6824 --- /dev/null +++ b/src/feature/mypage/mypage.main/components/empty/EmptyOrganizationCTA.tsx @@ -0,0 +1,35 @@ +import styled from "@emotion/styled"; +import Button from "components/button/Button"; +import IconEmptyOutlined from "components/icons/IconEmptyOutlined"; + +const EmptyOrganizationCTA = () => { + return ( + + +

가입된 단체가 없네요. 🥲

+

가입할 단체를 한번 찾아볼까요?

+ +
+ ); +}; + +export default EmptyOrganizationCTA; + +const EmotionWrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + + .cta-first-line { + margin-top: 24px; + } + + .cta-second-line { + margin-top: 8px; + } + + button { + margin-top: 36px; + } +`; diff --git a/src/feature/mypage/mypage.main/components/typography/MyPageInfoItem.tsx b/src/feature/mypage/mypage.main/components/typography/MyPageInfoItem.tsx new file mode 100644 index 0000000..eef7312 --- /dev/null +++ b/src/feature/mypage/mypage.main/components/typography/MyPageInfoItem.tsx @@ -0,0 +1,35 @@ +import styled from "@emotion/styled"; + +interface Props { + label: string; + value: string; +} + +const MyPageInfoItem = ({ label, value }: Props) => { + return ( + +

{label}

+

{value}

+
+ ); +}; + +export default MyPageInfoItem; + +const EmotionWrapper = styled.div` + display: flex; + align-items: center; + column-gap: 16px; + + .label { + font-size: 16px; + color: ${({ theme }) => theme.color.gray400}; + min-width: 60px; + } + + .value { + font-size: 16px; + font-weight: 700; + color: ${({ theme }) => theme.color.black}; + } +`; diff --git a/src/feature/mypage/mypage.main/components/typography/MyPageSectionTitle.tsx b/src/feature/mypage/mypage.main/components/typography/MyPageSectionTitle.tsx new file mode 100644 index 0000000..7d74391 --- /dev/null +++ b/src/feature/mypage/mypage.main/components/typography/MyPageSectionTitle.tsx @@ -0,0 +1,18 @@ +import styled from "@emotion/styled"; +import { ReactNode } from "react"; + +interface Props { + children?: ReactNode; +} + +const MyPageSectionTitle = ({ children }: Props) => { + return {children}; +}; + +export default MyPageSectionTitle; + +const EmotionWrapper = styled.h2` + font-size: 12px; + color: ${({ theme }) => theme.color.gray500}; + margin-bottom: 16px; +`; diff --git a/src/feature/mypage/mypage.main/views/ViewMypage.tsx b/src/feature/mypage/mypage.main/views/ViewMypage.tsx new file mode 100644 index 0000000..472d1c2 --- /dev/null +++ b/src/feature/mypage/mypage.main/views/ViewMypage.tsx @@ -0,0 +1,17 @@ +import MyPageActionButtons from "feature/mypage/mypage.main/components/MyPageActionButtons"; +import MyPageBasicInfo from "feature/mypage/mypage.main/components/MyPageBasicInfo"; +import MyPageOrganizationInfo from "feature/mypage/mypage.main/components/MyPageOrganizationInfo"; +import MyPageProfileInfo from "feature/mypage/mypage.main/components/MyPageProfileInfo"; + +const ViewMypage = () => { + return ( + <> + + + + + + ); +}; + +export default ViewMypage; diff --git a/src/pages/_error.tsx b/src/pages/_error.tsx index 5997381..e01c7e3 100644 --- a/src/pages/_error.tsx +++ b/src/pages/_error.tsx @@ -1,6 +1,7 @@ import Link from "next/link"; import styled from "@emotion/styled"; import { NextPageContext } from "next"; +import { LINK_MAIN_PAGE } from "constant/link"; interface Props { statusCode?: number; @@ -13,7 +14,7 @@ const CustomError = ({ statusCode, title }: Props) => {

{statusCode}

{title}

- 홈으로 + 홈으로
); }; diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx index 31aab46..9fd5fd2 100644 --- a/src/pages/login/index.tsx +++ b/src/pages/login/index.tsx @@ -1,5 +1,6 @@ import styled from "@emotion/styled"; import PageMarker from "components/pageMarker/PageMarker"; +import { LINK_MAIN_PAGE } from "constant/link"; import { GetServerSideProps } from "next"; const PageLogin = () => { @@ -25,7 +26,7 @@ export const getServerSideProps: GetServerSideProps = async ({ req, res, query } const isLoggedIn = req.cookies.accessToken; if (isLoggedIn) { - res.writeHead(302, { Location: "/" }); + res.writeHead(302, { Location: LINK_MAIN_PAGE }); res.end(); } diff --git a/src/pages/mypage/delete/index.tsx b/src/pages/mypage/delete/index.tsx new file mode 100644 index 0000000..e2e6564 --- /dev/null +++ b/src/pages/mypage/delete/index.tsx @@ -0,0 +1,30 @@ +import { LINK_MAIN_PAGE, LINK_MYPAGE } from "constant/link"; +import ViewMypageDelete from "feature/mypage/mypage.delete/views/ViewMypageDelete"; +import { GetServerSideProps } from "next"; + +const PageMyPageDelete = () => { + return ; +}; + +export default PageMyPageDelete; + +export const getServerSideProps: GetServerSideProps = async ({ req, res, query }) => { + /** + * 로그인하지 않은 경우 메인 페이지로 이동 + * 마이페이지로부터 유입되지 않은 경우 메인 페이지로 이동 + */ + + const referer = req.headers.referer; + const isLoggedIn = true; + const isFromMyPage = referer?.includes(LINK_MYPAGE); + const shouldRedirect = !isLoggedIn || !isFromMyPage; + + if (shouldRedirect) { + res.writeHead(302, { Location: LINK_MAIN_PAGE }); + res.end(); + } + + return { + props: {}, + }; +}; diff --git a/src/pages/mypage/delete/success.tsx b/src/pages/mypage/delete/success.tsx new file mode 100644 index 0000000..b5472f9 --- /dev/null +++ b/src/pages/mypage/delete/success.tsx @@ -0,0 +1,30 @@ +import { LINK_ACCOUNT_DELETE, LINK_MAIN_PAGE } from "constant/link"; +import ViewMypageDeleteSuccess from "feature/mypage/mypage.delete/views/ViewMypageDeleteSuccess"; +import { GetServerSideProps } from "next"; + +const PageMyPageDeleteSuccess = () => { + return ; +}; + +export default PageMyPageDeleteSuccess; + +export const getServerSideProps: GetServerSideProps = async ({ req, res, query }) => { + /** + * 로그인하지 않은 경우 메인 페이지로 이동 + * 로그인 되어 있으나, 탈퇴 페이지로부터 유입되지 않은 경우 메인 페이지로 이동 + */ + const referer = req.headers.referer; + const isLoggedIn = false; + const isFromDeletePage = referer?.includes(LINK_ACCOUNT_DELETE); + + const shouldRedirect = !isLoggedIn || !isFromDeletePage; + + if (shouldRedirect) { + res.writeHead(302, { Location: LINK_MAIN_PAGE }); + res.end(); + } + + return { + props: {}, + }; +}; diff --git a/src/pages/mypage/edit.tsx b/src/pages/mypage/edit.tsx new file mode 100644 index 0000000..2a6f6e3 --- /dev/null +++ b/src/pages/mypage/edit.tsx @@ -0,0 +1,25 @@ +import { GetServerSideProps } from "next"; +import ViewMyPageEdit from "feature/mypage/mypage.edit/views/ViewMypageEdit"; +import { LINK_LOGIN_PAGE } from "constant/link"; + +const PageMyPageEdit = () => { + return ; +}; + +export default PageMyPageEdit; + +export const getServerSideProps: GetServerSideProps = async ({ req, res, query }) => { + /** + * 로그인하지 않은 경우 로그인 페이지로 이동 + */ + const isLoggedIn = true; + + if (!isLoggedIn) { + res.writeHead(302, { Location: LINK_LOGIN_PAGE }); + res.end(); + } + + return { + props: {}, + }; +}; diff --git a/src/pages/mypage/index.tsx b/src/pages/mypage/index.tsx new file mode 100644 index 0000000..2c92b43 --- /dev/null +++ b/src/pages/mypage/index.tsx @@ -0,0 +1,25 @@ +import { GetServerSideProps } from "next"; +import ViewMypage from "feature/mypage/mypage.main/views/ViewMypage"; +import { LINK_LOGIN_PAGE } from "constant/link"; + +const PageMyPage = () => { + return ; +}; + +export default PageMyPage; + +export const getServerSideProps: GetServerSideProps = async ({ req, res, query }) => { + /** + * 로그인하지 않은 경우 로그인 페이지로 이동 + */ + const isLoggedIn = true; + + if (!isLoggedIn) { + res.writeHead(302, { Location: LINK_LOGIN_PAGE }); + res.end(); + } + + return { + props: {}, + }; +}; diff --git a/src/pages/register/index.tsx b/src/pages/register/index.tsx index c85f73d..0d8f138 100644 --- a/src/pages/register/index.tsx +++ b/src/pages/register/index.tsx @@ -1,5 +1,6 @@ import styled from "@emotion/styled"; import PageMarker from "components/pageMarker/PageMarker"; +import { LINK_MAIN_PAGE } from "constant/link"; import { GetServerSideProps } from "next"; const PageRegister = () => { @@ -24,7 +25,7 @@ export const getServerSideProps: GetServerSideProps = async ({ req, res, query } const hasAuthenticated = true; if (isNicknameAlreadySet || !hasAuthenticated) { - res.writeHead(302, { Location: "/" }); + res.writeHead(302, { Location: LINK_MAIN_PAGE }); res.end(); } diff --git a/src/pages/register/success.tsx b/src/pages/register/success.tsx index e0eaec5..61fb66e 100644 --- a/src/pages/register/success.tsx +++ b/src/pages/register/success.tsx @@ -1,5 +1,6 @@ import styled from "@emotion/styled"; import PageMarker from "components/pageMarker/PageMarker"; +import { LINK_MAIN_PAGE } from "constant/link"; import { GetServerSideProps } from "next"; const PageRegisterSuccess = () => { @@ -24,7 +25,7 @@ export const getServerSideProps: GetServerSideProps = async ({ req, res, query } const hasUserAlreadyViewedSuccessPage = false; if (hasUserAlreadyViewedSuccessPage) { - res.writeHead(302, { Location: "/" }); + res.writeHead(302, { Location: LINK_MAIN_PAGE }); res.end(); }