diff --git a/packages/web/src/app/credits/page.tsx b/packages/web/src/app/credits/page.tsx
new file mode 100644
index 0000000..f3e1604
--- /dev/null
+++ b/packages/web/src/app/credits/page.tsx
@@ -0,0 +1,65 @@
+"use client";
+
+import React from "react";
+
+import styled from "styled-components";
+
+import FlexWrapper from "@sparcs-students/web/common/components/FlexWrapper";
+import FoldableSectionTitle from "@sparcs-students/web/common/components/FoldableSectionTitle";
+import PageHead from "@sparcs-students/web/common/components/PageHead";
+
+import SectionTitle from "@sparcs-students/web/common/components/SectionTitle";
+import MemberCardSection from "@sparcs-students/web/features/credits/components/MemberCardSection";
+import credits from "@sparcs-students/web/features/credits/credits";
+
+const CreditCardsFlexWrapper = styled(FlexWrapper)`
+ gap: 40px;
+
+ @media (max-width: ${({ theme }) => theme.responsive.BREAKPOINT.sm}) {
+ gap: 20px;
+ }
+`;
+
+const ResponsiveMemberCardSectionWrapper = styled.div`
+ @media (max-width: ${({ theme }) => theme.responsive.BREAKPOINT.sm}) {
+ margin-left: 16px;
+ }
+`;
+
+const Credits: React.FC = () => {
+ const [toggleFold, setToggleFold] = React.useState(false);
+ return (
+
+
+ {credits.map((credit, index) => (
+
+ {index === 0 ? (
+ <>
+ {credit.semester}
+
+
+
+ >
+ ) : (
+ {
+ setToggleFold(!toggleFold);
+ }}
+ >
+
+
+
+
+ )}
+
+ ))}
+
+ );
+};
+export default Credits;
diff --git a/packages/web/src/assets/sparcs-orange.svg b/packages/web/src/assets/sparcs-orange.svg
new file mode 100644
index 0000000..efff173
--- /dev/null
+++ b/packages/web/src/assets/sparcs-orange.svg
@@ -0,0 +1,6 @@
+
diff --git a/packages/web/src/common/components/Card.tsx b/packages/web/src/common/components/Card.tsx
index 9eaaf51..4ef72c0 100644
--- a/packages/web/src/common/components/Card.tsx
+++ b/packages/web/src/common/components/Card.tsx
@@ -1,19 +1,29 @@
"use client";
-import React from "react";
+import isPropValid from "@emotion/is-prop-valid";
import styled from "styled-components";
-const Card: React.FC = styled.div<{
+interface CardProps {
outline?: boolean;
-}>`
+ padding?: string;
+ gap?: number;
+}
+
+const Card = styled.div.withConfig({
+ shouldForwardProp: prop => isPropValid(prop),
+})`
display: flex;
flex-direction: column;
position: relative;
- padding: 16px 20px;
+ align-self: stretch;
+ padding: ${({ padding }) => padding ?? `32px`};
+ gap: ${({ gap }) => (gap ? `${gap}px` : "inherit")};
+
font-family: ${({ theme }) => theme.fonts.FAMILY.PRETENDARD};
font-size: 16px;
line-height: 20px;
font-weight: ${({ theme }) => theme.fonts.WEIGHT.REGULAR};
+
color: ${({ theme }) => theme.colors.BLACK};
background-color: ${({ theme }) => theme.colors.WHITE};
border-radius: ${({ theme }) => theme.round.md};
diff --git a/packages/web/src/common/components/FlexWrapper.tsx b/packages/web/src/common/components/FlexWrapper.tsx
new file mode 100644
index 0000000..ac7bad3
--- /dev/null
+++ b/packages/web/src/common/components/FlexWrapper.tsx
@@ -0,0 +1,22 @@
+import isPropValid from "@emotion/is-prop-valid";
+import styled from "styled-components";
+
+interface FlexWrapperProps {
+ direction: "row" | "column";
+ gap: number;
+ justify?: string;
+ padding?: string;
+}
+
+const FlexWrapper = styled.div.withConfig({
+ shouldForwardProp: prop => isPropValid(prop),
+})`
+ display: flex;
+ position: relative;
+ flex-direction: ${({ direction }) => direction};
+ gap: ${({ gap }) => `${gap}px`};
+ justify-content: ${({ justify }) => justify ?? "flex-start"};
+ padding: ${({ padding }) => padding ?? 0};
+`;
+
+export default FlexWrapper;
diff --git a/packages/web/src/common/components/FoldableSectionTitle.tsx b/packages/web/src/common/components/FoldableSectionTitle.tsx
index bb6cf14..e9e8453 100644
--- a/packages/web/src/common/components/FoldableSectionTitle.tsx
+++ b/packages/web/src/common/components/FoldableSectionTitle.tsx
@@ -1,38 +1,67 @@
"use client";
import React from "react";
+
+import isPropValid from "@emotion/is-prop-valid";
import styled from "styled-components";
+import TextButton from "@sparcs-students/web/common/components/Buttons/TextButton";
import SectionTitle from "@sparcs-students/web/common/components/SectionTitle";
+import Icon from "@sparcs-students/web/common/components/Icon";
+
+const FoldableSectionOuter = styled.div`
+ width: 100%;
+ max-width: calc(100vw + (100% - 100vw));
+`;
const FoldableSectionTitleInner = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
-
width: 100%;
`;
-export const MoreInfo = styled.div`
- font-family: ${({ theme }) => theme.fonts.FAMILY.PRETENDARD};
- font-size: 14px;
- line-height: 20px;
- font-weight: ${({ theme }) => theme.fonts.WEIGHT.REGULAR};
- color: ${({ theme }) => theme.colors.BLACK};
- text-decoration-line: underline;
- cursor: pointer;
+const ChildrenOuter = styled.div.withConfig({
+ shouldForwardProp: prop => isPropValid(prop),
+})<{ margin?: string }>`
+ margin-top: ${({ margin }) => margin};
+ margin-left: 24px;
`;
const FoldableSectionTitle: React.FC<{
+ iconType?: string;
title: string;
- toggle: boolean;
- toggleHandler: () => void;
-}> = ({ title, toggle, toggleHandler }) => (
-
- {title}
- {toggle ? `접기` : `펼치기`}
-
-);
+ toggle?: boolean;
+ toggleHandler?: () => void;
+ children?: React.ReactNode;
+ childrenMargin?: string;
+}> = ({
+ iconType = null,
+ title,
+ toggle = null,
+ toggleHandler = null,
+ children = null,
+ childrenMargin = "40px",
+}) => {
+ const [open, setOpen] = React.useState(true);
+ const openHandler = () => setOpen(!open);
+
+ return (
+
+
+ {iconType && }
+ {title}
+
+
+ {(toggle ?? open) && children && (
+ {children}
+ )}
+
+ );
+};
export default FoldableSectionTitle;
diff --git a/packages/web/src/common/components/PageHead/_atomic/PageTitle.tsx b/packages/web/src/common/components/PageHead/_atomic/PageTitle.tsx
new file mode 100644
index 0000000..baf18c1
--- /dev/null
+++ b/packages/web/src/common/components/PageHead/_atomic/PageTitle.tsx
@@ -0,0 +1,25 @@
+"use client";
+
+import React from "react";
+
+import styled from "styled-components";
+
+const PageTitleInner = styled.div`
+ position: relative;
+ width: fit-content;
+ font-family: ${({ theme }) => theme.fonts.FAMILY.PRETENDARD};
+ font-size: 30px;
+ line-height: 24px;
+ @media (max-width: ${({ theme }) => theme.responsive.BREAKPOINT.sm}) {
+ font-size: 24px;
+ line-height: 36px;
+ }
+ font-weight: ${({ theme }) => theme.fonts.WEIGHT.SEMIBOLD};
+ color: ${({ theme }) => theme.colors.BLACK};
+`;
+
+const PageTitle: React.FC = ({
+ children = ,
+}) => {children};
+
+export default PageTitle;
diff --git a/packages/web/src/common/components/PageHead/index.tsx b/packages/web/src/common/components/PageHead/index.tsx
new file mode 100644
index 0000000..8745c45
--- /dev/null
+++ b/packages/web/src/common/components/PageHead/index.tsx
@@ -0,0 +1,53 @@
+import React from "react";
+
+import styled from "styled-components";
+
+import PageTitle from "./_atomic/PageTitle";
+
+// 주석 처리된 부분은 BreadCrumb 컴포넌트를 쓰게 된다면 다시 추가
+interface PageHeadProps {
+ // items: { name: string; path: string }[];
+ title: string;
+ // enableLast?: boolean;
+ action?: React.ReactNode;
+}
+
+const PageHeadWrapper = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ @media (max-width: ${({ theme }) => theme.responsive.BREAKPOINT.sm}) {
+ gap: 12px;
+ }
+ align-items: flex-start;
+ align-self: stretch;
+`;
+
+const TitleWrapper = styled.div`
+ display: flex;
+ width: 100%;
+ justify-content: space-between;
+ align-items: center;
+ @media (max-width: ${({ theme }) => theme.responsive.BREAKPOINT.sm}) {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 20px;
+ }
+`;
+
+const PageHead: React.FC = ({
+ // items,
+ title,
+ // enableLast = false,
+ action = null,
+}) => (
+
+ {/* */}
+
+ {title}
+ {action && {action}
}
+
+
+);
+
+export default PageHead;
diff --git a/packages/web/src/common/components/SectionTitle.tsx b/packages/web/src/common/components/SectionTitle.tsx
index 7e60510..e89c33f 100644
--- a/packages/web/src/common/components/SectionTitle.tsx
+++ b/packages/web/src/common/components/SectionTitle.tsx
@@ -21,8 +21,9 @@ const IdentityBar = styled.div`
const Title = styled.p<{ size: Size }>`
font-family: ${({ theme }) => theme.fonts.FAMILY.PRETENDARD};
- font-size: ${({ size }) => (size === "sm" ? "20px" : "24px")};
- line-height: ${({ size }) => (size === "sm" ? "28px" : "32px")};
+ font-size: ${({ size }) =>
+ size === "sm" ? "20px" : "20px"}; // TODO: 반응형 사이즈 재확인
+ line-height: ${({ size }) => (size === "sm" ? "24px" : "24px")};
font-weight: ${({ theme }) => theme.fonts.WEIGHT.MEDIUM};
color: ${({ theme }) => theme.colors.BLACK};
`;
diff --git a/packages/web/src/constants/paths.ts b/packages/web/src/constants/paths.ts
index 8668ca9..f2f268d 100644
--- a/packages/web/src/constants/paths.ts
+++ b/packages/web/src/constants/paths.ts
@@ -51,7 +51,7 @@ const paths = {
},
],
},
- MADE_BY: { name: "만든 사람들", path: "/" },
+ MADE_BY: { name: "만든 사람들", path: "/credits" },
LICENSE: { name: "라이센스", path: "/" },
TERMS_OF_SERVICE: { name: "이용 약관", path: "/" },
LOGIN: { name: "로그인", path: "/login" },
diff --git a/packages/web/src/features/credits/components/MemberCard.tsx b/packages/web/src/features/credits/components/MemberCard.tsx
new file mode 100644
index 0000000..b810a5d
--- /dev/null
+++ b/packages/web/src/features/credits/components/MemberCard.tsx
@@ -0,0 +1,70 @@
+import { useState } from "react";
+
+import Image from "next/image";
+import styled from "styled-components";
+
+import SparcsLogo from "@sparcs-students/web/assets/sparcs-orange.svg";
+import Card from "@sparcs-students/web/common/components/Card";
+import Typography from "@sparcs-students/web/common/components/Typography";
+
+import type { Member } from "../credits";
+
+const MemberWrapper = styled.div`
+ display: flex;
+ flex-direction: row;
+ align-items: flex-end;
+ gap: 6px;
+`;
+
+const MemberCard = ({ member }: { member: Member }) => {
+ const [displayText, setDisplayText] = useState(member.role);
+
+ const handleMouseEnter = () => {
+ if (member.comment) {
+ setDisplayText(member.comment);
+ }
+ };
+
+ const handleMouseLeave = () => {
+ setDisplayText(member.role);
+ };
+
+ return (
+
+
+
+
+ {member.nickname}
+
+
+ {member.name}
+
+
+
+ {displayText}
+
+
+ );
+};
+
+export default MemberCard;
diff --git a/packages/web/src/features/credits/components/MemberCardSection.tsx b/packages/web/src/features/credits/components/MemberCardSection.tsx
new file mode 100644
index 0000000..8e8a6a6
--- /dev/null
+++ b/packages/web/src/features/credits/components/MemberCardSection.tsx
@@ -0,0 +1,59 @@
+import React from "react";
+
+import styled from "styled-components";
+
+import { Member, SemesterCredit } from "../credits";
+
+import MemberCard from "./MemberCard";
+
+interface MemberCardSectionProps {
+ semesterCredit: SemesterCredit;
+ leftMargin?: number;
+}
+
+const MemberCardWrapper = styled.div<{ leftMargin: number }>`
+ display: grid;
+ grid-gap: 20px;
+ grid-template-columns: repeat(5, 1fr);
+ margin-left: ${({ leftMargin }) => leftMargin}px;
+
+ @media (max-width: ${({ theme }) => theme.responsive.BREAKPOINT.xl}) {
+ grid-template-columns: repeat(4, 1fr);
+ }
+
+ @media (max-width: ${({ theme }) => theme.responsive.BREAKPOINT.lg}) {
+ grid-template-columns: repeat(3, 1fr);
+ }
+
+ @media (max-width: ${({ theme }) => theme.responsive.BREAKPOINT.md}) {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ // @media (max-width: ${({ theme }) => theme.responsive.BREAKPOINT.sm}) {
+ // grid-template-columns: 1fr;
+ // }
+`;
+
+const MemberCardSection: React.FC = ({
+ semesterCredit,
+ leftMargin = 0,
+}) => {
+ const compareMembers = (a: Member, b: Member) => {
+ if (a.roleType === b.roleType) {
+ return a.nickname.localeCompare(b.nickname);
+ }
+ return a.roleType - b.roleType;
+ };
+
+ return (
+
+ {semesterCredit.members
+ .sort((a, b) => compareMembers(a, b))
+ .map(member => (
+
+ ))}
+
+ );
+};
+
+export default MemberCardSection;
diff --git a/packages/web/src/features/credits/credits.ts b/packages/web/src/features/credits/credits.ts
new file mode 100644
index 0000000..4e2f50d
--- /dev/null
+++ b/packages/web/src/features/credits/credits.ts
@@ -0,0 +1,93 @@
+enum RoleType {
+ PM,
+ APM_FE,
+ APM_BE,
+ member,
+ intern,
+}
+
+export interface Member {
+ nickname: string;
+ name: string;
+ role: string;
+ roleType: RoleType;
+ comment?: string;
+}
+
+export interface SemesterCredit {
+ semester: string;
+ members: Member[];
+}
+
+const credits: SemesterCredit[] = [
+ {
+ semester: "🍁 2024년 가을",
+ members: [
+ {
+ nickname: "eel",
+ name: "최우정",
+ role: "PM",
+ roleType: RoleType.PM,
+ comment: "eel",
+ },
+ {
+ nickname: "chacha",
+ name: "안채연",
+ role: "FE",
+ roleType: RoleType.member,
+ comment: "chacha",
+ },
+ {
+ nickname: "casio",
+ name: "임가은",
+ role: "FE",
+ roleType: RoleType.member,
+ comment: "casio",
+ },
+ {
+ nickname: "malloc",
+ name: "최지윤",
+ role: "FE",
+ roleType: RoleType.member,
+ comment: "malloc",
+ },
+ {
+ nickname: "mingle",
+ name: "민지연",
+ role: "BE",
+ roleType: RoleType.member,
+ comment: "mingle",
+ },
+ {
+ nickname: "gb",
+ name: "권혁원",
+ role: "BE",
+ roleType: RoleType.intern,
+ comment: "gb",
+ },
+ {
+ nickname: "dudu",
+ name: "이연희",
+ role: "Designer",
+ roleType: RoleType.member,
+ comment: "dudu",
+ },
+ {
+ nickname: "somato",
+ name: "장성원",
+ role: "Designer",
+ roleType: RoleType.member,
+ comment: "somato",
+ },
+ {
+ nickname: "siwon",
+ name: "박정원",
+ role: "Designer",
+ roleType: RoleType.intern,
+ comment: "siwon",
+ },
+ ],
+ },
+];
+
+export default credits;
diff --git a/packages/web/src/styles/fonts/googleFonts.ts b/packages/web/src/styles/fonts/googleFonts.ts
index 8979bee..fed99a7 100644
--- a/packages/web/src/styles/fonts/googleFonts.ts
+++ b/packages/web/src/styles/fonts/googleFonts.ts
@@ -1,3 +1,4 @@
+import { Raleway } from "next/font/google";
import localFont from "next/font/local";
export const pretendard = localFont({
@@ -5,7 +6,13 @@ export const pretendard = localFont({
variable: "--next-font-family-pretendard",
});
-export const raleway = localFont({
- src: "./local/RalewayVariable.woff2",
+export const raleway = Raleway({
+ subsets: ["latin"],
+ weight: ["700", "800"], // BOLD, EXTRABOLD only
variable: "--next-font-family-raleway",
});
+
+export const nanumSquare = localFont({
+ src: "./local/NanumSquare-ExtraBold.woff2",
+ variable: "--next-font-family-nanum-square",
+});
diff --git a/packages/web/src/styles/fonts/local/NanumSquare-ExtraBold.woff2 b/packages/web/src/styles/fonts/local/NanumSquare-ExtraBold.woff2
new file mode 100644
index 0000000..71c8bd7
Binary files /dev/null and b/packages/web/src/styles/fonts/local/NanumSquare-ExtraBold.woff2 differ
diff --git a/packages/web/src/styles/themes/colors.ts b/packages/web/src/styles/themes/colors.ts
index be6d447..e9e6b73 100644
--- a/packages/web/src/styles/themes/colors.ts
+++ b/packages/web/src/styles/themes/colors.ts
@@ -27,6 +27,10 @@ const colors = {
200: "#F5A3A8",
700: "#B7202A",
},
+ SPARCS: {
+ main: "#EBA12A",
+ member: "#EBA12A66",
+ },
};
export default colors;
diff --git a/packages/web/src/styles/themes/fonts.ts b/packages/web/src/styles/themes/fonts.ts
index 6ca93dc..639f6b9 100644
--- a/packages/web/src/styles/themes/fonts.ts
+++ b/packages/web/src/styles/themes/fonts.ts
@@ -8,12 +8,14 @@ const fonts = {
FAMILY: {
PRETENDARD: "var(--next-font-family-pretendard)",
RALEWAY: "var(--next-font-family-raleway)",
+ NANUM_SQUARE: "var(--next-font-family-nanum-square)",
},
WEIGHT: {
REGULAR: 400,
MEDIUM: 500,
SEMIBOLD: 600,
BOLD: 700,
+ EXTRABOLD: 800,
},
};