Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/common/component/Mandalart/Mandalart.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { style } from '@vanilla-extract/css';

export const gridDefault = style({
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: '1.6rem',
width: 'fit-content',
margin: '0 auto',
});

export const gridSmall = style({
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: '2rem',
width: 'fit-content',
margin: '0 auto',
});
107 changes: 107 additions & 0 deletions src/common/component/Mandalart/Mandalart.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import type { Meta, StoryObj } from '@storybook/react-vite';

import Mandalart, { type Cycle } from './Mandalart';
import { MOCK_MANDALART_DATA } from './mock';

const meta = {
title: 'Components/Mandalart',
component: Mandalart,
parameters: {
layout: 'centered',
backgrounds: {
default: 'dark',
},
},
tags: ['autodocs'],
} satisfies Meta<typeof Mandalart>;

export default meta;
type Story = StoryObj<typeof meta>;

const CUSTOM_GOALS = {
mainGoal: '나인도트 1등하기',
subGoals: [
{
title: '이현준 갈구기',
position: 0,
cycle: 'DAILY' as Cycle,
},
{
title: '매일 운동하기',
position: 1,
cycle: 'DAILY' as Cycle,
},
{
title: '일찍 일어나기',
position: 2,
cycle: 'DAILY' as Cycle,
},
{
title: '계획 세우기',
position: 3,
cycle: 'WEEKLY' as Cycle,
},
{
title: '시간 관리하기',
position: 4,
cycle: 'WEEKLY' as Cycle,
},
{
title: '건강 관리하기',
position: 5,
cycle: 'DAILY' as Cycle,
},
{
title: '긍정적으로 생각하기',
position: 6,
cycle: 'DAILY' as Cycle,
},
{
title: '꾸준히 노력하기',
position: 7,
cycle: 'DAILY' as Cycle,
},
],
completedGoals: [1, 3, 5],
};

export const Default: Story = {
args: {
mainGoal: '메인 목표를 입력하세요',
subGoals: MOCK_MANDALART_DATA.subGoals,
},
render: (args) => (
<div style={{ display: 'flex', gap: '2rem' }}>
<div>
<h3 style={{ color: 'white', marginBottom: '1rem' }}>Default 사이즈</h3>
<Mandalart {...args} />
</div>
<div>
<h3 style={{ color: 'white', marginBottom: '1rem' }}>Small 사이즈</h3>
<Mandalart {...args} size="small" />
</div>
</div>
),
};

export const Small: Story = {
args: {
mainGoal: '메인 목표를 입력하세요',
subGoals: CUSTOM_GOALS.subGoals,
size: 'small',
},
render: (args) => <Mandalart {...args} />,
};

export const WithCustomGoals: Story = {
args: CUSTOM_GOALS,
render: (args) => <Mandalart {...args} />,
};

export const WithCustomGoalsSmall: Story = {
args: {
...CUSTOM_GOALS,
size: 'small',
},
render: (args) => <Mandalart {...args} />,
};
61 changes: 61 additions & 0 deletions src/common/component/Mandalart/Mandalart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useState } from 'react';

import { Square } from './Square';
import * as styles from './Mandalart.css';
import { MOCK_MANDALART_DATA } from './mock';

export type Cycle = 'DAILY' | 'WEEKLY' | 'ONCE';
export type MandalartSize = 'small' | 'default';

export interface SubGoal {
title: string;
position: number;
cycle: Cycle;
}

interface MandalartProps {
mainGoal?: string;
subGoals?: SubGoal[];
size?: MandalartSize;
}

const CENTER_INDEX = 4;

const Mandalart = ({
mainGoal = MOCK_MANDALART_DATA.mainGoal,
subGoals = MOCK_MANDALART_DATA.subGoals,
size = 'default',
}: MandalartProps) => {
const [selectedGoal, setSelectedGoal] = useState<number | null>(null);

const handleGoalClick = (position: number) => {
setSelectedGoal(selectedGoal === position ? null : position);
Copy link
Copy Markdown
Contributor

@shinjigu shinjigu Jul 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p5) 명확해서 좋아요 !!

};

const renderSquare = (index: number) => {
if (index === CENTER_INDEX) {
return <Square.Main key={index} content={mainGoal} size={size} />;
}

const subGoalIndex = index > CENTER_INDEX ? index - 1 : index;
const subGoal = subGoals[subGoalIndex];

return (
<Square.Sub
key={index}
content={subGoal.title}
isCompleted={selectedGoal === subGoalIndex}
onClick={() => handleGoalClick(subGoalIndex)}
size={size}
/>
);
};

const squares = Array(9)
.fill(null)
.map((_, index) => renderSquare(index));

return <div className={size === 'small' ? styles.gridSmall : styles.gridDefault}>{squares}</div>;
};

export default Mandalart;
87 changes: 87 additions & 0 deletions src/common/component/Mandalart/Square/Square.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { style } from '@vanilla-extract/css';

import { colors, fonts } from '@/style/token';

export const squareContainer = style({
display: 'grid',
margin: '0 auto',
});

const baseCellDefault = style({
borderRadius: '8px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center',
width: '19.6rem',
height: '19.6rem',
boxSizing: 'border-box',
});

const baseCellSmall = style({
borderRadius: '8px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center',
width: '16rem',
height: '16rem',
boxSizing: 'border-box',
});

export const mainCellDefault = style([
baseCellDefault,
fonts.title03,
{
color: colors.white01,
backgroundImage: colors.gradient04,
},
]);

export const mainCellSmall = style([
baseCellSmall,
fonts.subtitle01,
{
color: colors.white01,
backgroundImage: colors.gradient04,
padding: '1.4rem',
},
]);

export const subCellDefault = style([
baseCellDefault,
fonts.subtitle01,
{
color: colors.grey8,
background: colors.grey2,
':hover': {
background: colors.grey3,
},
selectors: {
'&[data-completed="true"]': {
border: '0.4rem solid #305088',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p1) 하드코딩된 색상값 고쳐주시면 좋을 것 같습니다 🕶️

background: colors.grey2,
},
},
},
]);

export const subCellSmall = style([
baseCellSmall,
fonts.subtitle05,
{
color: colors.grey8,
background: colors.grey2,
':hover': {
background: colors.grey3,
},
selectors: {
'&[data-completed="true"]': {
border: '0.3rem solid #305088',
background: colors.grey2,
},
},
},
]);
116 changes: 116 additions & 0 deletions src/common/component/Mandalart/Square/Square.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import type { Meta, StoryObj } from '@storybook/react-vite';

import { Square } from '.';

import { colors } from '@/style/token';

const meta = {
title: 'Components/Square',
component: Square.Main,
parameters: {
layout: 'centered',
backgrounds: {
default: 'dark',
},
},
tags: ['autodocs'],
} satisfies Meta<typeof Square.Main>;

export default meta;
type Story = StoryObj<typeof meta>;

const handleClick = () => {};

export const Default: Story = {
args: {
content: '상위 목표',
},
render: (args) => (
<div style={{ display: 'flex', gap: '2rem' }}>
<div>
<h3 style={{ color: colors.white01, marginBottom: '1rem' }}>Default 사이즈</h3>
<p style={{ color: colors.white01, marginBottom: '1rem' }}>
메인: title03 / 서브: subtitle01
</p>
<div style={{ display: 'flex', gap: '2rem' }}>
<Square.Main {...args} />
<Square.Sub content="세부 목표" isCompleted={false} onClick={handleClick} />
</div>
</div>
<div>
<h3 style={{ color: colors.white01, marginBottom: '1rem' }}>Small 사이즈</h3>
<p style={{ color: colors.white01, marginBottom: '1rem' }}>
메인: body04 / 서브: subtitle05
</p>
<div style={{ display: 'flex', gap: '2rem' }}>
<Square.Main content="상위 목표" size="small" />
<Square.Sub content="세부 목표" isCompleted={false} onClick={handleClick} size="small" />
</div>
</div>
</div>
),
};

export const MainGoal: Story = {
args: {
content: '메인 목표를 입력하세요',
},
render: (args) => (
<div style={{ display: 'flex', gap: '2rem' }}>
<div>
<h3 style={{ color: colors.white01, marginBottom: '1rem' }}>Default 사이즈 (title03)</h3>
<Square.Main {...args} />
</div>
<div>
<h3 style={{ color: colors.white01, marginBottom: '1rem' }}>Small 사이즈 (body04)</h3>
<Square.Main content="메인 목표를 입력하세요" size="small" />
</div>
</div>
),
};

export const SubGoalStates: Story = {
args: {
content: '세부 목표를 입력하세요',
},
render: (args) => (
<div style={{ display: 'flex', gap: '2rem' }}>
<div>
<h3 style={{ color: colors.white01, marginBottom: '1rem' }}>Default 사이즈 (subtitle01)</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
<div>
<h4 style={{ color: colors.white01, marginBottom: '1rem' }}>기본 상태</h4>
<Square.Sub content={args.content} isCompleted={false} onClick={handleClick} />
</div>
<div>
<h4 style={{ color: colors.white01, marginBottom: '1rem' }}>완료 상태</h4>
<Square.Sub content="완료된 목표입니다" isCompleted={true} onClick={handleClick} />
</div>
</div>
</div>
<div>
<h3 style={{ color: colors.white01, marginBottom: '1rem' }}>Small 사이즈 (subtitle05)</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
<div>
<h4 style={{ color: colors.white01, marginBottom: '1rem' }}>기본 상태</h4>
<Square.Sub
content={args.content}
isCompleted={false}
onClick={handleClick}
size="small"
/>
</div>
<div>
<h4 style={{ color: colors.white01, marginBottom: '1rem' }}>완료 상태</h4>
<Square.Sub
content="완료된 목표입니다"
isCompleted={true}
onClick={handleClick}
size="small"
/>
</div>
</div>
</div>
</div>
),
};
Loading
Loading