Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: 마이페이지 카테고리 SSR + RSC 적용 #317

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
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
5 changes: 3 additions & 2 deletions src/api/hooks/categoryHooks/useGetCategoryList.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { useQuery } from '@tanstack/react-query';
import { getCategoryList } from '@/api/queryFn/categoryQueryFn';
import { Category, getCategoryList } from '@/api/queryFn/categoryQueryFn';

const useGetCategoryList = () => {
const useGetCategoryList = (initialData?: Category[]) => {
const { data: categoryList, isLoading } = useQuery({
queryKey: ['categoryList'],
queryFn: () => getCategoryList(),
initialData,
});

return { categoryList, isLoading };
Expand Down
2 changes: 2 additions & 0 deletions src/app/mypage/category/Category.styled.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client';

import styled from '@emotion/styled';

export const Container = styled.div`
Expand Down
1 change: 1 addition & 0 deletions src/app/mypage/category/CategoryModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Box, Button, FormControlLabel, Modal, Switch, TextField, Typography } f
import { TodoModalStyle } from '@/components/todo/Todo.styled';
import ColorPickerBox from '@/components/ColorPickerBox';
import { CreateCategoryInfo } from '@/api/queryFn/categoryQueryFn';

import DeleteCategoryButton from './DeleteCategoryButton';

interface CategoryModalProps {
Expand Down
146 changes: 30 additions & 116 deletions src/app/mypage/category/CategoryTableBody.tsx
Original file line number Diff line number Diff line change
@@ -1,127 +1,41 @@
import useGetCategoryList from '@/api/hooks/categoryHooks/useGetCategoryList';
import { Flex } from '@/components/common';
import { Button, Skeleton, styled, TableBody, TableCell, TableRow, Tooltip } from '@mui/material';
import useBooleanState from '@/hooks/utils/useBooleanState';
import { useState } from 'react';
import useUpdateCategory from '@/api/hooks/categoryHooks/useUpdateCategory';
import { CreateCategoryInfo } from '@/api/queryFn/categoryQueryFn';
import * as S from './Category.styled';
import CategoryModal from './CategoryModal';
import { Skeleton, TableBody, TableCell, TableRow } from '@mui/material';

interface StyledTableRowProps {
disabled: boolean;
}

function CategoryTableBody() {
const { value: isOpen, setTrue: open, setFalse: close } = useBooleanState();
const [id, setId] = useState<number | undefined>(undefined);

const { categoryList, isLoading } = useGetCategoryList();

const { mutate: updateCategory } = useUpdateCategory();

const [data, setData] = useState<CreateCategoryInfo>(() => ({
title: '',
isDisplayed: 0,
color: '',
}));

const setCategoryData = (unit: keyof typeof data, value: number | string | boolean) => {
setData((prevData) => ({
...prevData,
[unit]: value,
}));
};

const handleUpdateCategory = (categoryId: number, createInfo: CreateCategoryInfo) => {
updateCategory({ categoryId, createInfo });
};
import { getServerDataAboutCategory } from './server/categoryService';
import CategoryTableInfo from './CategoryTableInfo';

const mutateFunction = (body: CreateCategoryInfo) => {
handleUpdateCategory(id!, body);
close();
};
async function CategoryTableBody() {
const categories = await getServerDataAboutCategory().catch((error) => {
console.error(error);
return null;
});

const clickUpdateButton = (categoryId: number) => {
setId(categoryId);
open();
};

const handleCloseModal = () => {
setId(undefined);
close();
};

const convertBoolStateToString = (isDisplayed: number | null) => {
return isDisplayed ? '포함' : '미포함';
};

const StyledTableRow = styled(TableRow)<StyledTableRowProps>(({ theme, disabled }) => ({
backgroundColor: disabled ? theme.palette.action.hover : 'inherit',
cursor: disabled ? 'not-allowed' : 'default',
opacity: disabled ? 0.5 : 1,
'&:last-child td, &:last-child th': {
border: 0,
},
'& td': {
color: disabled ? theme.palette.text.disabled : theme.palette.text.primary,
},
}));

return (
<TableBody>
{isLoading ? (
if (categories && 'error' in categories) {
return (
<TableBody>
<TableRow>
<TableCell colSpan={4}>에러가 났습니다잇..</TableCell>
</TableRow>
</TableBody>
);
}
if (categories === null) {
return (
<TableBody>
<TableRow>
Comment on lines +21 to +23
Copy link
Collaborator

Choose a reason for hiding this comment

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

p4: category가 null일 때 Skeleton이 보이지 않습니다. 확인해보니 height이 0이 되어있는데, 나중에 수정이 필요할 것 같습니다.

<TableCell colSpan={4}>
<Skeleton variant="rectangular" width="100%" height="100%" />
</TableCell>
</TableRow>
) : (
categoryList?.map((category) => {
const isDisable = category.isDefault === 1;
return (
<Tooltip key={category.id} title="기본 카테고리는 수정 불가능 합니다." disableHoverListener={!isDisable}>
<StyledTableRow disabled={isDisable}>
<TableCell component="th" scope="row" sx={{ maxWidth: '200px', overflowX: 'auto' }}>
{category.title}
</TableCell>
<TableCell align="right">
<Flex $gap="20px">
<S.ColorState hexColor={category.color} />
<S.ColorCode>{category.color}</S.ColorCode>
</Flex>
</TableCell>
<TableCell align="right">{convertBoolStateToString(category.isDisplayed)}</TableCell>
<TableCell align="right">
<Button
disabled={isDisable}
onClick={() => {
setCategoryData('title', category.title);
setCategoryData('color', category.color || '');
setCategoryData('isDisplayed', category.isDisplayed || 0);
clickUpdateButton(category.id);
}}
>
수정
</Button>
</TableCell>
</StyledTableRow>
</Tooltip>
);
})
)}

<CategoryModal
isOpen={isOpen}
close={handleCloseModal}
title="수정"
mutateAction={mutateFunction}
data={data}
setData={setCategoryData}
id={id}
/>
</TableBody>
);
</TableBody>
);
}
if (Array.isArray(categories)) {
return (
<TableBody>
<CategoryTableInfo categoryList={categories} />
</TableBody>
);
}
}

export default CategoryTableBody;
2 changes: 2 additions & 0 deletions src/app/mypage/category/CategoryTableFooter.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client';

import useBooleanState from '@/hooks/utils/useBooleanState';
import { Button, TableCell, TableFooter, TableRow } from '@mui/material';
import useCreateCategory from '@/api/hooks/categoryHooks/useCreateCategory';
Expand Down
110 changes: 110 additions & 0 deletions src/app/mypage/category/CategoryTableInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
'use client';

import { styled, TableRow, Button, TableCell } from '@mui/material';
import randomColor from 'randomcolor';
import { useState } from 'react';
import { Category, CreateCategoryInfo } from '@/api/queryFn/categoryQueryFn';
import useUpdateCategory from '@/api/hooks/categoryHooks/useUpdateCategory';
import useGetCategoryList from '@/api/hooks/categoryHooks/useGetCategoryList';
import CategoryModal from './CategoryModal';

interface CategoryTableInfoProps {
categoryList: Category[];
}

interface StyledTableRowProps {
disabled: boolean;
}

const StyledTableRow = styled(TableRow)<StyledTableRowProps>(({ theme, disabled }) => ({
backgroundColor: disabled ? theme.palette.action.hover : 'inherit',
cursor: disabled ? 'not-allowed' : 'default',
opacity: disabled ? 0.5 : 1,

'&:last-child td, &:last-child th': {
border: 0,
},
'& td': {
color: disabled ? theme.palette.text.disabled : theme.palette.text.primary,
},
}));

function CategoryTableInfo({ categoryList }: CategoryTableInfoProps) {
const { categoryList: data } = useGetCategoryList(categoryList);

const [isEditing, setIsEditing] = useState(false);
const [id, setId] = useState<number | undefined>(undefined);

const [categoryData, setCategoryData] = useState<CreateCategoryInfo>({
title: '',
color: '',
isDisplayed: 0,
});

const setCategoryDataForUnit = (unit: keyof typeof categoryData, value: number | string | boolean) => {
setCategoryData((prevData) => ({
...prevData,
[unit]: value,
}));
};

const handleEditClick = (category: Category) => {
setId(category.id);
setIsEditing(true);
categoryData.title = category.title;
categoryData.color = category.color;
categoryData.isDisplayed = category.isDisplayed;
};

const handleCloseModal = () => {
setId(undefined);
setIsEditing(false);
};

const { mutate: updateCategory } = useUpdateCategory();

const handleUpdateCategory = (categoryId: number, createInfo: CreateCategoryInfo) => {
updateCategory({ categoryId, createInfo });
};

const mutateFunction = (body: CreateCategoryInfo) => {
handleUpdateCategory(id!, body);
setIsEditing(false);
};

if (!data) {
return null;
}

return data.map((category) => {
const isDisable = category.isDefault === 1;
return (
<StyledTableRow key={category.id} disabled={isDisable}>
<TableCell sx={{ width: '300px', maxWidth: '300px', overflow: 'auto', whiteSpace: 'nowrap' }}>{category.title}</TableCell>
<TableCell align="right">
<div style={{ display: 'flex', gap: '20px' }}>
<div style={{ backgroundColor: category.color || randomColor(), width: '20px', height: '20px' }} />
<span>{category.color}</span>
</div>
</TableCell>
<TableCell align="right">{category.isDisplayed ? '포함' : '미포함'}</TableCell>
<TableCell align="right">
<Button disabled={isDisable} onClick={() => handleEditClick(category)}>
수정
</Button>
</TableCell>
<CategoryModal
isOpen={isEditing}
close={handleCloseModal}
title="수정"
mutateAction={mutateFunction}
data={categoryData}
setData={setCategoryDataForUnit}
id={id}
/>
</StyledTableRow>
);
});
}

export default CategoryTableInfo;
2 changes: 0 additions & 2 deletions src/app/mypage/category/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
'use client';

import Paper from '@mui/material/Paper';
import { Table, TableContainer } from '@mui/material';

Expand Down
38 changes: 38 additions & 0 deletions src/app/mypage/category/server/categoryService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { eq } from 'drizzle-orm';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { db } from '@/db';
import { categoriesTable } from '@/db/schema';
import { NextResponse } from 'next/server';

export async function getServerDataAboutCategory() {
const session = await getServerSession(authOptions);
if (!session) {
return NextResponse.json({ error: 'unAuthorized Error' }, { status: 401 });
}

const userId = session.user.id;

try {
const categories = await db
.select({
id: categoriesTable.id,
userId: categoriesTable.userId,
title: categoriesTable.title,
color: categoriesTable.color,
isDisplayed: categoriesTable.isDisplayed,
isDefault: categoriesTable.isDefault,
})
.from(categoriesTable)
.where(eq(categoriesTable.userId, userId))
.all();

if (categories.length === 0) {
throw new Error('카테고리가 존재하지 않습니다.');
}

return categories;
} catch (error) {
throw new Error('Failed to fetch categories');
Copy link

Choose a reason for hiding this comment

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

여기서 error를 catch해서 뭔가 다른 로직을 넣을 것이 아니라면,
catch 이후에 곧바로 throw 하는 것 보다 그냥 이 함수를 사용하는 바깥쪽에서 예외 처리를 해도 상관없지 않을까요?

Copy link
Collaborator

Choose a reason for hiding this comment

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

바깥에서 외부 처리를 하는 것이 코드를 추가적으로 작성해야하는 거라 상대적으로 더 품이 드는 행위일 것이라 생각하는데 혹시 바깥쪽에서 예외처리를 해주었을 때 좋은 점이 무엇일까요?

Copy link

Choose a reason for hiding this comment

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

@godhyzzang 이 코드의 경우에는 catch문 내에서 아무런 처리를 하고 있지 않기 때문에 드린 말이었습니다.
만약 공통적으로 에러를 핸들링 하거나 로깅하는 코드가 있어서 그걸 적용하고 싶다면 여기에서 처리를 하는게 맞을텐데,
지금과 같은 경우라면 catch문의 역할이 새로운 에러를 throw하는 것 말고는 딱히 없기 때문이에요~
오히려 공통된 에러 메시지로 throw 하기 때문에, 이 함수를 호출하는 쪽에서는 catch에 실제로 잡힌 error가 뭔지 파악할 수 없게 되는 단점도 있을 수 있을 것 같습니다.

Copy link
Collaborator

Choose a reason for hiding this comment

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

아하 그런 의미였군요! 이해했습니다 감사합니다~

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

공통적으로 쓰이지 않는다면, 차라리 호출부에서 처리하는 것이 명확하게 보일 수 있을 것 같습니다! 의견 감사합니다!

}
}
2 changes: 2 additions & 0 deletions src/components/common/Container.styled.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client';

import styled from '@emotion/styled';

interface ContainerProps {
Expand Down
Loading