Monthly 페이지 디자인
Monthly 페이지는 고정된 높이의 카드들이 위에서 아래로 정렬된 형태이다.
- 위에서 아래로 일정한 간격으로 배치되어 있으며, 스크롤링 된다.
- 일기들이 연속된 일 수로 작성된 경우 일정한 간격으로 배치된다.
하루가 빠진 경우 : 카드 대신 점이 찍힌다.
이틀 이상 빠진 경우 : 카드 대신 점이 2개 찍힌다. - 하단 바에는 월, 년, 새글작성, '카드보기'/'일기로 보기' 토글버튼, 설정버튼, 총 5개의 버튼이 들어간다.
Monthly 페이지 Atomic 디자인 시스템
Atoms
Box : 박스 컨테이너이다. 사선으로 된 배경이 포함될 수 있다.
text : 총 4개의 글씨체가 사용된다. 본문, 년월일, 요일, 날짜. 일요일의 경우 빨간색의 색상으로 표시될 수 있다.
button : 4개의 버튼이 있다. 월, 새글추가, 토글, 설정
Molecules
Monthly Card : 날짜와 내용 크게 두 부분으로 나뉜다. 날짜 부분은 60px, 본문은 100%로 지정한다.
Bottom Bar : 총 5개의 버튼이 있는 하단 바이다. 배경이 불투명하여 카드가 가려진다.
Templates
Monthly Templates : 카드들을 18px의 gap으로 정렬하는 템플릿이다. 좌우 패딩이 20px씩 들어간다.
Monthly Card 만들기
Atoms
3개의 Atom을 만들었다.
Stack
display: flex 속성을 지니는 Stack이다. Stack이라는 이름에 걸맞게 flexDirection="col"을 기본값으로 지정하였고, 자주 사용되는 TJustifyContents, TAlignItems 타입을 지정하였다.
interface StackProps {
flexDirection?: "row" | "col";
justifyContent?: TJustifyContents;
alignItems?: TAlignItems;
gap?: number;
className?: string;
}
const Stack = ({
flexDirection = "col",
justifyContent = "start",
alignItems = "stretch",
className = "",
gap = 0,
children,
}: React.PropsWithChildren<StackProps>) => {
return (
<div
className={`flex flex-${flexDirection} gap-${~~gap} ${
JustifyContentMap[justifyContent]
} ${AlignItemsMap[alignItems]} w-full h-full ${className}`}
>
{children}
</div>
);
};
export default Stack;
export type TJustifyContents =
| "normal"
| "start"
| "end"
| "center"
| "between"
| "around"
| "evenly"
| "stretch";
export const JustifyContentMap: Record<TJustifyContents, string> = {
normal: "justify-normal",
start: "justify-start",
end: "justify-end",
center: "justify-center",
between: "justify-between",
around: "justify-around",
evenly: "justify-evenly",
stretch: "justify-stretch",
};
export type TAlignItems = "start" | "end" | "center" | "baseline" | "stretch";
// AlignItemsMap을 수정했습니다.
export const AlignItemsMap: Record<TAlignItems, string> = {
start: "items-start",
end: "items-end",
center: "items-center",
baseline: "items-baseline",
stretch: "items-stretch",
};
Box
회색의 border를 갖는 박스이다. 가로, 세로를 100%로 두었기에 크기를 지정하려면 그 밖 div 태그 등으로 감싸야 한다.
import {
AlignItemsMap,
JustifyContentMap,
TAlignItems,
TJustifyContents,
} from "./Stack";
const Box = ({
background,
justifyContent = "start",
alignItems = "stretch",
children,
padding,
className,
}: React.PropsWithChildren<{
background?: "diagonal";
justifyContent?: TJustifyContents;
alignItems?: TAlignItems;
padding?: "p-4";
className?: string;
}>) => {
let bg;
switch (background) {
case "diagonal":
bg = "bg-gradient-diagonal";
break;
default:
bg = "";
break;
}
return (
<div
className={`border-solid border border-gray-800 ${bg} w-full flex ${AlignItemsMap[alignItems]} ${JustifyContentMap[justifyContent]} ${padding} ${className}`}
>
{children}
</div>
);
};
export default Box;
Typography
폰트를 지정할 수 있는 Typography 컴포넌트이다.
type을 "content" | "bottom" | "index" | "indexMonth"; 네 개로 지정하고,
해당 타입에 맞게 폰트를 next/font로부터 import하여 사용한다.
import { Preahvihear, Noto_Serif, Share_Tech_Mono } from "next/font/google";
const preahvihear = Preahvihear({
weight: "400",
subsets: ["latin"],
});
const notoSerif = Noto_Serif({
weight: "700",
subsets: ["latin"],
});
const shareTechMono = Share_Tech_Mono({
weight: "400",
subsets: ["latin"],
});
const Typography = ({
children,
type = "content",
className = "",
}: React.PropsWithChildren<{
type?: "content" | "bottom" | "index" | "indexMonth";
className?: string;
}>) => {
let style;
switch (type) {
case "content": {
style = "";
break;
}
case "indexMonth":
style = `text-sm ${preahvihear.className}`;
break;
case "index":
style = `text-xl ${notoSerif.className}`;
break;
case "bottom":
style = `text-xl -tracking-widest ${shareTechMono.className}`;
break;
default:
style = "";
break;
}
return <div className={style + " " + className}>{children}</div>;
};
export default Typography;
Molecule
이러한 Atom들을 모아 날짜를 보여주는 box, 요일을 보여주는 box, 일기 내용을 2줄 보여주는 box를 만들었다.
// components\molecules\DateIndex.tsx
import Box from "../atoms/Box";
import Stack from "../atoms/Stack";
import Typography from "../atoms/Typography";
const DateIndex = ({
children,
className,
}: React.PropsWithChildren<{ className?: string }>) => {
return (
<Box className={className}>
<Stack alignItems="center" justifyContent="center">
<Typography type="index">{children}</Typography>
</Stack>
</Box>
);
};
export default DateIndex;
// components\molecules\DayIndex.tsx
import Box from "../atoms/Box";
import Typography from "../atoms/Typography";
const DayIndex = ({
children,
className,
}: React.PropsWithChildren<{ className?: string }>) => {
return (
<Box
justifyContent="center"
alignItems="center"
background="diagonal"
className={className}
>
<Typography type="indexMonth">{children}</Typography>
</Box>
);
};
export default DayIndex;
// components\molecules\MonthlyContents.tsx
import Box from "../atoms/Box";
const MonthlyContents = ({
children,
className,
}: React.PropsWithChildren<{ className: string }>) => {
return (
<Box className={className} padding="p-4" alignItems="center">
<div className="line-clamp-2">{children}</div>
</Box>
);
};
export default MonthlyContents;
Orgarnisms
위의 molcules를 모아 아래와 같은 카드를 만들 수 있다.
크기를 지정하거나, 옆 혹은 아래의 border를 제거하는 등의 특수한 작업은 Orgarnisms 단에서 디자인을 만져주어야 한다.
//components\orgarnisms\MonthlyCard.tsx
import Box from "../atoms/Box";
import Stack from "../atoms/Stack";
import Typography from "../atoms/Typography";
import DateIndex from "../molecules/DateIndex";
import DayIndex from "../molecules/DayIndex";
import MonthlyContents from "../molecules/MonthlyContents";
const MonthlyCard = ({
day,
days,
contents,
}: {
day: string;
days: number;
contents: string;
}) => {
return (
<Stack flexDirection="row" className="w-full h-[90px]">
<div className="w-[60px]">
<Stack>
<DayIndex className="border-b-0 h-[32px]">{day}</DayIndex>
<DateIndex className="h-[58px]">{days}</DateIndex>
</Stack>
</div>
<MonthlyContents className="border-l-0 h-[90px]">
{contents}
</MonthlyContents>
</Stack>
);
};
export default MonthlyCard;
바텀바 만들기
Atoms
먼더 바텀바의 아이콘을 삽입하기 위해 react-feather 라이브러리를 선택했다. 경량화되어 있으며, stroke를 추가할 수 있다. 원하던 각진 디자인이 나오지 않는 것은 고민중.
Icons
Icons 컴포넌트를 만들고, IconsMap이라는 이름으로 사용할 아이콘들을 삽입하였다.
const IconsMap = {
minus: Minus,
plus: Plus,
menu: Menu,
clock: Clock,
} as const;
이를 이용해서 타입을 만들고
type TIconNames = keyof typeof IconsMap;
props의 타입은 IconProps라는 react-feather의 타입을 확장하였다.
import { PropsWithChildren } from "react";
import { Clock, IconProps, Menu, Minus, Plus } from "react-feather";
const Icons = ({
iconName,
children,
...rest
}: PropsWithChildren<CustomIconsProps>) => {
const Icon = IconsMap[iconName];
const { stroke } = rest;
rest.strokeWidth = Number(stroke) || rest.strokeWidth;
return <Icon {...rest}>{children}</Icon>;
};
export default Icons;
type TIconNames = keyof typeof IconsMap;
interface CustomIconsProps extends IconProps {
iconName: TIconNames;
}
const IconsMap = {
minus: Minus,
plus: Plus,
menu: Menu,
clock: Clock,
} as const;
각진 디자인을 넣는 문제는 svgProps의 stroke-line-cap을 지정하는 방식으로 해결할 수 있었다.
import Icons from "@/components/atoms/Icons";
export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<Icons IconName="clock" size={48} />
<Icons IconName="menu" size={48} strokeWidth={4} />
<Icons IconName="minus" size={48} strokeWidth={10} />
<Icons
className="transform rotate-90"
IconName="minus"
size={48}
strokeWidth={10}
strokeLinecap="square"
/>
</main>
);
}
Molecules
하단 년도
바텀 시트에 들어가는 년도는 아래와 같이 작성하였다.
Icons에 "rotate"라는 프롭스를 추가하였고, 몇가지 리팩토링을 진행하였다.
가운데 정렬을 맞추기 위해 예외적으로 아이콘에 marginBottom을 넣었다.
// components\atoms\Icons.tsx
import { PropsWithChildren } from "react";
import { Clock, IconProps, Menu, Minus, Plus } from "react-feather";
const Icons = ({
iconName,
children,
rotate,
...rest
}: PropsWithChildren<CustomIconsProps>) => {
const Icon = IconsMap[iconName];
rest.className += ` transform ${rotate}`;
return <Icon {...rest}>{children}</Icon>;
};
export default Icons;
type TIconNames = keyof typeof IconsMap;
interface CustomIconsProps extends IconProps {
iconName: TIconNames;
rotate?: "rotate-45" | "rotate-90" | "rotate-180";
}
const IconsMap = {
minus: Minus,
plus: Plus,
menu: Menu,
clock: Clock,
} as const;
import Icons from "@components/atoms/Icons";
import Stack from "@components/atoms/Stack";
import Typography from "@components/atoms/Typography";
const BottomMonthIndex = ({ date = new Date() }: { date?: Date }) => {
// 날짜로부터 영어 Month 이름을 추출합니다.
const formatter = new Intl.DateTimeFormat("en", { month: "long" });
const month = formatter.format(date);
return (
<Stack flexDirection="row" alignItems="center">
<Icons
iconName="minus"
rotate="rotate-90"
className="mb-[1px]"
size={16}
strokeWidth={5}
strokeLinecap="square"
/>
<Typography type="bottom" className="uppercase">
{month}
</Typography>
</Stack>
);
};
export default BottomMonthIndex;
바텀 토글 아이콘
바텀 토글 아이콘을 어떻게 구현할까 고민하다가,
SVG로 구현하는 것을 고려해보았으나, 단순한 SVG로 구현한 것은 react feather 아이콘들과 호환되지 않아 크기 조절이 불편하다는 단점에도 불구하고 소요가 많았다.
때문에 div 태그를 이용하여 1회성으로 구현하기로 하였다.
components\atoms\ToggleIcon.tsx
import Stack from "./Stack";
const ToggleIcon = () => {
return (
<Stack className="gap-[3px] w-full">
<div className="bg-gray-900 w-full h-[2px]" />
<div className="bg-gray-900 w-full h-[6px]" />
<div className="bg-gray-900 w-full h-[2px]" />
</Stack>
);
};
export default ToggleIcon;
이렇게 Div 태그로 구현하니 크기 조절이 훨씬 용이했다.
components\molecules\BottomToggleButton.tsx
import ToggleIcon from "../atoms/ToggleIcon";
const BottomToggleButton = () => {
return (
<div className="w-[90px]">
<ToggleIcon />
</div>
);
};
export default BottomToggleButton;