Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
baa433ec46 | ||
|
|
3a93b1421b | ||
|
|
2d4d3bd1dc | ||
|
|
d2f1066336 | ||
|
|
e0d6a805c6 | ||
|
|
44894701a8 | ||
|
|
1b769d2f16 | ||
|
|
79de46207e | ||
|
|
d11ab4ad03 | ||
|
|
b786883310 | ||
|
|
adb417ea5d | ||
|
|
9fd1fd7a2e | ||
|
|
348cc486e5 | ||
|
|
913a624631 | ||
|
|
e8d7c80fc3 | ||
|
|
87f90ac52a | ||
|
|
0dafb5b639 | ||
|
|
28d40b2a65 | ||
|
|
bf87f5babc | ||
|
|
488c4180ed | ||
|
|
8a6f0ef3b7 | ||
|
|
b5b22fda13 | ||
|
|
b7db3d799a | ||
|
|
6ae443fe17 | ||
|
|
1ad5516366 | ||
|
|
1fdb95f3f6 | ||
|
|
5072aed994 | ||
|
|
fb755b6a2e | ||
|
|
e9190e447a | ||
|
|
16848c0c83 | ||
|
|
952e6bdc47 | ||
|
|
5b59d42fc5 | ||
|
|
f7170f1d75 | ||
|
|
1b8600ee27 | ||
|
|
70afd666fe | ||
|
|
4133c21a30 | ||
|
|
61774665b7 |
@@ -20,8 +20,7 @@
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||
"deploy": "serverless deploy --stage prod",
|
||||
"seed:config": "ts-node ./node_modules/typeorm-seeding/dist/cli.js config",
|
||||
"seed:run": "ts-node ./node_modules/typeorm-seeding/dist/cli.js seed",
|
||||
"seed:run": "ts-node -r tsconfig-paths/register ./node_modules/typeorm-extension/dist/cli/index.js seed",
|
||||
"deploy:dev": "serverless deploy --stage dev",
|
||||
"deploy:prod": "serverless deploy --stage prod"
|
||||
},
|
||||
@@ -78,8 +77,8 @@
|
||||
"tedious": "^15.1.0",
|
||||
"ts-jenum": "^2.2.2",
|
||||
"typeorm": "^0.3.9",
|
||||
"typeorm-extension": "^2.8.0",
|
||||
"typeorm-naming-strategies": "^4.1.0",
|
||||
"typeorm-seeding": "^1.6.1",
|
||||
"uuid": "^9.0.0",
|
||||
"winston": "^3.8.2"
|
||||
},
|
||||
|
||||
15
backend-api/src/data-source.ts
Normal file
15
backend-api/src/data-source.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { config } from 'dotenv';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
config();
|
||||
|
||||
const configService = new ConfigService();
|
||||
|
||||
export default new DataSource({
|
||||
type: 'sqlite',
|
||||
database: 'sqlite.db',
|
||||
synchronize: true,
|
||||
entities: ['src/**/*.entity{.ts,.js}', 'src/**/*.enum{.ts,.js}'],
|
||||
logging: true,
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Connection } from "typeorm";
|
||||
import { Seeder,Factory } from "typeorm-seeding"
|
||||
import { DataSource } from "typeorm";
|
||||
import { SeederFactoryManager, Seeder } from "typeorm-extension"
|
||||
import { Component } from "../../component/entities/component.entity";
|
||||
import { DatabaseType } from "../../database/entities/database_type.entity";
|
||||
import { Template } from "../../template/entities/template.entity";
|
||||
@@ -7,10 +7,13 @@ import { TemplateItem } from "../../template/entities/template-item.entity";
|
||||
import { User } from "../../user/entities/user.entity"
|
||||
import { YesNo } from '../../common/enum/yn.enum';
|
||||
|
||||
import { setSeederFactory } from 'typeorm-extension';
|
||||
|
||||
export class CreateInitialData implements Seeder {
|
||||
public async run(factory:Factory , connection:Connection) : Promise<any>{
|
||||
await connection
|
||||
|
||||
|
||||
export default class CreateInitialData implements Seeder {
|
||||
public async run ( dataSource: DataSource) : Promise<any>{
|
||||
await dataSource
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(Component)
|
||||
@@ -696,8 +699,7 @@ export class CreateInitialData implements Seeder {
|
||||
|
||||
)
|
||||
.execute()
|
||||
|
||||
await connection
|
||||
await dataSource
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(DatabaseType)
|
||||
@@ -836,7 +838,7 @@ export class CreateInitialData implements Seeder {
|
||||
.execute();
|
||||
|
||||
|
||||
await connection
|
||||
await dataSource
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(Template)
|
||||
@@ -926,7 +928,7 @@ export class CreateInitialData implements Seeder {
|
||||
|
||||
|
||||
|
||||
await connection
|
||||
await dataSource
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(TemplateItem)
|
||||
@@ -1486,7 +1488,7 @@ export class CreateInitialData implements Seeder {
|
||||
])
|
||||
.execute();
|
||||
|
||||
await connection
|
||||
await dataSource
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(User)
|
||||
@@ -1499,4 +1501,5 @@ export class CreateInitialData implements Seeder {
|
||||
}
|
||||
])
|
||||
.execute();
|
||||
}}
|
||||
|
||||
}}
|
||||
@@ -28,6 +28,7 @@
|
||||
"echarts": "^5.3.3",
|
||||
"echarts-for-react": "^3.0.2",
|
||||
"echarts-gl": "^2.0.9",
|
||||
"lodash": "^4.17.21",
|
||||
"mathjs": "^11.3.3",
|
||||
"react": "^18.2.0",
|
||||
"react-ace": "^10.1.0",
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16 3.516v1.509c0 1.39-3.134 2.516-7 2.516S2 6.415 2 5.025v-1.51C2 2.127 5.134 1 9 1s7 1.126 7 2.516zM14.287 7.75c.622-.232 1.247-.531 1.713-.899v3.204c0 1.39-3.134 2.516-7 2.516s-7-1.126-7-2.516V6.852c.467.368 1.063.667 1.714.9 1.4.502 3.27.795 5.286.795 2.016 0 3.884-.293 5.287-.796zM3.714 12.783c1.4.503 3.27.795 5.286.795 2.016 0 3.884-.292 5.287-.795.622-.233 1.247-.532 1.713-.9v2.701c0 1.39-3.134 2.516-7 2.516s-7-1.126-7-2.516v-2.7c.467.367 1.063.666 1.714.899z" fill="#0F5AB3"/>
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="inherit" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16 3.516v1.509c0 1.39-3.134 2.516-7 2.516S2 6.415 2 5.025v-1.51C2 2.127 5.134 1 9 1s7 1.126 7 2.516zM14.287 7.75c.622-.232 1.247-.531 1.713-.899v3.204c0 1.39-3.134 2.516-7 2.516s-7-1.126-7-2.516V6.852c.467.368 1.063.667 1.714.9 1.4.502 3.27.795 5.286.795 2.016 0 3.884-.293 5.287-.796zM3.714 12.783c1.4.503 3.27.795 5.286.795 2.016 0 3.884-.292 5.287-.795.622-.233 1.247-.532 1.713-.9v2.701c0 1.39-3.134 2.516-7 2.516s-7-1.126-7-2.516v-2.7c.467.367 1.063.666 1.714.899z" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 605 B After Width: | Height: | Size: 594 B |
11
frontend-web/src/assets/images/icon/ic-link.svg
Normal file
11
frontend-web/src/assets/images/icon/ic-link.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#wy8iit3taa)" fill="#fff">
|
||||
<path d="M4.94 17.32a3.998 3.998 0 0 1-2.83-6.83l1.41-1.41a.996.996 0 1 1 1.41 1.41L3.52 11.9c-.78.78-.78 2.05 0 2.83.78.78 2.05.78 2.83 0l2.83-2.83c.38-.38.58-.88.58-1.41 0-.53-.21-1.04-.58-1.41a.996.996 0 1 1 1.41-1.41c.75.75 1.17 1.76 1.17 2.83s-.41 2.08-1.17 2.83l-2.83 2.83c-.78.78-1.8 1.17-2.83 1.17l.01-.01z"/>
|
||||
<path d="M7.77 11.5c-.26 0-.51-.1-.71-.29a3.987 3.987 0 0 1-1.17-2.83c0-1.07.41-2.08 1.17-2.83l2.83-2.83c.75-.75 1.76-1.17 2.83-1.17s2.08.41 2.83 1.17c.75.75 1.17 1.76 1.17 2.83s-.42 2.08-1.17 2.83l-1.41 1.41a.996.996 0 1 1-1.41-1.41l1.41-1.41c.38-.38.58-.88.58-1.41 0-.53-.21-1.04-.58-1.41-.75-.75-2.08-.75-2.83 0L8.48 6.98c-.38.38-.58.88-.58 1.41 0 .53.21 1.04.58 1.41a.996.996 0 0 1-.71 1.7z"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="wy8iit3taa">
|
||||
<path fill="#fff" d="M0 0h18v18H0z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1021 B |
137
frontend-web/src/components/BoardItem.tsx
Normal file
137
frontend-web/src/components/BoardItem.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import React from 'react';
|
||||
import { Box, Hidden, ListItem, ListItemIcon, Stack, useMediaQuery, useTheme } from '@mui/material';
|
||||
import { Link as RouterLink, useLocation, useNavigate } from 'react-router-dom';
|
||||
import DeleteButton from '@/components/button/DeleteButton';
|
||||
import ModifyButton from '@/components/button/ModifyButton';
|
||||
import { styled } from '@mui/system';
|
||||
import { dateData } from '@/utils/util';
|
||||
|
||||
interface BoardItemDataProps {
|
||||
id: string;
|
||||
title: string;
|
||||
updatedAt: string;
|
||||
componentType?: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
interface BoardItemProps {
|
||||
data: BoardItemDataProps;
|
||||
handleDeleteSelect: (id, title) => void;
|
||||
}
|
||||
|
||||
const tableBorder = '1px solid #DADDDD';
|
||||
|
||||
const MobileTitleSpan = styled('span')({
|
||||
display: 'block',
|
||||
flexGrow: 0,
|
||||
width: '100%',
|
||||
fontSize: '14px',
|
||||
fontWeight: '600',
|
||||
lineHeight: '1.43',
|
||||
color: '#333333',
|
||||
});
|
||||
|
||||
const TitleSpan = styled('span')({
|
||||
display: 'block',
|
||||
flexGrow: 0,
|
||||
width: '100%',
|
||||
height: '14px',
|
||||
fontSize: '14px',
|
||||
fontWeight: '600',
|
||||
lineHeight: '1.14',
|
||||
color: '#333333',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
});
|
||||
|
||||
interface SubTitleSpanProps {
|
||||
children: React.ReactNode;
|
||||
matches?: boolean;
|
||||
}
|
||||
|
||||
const SubTitleSpan = styled('span')<SubTitleSpanProps>(matches => ({
|
||||
display: 'flex',
|
||||
height: '14px',
|
||||
justifyContent: 'space-between',
|
||||
fontSize: matches ? '14px' : '10px',
|
||||
fontWeight: '500',
|
||||
lineHeight: '1.14',
|
||||
color: '#333333',
|
||||
}));
|
||||
|
||||
const IconRowHeader = ({ icon }) => {
|
||||
return (
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
minWidth: '24px',
|
||||
mr: '18px',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="img"
|
||||
src={`static/images/${icon}`}
|
||||
sx={{ width: 'auto', height: '30px', borderRadius: 0, objectFit: 'contain', backgroundColor: 'transparent' }}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
);
|
||||
};
|
||||
|
||||
function BoardItem(props: BoardItemProps) {
|
||||
const { data, handleDeleteSelect } = props;
|
||||
const { id, title, componentType, icon, updatedAt } = data;
|
||||
|
||||
const theme = useTheme();
|
||||
const matches = useMediaQuery(theme.breakpoints.up('sm'));
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const pathname = location.pathname;
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
disablePadding
|
||||
sx={{
|
||||
py: { xs: '20px', sm: '7px' },
|
||||
px: { xs: '16px', sm: '20px' },
|
||||
pr: { sm: '28px' },
|
||||
borderBottom: tableBorder,
|
||||
'&:last-of-type': { borderBottom: 0 },
|
||||
}}
|
||||
component={RouterLink}
|
||||
to={`${id}`}
|
||||
state={{ from: pathname }}
|
||||
>
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ width: '100%' }}>
|
||||
<Stack direction="row" alignItems="center" sx={{ width: '100%', maxWidth: `calc(100% - ${matches ? 300 : 110}px)` }}>
|
||||
{matches && componentType && <IconRowHeader icon={icon} />}
|
||||
{matches ? <TitleSpan>{title}</TitleSpan> : <MobileTitleSpan>{title}</MobileTitleSpan>}
|
||||
</Stack>
|
||||
<Stack alignItems="center" direction="row">
|
||||
<SubTitleSpan matches={matches}>{dateData(updatedAt)}</SubTitleSpan>
|
||||
<Hidden smDown>
|
||||
<Stack direction="row" gap="18px" ml="48px">
|
||||
<ModifyButton
|
||||
size="medium"
|
||||
onClick={event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
navigate(`modify?id=${id}&title=${title}`, { state: { from: pathname } });
|
||||
}}
|
||||
/>
|
||||
<DeleteButton
|
||||
size="medium"
|
||||
onClick={event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleDeleteSelect(id, title);
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Hidden>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
|
||||
export default BoardItem;
|
||||
@@ -1,7 +1,25 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Box, List, Pagination, Stack, useMediaQuery, useTheme } from '@mui/material';
|
||||
import BoardListItem from './BoardListItem';
|
||||
import BoardItem from './BoardItem';
|
||||
import { styled } from '@mui/system';
|
||||
import { MAX_WIDTH } from '@/constant';
|
||||
|
||||
interface GTSpanProps {
|
||||
children: React.ReactNode;
|
||||
matches?: boolean;
|
||||
isWidget?: boolean;
|
||||
}
|
||||
|
||||
// TODO: 오류 수정
|
||||
const GTSpan = styled('span')<GTSpanProps>(props => ({
|
||||
marginLeft: props.matches && props.isWidget && '50px',
|
||||
fontSize: props.matches ? '13px' : '10px',
|
||||
fontWeight: '500',
|
||||
lineHeight: '1.23',
|
||||
color: '#767676',
|
||||
}));
|
||||
|
||||
const tableBorder = '1px solid #DADDDD';
|
||||
|
||||
function BoardList(props) {
|
||||
const { postList, handleDeleteSelect } = props;
|
||||
@@ -12,32 +30,18 @@ function BoardList(props) {
|
||||
setTotalCount(Math.ceil(postList.length / 10));
|
||||
}, [postList]);
|
||||
|
||||
const GTSpan = styled('span')({
|
||||
marginLeft: postList[0]?.componentType && '50px',
|
||||
fontFamily: 'Pretendard',
|
||||
fontSize: '13px',
|
||||
fontWeight: '500',
|
||||
fontStretch: 'normal',
|
||||
fontStyle: 'normal',
|
||||
lineHeight: '1.23',
|
||||
letterSpacing: 'normal',
|
||||
textAlign: 'left',
|
||||
color: '#767676',
|
||||
});
|
||||
|
||||
const theme = useTheme();
|
||||
const matches = useMediaQuery(theme.breakpoints.up('sm'));
|
||||
const tableBorder = '1px solid #DADDDD';
|
||||
|
||||
const handlePageChange = (e, p) => {
|
||||
setPage(p);
|
||||
};
|
||||
|
||||
const generateBoardItem = () => {
|
||||
return postList.map((item, i) => {
|
||||
return postList.map((item, index) => {
|
||||
const currPage = (page - 1) * 10;
|
||||
if (i >= currPage && i < currPage + 10) {
|
||||
return <BoardListItem postItem={item} key={item.id} handleDeleteSelect={handleDeleteSelect} />;
|
||||
if (index >= currPage && index < currPage + 10) {
|
||||
return <BoardItem data={item} key={item.id} handleDeleteSelect={handleDeleteSelect} />;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
@@ -45,16 +49,24 @@ function BoardList(props) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box sx={{ maxWidth: MAX_WIDTH, width: '100%', mx: 'auto' }}>
|
||||
<Stack
|
||||
flexDirection="row"
|
||||
justifyContent="space-between"
|
||||
sx={{ paddingLeft: '20px', paddingRight: '206px', marginBottom: '11px', marginTop: '36px' }}
|
||||
sx={{
|
||||
width: '100%',
|
||||
paddingLeft: '20px',
|
||||
paddingRight: { xs: '60px', sm: '216px' },
|
||||
marginBottom: '11px',
|
||||
marginTop: { xs: '21px', sm: '36px' },
|
||||
}}
|
||||
>
|
||||
<GTSpan>이름</GTSpan>
|
||||
{matches && <GTSpan>수정일</GTSpan>}
|
||||
<GTSpan isWidget={Boolean(postList?.[0]?.componentType)} matches={matches}>
|
||||
이름
|
||||
</GTSpan>
|
||||
<GTSpan matches={matches}>수정일</GTSpan>
|
||||
</Stack>
|
||||
<List sx={{ m: 'auto', border: tableBorder, borderRadius: 2, backgroundColor: '#fff' }} disablePadding>
|
||||
<List sx={{ width: '100%', m: 'auto', border: tableBorder, borderRadius: 2, backgroundColor: '#fff' }} disablePadding>
|
||||
{generateBoardItem()}
|
||||
</List>
|
||||
<Stack alignItems="center" sx={{ marginTop: '47px' }}>
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Avatar, ListItem, ListItemIcon, Stack } from '@mui/material';
|
||||
import { Link as RouterLink, useLocation, useNavigate } from 'react-router-dom';
|
||||
import DeleteButton from '@/components/button/DeleteButton';
|
||||
import ModifyButton from '@/components/button/ModifyButton';
|
||||
import { styled } from '@mui/system';
|
||||
import { dateData } from '@/utils/util';
|
||||
|
||||
const tableBorder = '1px solid #DADDDD';
|
||||
|
||||
function BoardListItem(props) {
|
||||
const { postItem, handleDeleteSelect } = props;
|
||||
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const pathname = location.pathname;
|
||||
|
||||
const TitleSpan = styled('span')({
|
||||
display: 'flex',
|
||||
height: '14px',
|
||||
justifyContent: 'space-between',
|
||||
fontFamily: 'Pretendard',
|
||||
fontSize: '14px',
|
||||
fontWeight: '600',
|
||||
fontStretch: 'normal',
|
||||
fontStyle: 'normal',
|
||||
lineHeight: '1.14',
|
||||
letterSpacing: 'normal',
|
||||
textAlign: 'left',
|
||||
color: '#333333',
|
||||
});
|
||||
|
||||
const SubTitleSpan = styled('span')({
|
||||
display: 'flex',
|
||||
height: '14px',
|
||||
justifyContent: 'space-between',
|
||||
fontFamily: 'Pretendard',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
fontStretch: 'normal',
|
||||
fontStyle: 'normal',
|
||||
lineHeight: '1.14',
|
||||
letterSpacing: 'normal',
|
||||
textAlign: 'left',
|
||||
color: '#333333',
|
||||
});
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
key={postItem.id}
|
||||
disablePadding
|
||||
sx={{
|
||||
borderBottom: tableBorder,
|
||||
'&:last-of-type': { borderBottom: 0 },
|
||||
height: '56px',
|
||||
paddingRight: 0,
|
||||
}}
|
||||
component={RouterLink}
|
||||
to={`${postItem.id}`}
|
||||
state={{ from: pathname }}
|
||||
>
|
||||
{postItem.componentType ? (
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
minWidth: '24px',
|
||||
ml: '20px',
|
||||
mr: '-2px',
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
src={`static/images/${postItem.icon}`}
|
||||
sx={{ width: 'auto', height: '30px', borderRadius: 0, objectFit: 'contain', backgroundColor: 'transparent' }}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
sx={{ x: 0, paddingLeft: '20px', width: '100%' }}
|
||||
>
|
||||
<TitleSpan>{postItem.title}</TitleSpan>
|
||||
<Stack alignItems="center" direction="row" sx={{ paddingRight: '36px' }}>
|
||||
<SubTitleSpan>{dateData(postItem.updatedAt)}</SubTitleSpan>
|
||||
<span style={{ width: '56px' }} />
|
||||
<ModifyButton
|
||||
size="medium"
|
||||
sx={{ padding: 0 }}
|
||||
onClick={event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
navigate(`modify?id=${postItem.id}&title=${postItem.title}`, { state: { from: pathname } });
|
||||
}}
|
||||
/>
|
||||
<span style={{ width: '36px' }} />
|
||||
<DeleteButton
|
||||
size="medium"
|
||||
sx={{ padding: 0 }}
|
||||
onClick={event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleDeleteSelect(postItem);
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
|
||||
export default BoardListItem;
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Link, Typography } from '@mui/material';
|
||||
import React from 'react';
|
||||
|
||||
const Copyright = (props: any) => {
|
||||
return (
|
||||
<Typography color="text.secondary" align="center" {...props}>
|
||||
<Link
|
||||
color="inherit"
|
||||
href="https://vanillabrain.com/"
|
||||
sx={{ fontSize: '13px', color: '#767676', fontWeight: 'bold', textDecoration: 'none' }}
|
||||
>
|
||||
ⓒ VanillaBrain Inc.
|
||||
</Link>
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
||||
export default Copyright;
|
||||
@@ -1,6 +1,29 @@
|
||||
import React from 'react';
|
||||
import { Box, CardContent, Typography } from '@mui/material';
|
||||
import { CardWrapper } from '@/components/list/CardListWrapper';
|
||||
import { Box, Card, CardContent, Typography } from '@mui/material';
|
||||
|
||||
const CardWrapper = ({ children, selected, onClick, sx = null }) => {
|
||||
return (
|
||||
<Card
|
||||
sx={{
|
||||
padding: '20px 8px 20px 21px',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '2px 2px 6px 0 rgba(0, 42, 105, 0.1)',
|
||||
border: selected ? 'solid 1px #4481c9' : 'solid 1px #ddd',
|
||||
backgroundColor: selected ? '#edf8ff' : '#fff',
|
||||
cursor: 'pointer',
|
||||
position: 'relative',
|
||||
alignItems: 'center',
|
||||
'&:hover': {
|
||||
backgroundColor: '#ebfbff',
|
||||
},
|
||||
...sx,
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
function ImgCardList(props) {
|
||||
const { data, selectedType, handleTypeClick } = props;
|
||||
|
||||
@@ -1,6 +1,29 @@
|
||||
import React from 'react';
|
||||
import { Box, CardContent, Typography } from '@mui/material';
|
||||
import { CardWrapper } from '@/components/list/CardListWrapper';
|
||||
import { Box, Card, CardContent, Typography } from '@mui/material';
|
||||
|
||||
const CardWrapper = ({ children, selected, onClick, sx = null }) => {
|
||||
return (
|
||||
<Card
|
||||
sx={{
|
||||
padding: '20px 8px 20px 21px',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '2px 2px 6px 0 rgba(0, 42, 105, 0.1)',
|
||||
border: selected ? 'solid 1px #4481c9' : 'solid 1px #ddd',
|
||||
backgroundColor: selected ? '#edf8ff' : '#fff',
|
||||
cursor: 'pointer',
|
||||
position: 'relative',
|
||||
alignItems: 'center',
|
||||
'&:hover': {
|
||||
backgroundColor: '#ebfbff',
|
||||
},
|
||||
...sx,
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
function LargeImgCardList(props) {
|
||||
const { data, selectedType, setSelectedType } = props;
|
||||
|
||||
70
frontend-web/src/components/ModalPopup.tsx
Normal file
70
frontend-web/src/components/ModalPopup.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { Box, IconButton, Modal, Paper, Stack, Typography } from '@mui/material';
|
||||
import { ReactComponent as CloseIcon } from '@/assets/images/icon/ic-xmark.svg';
|
||||
import React from 'react';
|
||||
import { MAX_WIDTH } from '@/constant';
|
||||
|
||||
interface ModalPopupProps {
|
||||
open: boolean;
|
||||
handleClose: () => void;
|
||||
title: string | React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const ModalPopup = (props: ModalPopupProps) => {
|
||||
const { open, handleClose, title, children } = props;
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
BackdropProps={{
|
||||
sx: {
|
||||
boxShadow: '0 4px 4px 0 rgba(0, 0, 0, 0.25)',
|
||||
backgroundColor: 'rgba(122, 130, 144, 0.45)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: { xs: '90%', sm: '80%' },
|
||||
maxWidth: MAX_WIDTH,
|
||||
height: '70%',
|
||||
maxHeight: '754px',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '5px 5px 8px 0 rgba(0, 28, 71, 0.15)',
|
||||
border: 'solid 1px #ddd',
|
||||
p: '10px',
|
||||
pt: 0,
|
||||
backgroundColor: '#fff',
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" m="20px" mr="10px">
|
||||
<Typography sx={{ fontSize: '20px', fontWeight: 600, color: '#141414' }}>{title}</Typography>
|
||||
<IconButton onClick={handleClose} sx={{ p: '10px' }}>
|
||||
<CloseIcon width="16" height="16" />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
</Paper>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalPopup;
|
||||
@@ -1,8 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Stack } from '@mui/material';
|
||||
|
||||
function PageContainer(props) {
|
||||
return <Stack sx={{ width: '100%', height: '100%' }}>{props.children}</Stack>;
|
||||
}
|
||||
|
||||
export default PageContainer;
|
||||
@@ -1,15 +1,26 @@
|
||||
import React from 'react';
|
||||
import { Box, Stack, Typography } from '@mui/material';
|
||||
import { Hidden, Stack, SxProps, Typography } from '@mui/material';
|
||||
import { Link as RouterLink, useNavigate } from 'react-router-dom';
|
||||
|
||||
function PageTitleBox(props) {
|
||||
const { title, upperTitle, upperTitleLink = '', button, sx = {}, fixed } = props;
|
||||
interface PageTitleBoxProps {
|
||||
title: string;
|
||||
upperTitle?: string;
|
||||
upperTitleLink?: string;
|
||||
sx?: SxProps;
|
||||
button?: React.ReactNode;
|
||||
fixed?: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function PageTitleBox(props: PageTitleBoxProps) {
|
||||
const { title, upperTitle, upperTitleLink, button, sx, fixed, children } = props;
|
||||
const navigate = useNavigate();
|
||||
|
||||
const defaultBoxSx = {
|
||||
flex: '1 1 auto',
|
||||
paddingLeft: '25px',
|
||||
paddingRight: '25px',
|
||||
width: '100%',
|
||||
width: '100vw',
|
||||
height: '100%',
|
||||
};
|
||||
|
||||
@@ -18,7 +29,7 @@ function PageTitleBox(props) {
|
||||
Object.assign(defaultBoxSx, sx);
|
||||
|
||||
return (
|
||||
<Box sx={{ width: '100%', height: '100%' }}>
|
||||
<Stack direction="column" sx={{ width: '100%', height: '100%', flex: '1 1 auto' }}>
|
||||
<Stack
|
||||
sx={{
|
||||
width: '100%',
|
||||
@@ -30,8 +41,7 @@ function PageTitleBox(props) {
|
||||
justifyContent="space-between"
|
||||
sx={{
|
||||
width: '100%',
|
||||
minWidth: '400px',
|
||||
height: 56,
|
||||
height: { xs: 40, sm: 56 },
|
||||
paddingLeft: '24px',
|
||||
paddingRight: '24px',
|
||||
borderBottom: '1px solid #e3e7ea',
|
||||
@@ -39,7 +49,7 @@ function PageTitleBox(props) {
|
||||
...fixedSx,
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" gap="10px" alignItems="center">
|
||||
<Stack direction="row" gap={{ xs: '6px', sm: '10px' }} alignItems="center">
|
||||
{upperTitle && (
|
||||
<>
|
||||
<Typography
|
||||
@@ -49,7 +59,7 @@ function PageTitleBox(props) {
|
||||
height: '19px',
|
||||
flexGrow: 0,
|
||||
fontFamily: 'Pretendard',
|
||||
fontSize: '16px',
|
||||
fontSize: { xs: '14px', sm: '16px' },
|
||||
fontWeight: '500',
|
||||
fontStretch: 'normal',
|
||||
fontStyle: 'normal',
|
||||
@@ -67,7 +77,7 @@ function PageTitleBox(props) {
|
||||
height: '19px',
|
||||
flexGrow: 0,
|
||||
fontFamily: 'Pretendard',
|
||||
fontSize: '16px',
|
||||
fontSize: 'inherit',
|
||||
fontWeight: '500',
|
||||
fontStretch: 'normal',
|
||||
fontStyle: 'normal',
|
||||
@@ -94,7 +104,7 @@ function PageTitleBox(props) {
|
||||
padding: 0,
|
||||
cursor: 'pointer',
|
||||
fontFamily: 'Pretendard',
|
||||
fontSize: '16px',
|
||||
fontSize: { xs: '14px', sm: '16px' },
|
||||
fontWeight: '600',
|
||||
fontStretch: 'normal',
|
||||
fontStyle: 'normal',
|
||||
@@ -109,19 +119,19 @@ function PageTitleBox(props) {
|
||||
{title}
|
||||
</Typography>
|
||||
</Stack>
|
||||
{button}
|
||||
<Hidden smDown>{button}</Hidden>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Box sx={{ width: '100%', height: 'calc(100% - 56px)', display: 'flex', justifyContent: 'center' }}>
|
||||
<Box sx={defaultBoxSx}>{props.children}</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Stack
|
||||
direction="column"
|
||||
sx={{ justifyContent: 'flex-start', flex: '1 1 auto', width: '100%', height: 'calc(100% - 56px)' }}
|
||||
>
|
||||
<Stack direction="column" sx={defaultBoxSx}>
|
||||
{children}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
PageTitleBox.defaultProps = {
|
||||
title: '',
|
||||
upperTitle: '',
|
||||
};
|
||||
|
||||
export default PageTitleBox;
|
||||
|
||||
228
frontend-web/src/components/PageViewBox.tsx
Normal file
228
frontend-web/src/components/PageViewBox.tsx
Normal file
@@ -0,0 +1,228 @@
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { Avatar, Box, Divider, Stack, SxProps, Typography, useMediaQuery, useTheme } from '@mui/material';
|
||||
import { MAX_WIDTH } from '@/constant';
|
||||
import { LayoutContext } from '@/contexts/LayoutContext';
|
||||
|
||||
interface PageViewBoxProps {
|
||||
iconName?: string;
|
||||
title?: string;
|
||||
titleElement?: React.ReactNode;
|
||||
date?: string;
|
||||
button?: React.ReactNode;
|
||||
sx?: SxProps;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
function PageViewBox(props: PageViewBoxProps) {
|
||||
const theme = useTheme();
|
||||
const matches = useMediaQuery(theme.breakpoints.up('sm'));
|
||||
return matches ? <DesktopViewBox {...props} /> : <MobileViewBox {...props} />;
|
||||
}
|
||||
export default PageViewBox;
|
||||
|
||||
const MobileViewBox = props => {
|
||||
const { iconName, title, titleElement, date, button, sx } = props;
|
||||
const { changeFooterBg } = useContext(LayoutContext);
|
||||
|
||||
useEffect(() => {
|
||||
changeFooterBg('#f9f9fa');
|
||||
return () => {
|
||||
changeFooterBg(null);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flex: '1 1 auto',
|
||||
width: '100%',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
sx={{
|
||||
width: '100%',
|
||||
minHeight: '66px',
|
||||
px: '20px',
|
||||
backgroundColor: '#ffffff',
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" alignItems="center">
|
||||
{iconName && (
|
||||
<Avatar
|
||||
src={`/static/images/${iconName}`}
|
||||
sx={{
|
||||
width: '30px',
|
||||
height: '30px',
|
||||
marginRight: '12px',
|
||||
borderRadius: 0,
|
||||
objectFit: 'contain',
|
||||
backgroundColor: 'transparent',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Stack direction="column" gap="4px" sx={{ mt: '18px', mb: '10px' }}>
|
||||
{titleElement ? (
|
||||
titleElement
|
||||
) : (
|
||||
<Typography
|
||||
sx={{
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: '3',
|
||||
WebkitBoxOrient: 'vertical',
|
||||
maxHeight: '60px',
|
||||
pr: '12px',
|
||||
fontSize: '16px',
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.3,
|
||||
color: '#333',
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
wordWrap: 'break-word',
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
)}
|
||||
{date && (
|
||||
<Typography sx={{ fontSize: '10px', fontWeight: 500, lineHeight: 1.6, color: '#333' }}>
|
||||
수정일: {date}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>{' '}
|
||||
</Stack>
|
||||
|
||||
{button}
|
||||
</Stack>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
minWidth: '100%',
|
||||
height: '100%',
|
||||
flex: '1 1 auto',
|
||||
backgroundColor: '#f9f9fa',
|
||||
}}
|
||||
>
|
||||
<Divider sx={{ width: '100%', height: '1px' }} />
|
||||
{props.children}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const DesktopViewBox = props => {
|
||||
const { iconName, title, titleElement, date, button, sx } = props;
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
px: '20px',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
maxWidth: MAX_WIDTH,
|
||||
height: '100%',
|
||||
borderRadius: '6px',
|
||||
border: 'solid 1px #ddd',
|
||||
backgroundColor: '#f9f9fa',
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
sx={{ width: '100%', height: '57px', px: '20px', backgroundColor: '#ffffff', borderRadius: '6px 6px 0px 0px' }}
|
||||
>
|
||||
{/* title */}
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
sx={{
|
||||
width: '100%',
|
||||
maxWidth: { sm: `calc(100% - ${button ? 390 : 100}px)`, md: `calc(100% - ${button ? 360 : 100}px)` },
|
||||
}}
|
||||
>
|
||||
{iconName && (
|
||||
<Avatar
|
||||
src={`/static/images/${iconName}`}
|
||||
sx={{
|
||||
width: '30px',
|
||||
height: '30px',
|
||||
borderRadius: 0,
|
||||
objectFit: 'contain',
|
||||
backgroundColor: 'transparent',
|
||||
mr: '18px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{titleElement ? (
|
||||
titleElement
|
||||
) : (
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
component="span"
|
||||
sx={{
|
||||
flexGrow: 0,
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
maxWidth: MAX_WIDTH,
|
||||
height: '16px',
|
||||
fontWeight: 500,
|
||||
fontSize: { xs: '16px', sm: '18px' },
|
||||
lineHeight: 0.89,
|
||||
letterSpacing: '-0.18px',
|
||||
color: '#141414',
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{/* date, button */}
|
||||
<Stack direction="row" justifyContent="flex-end" alignItems="center" flexShrink="0">
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
marginRight: button ? { sm: '26px', md: '36px' } : {},
|
||||
height: '16px',
|
||||
fontFamily: 'Pretendard',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
fontStretch: 'normal',
|
||||
fontStyle: 'normal',
|
||||
lineHeight: '1.14',
|
||||
letterSpacing: 'normal',
|
||||
textAlign: 'left',
|
||||
color: '#333333',
|
||||
}}
|
||||
>
|
||||
{date}
|
||||
</Box>
|
||||
<Stack direction="row" justifyContent="flex-end" alignItems="center">
|
||||
{button}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Divider sx={{ width: '100%', height: '1px' }} />
|
||||
{props.children}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -41,7 +41,7 @@ const AlertTemplate = ({ close, message, options }: IProps) => {
|
||||
return (
|
||||
<Dialog
|
||||
open={true}
|
||||
// onClose={close}
|
||||
onClose={close}
|
||||
keepMounted
|
||||
aria-labelledby="alert-dialog-slide-title"
|
||||
aria-describedby="alert-dialog-slide-description"
|
||||
@@ -65,7 +65,8 @@ const AlertTemplate = ({ close, message, options }: IProps) => {
|
||||
fontSize: '14px',
|
||||
color: 'black',
|
||||
paddingTop: hasTitle ? 0 : '20px',
|
||||
minWidth: 300,
|
||||
width: '100%',
|
||||
minWidth: { xs: 0, sm: 300 },
|
||||
maxWidth: 500,
|
||||
whiteSpace: 'pre-wrap',
|
||||
textAlign: 'center',
|
||||
|
||||
@@ -97,9 +97,9 @@ export const AddMenuIconButton = ({
|
||||
aria-expanded={open ? 'true' : undefined}
|
||||
onClick={handleClick}
|
||||
color="primary"
|
||||
sx={{ minWidth: 0, padding: 0 }}
|
||||
sx={{ minWidth: 0, padding: 0, flex: '1 1 auto' }}
|
||||
>
|
||||
<Box component="img" src={iconUrl} sx={sizeOption} alt="추가메뉴 활성화" />
|
||||
<Box component="img" src={iconUrl} sx={sizeOption} alt="추가메뉴" />
|
||||
</Button>
|
||||
<Menu id="styled-menu" anchorEl={anchorEl} open={open} onClose={handleClose} sx={{ width: menuWidth }}>
|
||||
{menuList.map(item => (
|
||||
|
||||
@@ -1,26 +1,48 @@
|
||||
import React from 'react';
|
||||
import { Box, Button, ClickAwayListener, IconButton, Paper, Popper, Stack, TextField, Typography } from '@mui/material';
|
||||
import React, { createRef, forwardRef, Ref, useRef } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ClickAwayListener,
|
||||
IconButton,
|
||||
Paper,
|
||||
Popper,
|
||||
Stack,
|
||||
SxProps,
|
||||
TextField,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from '@mui/material';
|
||||
import { ReactComponent as IconShare } from '@/assets/images/icon/ic-share.svg';
|
||||
import { ReactComponent as IconToggleOn } from '@/assets/images/icon/toggle-on.svg';
|
||||
import { ReactComponent as IconToggleOff } from '@/assets/images/icon/toggle-off.svg';
|
||||
import { ReactComponent as IconLink } from '@/assets/images/icon/ic-link.svg';
|
||||
import { useAlert } from 'react-alert';
|
||||
import { ROUTE_URL } from '@/constant';
|
||||
import DatePicker from '@/components/form/DatePicker';
|
||||
|
||||
const ShareButton = ({ handleSubmit, isShareOn, shareId, shareLimitDate, setShareLimitDate }) => {
|
||||
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
const id = open ? 'simple-popover' : undefined;
|
||||
const shareLink = `${ROUTE_URL}/share/${shareId ? shareId : ''}`;
|
||||
interface ShareButtonProps {
|
||||
handleShareToggle?: () => void;
|
||||
isShareOn?: boolean;
|
||||
shareId?: string;
|
||||
shareLimitDate?: string;
|
||||
setShareLimitDate?: React.Dispatch<React.SetStateAction<string>>;
|
||||
}
|
||||
|
||||
interface SharePopupProps extends ShareButtonProps {
|
||||
matches: boolean;
|
||||
}
|
||||
|
||||
const paperSx: SxProps = {
|
||||
marginTop: '3px',
|
||||
border: 'solid 1px #ddd',
|
||||
borderRadius: '6px',
|
||||
boxShadow: '2px 2px 9px 0 rgba(42, 50, 62, 0.1), 0 4px 4px 0 rgba(0, 0, 0, 0.02)',
|
||||
};
|
||||
|
||||
const ShareOnPopup = forwardRef((props: SharePopupProps, ref: Ref<HTMLDivElement>) => {
|
||||
const { matches, handleShareToggle, shareLimitDate, shareId } = props;
|
||||
const alert = useAlert();
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
const shareLink = `${window.location.origin}/share/${shareId ? shareId : ''}`;
|
||||
|
||||
const handleCopyClick = async () => {
|
||||
await navigator.clipboard
|
||||
@@ -34,22 +56,192 @@ const ShareButton = ({ handleSubmit, isShareOn, shareId, shareLimitDate, setShar
|
||||
});
|
||||
};
|
||||
|
||||
return matches ? (
|
||||
<Paper sx={{ ...paperSx, minWidth: '410px' }} ref={ref}>
|
||||
<Stack sx={{ width: '100%', padding: '24px' }}>
|
||||
<Typography sx={{ mb: '6px', fontSize: '16px', fontWeight: 600, color: '#141414' }}>페이지 공유</Typography>
|
||||
<Stack
|
||||
component="form"
|
||||
onSubmit={event => {
|
||||
event.preventDefault();
|
||||
handleShareToggle();
|
||||
}}
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
mb="3px"
|
||||
>
|
||||
<Typography sx={{ mr: '12px', fontSize: '14px', color: '#141414' }}>링크를 통한 읽기를 허용합니다.</Typography>
|
||||
<IconButton type="submit" sx={{ minWidth: '44px', width: '44px', height: '24px', m: 0, p: 0 }}>
|
||||
<IconToggleOn />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
<Typography>
|
||||
설정하신
|
||||
<Box component="span" sx={{ color: '#0f5ab2' }}>
|
||||
{shareLimitDate}
|
||||
</Box>
|
||||
까지
|
||||
<Box component="span" sx={{ fontWeight: 600, color: '#0f5ab2' }}>
|
||||
공유중
|
||||
</Box>
|
||||
입니다.
|
||||
</Typography>
|
||||
<Stack direction="row" justifyContent="space-between" mt="18px">
|
||||
<TextField sx={{ width: '298px', height: '32px' }} disabled value={shareLink} />
|
||||
<Button variant="contained" sx={{ minWidth: '55px' }} onClick={handleCopyClick}>
|
||||
복사
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Paper>
|
||||
) : (
|
||||
<Paper sx={{ ...paperSx, width: '243px' }} ref={ref}>
|
||||
<Stack sx={{ width: '100%', p: '22px 24px 20px' }}>
|
||||
<Stack
|
||||
component="form"
|
||||
onSubmit={event => {
|
||||
event.preventDefault();
|
||||
handleShareToggle();
|
||||
}}
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
sx={{ mb: '22px' }}
|
||||
>
|
||||
<Typography sx={{ fontSize: '16px', fontWeight: 600, color: '#141414' }}>페이지 공유 중</Typography>
|
||||
<IconButton type="submit" sx={{ minWidth: '44px', width: '44px', height: '24px', m: 0, p: 0 }}>
|
||||
<IconToggleOn />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
<Typography sx={{ fontSize: '14px', color: '#141414' }}>
|
||||
공유 기한:
|
||||
<Box component="span" sx={{ ml: '4px', fontWeight: 'bold', color: '#333' }}>
|
||||
{shareLimitDate}
|
||||
</Box>
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{ width: '32px', minWidth: '32px', height: '32px', p: 0 }}
|
||||
onClick={handleCopyClick}
|
||||
>
|
||||
<IconLink />
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
});
|
||||
|
||||
const ShareOffPopup = forwardRef((props: SharePopupProps, ref: Ref<HTMLDivElement>) => {
|
||||
const { matches, handleShareToggle, shareLimitDate, setShareLimitDate } = props;
|
||||
|
||||
return matches ? (
|
||||
<Paper sx={{ ...paperSx, minWidth: '410px' }} ref={ref}>
|
||||
<Stack sx={{ width: '100%', padding: '24px' }}>
|
||||
<Typography sx={{ mb: '6px', fontSize: '16px', fontWeight: 600, color: '#141414' }}>페이지 공유</Typography>
|
||||
<Stack
|
||||
component="form"
|
||||
onSubmit={event => {
|
||||
event.preventDefault();
|
||||
handleShareToggle();
|
||||
}}
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
mb="16px"
|
||||
>
|
||||
<Typography sx={{ mr: '12px', fontSize: '14px', color: '#141414' }}>
|
||||
링크를 통한 읽기를 허용하지 않습니다.
|
||||
</Typography>
|
||||
<IconButton type="submit" sx={{ minWidth: '44px', width: '44px', height: '24px', m: 0, p: 0 }}>
|
||||
<IconToggleOff />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
<Stack sx={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
<Typography component="span" mr="8px">
|
||||
공유 기한:
|
||||
</Typography>
|
||||
<DatePicker shareLimitDate={shareLimitDate} setShareLimitDate={setShareLimitDate} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Paper>
|
||||
) : (
|
||||
<Paper sx={{ ...paperSx, width: '243px' }} ref={ref}>
|
||||
<Stack sx={{ width: '100%', p: '22px 24px 20px' }}>
|
||||
<Stack
|
||||
component="form"
|
||||
onSubmit={event => {
|
||||
event.preventDefault();
|
||||
handleShareToggle();
|
||||
}}
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
sx={{ mb: '22px' }}
|
||||
>
|
||||
<Typography sx={{ fontSize: '16px', fontWeight: 600, color: '#141414' }}>페이지 공유하지 않음</Typography>
|
||||
<IconButton type="submit" sx={{ minWidth: '44px', width: '44px', height: '24px', m: 0, p: 0 }}>
|
||||
<IconToggleOff />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
<Typography sx={{ fontSize: '14px', color: '#141414' }}>공유 기한:</Typography>
|
||||
<DatePicker shareLimitDate={shareLimitDate} setShareLimitDate={setShareLimitDate} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
});
|
||||
|
||||
const shareOnButtonSx: SxProps = {
|
||||
flexShrink: 0,
|
||||
border: '1px solid #0f5ab2',
|
||||
backgroundColor: '#fff',
|
||||
color: '#0f5ab2',
|
||||
fill: '#0f5ab2',
|
||||
'& span': { mr: '6px' },
|
||||
'&:hover': { color: '#fff', fill: '#fff' },
|
||||
};
|
||||
|
||||
const shareOffButtonSx: SxProps = {
|
||||
flexShrink: 0,
|
||||
fill: '#fff',
|
||||
'& span': { mr: '6px' },
|
||||
'&:hover': { border: '1px solid #0f5ab2', backgroundColor: '#fff', color: '#0f5ab2', fill: '#0f5ab2' },
|
||||
};
|
||||
|
||||
function ShareButton(props: ShareButtonProps) {
|
||||
const { handleShareToggle, isShareOn, shareId, shareLimitDate, setShareLimitDate } = props;
|
||||
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
const id = open ? 'simple-popover' : undefined;
|
||||
const theme = useTheme();
|
||||
const matches = useMediaQuery(theme.breakpoints.up('sm'));
|
||||
const ref: Ref<HTMLDivElement> = useRef();
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose = event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{isShareOn ? (
|
||||
<Button
|
||||
startIcon={<IconShare fill="inherit" />}
|
||||
variant="contained"
|
||||
sx={{
|
||||
px: '12px',
|
||||
border: '1px solid #0f5ab2',
|
||||
backgroundColor: '#fff',
|
||||
color: '#0f5ab2',
|
||||
fill: '#0f5ab2',
|
||||
'& span': { mr: '6px' },
|
||||
'&:hover': { color: '#fff', fill: '#fff' },
|
||||
}}
|
||||
onClick={handleClick}
|
||||
sx={{
|
||||
...shareOnButtonSx,
|
||||
px: { xs: '8px', sm: '12px' },
|
||||
}}
|
||||
>
|
||||
공유중
|
||||
</Button>
|
||||
@@ -57,107 +249,38 @@ const ShareButton = ({ handleSubmit, isShareOn, shareId, shareLimitDate, setShar
|
||||
<Button
|
||||
startIcon={<IconShare fill="inherit" />}
|
||||
variant="contained"
|
||||
sx={{
|
||||
px: '12px',
|
||||
fill: '#fff',
|
||||
'& span': { mr: '6px' },
|
||||
'&:hover': { border: '1px solid #0f5ab2', backgroundColor: '#fff', color: '#0f5ab2', fill: '#0f5ab2' },
|
||||
}}
|
||||
onClick={handleClick}
|
||||
sx={{
|
||||
...shareOffButtonSx,
|
||||
px: { xs: '8px', sm: '12px' },
|
||||
}}
|
||||
>
|
||||
공유
|
||||
</Button>
|
||||
)}
|
||||
<Popper id={id} open={open} anchorEl={anchorEl} disablePortal={false} placement="bottom-end">
|
||||
<ClickAwayListener onClickAway={handleClose}>
|
||||
<Paper
|
||||
sx={{
|
||||
display: 'flex',
|
||||
gap: '32px',
|
||||
minWidth: '410px',
|
||||
width: '410px',
|
||||
mt: '3px',
|
||||
p: '24px',
|
||||
border: 'solid 1px #ddd',
|
||||
borderRadius: '6px',
|
||||
boxShadow: '2px 2px 9px 0 rgba(42, 50, 62, 0.1), 0 4px 4px 0 rgba(0, 0, 0, 0.02)',
|
||||
}}
|
||||
>
|
||||
<Stack sx={{ width: '100%' }}>
|
||||
<Typography sx={{ mb: '6px', fontSize: '16px', fontWeight: 600, color: '#141414' }}>페이지 공유</Typography>
|
||||
{isShareOn ? (
|
||||
<Box>
|
||||
<Stack
|
||||
component="form"
|
||||
onSubmit={event => {
|
||||
event.preventDefault();
|
||||
handleSubmit();
|
||||
}}
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
mb="3px"
|
||||
>
|
||||
<Typography sx={{ mr: '12px', fontSize: '14px', color: '#141414' }}>
|
||||
링크를 통한 읽기를 허용합니다.
|
||||
</Typography>
|
||||
<IconButton type="submit" sx={{ minWidth: '44px', width: '44px', height: '24px', m: 0, p: 0 }}>
|
||||
<IconToggleOn />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
<Typography>
|
||||
설정하신
|
||||
<Box component="span" sx={{ color: '#0f5ab2' }}>
|
||||
{shareLimitDate}
|
||||
</Box>
|
||||
까지
|
||||
<Box component="span" sx={{ fontWeight: 600, color: '#0f5ab2' }}>
|
||||
공유중
|
||||
</Box>
|
||||
입니다.
|
||||
</Typography>
|
||||
|
||||
<Stack direction="row" justifyContent="space-between" mt="18px">
|
||||
<TextField sx={{ width: '298px', height: '32px' }} disabled value={shareLink} />
|
||||
<Button variant="contained" sx={{ minWidth: '55px' }} onClick={handleCopyClick}>
|
||||
복사
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
) : (
|
||||
<Box>
|
||||
<Stack
|
||||
component="form"
|
||||
onSubmit={event => {
|
||||
event.preventDefault();
|
||||
handleSubmit();
|
||||
}}
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
mb="16px"
|
||||
>
|
||||
<Typography sx={{ mr: '12px', fontSize: '14px', color: '#141414' }}>
|
||||
링크를 통한 읽기를 허용하지 않습니다.
|
||||
</Typography>
|
||||
<IconButton type="submit" sx={{ minWidth: '44px', width: '44px', height: '24px', m: 0, p: 0 }}>
|
||||
<IconToggleOff />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
<Stack sx={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
<Typography component="span" mr="8px">
|
||||
공유 기한:
|
||||
</Typography>
|
||||
<DatePicker shareLimitDate={shareLimitDate} setShareLimitDate={setShareLimitDate} />
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
{isShareOn ? (
|
||||
<ShareOnPopup
|
||||
ref={ref}
|
||||
matches={matches}
|
||||
handleShareToggle={handleShareToggle}
|
||||
shareLimitDate={shareLimitDate}
|
||||
shareId={shareId}
|
||||
/>
|
||||
) : (
|
||||
<ShareOffPopup
|
||||
ref={ref}
|
||||
matches={matches}
|
||||
handleShareToggle={handleShareToggle}
|
||||
shareLimitDate={shareLimitDate}
|
||||
setShareLimitDate={setShareLimitDate}
|
||||
/>
|
||||
)}
|
||||
</ClickAwayListener>
|
||||
</Popper>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default ShareButton;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Grid from '@toast-ui/react-grid';
|
||||
import React, { HTMLAttributes, useContext, useEffect, useRef } from 'react';
|
||||
import React, { forwardRef, HTMLAttributes, PropsWithChildren, useEffect, useRef } from 'react';
|
||||
import { GridEventListener, GridOptions } from 'tui-grid';
|
||||
import { Stack } from '@mui/material';
|
||||
|
||||
type EventNameMapping = {
|
||||
onClick: 'click';
|
||||
@@ -37,8 +38,36 @@ type Props = Omit<GridOptions, 'el'> &
|
||||
oneTimeBindingProps?: Array<'data' | 'columns' | 'bodyHeight' | 'frozenColumnCount'>;
|
||||
};
|
||||
|
||||
const DataGrid = (props: Props) => {
|
||||
const gridRef = useRef();
|
||||
export const DataGridWrapper = forwardRef((props: PropsWithChildren, ref: React.Ref<HTMLDivElement>) => {
|
||||
const { children } = props;
|
||||
return (
|
||||
<Stack
|
||||
ref={ref}
|
||||
sx={{
|
||||
flex: 1,
|
||||
height: '100%',
|
||||
minHeight: '100%',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
});
|
||||
|
||||
interface DataGridProps extends Props {
|
||||
resizeObserver?: any;
|
||||
}
|
||||
|
||||
const DataGrid = (props: DataGridProps) => {
|
||||
const gridRef = useRef<Grid>();
|
||||
const { resizeObserver, ...rest } = props;
|
||||
|
||||
useEffect(() => {
|
||||
if (resizeObserver) {
|
||||
gridRef.current.getInstance().refreshLayout();
|
||||
}
|
||||
}, [resizeObserver]);
|
||||
|
||||
return (
|
||||
<Grid
|
||||
@@ -49,12 +78,12 @@ const DataGrid = (props: Props) => {
|
||||
}}
|
||||
rowHeight={36}
|
||||
minRowHeight={36}
|
||||
minBodyHeight={300}
|
||||
minBodyHeight={100}
|
||||
usageStatistics={true}
|
||||
columnOptions={{
|
||||
resizable: true,
|
||||
}}
|
||||
{...props}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,15 +3,25 @@ import TextField from '@mui/material/TextField';
|
||||
import 'dayjs/locale/ko';
|
||||
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
||||
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
||||
import { DatePicker as MuiDatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
|
||||
const DatePicker = ({ shareLimitDate, setShareLimitDate }) => {
|
||||
console.log(shareLimitDate);
|
||||
// TODO: MobileDatePicker 활성화시 ClickAwayListener 간섭 버그 해결
|
||||
// import { DatePicker as MuiDatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import { DesktopDatePicker as MuiDatePicker } from '@mui/x-date-pickers/DesktopDatePicker';
|
||||
|
||||
interface DatePickerProps {
|
||||
shareLimitDate: string;
|
||||
setShareLimitDate: React.Dispatch<React.SetStateAction<any>>;
|
||||
}
|
||||
|
||||
const DatePicker = (props: DatePickerProps) => {
|
||||
const { shareLimitDate, setShareLimitDate } = props;
|
||||
return (
|
||||
<LocalizationProvider adapterLocale="ko" dateAdapter={AdapterDayjs}>
|
||||
<MuiDatePicker
|
||||
disablePast
|
||||
closeOnSelect={false}
|
||||
value={shareLimitDate}
|
||||
onChange={newValue => {
|
||||
onChange={(newValue: any) => {
|
||||
const selectDate = new Date(Date.parse(newValue.$d)).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
@@ -20,7 +30,6 @@ const DatePicker = ({ shareLimitDate, setShareLimitDate }) => {
|
||||
setShareLimitDate(selectDate);
|
||||
}}
|
||||
renderInput={params => {
|
||||
console.log(params, 'ddd');
|
||||
return (
|
||||
<TextField
|
||||
{...params}
|
||||
@@ -35,8 +44,8 @@ const DatePicker = ({ shareLimitDate, setShareLimitDate }) => {
|
||||
/>
|
||||
);
|
||||
}}
|
||||
disablePast
|
||||
PopperProps={{
|
||||
disablePortal: true,
|
||||
sx: {
|
||||
'& .MuiPickersDay-root': {
|
||||
fontFamily: 'Pretendard',
|
||||
|
||||
@@ -1,56 +1,61 @@
|
||||
import { Card, Grid } from '@mui/material';
|
||||
import { Stack, SxProps } from '@mui/material';
|
||||
import React from 'react';
|
||||
|
||||
const CardListWrapper = ({ children, minWidth }) => {
|
||||
interface CardListWrapperProps {
|
||||
children: React.ReactNode;
|
||||
sx?: SxProps;
|
||||
}
|
||||
|
||||
interface CardWrapperProps {
|
||||
children: React.ReactNode;
|
||||
sx?: SxProps;
|
||||
handleClick?: (item) => void;
|
||||
}
|
||||
|
||||
export const CardListWrapper = (props: CardListWrapperProps) => {
|
||||
const { children, sx } = props;
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
spacing={2}
|
||||
<Stack
|
||||
component="ul"
|
||||
sx={{
|
||||
display: { xs: 'flex', md: 'grid' },
|
||||
gridTemplateColumns: { xs: 'repeat(100%)', sm: 'repeat(auto-fit, minmax(0, 228px))' },
|
||||
gap: '8px',
|
||||
minHeight: '20px',
|
||||
listStyle: 'none',
|
||||
pl: 0,
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(${minWidth || 'auto-fit, minmax(0, 228px)'})`,
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Grid>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
CardListWrapper.defaultProps = {
|
||||
children: '',
|
||||
minWidth: false,
|
||||
};
|
||||
|
||||
export default CardListWrapper;
|
||||
|
||||
export const CardWrapper = ({ children, selected, onClick, sx = null }) => {
|
||||
export const CardWrapper = (props: CardWrapperProps) => {
|
||||
const { children, sx, handleClick } = props;
|
||||
|
||||
return (
|
||||
<Card
|
||||
sx={{
|
||||
padding: '20px 8px 20px 21px',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '2px 2px 6px 0 rgba(0, 42, 105, 0.1)',
|
||||
border: selected ? 'solid 1px #4481c9' : 'solid 1px #ddd',
|
||||
backgroundColor: selected ? '#edf8ff' : '#fff',
|
||||
cursor: 'pointer',
|
||||
position: 'relative',
|
||||
alignItems: 'center',
|
||||
'&:hover': {
|
||||
backgroundColor: '#ebfbff',
|
||||
},
|
||||
...sx,
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</Card>
|
||||
<Stack component="li" sx={{ flex: '1 1 auto' }}>
|
||||
<Stack
|
||||
component={handleClick ? 'button' : 'div'}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: '20px',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '2px 2px 6px 0 rgba(0, 42, 105, 0.1)',
|
||||
border: 'solid 1px #ddd',
|
||||
backgroundColor: '#fff',
|
||||
...(handleClick ? { '&:hover': { backgroundColor: '#ebfbff' } } : {}),
|
||||
...sx,
|
||||
}}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{children}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
CardListWrapper.defaultProps = {
|
||||
children: '',
|
||||
minWidth: false,
|
||||
};
|
||||
|
||||
@@ -1,96 +1,99 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { CardActions, CardContent, Grid, Typography } from '@mui/material';
|
||||
import React from 'react';
|
||||
import { Box, Stack, Typography } from '@mui/material';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import CardListWrapper, { CardWrapper } from '@/components/list/CardListWrapper';
|
||||
import { any } from 'prop-types';
|
||||
import { ReactComponent as IconDatabase } from '@/assets/images/icon/ic-data.svg';
|
||||
import DeleteButton from '@/components/button/DeleteButton';
|
||||
import ModifyButton from '@/components/button/ModifyButton';
|
||||
import { DatabaseProps } from '@/pages/Data/DataLayout';
|
||||
import { ReactComponent as IconDatabase } from '@/assets/images/icon/ic-data.svg';
|
||||
|
||||
export const DatabaseCardList = props => {
|
||||
const { data, onUpdate, selectedDatabase, minWidth, disabledIcons, onRemove } = props;
|
||||
interface DatabaseCardListProps {
|
||||
data: DatabaseProps[];
|
||||
selectedData: DatabaseProps;
|
||||
handleDataClick?: (item) => void;
|
||||
handleDataRemove?: (item) => void;
|
||||
isViewMode?: boolean;
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
const handleClick = id => {
|
||||
if (onUpdate !== undefined) {
|
||||
onUpdate({ databaseId: id });
|
||||
}
|
||||
};
|
||||
const selectedSx = { border: 'solid 1px #0f5ab2', backgroundColor: '#edf8ff' };
|
||||
const selectedIconSx = { fill: '#0f5ab2' };
|
||||
const selectedTypoSx = { fontWeight: 'bold', color: '#0f5ab2' };
|
||||
|
||||
export const DatabaseCardList = (props: DatabaseCardListProps) => {
|
||||
const { data, selectedData, handleDataClick, handleDataRemove, isViewMode, icon = <IconDatabase /> } = props;
|
||||
|
||||
return (
|
||||
<CardListWrapper minWidth={minWidth}>
|
||||
{data.map
|
||||
? data.map(item => {
|
||||
const selected = selectedDatabase.databaseId == item.id;
|
||||
return (
|
||||
<Grid item xs={12} md component="li" key={item.id}>
|
||||
<CardWrapper selected={selected} onClick={() => handleClick(item.id)}>
|
||||
<CardContent sx={{ p: '0 !important', alignItems: 'center', display: 'flex' }}>
|
||||
<IconDatabase />
|
||||
<Typography
|
||||
component="span"
|
||||
variant="subtitle2"
|
||||
sx={{
|
||||
pl: '10px',
|
||||
width: disabledIcons ? '100%' : '70%',
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<CardListWrapper sx={{ gridTemplateColumns: 'repeat(100%)' }}>
|
||||
{data.length > 0 &&
|
||||
data.map(item => (
|
||||
<CardWrapper
|
||||
key={item.id}
|
||||
sx={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
height: '62px',
|
||||
...(selectedData?.id == item.id && selectedSx),
|
||||
}}
|
||||
handleClick={() => handleDataClick(item)}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
mr: '8px',
|
||||
fill: '#767676',
|
||||
...(selectedData?.id == item.id && selectedIconSx),
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</Box>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
sx={{
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
mr: '18px',
|
||||
textAlign: 'left',
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
color: '#333',
|
||||
...(selectedData?.id == item.id && selectedTypoSx),
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</Typography>
|
||||
|
||||
{/* 아이콘 */}
|
||||
{disabledIcons ? (
|
||||
<Fragment />
|
||||
) : (
|
||||
<CardActions
|
||||
disableSpacing
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
right: 10,
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end,',
|
||||
m: 0,
|
||||
p: 0,
|
||||
}}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<ModifyButton
|
||||
size="medium"
|
||||
component={RouterLink}
|
||||
to={`/data/source/modify/${item.id}`}
|
||||
width="20"
|
||||
height="20"
|
||||
fill="#767676"
|
||||
/>
|
||||
<DeleteButton
|
||||
size="medium"
|
||||
onClick={event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onRemove(item.id, item.name);
|
||||
}}
|
||||
width="20"
|
||||
height="20"
|
||||
fill="#767676"
|
||||
/>
|
||||
</CardActions>
|
||||
)}
|
||||
</CardWrapper>
|
||||
</Grid>
|
||||
);
|
||||
})
|
||||
: ''}
|
||||
{!isViewMode && (
|
||||
<Stack direction="row" justifyContent="flex-end" width="100%" flex={0}>
|
||||
<ModifyButton
|
||||
size="medium"
|
||||
component={RouterLink}
|
||||
to={`/data/source/modify/${item.id}`}
|
||||
width="20"
|
||||
height="20"
|
||||
padding="0"
|
||||
fill="#767676"
|
||||
sx={{ p: 0, mr: '18px' }}
|
||||
/>
|
||||
<DeleteButton
|
||||
component="span"
|
||||
size="medium"
|
||||
onClick={event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleDataRemove(item);
|
||||
}}
|
||||
width="20"
|
||||
height="20"
|
||||
fill="#767676"
|
||||
sx={{ p: 0 }}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</CardWrapper>
|
||||
))}
|
||||
</CardListWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
DatabaseCardList.defaultProps = {
|
||||
data: any,
|
||||
minWidth: false,
|
||||
disabledIcons: false,
|
||||
};
|
||||
|
||||
@@ -1,92 +1,81 @@
|
||||
import { CardActions, CardContent, Grid, Typography } from '@mui/material';
|
||||
import { Stack, SxProps, Typography } from '@mui/material';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import React from 'react';
|
||||
import CardListWrapper, { CardWrapper } from '@/components/list/CardListWrapper';
|
||||
import { any } from 'prop-types';
|
||||
import ModifyButton from '@/components/button/ModifyButton';
|
||||
import DeleteButton from '@/components/button/DeleteButton';
|
||||
import { DataSetProps, DataTableProps } from '@/pages/Data/DataLayout';
|
||||
|
||||
export const DatasetCardList = props => {
|
||||
const { data, minWidth, disabledIcons, selectedDataset, onSelectDataset, onDeleteDataset } = props;
|
||||
interface DatasetCardListProps {
|
||||
data: DataSetProps[] | DataTableProps[];
|
||||
selectedData: DataSetProps | DataTableProps;
|
||||
handleDataClick?: (item) => void;
|
||||
handleDataRemove?: (item) => void;
|
||||
sx?: SxProps;
|
||||
isViewMode?: boolean;
|
||||
}
|
||||
|
||||
const selectedSx = { border: 'solid 1px #0f5ab2', backgroundColor: '#edf8ff' };
|
||||
|
||||
export const DatasetCardList = (props: DatasetCardListProps) => {
|
||||
const { data, selectedData, handleDataClick, handleDataRemove, isViewMode } = props;
|
||||
|
||||
return (
|
||||
<CardListWrapper minWidth={minWidth}>
|
||||
{data.map
|
||||
? data.map(item => {
|
||||
const selected = selectedDataset?.id == item.id;
|
||||
return (
|
||||
<Grid item component="li" key={item.id}>
|
||||
<CardWrapper selected={selected} onClick={() => onSelectDataset(item)}>
|
||||
<CardContent
|
||||
sx={{
|
||||
p: '0 !important',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
variant="subtitle2"
|
||||
sx={{
|
||||
width: { xs: '50%', md: '100%' },
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{item.title || item.tableName}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<CardListWrapper>
|
||||
{data.length > 0 &&
|
||||
data.map(item => (
|
||||
<CardWrapper
|
||||
key={item.id}
|
||||
sx={{
|
||||
maxHeight: '90px',
|
||||
...(isViewMode && selectedData?.id == item.id && selectedSx),
|
||||
}}
|
||||
handleClick={() => handleDataClick(item)}
|
||||
>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
sx={{
|
||||
width: '100%',
|
||||
textAlign: 'left',
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
color: '#333',
|
||||
}}
|
||||
>
|
||||
{item.title || item.tableName}
|
||||
</Typography>
|
||||
|
||||
{/* 아이콘 */}
|
||||
{disabledIcons ? (
|
||||
<></>
|
||||
) : (
|
||||
<CardActions
|
||||
disableSpacing
|
||||
sx={{
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
right: 10,
|
||||
display: 'flex',
|
||||
justifyContent: 'end',
|
||||
width: '100%',
|
||||
m: 0,
|
||||
p: 0,
|
||||
}}
|
||||
>
|
||||
<ModifyButton
|
||||
size="medium"
|
||||
component={RouterLink}
|
||||
to={`/data/set/modify/${item.id}`}
|
||||
width="20"
|
||||
height="20"
|
||||
fill="#767676"
|
||||
/>
|
||||
<DeleteButton
|
||||
size="medium"
|
||||
onClick={event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onDeleteDataset(item);
|
||||
}}
|
||||
width="20"
|
||||
height="20"
|
||||
fill="#767676"
|
||||
/>
|
||||
</CardActions>
|
||||
)}
|
||||
</CardWrapper>
|
||||
</Grid>
|
||||
);
|
||||
})
|
||||
: ''}
|
||||
{/* 아이콘 */}
|
||||
{!isViewMode && (
|
||||
<Stack direction="row" justifyContent="flex-end" width="100%" mt="11px">
|
||||
<ModifyButton
|
||||
size="medium"
|
||||
component={RouterLink}
|
||||
to={`/data/set/modify/${item.id}`}
|
||||
width="20"
|
||||
height="20"
|
||||
padding="0"
|
||||
fill="#767676"
|
||||
sx={{ p: 0, mr: '18px' }}
|
||||
/>
|
||||
<DeleteButton
|
||||
component="span"
|
||||
size="medium"
|
||||
onClick={event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleDataRemove(item);
|
||||
}}
|
||||
width="20"
|
||||
height="20"
|
||||
fill="#767676"
|
||||
sx={{ p: 0 }}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</CardWrapper>
|
||||
))}
|
||||
</CardListWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
DatasetCardList.defaultProps = {
|
||||
data: any,
|
||||
minWidth: false,
|
||||
disabledIcons: false,
|
||||
};
|
||||
|
||||
@@ -4,6 +4,12 @@ import { Box } from '@mui/material';
|
||||
import { ReactElement } from 'react';
|
||||
import loadingGif from '@/assets/images/loading.gif';
|
||||
|
||||
interface LoadingProps {
|
||||
in: boolean;
|
||||
style?: React.CSSProperties;
|
||||
rest?: any;
|
||||
}
|
||||
|
||||
const duration = 100;
|
||||
|
||||
const defaultStyle = {
|
||||
@@ -32,7 +38,7 @@ const LoadingBox = styled(Box)(() => ({
|
||||
zIndex: 100,
|
||||
}));
|
||||
|
||||
export const Loading = ({ in: inProp, ...rest }): ReactElement => {
|
||||
export const Loading = ({ in: inProp, style, ...rest }: LoadingProps): ReactElement => {
|
||||
return (
|
||||
<Transition in={inProp} timeout={duration}>
|
||||
{state => (
|
||||
@@ -40,6 +46,7 @@ export const Loading = ({ in: inProp, ...rest }): ReactElement => {
|
||||
style={{
|
||||
...defaultStyle,
|
||||
...transitionStyles[state],
|
||||
...style,
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
|
||||
@@ -6,7 +6,8 @@ import { useAlert } from 'react-alert';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { removeToken } from '@/helpers/authHelper';
|
||||
|
||||
const Logout = () => {
|
||||
const Logout = props => {
|
||||
const { sx } = props;
|
||||
const { showLoading, hideLoading } = useContext(LoadingContext);
|
||||
const navigate = useNavigate();
|
||||
const alert = useAlert();
|
||||
@@ -53,29 +54,18 @@ const Logout = () => {
|
||||
disableFocusRipple
|
||||
disableTouchRipple
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minWidth: 0,
|
||||
minHeight: 0,
|
||||
m: 0,
|
||||
p: 0,
|
||||
gap: '12px',
|
||||
fontSize: 'inherit',
|
||||
fontWeight: 'inherit',
|
||||
textDecoration: 'underline',
|
||||
color: 'inherit',
|
||||
whiteSpace: 'nowrap',
|
||||
'&:hover': {
|
||||
textDecoration: 'underline',
|
||||
backgroundColor: 'inherit',
|
||||
},
|
||||
|
||||
'&:after': {
|
||||
content: `""`,
|
||||
width: '1px',
|
||||
height: '10px',
|
||||
backgroundColor: '#cccfd8',
|
||||
},
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
로그아웃
|
||||
|
||||
@@ -63,22 +63,29 @@ const ProfileViewButton = () => {
|
||||
{userState.userEmail}
|
||||
</Typography>
|
||||
<Stack>
|
||||
<Typography
|
||||
component="div"
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
mt: '32px',
|
||||
fontSize: '14px',
|
||||
textAlign: 'center',
|
||||
color: '#4a4a4a',
|
||||
}}
|
||||
>
|
||||
<Logout />
|
||||
<Logout
|
||||
sx={{
|
||||
textDecoration: 'underline',
|
||||
'&:hover': {
|
||||
backgroundColor: 'transparent',
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Box sx={{ width: '1px', height: '10px', mx: '12px', backgroundColor: '#cccfd8' }} />
|
||||
<ProfileModify />
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
@@ -106,4 +106,9 @@ export const LEGEND_LIST = [
|
||||
|
||||
export const STATUS = {
|
||||
SUCCESS: 'SUCCESS',
|
||||
};
|
||||
PENDING: 'PENDING',
|
||||
FAILURE: 'FAILURE',
|
||||
FINISH: 'FINISH',
|
||||
} as const;
|
||||
|
||||
export const MAX_WIDTH = '1392px';
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
import { createContext, FC, useState } from 'react';
|
||||
import { createContext, useState } from 'react';
|
||||
|
||||
type LayoutContext = { fixed: boolean; fixLayout: (visible: boolean) => void };
|
||||
type LayoutContext = {
|
||||
fixed: boolean;
|
||||
fixLayout: (visible: boolean) => void;
|
||||
footerBg: string | null;
|
||||
changeFooterBg: (color: string) => void;
|
||||
};
|
||||
|
||||
export const LayoutContext = createContext<LayoutContext>({} as LayoutContext);
|
||||
|
||||
export const LayoutProvider = ({ children }) => {
|
||||
const [fixed, setLayoutFix] = useState(false);
|
||||
const [footerBg, setFooterBg] = useState(null);
|
||||
|
||||
const fixLayout = (visible: boolean) => {
|
||||
setLayoutFix(visible);
|
||||
};
|
||||
|
||||
return <LayoutContext.Provider value={{ fixed, fixLayout }}>{children}</LayoutContext.Provider>;
|
||||
const changeFooterBg = (color: string) => {
|
||||
setFooterBg(color);
|
||||
};
|
||||
|
||||
return <LayoutContext.Provider value={{ fixed, fixLayout, footerBg, changeFooterBg }}>{children}</LayoutContext.Provider>;
|
||||
};
|
||||
|
||||
@@ -13,12 +13,42 @@ const instance = axios.create({
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
let pendingRequests = {};
|
||||
let isLoginUser = true;
|
||||
let isAlreadyFetchingAccessToken = false;
|
||||
let subscribers: ((token: string) => void)[] = [];
|
||||
// console.log('pendingRequests', pendingRequests);
|
||||
|
||||
// request interceptor
|
||||
instance.interceptors.request.use(async config => {
|
||||
// 요청에 대한 unique key 생성
|
||||
const generateReqKey = config => {
|
||||
const { method, url, params, data } = config;
|
||||
return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&');
|
||||
};
|
||||
|
||||
// 진행중인 요청 저장
|
||||
const addPendingRequest = config => {
|
||||
const requestKey = generateReqKey(config);
|
||||
config.cancelToken =
|
||||
config.cancelToken ||
|
||||
new axios.CancelToken(cancel => {
|
||||
if (!pendingRequests[requestKey]) {
|
||||
pendingRequests[requestKey] = [];
|
||||
}
|
||||
pendingRequests[requestKey].push(cancel);
|
||||
});
|
||||
};
|
||||
|
||||
// 저장된 요청 취소
|
||||
const removePendingRequest = config => {
|
||||
const requestKey = generateReqKey(config);
|
||||
if (pendingRequests[requestKey]) {
|
||||
pendingRequests[requestKey].forEach(cancel => {
|
||||
cancel('Request canceled due to new request.');
|
||||
});
|
||||
delete pendingRequests[requestKey];
|
||||
}
|
||||
};
|
||||
|
||||
// 토큰 정보 요청 header에 삽입
|
||||
const addAuthToHeaders = config => {
|
||||
const token = getToken();
|
||||
const shareToken = getShareToken();
|
||||
|
||||
@@ -30,40 +60,56 @@ instance.interceptors.request.use(async config => {
|
||||
config.headers['Authorization-url'] = `Bearer ${shareToken}`;
|
||||
}
|
||||
return config;
|
||||
};
|
||||
|
||||
// 요청 인터셉터
|
||||
instance.interceptors.request.use(async config => {
|
||||
const newConfig = addAuthToHeaders(config);
|
||||
// removePendingRequest(newConfig); // 같은 요청이 갔을 경우 기존 요청 취소
|
||||
// addPendingRequest(newConfig);
|
||||
return newConfig;
|
||||
});
|
||||
|
||||
// response interceptor
|
||||
// 중단 이벤트 발생 시 모든 요청 중단
|
||||
export function cancelAllRequests() {
|
||||
Object.keys(pendingRequests).forEach(key => {
|
||||
pendingRequests[key].forEach(cancel => {
|
||||
cancel('All requests canceled');
|
||||
});
|
||||
});
|
||||
pendingRequests = {};
|
||||
}
|
||||
|
||||
let isAlreadyFetchingAccessToken = false;
|
||||
const subscribers: ((token: string) => void)[] = [];
|
||||
|
||||
// 응답 인터셉터
|
||||
instance.interceptors.response.use(
|
||||
response => {
|
||||
// removePendingRequest(response.config); // 완료된 요청 삭제
|
||||
return response;
|
||||
},
|
||||
async error => {
|
||||
const {
|
||||
response: { status },
|
||||
} = error;
|
||||
if (status === 401) {
|
||||
if (error.response.data.data === 'accessTokenExpired' && isLoginUser) {
|
||||
// 로그인 사용자의 token 만료 후 첫 요청
|
||||
return await resetTokenAndReattemptRequest(error);
|
||||
}
|
||||
const { response: errorResponse } = error;
|
||||
if (errorResponse?.status === 401 && errorResponse?.data?.message === 'accessTokenExpired' && isLoginUser) {
|
||||
// 로그인 사용자의 token 만료 후 첫 요청
|
||||
await resetTokenAndReattemptRequest(errorResponse);
|
||||
}
|
||||
error.message =
|
||||
(error.response && error.response.data && error.response.data.message) || error.message || error.toString();
|
||||
(errorResponse && errorResponse?.data && errorResponse?.data?.message) || error?.message || error.toString();
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
async function resetTokenAndReattemptRequest(error) {
|
||||
async function resetTokenAndReattemptRequest(errorResponse) {
|
||||
// subscribers에 access token을 받은 이후 재요청할 함수 추가 (401로 실패했던)
|
||||
// retryOriginalRequest는 pending 상태로 있다가
|
||||
// access token을 받은 이후 onAccessTokenFetched가 호출될 때
|
||||
// access token을 넘겨 다시 axios로 요청하고
|
||||
// 결과값을 처음 요청했던 promise의 resolve로 settle시킨다.
|
||||
try {
|
||||
const { response: errorResponse } = error;
|
||||
|
||||
// subscribers에 access token을 받은 이후 재요청할 함수 추가 (401로 실패했던)
|
||||
// retryOriginalRequest는 pending 상태로 있다가
|
||||
// access token을 받은 이후 onAccessTokenFetched가 호출될 때
|
||||
// access token을 넘겨 다시 axios로 요청하고
|
||||
// 결과값을 처음 요청했던 promise의 resolve로 settle시킨다.
|
||||
const retryOriginalRequest = new Promise((resolve, reject) => {
|
||||
addSubscriber(async accessToken => {
|
||||
subscribers.push(async accessToken => {
|
||||
try {
|
||||
errorResponse.config.headers.Authorization = accessToken;
|
||||
resolve(instance(errorResponse.config));
|
||||
@@ -84,10 +130,8 @@ async function resetTokenAndReattemptRequest(error) {
|
||||
setToken(newAccessToken);
|
||||
|
||||
isAlreadyFetchingAccessToken = false; // 문열기 (초기화)
|
||||
|
||||
onAccessTokenFetched(data.accessToken);
|
||||
}
|
||||
|
||||
return retryOriginalRequest; // pending 됐다가 onAccessTokenFetched가 호출될 때 resolve
|
||||
} catch (error) {
|
||||
console.log('error', error);
|
||||
@@ -99,13 +143,9 @@ async function resetTokenAndReattemptRequest(error) {
|
||||
}
|
||||
}
|
||||
|
||||
function addSubscriber(callback) {
|
||||
subscribers.push(callback);
|
||||
}
|
||||
|
||||
function onAccessTokenFetched(accessToken) {
|
||||
subscribers.forEach(callback => callback(accessToken));
|
||||
subscribers = [];
|
||||
subscribers.length = 0;
|
||||
}
|
||||
|
||||
export async function get(url, data?, config = {}) {
|
||||
@@ -124,10 +164,6 @@ export async function del(url, config = {}) {
|
||||
return instance.delete(url, { ...config });
|
||||
}
|
||||
|
||||
export async function postForm(url, data?, config = {}) {
|
||||
return instance.post(url, data, { ...config });
|
||||
}
|
||||
|
||||
export async function patch(url, data?, config = {}) {
|
||||
return instance.patch(url, { ...data }, { ...config });
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Link, Stack, Typography } from '@mui/material';
|
||||
|
||||
const Footer = props => {
|
||||
const { height } = props;
|
||||
|
||||
return (
|
||||
<Stack sx={{ height: height, alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Link
|
||||
color="inherit"
|
||||
href="https://vanillabrain.com/"
|
||||
target="_blank"
|
||||
sx={{ fontSize: '13px', color: '#767676', textDecoration: 'none' }}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: '16px',
|
||||
fontFamily: 'Pretendard',
|
||||
fontSize: '13px',
|
||||
fontWeight: 'normal',
|
||||
fontStretch: 'normal',
|
||||
fontStyle: 'normal',
|
||||
lineHeight: 'normal',
|
||||
letterSpacing: 'normal',
|
||||
textAlign: 'center',
|
||||
color: '#767676',
|
||||
}}
|
||||
>
|
||||
ⓒ VanillaBrain Inc.
|
||||
</Typography>
|
||||
</Link>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
37
frontend-web/src/layouts/Footer/index.tsx
Normal file
37
frontend-web/src/layouts/Footer/index.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Link, Stack, Typography } from '@mui/material';
|
||||
import { LayoutContext } from '@/contexts/LayoutContext';
|
||||
|
||||
export const Copyright = (props: any) => {
|
||||
return (
|
||||
<Typography color="text.secondary" align="center" {...props}>
|
||||
<Link
|
||||
color="inherit"
|
||||
href="https://vanillabrain.com/"
|
||||
target="_blank"
|
||||
sx={{ fontSize: '13px', color: '#767676', fontWeight: 'bold', textDecoration: 'none' }}
|
||||
>
|
||||
ⓒ VanillaBrain Inc.
|
||||
</Link>
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
||||
const Footer = () => {
|
||||
const { footerBg } = useContext(LayoutContext);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
sx={{
|
||||
height: '50px',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: footerBg ? footerBg : '#fff',
|
||||
}}
|
||||
>
|
||||
<Copyright />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
@@ -30,17 +30,19 @@ function NavBar(props) {
|
||||
component={NavLink}
|
||||
to={item.link}
|
||||
sx={{
|
||||
minWidth: 0,
|
||||
color: 'inherit',
|
||||
padding: 0,
|
||||
fontSize: 15,
|
||||
fontSize: { xs: 13, sm: 15 },
|
||||
height: 50,
|
||||
px: '18px',
|
||||
px: { xs: '14px', sm: '18px' },
|
||||
fontFamily: 'Pretendard',
|
||||
fontWeight: 500,
|
||||
fontStretch: 'normal',
|
||||
fontStyle: 'normal',
|
||||
lineHeight: 'normal',
|
||||
letterSpacing: 'normal',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React from 'react';
|
||||
import { AppBar, Box, Divider, Toolbar } from '@mui/material';
|
||||
import { AppBar, Box, Divider, Hidden, Toolbar } from '@mui/material';
|
||||
import { AddMenuIconButton } from '@/components/button/AddIconButton';
|
||||
import Logo from './Logo';
|
||||
import NavBar from './NavBar';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import ProfileViewButton from '@/components/user/ProfileViewButton';
|
||||
import Logout from '@/components/user/Logout';
|
||||
|
||||
const menuList = [
|
||||
{ name: '데이터 소스', link: '/data/source/create' },
|
||||
@@ -13,8 +14,7 @@ const menuList = [
|
||||
{ name: '대시보드', link: '/dashboard/create?createType=dashboard' },
|
||||
];
|
||||
|
||||
function Header(props) {
|
||||
const headerHeight = props.height;
|
||||
function Header() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const navItems = [
|
||||
@@ -24,20 +24,26 @@ function Header(props) {
|
||||
];
|
||||
|
||||
const handleMenuSelect = item => {
|
||||
if (item.link !== undefined) {
|
||||
if (item.link) {
|
||||
navigate(item.link);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AppBar elevation={0} component="nav" sx={{ minWidth: '600px', left: 0, height: '65px' }}>
|
||||
<Toolbar variant="dense" sx={{ height: headerHeight, justifyContent: 'space-between', columnGap: '30px' }}>
|
||||
<AppBar elevation={0} component="nav" sx={{ left: 0, height: { xs: '56px', sm: '65px' } }}>
|
||||
<Toolbar variant="dense" sx={{ height: 65, justifyContent: 'space-between', columnGap: { xs: '20px', sm: '32px' } }}>
|
||||
<Logo />
|
||||
<NavBar navItems={navItems} />
|
||||
<Box sx={{ display: 'flex', gap: '16px' }}>
|
||||
<AddMenuIconButton menuList={menuList} handleSelect={handleMenuSelect} />
|
||||
<ProfileViewButton />
|
||||
</Box>
|
||||
<Hidden smDown>
|
||||
<NavBar navItems={navItems} />
|
||||
<Box sx={{ display: 'flex', gap: '16px' }}>
|
||||
<AddMenuIconButton menuList={menuList} handleSelect={handleMenuSelect} />
|
||||
<ProfileViewButton />
|
||||
</Box>
|
||||
</Hidden>
|
||||
<Hidden smUp>
|
||||
<NavBar navItems={navItems.slice(0, 2)} />
|
||||
<Logout sx={{ fontSize: '12px', color: '#767676' }} />
|
||||
</Hidden>
|
||||
</Toolbar>
|
||||
<Divider />
|
||||
</AppBar>
|
||||
@@ -1,46 +0,0 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Box, Stack } from '@mui/material';
|
||||
|
||||
import Header from './Header/Header';
|
||||
import Footer from './Footer/Footer';
|
||||
import { LayoutContext } from '@/contexts/LayoutContext';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
const Layout = props => {
|
||||
const { children } = props;
|
||||
const headerHeight = 65;
|
||||
const footerHeight = 50;
|
||||
|
||||
const { fixed } = useContext(LayoutContext);
|
||||
|
||||
const defaultSx = {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
};
|
||||
|
||||
const fixSx = {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
flex: '1 1 auto',
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={fixed ? fixSx : defaultSx}>
|
||||
<Header height={headerHeight} />
|
||||
<Stack
|
||||
sx={{
|
||||
width: '100%',
|
||||
minWidth: 'min-content',
|
||||
paddingTop: headerHeight + 'px',
|
||||
minHeight: `calc(100% - ${footerHeight}px)`,
|
||||
}}
|
||||
>
|
||||
{children || <Outlet />}
|
||||
</Stack>
|
||||
<Footer height={footerHeight} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
38
frontend-web/src/layouts/Layout/index.tsx
Normal file
38
frontend-web/src/layouts/Layout/index.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Box, Stack } from '@mui/material';
|
||||
import Header from '@/layouts/Header';
|
||||
import Footer from '@/layouts/Footer';
|
||||
import { LayoutContext } from '@/contexts/LayoutContext';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
const Layout = props => {
|
||||
const { children } = props;
|
||||
const { fixed, footerBg } = useContext(LayoutContext);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
flex: '1 1 auto',
|
||||
overflow: fixed ? 'hidden' : 'visible',
|
||||
backgroundColor: footerBg ? footerBg : '#fff',
|
||||
}}
|
||||
>
|
||||
<Header />
|
||||
<Stack
|
||||
sx={{
|
||||
flex: '1 1 auto',
|
||||
width: '100%',
|
||||
pt: { xs: '56px', sm: '65px' },
|
||||
minHeight: `calc(100% - 50px)`,
|
||||
}}
|
||||
>
|
||||
{children || <Outlet />}
|
||||
</Stack>
|
||||
<Footer />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
34
frontend-web/src/layouts/PublicLayout/index.tsx
Normal file
34
frontend-web/src/layouts/PublicLayout/index.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Box, Stack } from '@mui/material';
|
||||
import Footer from '@/layouts/Footer';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { LandingLogo } from '@/layouts/Header/Logo';
|
||||
import { MAX_WIDTH } from '@/constant';
|
||||
import { LayoutContext } from '@/contexts/LayoutContext';
|
||||
|
||||
const PublicLayout = props => {
|
||||
const { children } = props;
|
||||
const { fixed, footerBg } = useContext(LayoutContext);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
flex: '1 1 auto',
|
||||
overflow: fixed ? 'hidden' : 'visible',
|
||||
backgroundColor: footerBg ? footerBg : '#fff',
|
||||
}}
|
||||
>
|
||||
<Stack sx={{ width: '100%', maxWidth: MAX_WIDTH, mx: 'auto', backgroundColor: '#fff' }}>
|
||||
<Stack sx={{ height: 65, justifyContent: 'center', pl: '20px' }}>
|
||||
<LandingLogo />
|
||||
</Stack>
|
||||
{children || <Outlet />}
|
||||
</Stack>
|
||||
<Footer />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default PublicLayout;
|
||||
@@ -176,7 +176,7 @@ function AddWidgetPopup({ label, useWidgetIds = [], widgetOpen = false, widgetSe
|
||||
<ListItemButton
|
||||
key={index}
|
||||
selected={isItemSelection(item)}
|
||||
sx={{ paddingX: '20px', height: '50px' }}
|
||||
sx={{ width: '100%', paddingX: '20px', height: '50px' }}
|
||||
onClick={() => handleClick(item)}
|
||||
>
|
||||
<Checkbox checked={isItemSelection(item)} />
|
||||
@@ -198,7 +198,15 @@ function AddWidgetPopup({ label, useWidgetIds = [], widgetOpen = false, widgetSe
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primaryTypographyProps={{
|
||||
sx: {
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
// maxWidth: '470px',
|
||||
marginLeft: '16px',
|
||||
fontFamily: 'Pretendard',
|
||||
fontSize: '14px',
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box, Divider, Stack } from '@mui/material';
|
||||
|
||||
function DashboardTitleBox(props) {
|
||||
const { title, button, sx } = props;
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: '1392px',
|
||||
minWidth: '1392px',
|
||||
height: '100%',
|
||||
borderRadius: '6px',
|
||||
border: 'solid 1px #ddd',
|
||||
backgroundColor: '#f9f9fa',
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
sx={{ width: '100%', height: '57px', backgroundColor: '#ffffff', borderRadius: '6px 6px 0px 0px' }}
|
||||
>
|
||||
{title}
|
||||
{button}
|
||||
</Stack>
|
||||
<Divider sx={{ width: '100%', height: '1px' }} />
|
||||
{props.children}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
DashboardTitleBox.defaultProps = {
|
||||
title: '',
|
||||
width: '100%',
|
||||
menuList: false,
|
||||
naviUrl: false,
|
||||
button: false,
|
||||
};
|
||||
|
||||
export default DashboardTitleBox;
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
import { useAlert } from 'react-alert';
|
||||
import TemplateService from '@/api/templateService';
|
||||
import WidgetService from '@/api/widgetService';
|
||||
import { STATUS } from '@/constant';
|
||||
import { MAX_WIDTH, STATUS } from '@/constant';
|
||||
import CloseButton from '@/components/button/CloseButton';
|
||||
import { ReactComponent as TemplateIcon01 } from '@/assets/images/template/template01.svg';
|
||||
import { ReactComponent as TemplateIcon02 } from '@/assets/images/template/template02.svg';
|
||||
@@ -186,6 +186,13 @@ export const WidgetList = ({
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primaryTypographyProps={{
|
||||
sx: {
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
marginLeft: '16px',
|
||||
fontFamily: 'Pretendard',
|
||||
@@ -446,7 +453,7 @@ function RecommendDashboardPopup({ recommendOpen = false, handleComplete = null
|
||||
setDialogWidth('600px');
|
||||
} else if (step == 2) {
|
||||
setSubTitle('템플릿을 선택하세요');
|
||||
setDialogWidth('1392px');
|
||||
setDialogWidth(MAX_WIDTH);
|
||||
}
|
||||
}, [step]);
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Box, Button, Card, Stack, TextField } from '@mui/material';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import PageContainer from '@/components/PageContainer';
|
||||
import PageTitleBox from '@/components/PageTitleBox';
|
||||
import AddWidgetPopup from '@/pages/Dashboard/Components/AddWidgetPopup';
|
||||
import ConfirmCancelButton from '@/components/button/ConfirmCancelButton';
|
||||
@@ -16,7 +15,7 @@ import AddIcon from '@mui/icons-material/Add';
|
||||
import RecommendDashboardPopup from '@/pages/Dashboard/Components/RecommendDashboardPopup';
|
||||
import DashboardService from '@/api/dashboardService';
|
||||
import { STATUS } from '@/constant';
|
||||
import DashboardTitleBox from '../Components/DashboardTitleBox';
|
||||
import PageViewBox from '../../../components/PageViewBox';
|
||||
import CloseButton from '@/components/button/CloseButton';
|
||||
import bg from '@/assets/images/dashboard-bg.svg';
|
||||
import { LoadingContext } from '@/contexts/LoadingContext';
|
||||
@@ -346,129 +345,126 @@ function DashboardModify() {
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageTitleBox
|
||||
upperTitle="대시보드"
|
||||
upperTitleLink="/dashboard"
|
||||
title={topTitle}
|
||||
sx={{ width: '100%', marginTop: '22px' }}
|
||||
button={
|
||||
<Stack direction="row" spacing={3} sx={{ marginRight: '20px' }}>
|
||||
<ConfirmCancelButton
|
||||
confirmProps={{ disabled: false, onClick: handleSaveDialogSelect }}
|
||||
cancelProps={{ onClick: handleCancelDialogSelect }}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
<DashboardTitleBox
|
||||
title={
|
||||
<TextField
|
||||
id="userDashboardName"
|
||||
label="대시보드 이름"
|
||||
required
|
||||
sx={{
|
||||
width: '960px',
|
||||
height: '32px',
|
||||
marginLeft: '16px',
|
||||
marginTop: 0,
|
||||
borderRadius: '4px',
|
||||
backgroundColor: '#fff',
|
||||
input: {
|
||||
fontWeight: 500,
|
||||
paddingLeft: '18px',
|
||||
<PageTitleBox
|
||||
upperTitle="대시보드"
|
||||
upperTitleLink="/dashboard"
|
||||
title={topTitle}
|
||||
sx={{ width: '100%', marginTop: { xs: 0, sm: '22px' }, flex: '1 1 auto', p: { xs: 0 } }}
|
||||
button={
|
||||
<Stack direction="row" spacing={3} sx={{ marginRight: '20px' }}>
|
||||
<ConfirmCancelButton
|
||||
confirmProps={{ disabled: false, onClick: handleSaveDialogSelect }}
|
||||
cancelProps={{ onClick: handleCancelDialogSelect }}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
<PageViewBox
|
||||
titleElement={
|
||||
<TextField
|
||||
id="userDashboardName"
|
||||
label="대시보드 이름"
|
||||
required
|
||||
sx={{
|
||||
width: { xs: '100%', sm: '960px' },
|
||||
height: '32px',
|
||||
marginLeft: { sm: '16px' },
|
||||
marginTop: 0,
|
||||
borderRadius: '4px',
|
||||
backgroundColor: '#fff',
|
||||
input: {
|
||||
fontWeight: 500,
|
||||
paddingLeft: '18px',
|
||||
height: '16px',
|
||||
fontFamily: 'Pretendard',
|
||||
fontSize: '14px',
|
||||
fontStretch: 'normal',
|
||||
fontStyle: 'normal',
|
||||
lineHeight: 0.89,
|
||||
letterSpacing: '-0.18px',
|
||||
textAlign: 'left',
|
||||
color: '#141414',
|
||||
'&::placeholder': {
|
||||
height: '16px',
|
||||
flexGrow: 0,
|
||||
fontFamily: 'Pretendard',
|
||||
fontSize: '14px',
|
||||
fontWeight: 'normal',
|
||||
fontStretch: 'normal',
|
||||
fontStyle: 'normal',
|
||||
lineHeight: 0.89,
|
||||
letterSpacing: '-0.18px',
|
||||
lineHeight: '1.14',
|
||||
letterSpacing: 'normal',
|
||||
textAlign: 'left',
|
||||
color: '#141414',
|
||||
'&::placeholder': {
|
||||
height: '16px',
|
||||
flexGrow: 0,
|
||||
fontFamily: 'Pretendard',
|
||||
fontSize: '14px',
|
||||
fontWeight: 'normal',
|
||||
fontStretch: 'normal',
|
||||
fontStyle: 'normal',
|
||||
lineHeight: '1.14',
|
||||
letterSpacing: 'normal',
|
||||
textAlign: 'left',
|
||||
color: '#929292',
|
||||
opacity: 1,
|
||||
},
|
||||
color: '#929292',
|
||||
opacity: 1,
|
||||
},
|
||||
}}
|
||||
placeholder="대시보드의 이름을 입력해 주세요"
|
||||
value={dashboardTitle}
|
||||
onChange={event => {
|
||||
setDashboardTitle(event.target.value);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
button={
|
||||
<>
|
||||
<Button
|
||||
onClick={handleWidgetOpen}
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
color="primary"
|
||||
sx={{
|
||||
borderRadius: '8px',
|
||||
backgroundColor: '#043f84',
|
||||
width: '97px',
|
||||
height: '32px',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: '20px',
|
||||
padding: '7px 0',
|
||||
objectFit: 'contain',
|
||||
border: 'solid 1px #0f5ab2',
|
||||
}}
|
||||
>
|
||||
위젯 추가
|
||||
</Button>
|
||||
<AddWidgetPopup
|
||||
label="위젯 추가"
|
||||
widgetSelect={handleWidgetSelect}
|
||||
useWidgetIds={useWidgetIds}
|
||||
widgetOpen={widgetOpen}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: '1390px',
|
||||
minWidth: '1390px',
|
||||
minHeight: '1080px',
|
||||
backgroundImage: `url(${bg})`,
|
||||
backgroundRepeat: 'repeat',
|
||||
borderRadius: '0px 0px 6px 6px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ResponsiveGridLayout
|
||||
rowHeight={88}
|
||||
compactType={null}
|
||||
cols={{ lg: 12 }}
|
||||
layouts={{ lg: layout }}
|
||||
preventCollision={true}
|
||||
containerPadding={{ lg: [24, 24] }}
|
||||
margin={{ lg: [24, 24] }}
|
||||
onLayoutChange={onLayoutChange}
|
||||
placeholder="대시보드의 이름을 입력해 주세요"
|
||||
value={dashboardTitle}
|
||||
onChange={event => {
|
||||
setDashboardTitle(event.target.value);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
button={
|
||||
<>
|
||||
<Button
|
||||
onClick={handleWidgetOpen}
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
color="primary"
|
||||
sx={{
|
||||
borderRadius: '8px',
|
||||
backgroundColor: '#043f84',
|
||||
width: '97px',
|
||||
height: '32px',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: '7px 0',
|
||||
objectFit: 'contain',
|
||||
border: 'solid 1px #0f5ab2',
|
||||
}}
|
||||
>
|
||||
{generateWidget()}
|
||||
</ResponsiveGridLayout>
|
||||
<RecommendDashboardPopup recommendOpen={recommendOpen} handleComplete={handleCompleteRecommend} />
|
||||
</Box>
|
||||
</DashboardTitleBox>
|
||||
</PageTitleBox>
|
||||
</PageContainer>
|
||||
위젯 추가
|
||||
</Button>
|
||||
<AddWidgetPopup
|
||||
label="위젯 추가"
|
||||
widgetSelect={handleWidgetSelect}
|
||||
useWidgetIds={useWidgetIds}
|
||||
widgetOpen={widgetOpen}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: '1390px',
|
||||
minWidth: '1390px',
|
||||
minHeight: '1080px',
|
||||
backgroundImage: `url(${bg})`,
|
||||
backgroundRepeat: 'repeat',
|
||||
borderRadius: '0px 0px 6px 6px',
|
||||
}}
|
||||
>
|
||||
<ResponsiveGridLayout
|
||||
rowHeight={88}
|
||||
compactType={null}
|
||||
cols={{ lg: 12 }}
|
||||
layouts={{ lg: layout }}
|
||||
preventCollision={true}
|
||||
containerPadding={{ lg: [24, 24] }}
|
||||
margin={{ lg: [24, 24] }}
|
||||
onLayoutChange={onLayoutChange}
|
||||
>
|
||||
{generateWidget()}
|
||||
</ResponsiveGridLayout>
|
||||
<RecommendDashboardPopup recommendOpen={recommendOpen} handleComplete={handleCompleteRecommend} />
|
||||
</Box>
|
||||
</PageViewBox>
|
||||
</PageTitleBox>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Box, Card, Stack, Typography } from '@mui/material';
|
||||
import { Box, Card, Hidden, useMediaQuery, useTheme } from '@mui/material';
|
||||
import { Link as RouterLink, useNavigate, useParams } from 'react-router-dom';
|
||||
import PageTitleBox from '@/components/PageTitleBox';
|
||||
import WidgetWrapper from '@/widget/wrapper/WidgetWrapper';
|
||||
@@ -9,7 +9,7 @@ import '/node_modules/react-grid-layout/css/styles.css';
|
||||
import '/node_modules/react-resizable/css/styles.css';
|
||||
import DashboardService from '@/api/dashboardService';
|
||||
import { STATUS } from '@/constant';
|
||||
import DashboardTitleBox from '../Components/DashboardTitleBox';
|
||||
import PageViewBox from '../../../components/PageViewBox';
|
||||
import ModifyButton from '@/components/button/ModifyButton';
|
||||
import DeleteButton from '@/components/button/DeleteButton';
|
||||
import ReloadButton from '@/components/button/ReloadButton';
|
||||
@@ -30,6 +30,8 @@ const DashboardView = () => {
|
||||
const snackbar = useAlert(SnackbarContext);
|
||||
const { userState } = useContext(AuthContext);
|
||||
const { showLoading, hideLoading } = useContext(LoadingContext);
|
||||
const theme = useTheme();
|
||||
const matches = useMediaQuery(theme.breakpoints.up('sm'));
|
||||
const [dashboardInfo, setDashboardInfo] = useState({
|
||||
title: '',
|
||||
widgets: [],
|
||||
@@ -88,7 +90,7 @@ const DashboardView = () => {
|
||||
|
||||
// widget 생성
|
||||
const generateWidget = () => {
|
||||
return dashboardInfo.widgets.map(item => {
|
||||
return dashboardInfo.widgets.map((item, index) => {
|
||||
return (
|
||||
<Card
|
||||
key={item.id}
|
||||
@@ -101,36 +103,43 @@ const DashboardView = () => {
|
||||
backgroundColor: '#fff',
|
||||
}}
|
||||
>
|
||||
<WidgetWrapper widgetOption={item} dataSetId={item.datasetId} />
|
||||
<WidgetWrapper widgetOption={item} dataSetId={item.datasetId} size={dashboardInfo.layout[index].w} />
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteSelect = () => {
|
||||
alert.success(dashboardInfo.title + '\n대시보드를 삭제하시겠습니까?', {
|
||||
closeCopy: '취소',
|
||||
actions: [
|
||||
{
|
||||
copy: '확인',
|
||||
onClick: () => {
|
||||
showLoading();
|
||||
DashboardService.deleteDashboard(dashboardId)
|
||||
.then(response => {
|
||||
if (response.data.status == STATUS.SUCCESS) {
|
||||
navigate('/dashboard', { replace: true });
|
||||
snackbar.success('대시보드가 삭제되었습니다.');
|
||||
} else {
|
||||
alert.error('대시보드 삭제에 실패했습니다.\n다시 시도해 주세요.');
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
hideLoading();
|
||||
});
|
||||
alert.success(
|
||||
<Box sx={{ span: { fontWeight: 600 } }}>
|
||||
<span>{dashboardInfo.title}</span>
|
||||
<br />
|
||||
대시보드를 삭제하시겠습니까?
|
||||
</Box>,
|
||||
{
|
||||
closeCopy: '취소',
|
||||
actions: [
|
||||
{
|
||||
copy: '확인',
|
||||
onClick: () => {
|
||||
showLoading();
|
||||
DashboardService.deleteDashboard(dashboardId)
|
||||
.then(response => {
|
||||
if (response.data.status == STATUS.SUCCESS) {
|
||||
navigate('/dashboard', { replace: true });
|
||||
snackbar.success('대시보드가 삭제되었습니다.');
|
||||
} else {
|
||||
alert.error('대시보드 삭제에 실패했습니다.\n다시 시도해 주세요.');
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
hideLoading();
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
],
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const handleShareToggle = () => {
|
||||
@@ -183,105 +192,73 @@ const DashboardView = () => {
|
||||
upperTitle="대시보드"
|
||||
upperTitleLink="/dashboard"
|
||||
title="대시보드 조회"
|
||||
sx={{ width: '100%', marginTop: '22px' }}
|
||||
sx={{ width: '100%', marginTop: { xs: 0, sm: '22px' }, flex: '1 1 auto', p: { xs: 0 } }}
|
||||
>
|
||||
<Seo title={dashboardInfo.title} />
|
||||
<DashboardTitleBox
|
||||
title={
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
component="span"
|
||||
<>
|
||||
<Seo title={dashboardInfo.title} />
|
||||
<PageViewBox
|
||||
title={dashboardInfo.title}
|
||||
date={dateData(dashboardInfo.updatedAt)}
|
||||
button={
|
||||
<>
|
||||
<Hidden smDown>
|
||||
<ReloadButton
|
||||
size="medium"
|
||||
sx={{ marginRight: { sm: '14px', md: '24px' }, padding: 0 }}
|
||||
onClick={event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleRefreshClick();
|
||||
}}
|
||||
/>
|
||||
<ModifyButton
|
||||
size="medium"
|
||||
sx={{ marginRight: { sm: '14px', md: '24px' }, padding: 0 }}
|
||||
component={RouterLink}
|
||||
to={`/dashboard/modify?id=${dashboardId}&name=${dashboardInfo.title}`}
|
||||
/>
|
||||
<DeleteButton
|
||||
size="medium"
|
||||
sx={{ marginRight: { sm: '14px', md: '24px' }, padding: 0 }}
|
||||
onClick={event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleDeleteSelect();
|
||||
}}
|
||||
/>
|
||||
</Hidden>
|
||||
<ShareButton
|
||||
handleShareToggle={handleShareToggle}
|
||||
isShareOn={isShareOn}
|
||||
shareId={dashboardInfo.uuid}
|
||||
shareLimitDate={shareLimitDate}
|
||||
setShareLimitDate={setShareLimitDate}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
paddingLeft: '18px',
|
||||
height: '16px',
|
||||
fontFamily: 'Pretendard',
|
||||
fontSize: '18px',
|
||||
fontStretch: 'normal',
|
||||
fontStyle: 'normal',
|
||||
lineHeight: 0.89,
|
||||
letterSpacing: '-0.18px',
|
||||
textAlign: 'left',
|
||||
color: '#141414',
|
||||
flex: '1 1 auto',
|
||||
minHeight: { sm: '1080px' },
|
||||
backgroundColor: '#f9f9fa',
|
||||
borderRadius: '0px 0px 6px 6px',
|
||||
}}
|
||||
>
|
||||
{dashboardInfo.title}
|
||||
</Typography>
|
||||
}
|
||||
button={
|
||||
<Stack direction="row" alignItems="center" sx={{ marginRight: '20px' }}>
|
||||
<span
|
||||
style={{
|
||||
marginRight: '36px',
|
||||
height: '16px',
|
||||
fontFamily: 'Pretendard',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
fontStretch: 'normal',
|
||||
fontStyle: 'normal',
|
||||
lineHeight: '1.14',
|
||||
letterSpacing: 'normal',
|
||||
textAlign: 'left',
|
||||
color: '#333333',
|
||||
}}
|
||||
<ResponsiveGridLayout
|
||||
rowHeight={88}
|
||||
compactType={null}
|
||||
breakpoints={{ xs: 0, md: 800, lg: 1000 }}
|
||||
cols={{ xs: 2, md: 8, lg: 12 }}
|
||||
layouts={{ xs: layout, md: layout, lg: layout }}
|
||||
containerPadding={{ xs: [20, 20], md: [20, 20], lg: [24, 24] }}
|
||||
margin={{ xs: [20, 20], md: [20, 20], lg: [24, 24] }}
|
||||
>
|
||||
{dateData(dashboardInfo.updatedAt)}
|
||||
</span>
|
||||
<ReloadButton
|
||||
size="medium"
|
||||
sx={{ marginRight: '24px', padding: 0 }}
|
||||
onClick={event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleRefreshClick();
|
||||
}}
|
||||
/>
|
||||
<ModifyButton
|
||||
size="medium"
|
||||
sx={{ marginRight: '24px', padding: 0 }}
|
||||
component={RouterLink}
|
||||
to={`/dashboard/modify?id=${dashboardId}&name=${dashboardInfo.title}`}
|
||||
/>
|
||||
<DeleteButton
|
||||
size="medium"
|
||||
sx={{ marginRight: '24px', padding: 0 }}
|
||||
onClick={event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleDeleteSelect();
|
||||
}}
|
||||
/>
|
||||
<ShareButton
|
||||
handleSubmit={handleShareToggle}
|
||||
isShareOn={isShareOn}
|
||||
shareId={dashboardInfo.uuid}
|
||||
shareLimitDate={shareLimitDate}
|
||||
setShareLimitDate={setShareLimitDate}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: '1390px',
|
||||
minWidth: '1390px',
|
||||
minHeight: '1080px',
|
||||
backgroundColor: '#f9f9fa',
|
||||
borderRadius: '0px 0px 6px 6px',
|
||||
}}
|
||||
>
|
||||
<ResponsiveGridLayout
|
||||
rowHeight={88}
|
||||
compactType={null}
|
||||
cols={{ lg: 12 }}
|
||||
layouts={{ lg: layout }}
|
||||
containerPadding={{ lg: [24, 24] }}
|
||||
margin={{ lg: [24, 24] }}
|
||||
>
|
||||
{generateWidget()}
|
||||
</ResponsiveGridLayout>
|
||||
</Box>
|
||||
</DashboardTitleBox>
|
||||
{generateWidget()}
|
||||
</ResponsiveGridLayout>
|
||||
</Box>
|
||||
</PageViewBox>
|
||||
</>
|
||||
</PageTitleBox>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import PageContainer from '@/components/PageContainer';
|
||||
import PageTitleBox from '@/components/PageTitleBox';
|
||||
import BoardList from '@/components/BoardList';
|
||||
import { Outlet, useNavigate, useParams } from 'react-router-dom';
|
||||
import { MenuButton } from '@/components/button/AddIconButton';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import { Box, Stack } from '@mui/material';
|
||||
import { Box, Stack, useMediaQuery, useTheme } from '@mui/material';
|
||||
import DashboardService from '@/api/dashboardService';
|
||||
import { STATUS } from '@/constant';
|
||||
import { useAlert } from 'react-alert';
|
||||
@@ -24,10 +23,12 @@ function Dashboard() {
|
||||
const [loadedDashboardData, setLoadedDashboardData] = useState([]);
|
||||
const [noData, setNoData] = useState(false);
|
||||
const { showLoading, hideLoading } = useContext(LoadingContext);
|
||||
const theme = useTheme();
|
||||
const matches = useMediaQuery(theme.breakpoints.up('sm'));
|
||||
|
||||
const GTSpan = styled('span')({
|
||||
fontFamily: 'Pretendard',
|
||||
fontSize: '13px',
|
||||
fontSize: matches ? '13px' : '10px',
|
||||
fontWeight: '500',
|
||||
fontStretch: 'normal',
|
||||
fontStyle: 'normal',
|
||||
@@ -63,31 +64,37 @@ function Dashboard() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteSelect = item => {
|
||||
console.log(item);
|
||||
alert.success(item.title + '\n대시보드를 삭제하시겠습니까?', {
|
||||
closeCopy: '취소',
|
||||
actions: [
|
||||
{
|
||||
copy: '확인',
|
||||
onClick: () => {
|
||||
showLoading();
|
||||
DashboardService.deleteDashboard(item.id)
|
||||
.then(response => {
|
||||
if (response.data.status == STATUS.SUCCESS) {
|
||||
getDashboardList();
|
||||
snackbar.success('대시보드가 삭제되었습니다.');
|
||||
} else {
|
||||
alert.error('대시보드 삭제에 실패했습니다.\n다시 시도해 주세요.');
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
hideLoading();
|
||||
});
|
||||
const handleDeleteSelect = (id, title) => {
|
||||
alert.success(
|
||||
<Box sx={{ span: { fontWeight: 600 } }}>
|
||||
<span>{title}</span>
|
||||
<br />
|
||||
대시보드를 삭제하시겠습니까?
|
||||
</Box>,
|
||||
{
|
||||
closeCopy: '취소',
|
||||
actions: [
|
||||
{
|
||||
copy: '확인',
|
||||
onClick: () => {
|
||||
showLoading();
|
||||
DashboardService.deleteDashboard(id)
|
||||
.then(response => {
|
||||
if (response.data.status == STATUS.SUCCESS) {
|
||||
getDashboardList();
|
||||
snackbar.success('대시보드가 삭제되었습니다.');
|
||||
} else {
|
||||
alert.error('대시보드 삭제에 실패했습니다.\n다시 시도해 주세요.');
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
hideLoading();
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
],
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const handleMenuSelect = item => {
|
||||
@@ -108,18 +115,18 @@ function Dashboard() {
|
||||
<Stack
|
||||
flexDirection="row"
|
||||
justifyContent="space-between"
|
||||
sx={{ paddingLeft: '20px', paddingRight: '217px', marginBottom: '11px', marginTop: '36px' }}
|
||||
sx={{ paddingLeft: '20px', paddingRight: { xs: '44px', sm: '217px' }, marginBottom: '11px', marginTop: '36px' }}
|
||||
>
|
||||
<GTSpan>이름</GTSpan>
|
||||
<GTSpan>수정일</GTSpan>
|
||||
</Stack>
|
||||
<Box
|
||||
sx={{
|
||||
height: '57px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
flexGrow: '0',
|
||||
py: '18px',
|
||||
margin: '0 0 0 0',
|
||||
borderRadius: '6px',
|
||||
border: 'solid 1px #ddd',
|
||||
@@ -128,20 +135,20 @@ function Dashboard() {
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
height: '16px',
|
||||
flexGrow: 0,
|
||||
fontFamily: 'Pretendard',
|
||||
fontSize: '16px',
|
||||
fontSize: matches ? '16px' : '14px',
|
||||
fontWeight: 600,
|
||||
fontStretch: 'normal',
|
||||
fontStyle: 'normal',
|
||||
lineHeight: 1,
|
||||
lineHeight: 1.43,
|
||||
letterSpacing: 'normal',
|
||||
textAlign: 'center',
|
||||
color: '#333333',
|
||||
}}
|
||||
>
|
||||
대시보드가 존재하지 않습니다. 대시보드를 생성해보세요.
|
||||
생성한 대시보드가 없습니다.
|
||||
{matches ? ' ' : <br />}
|
||||
대시보드를 생성 후 확인해 보세요.
|
||||
</span>
|
||||
</Box>
|
||||
</>
|
||||
@@ -149,14 +156,13 @@ function Dashboard() {
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<Stack sx={{ width: '100%', height: '100%', flex: '1 1 auto' }}>
|
||||
<Seo title={title} />
|
||||
|
||||
{!dashboardId ? (
|
||||
<>
|
||||
<PageTitleBox
|
||||
title={title}
|
||||
sx={{ width: '100%' }}
|
||||
button={
|
||||
<MenuButton
|
||||
menuList={menuList}
|
||||
@@ -179,7 +185,7 @@ function Dashboard() {
|
||||
) : (
|
||||
<Outlet />
|
||||
)}
|
||||
</PageContainer>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Box, Stack, Typography } from '@mui/material';
|
||||
import { Stack, Typography } from '@mui/material';
|
||||
import DatabaseService from '@/api/databaseService';
|
||||
import { STATUS } from '@/constant';
|
||||
import { useAlert } from 'react-alert';
|
||||
@@ -10,33 +10,63 @@ import AddButton from '@/components/button/AddButton';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { LoadingContext } from '@/contexts/LoadingContext';
|
||||
import { SnackbarContext } from '@/contexts/AlertContext';
|
||||
import { Loading } from '@/components/loading';
|
||||
import ModalPopup from '@/components/ModalPopup';
|
||||
import { createColumns } from '@/utils/util';
|
||||
import DataGrid, { DataGridWrapper } from '@/components/datagrid';
|
||||
// import { cancelAllRequests } from '@/helpers/apiHelper';
|
||||
|
||||
export interface DatabaseProps {
|
||||
id: number | string | null;
|
||||
name: string | null;
|
||||
createdAt?: string;
|
||||
description?: string;
|
||||
engine?: string;
|
||||
timezone?: string;
|
||||
type?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface DataSetProps {
|
||||
id: number;
|
||||
databaseId: number;
|
||||
datasetType: 'DATASET';
|
||||
title: string;
|
||||
query: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface DataTableProps {
|
||||
id: string;
|
||||
tableName: string;
|
||||
databaseId: number;
|
||||
datasetType: 'TABLE';
|
||||
}
|
||||
|
||||
const DataLayout = props => {
|
||||
const { isViewMode, setDataSet } = props;
|
||||
const [databaseList, setDatabaseList] = useState([]);
|
||||
const [datasetList, setDatasetList] = useState([]);
|
||||
const [tableList, setTableList] = useState([]);
|
||||
const [databaseList, setDatabaseList] = useState<DatabaseProps[] | []>([]);
|
||||
const [datasetList, setDatasetList] = useState<DataSetProps[] | []>([]);
|
||||
const [tableList, setTableList] = useState<DataTableProps[] | []>([]);
|
||||
const alert = useAlert();
|
||||
const snackbar = useAlert(SnackbarContext);
|
||||
const { showLoading, hideLoading } = useContext(LoadingContext);
|
||||
|
||||
const [selectedDatabase, setSelectedDatabase] = useState({
|
||||
databaseId: null,
|
||||
});
|
||||
|
||||
const [selectedDataset, setSelectedDataset] = useState(null);
|
||||
|
||||
const handleSelectDatabase = enteredData => {
|
||||
return setSelectedDatabase(prevState => ({ ...prevState, ...enteredData }));
|
||||
};
|
||||
const { loading, showLoading, hideLoading } = useContext(LoadingContext);
|
||||
const [selectedDatabase, setSelectedDatabase] = useState<DatabaseProps>({ id: null, name: null });
|
||||
const [selectedDataset, setSelectedDataset] = useState<DataSetProps | DataTableProps | null>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [gridData, setGridData] = useState<any[]>([]);
|
||||
const [gridColumns, setGridColumns] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
getDatabaseList();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedDatabase.databaseId) getDatabaseInfo();
|
||||
}, [selectedDatabase.databaseId]);
|
||||
if (selectedDatabase.id) {
|
||||
getDatabaseInfo(selectedDatabase.id);
|
||||
}
|
||||
}, [selectedDatabase.id]);
|
||||
|
||||
/**
|
||||
* 데이터베이스 목록조회
|
||||
@@ -45,9 +75,11 @@ const DataLayout = props => {
|
||||
showLoading();
|
||||
DatabaseService.selectDatabaseList()
|
||||
.then(response => {
|
||||
setDatabaseList(response.data.data);
|
||||
if (response.data.data.length > 0) {
|
||||
setSelectedDatabase({ databaseId: response.data.data[0].id });
|
||||
const resData = response.data.data;
|
||||
setDatabaseList(resData);
|
||||
if (resData.length > 0) {
|
||||
const [firstItem] = resData;
|
||||
setSelectedDatabase(firstItem);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -55,9 +87,9 @@ const DataLayout = props => {
|
||||
});
|
||||
};
|
||||
|
||||
const getDatabaseInfo = () => {
|
||||
const getDatabaseInfo = databaseId => {
|
||||
showLoading();
|
||||
DatabaseService.selectDatabase(selectedDatabase.databaseId)
|
||||
DatabaseService.selectDatabase(databaseId)
|
||||
.then(response => {
|
||||
if (response.data.status === 'SUCCESS') {
|
||||
setDatasetList(response.data.data.datasets);
|
||||
@@ -78,16 +110,51 @@ const DataLayout = props => {
|
||||
});
|
||||
};
|
||||
|
||||
const removeDatabase = (id, name) => {
|
||||
console.log('removeDatabase', id);
|
||||
alert.success(`${name}\n데이터베이스를 삭제하시겠습니까?`, {
|
||||
const getData = selectedData => {
|
||||
let param;
|
||||
switch (selectedData?.datasetType) {
|
||||
case 'DATASET':
|
||||
const { id, ...rest } = selectedData;
|
||||
param = { ...rest, datasetId: id };
|
||||
break;
|
||||
case 'TABLE':
|
||||
param = { ...selectedData };
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
showLoading();
|
||||
DatabaseService.selectData(param)
|
||||
.then(response => {
|
||||
if (response.data.status === STATUS.SUCCESS) {
|
||||
setGridData(response.data.data.datas);
|
||||
setGridColumns(createColumns(response.data.data.datas));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('error', error);
|
||||
setGridData([]);
|
||||
setGridColumns([]);
|
||||
})
|
||||
.finally(() => {
|
||||
hideLoading();
|
||||
});
|
||||
};
|
||||
|
||||
const handleDatabaseClick = (item: DatabaseProps) => {
|
||||
setSelectedDatabase(item);
|
||||
};
|
||||
|
||||
const handleDatabaseRemove = item => {
|
||||
console.log('handleDatabaseRemove', item);
|
||||
alert.success(`${item.name}\n데이터베이스를 삭제하시겠습니까?`, {
|
||||
title: '데이터베이스 삭제',
|
||||
closeCopy: '취소',
|
||||
actions: [
|
||||
{
|
||||
copy: '삭제',
|
||||
onClick: () => {
|
||||
DatabaseService.deleteDatabase(id).then(response => {
|
||||
DatabaseService.deleteDatabase(item.databaseId).then(response => {
|
||||
if (response.data.status === STATUS.SUCCESS) {
|
||||
getDatabaseList();
|
||||
snackbar.success('데이터베이스가 삭제되었습니다.');
|
||||
@@ -99,14 +166,29 @@ const DataLayout = props => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectDataset = item => {
|
||||
console.log('handleSelectDataset', item);
|
||||
if (setDataSet) setDataSet(item);
|
||||
const handleDataSetClick = (item: DataTableProps | DataSetProps) => {
|
||||
console.log('selected Data', item);
|
||||
if (isViewMode) {
|
||||
setDataSet(item);
|
||||
} else {
|
||||
setOpen(true);
|
||||
getData(item);
|
||||
}
|
||||
setSelectedDataset(item);
|
||||
};
|
||||
|
||||
const handleDeleteDataset = item => {
|
||||
console.log('handleDeleteDataset', item);
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
setGridData([]);
|
||||
setGridColumns([]);
|
||||
// if (loading) {
|
||||
// // 진행되고 있는 모든 요청 취소
|
||||
// cancelAllRequests();
|
||||
// }
|
||||
};
|
||||
|
||||
const handleDataSetRemove = item => {
|
||||
console.log('handleDataSetRemove', item);
|
||||
alert.success(`${item.title}\n데이터셋을 삭제하시겠습니까?`, {
|
||||
title: '데이터베이스 삭제',
|
||||
closeCopy: '취소',
|
||||
@@ -115,7 +197,7 @@ const DataLayout = props => {
|
||||
copy: '삭제',
|
||||
onClick: () => {
|
||||
DatasetService.deleteDataset(item.id).then(() => {
|
||||
getDatabaseInfo();
|
||||
getDatabaseInfo(item.id);
|
||||
snackbar.success('데이터셋이 삭제되었습니다.');
|
||||
});
|
||||
},
|
||||
@@ -125,9 +207,13 @@ const DataLayout = props => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack sx={{ width: '100%' }} direction="row">
|
||||
<Stack sx={{ width: { xs: '270px', md: '404px' }, px: '24px', pt: '30px' }}>
|
||||
<Stack direction="row" sx={{ mb: '12px' }}>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} flex="1 1 auto" sx={{ width: '100%' }}>
|
||||
<Stack
|
||||
direction="column"
|
||||
flex="1 1 auto"
|
||||
sx={{ width: { xs: '100%', md: '404px' }, height: '100%', px: '24px', pt: '30px' }}
|
||||
>
|
||||
<Stack direction="row">
|
||||
<Typography variant="subtitle1" component="span" sx={{ fontWeight: 'bold', fontSize: '16px', color: '#141414' }}>
|
||||
데이터 소스
|
||||
</Typography>
|
||||
@@ -135,59 +221,73 @@ const DataLayout = props => {
|
||||
</Stack>
|
||||
<DatabaseCardList
|
||||
data={databaseList}
|
||||
selectedDatabase={selectedDatabase}
|
||||
disabledIcons={!!isViewMode}
|
||||
onUpdate={handleSelectDatabase}
|
||||
onRemove={removeDatabase}
|
||||
minWidth="100%"
|
||||
selectedData={selectedDatabase}
|
||||
isViewMode={isViewMode}
|
||||
handleDataClick={handleDatabaseClick}
|
||||
handleDataRemove={handleDatabaseRemove}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Stack sx={{ width: { xs: 'calc(100% - 270px)', md: 'calc(100% - 404px)' }, backgroundColor: '#f5f6f8' }}>
|
||||
<Stack sx={{ width: '100%', px: '24px', pt: '30px' }}>
|
||||
<Stack direction="row" sx={{ mb: '12px' }}>
|
||||
<Stack
|
||||
direction="column"
|
||||
sx={{ flex: '1 1 auto', width: { xs: '100%', md: 'calc(100% - 404px)' }, backgroundColor: '#f5f6f8' }}
|
||||
>
|
||||
<Stack direction="column" sx={{ width: '100%', px: '24px', pt: '30px' }}>
|
||||
<Stack direction="row">
|
||||
<Typography variant="subtitle1" component="span" sx={{ fontWeight: 'bold', fontSize: '16px', color: '#141414' }}>
|
||||
데이터 셋
|
||||
</Typography>
|
||||
{isViewMode ? (
|
||||
<></>
|
||||
) : (
|
||||
<AddButton component={RouterLink} to={`set/create/${selectedDatabase.databaseId}`} sx={{ ml: '14px' }} />
|
||||
<AddButton component={RouterLink} to={`set/create/${selectedDatabase.id}`} sx={{ ml: '14px' }} />
|
||||
)}
|
||||
</Stack>
|
||||
{datasetList.length > 0 ? (
|
||||
<DatasetCardList
|
||||
data={datasetList}
|
||||
selectedDataset={selectedDataset}
|
||||
onSelectDataset={handleSelectDataset}
|
||||
onDeleteDataset={handleDeleteDataset}
|
||||
disabledIcons={!!isViewMode}
|
||||
/>
|
||||
) : (
|
||||
<Box sx={{ height: '100px' }} />
|
||||
)}
|
||||
<DatasetCardList
|
||||
isViewMode={isViewMode}
|
||||
data={datasetList}
|
||||
selectedData={selectedDataset}
|
||||
handleDataClick={handleDataSetClick}
|
||||
handleDataRemove={handleDataSetRemove}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack sx={{ width: '100%', px: '24px', pt: '30px' }}>
|
||||
<Stack direction="row" sx={{ mb: '12px' }}>
|
||||
<Stack direction="column" sx={{ flex: '1 1 auto', width: '100%', minHeight: '50%', px: '24px', pt: '30px' }}>
|
||||
<Stack direction="row">
|
||||
<Typography variant="subtitle1" component="span" sx={{ fontWeight: 'bold', fontSize: '16px', color: '#141414' }}>
|
||||
테이블 목록
|
||||
</Typography>
|
||||
</Stack>
|
||||
<DatasetCardList
|
||||
isViewMode={isViewMode}
|
||||
data={tableList}
|
||||
selectedDataset={selectedDataset}
|
||||
onSelectDataset={handleSelectDataset}
|
||||
disabledIcons={true}
|
||||
selectedData={selectedDataset}
|
||||
handleDataClick={handleDataSetClick}
|
||||
/>
|
||||
<ModalPopup
|
||||
open={open}
|
||||
handleClose={handleClose}
|
||||
title={selectedDataset && (selectedDataset?.['tableName'] || selectedDataset?.['title'])}
|
||||
>
|
||||
{loading ? (
|
||||
<Loading in={loading} style={{ position: 'static', backgroundColor: 'transparent' }} />
|
||||
) : (
|
||||
<DataGridWrapper>
|
||||
<DataGrid
|
||||
minBodyHeight={100}
|
||||
bodyHeight={'fitToParent'}
|
||||
data={gridData}
|
||||
columns={gridColumns}
|
||||
columnOptions={{
|
||||
resizable: true,
|
||||
}}
|
||||
/>
|
||||
</DataGridWrapper>
|
||||
)}
|
||||
</ModalPopup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
DataLayout.defaultProps = {
|
||||
data: {},
|
||||
naviUrl: {},
|
||||
};
|
||||
|
||||
export default DataLayout;
|
||||
|
||||
@@ -17,6 +17,7 @@ import { STATUS } from '@/constant';
|
||||
import { getDatabaseIcon } from '@/widget/utils/iconUtil';
|
||||
import { LoadingContext } from '@/contexts/LoadingContext';
|
||||
import { SnackbarContext } from '@/contexts/AlertContext';
|
||||
import { createColumns } from '@/utils/util';
|
||||
|
||||
const DataSet = () => {
|
||||
const { setId, sourceId } = useParams();
|
||||
@@ -61,22 +62,6 @@ const DataSet = () => {
|
||||
if (!databaseId) getDatabaseId();
|
||||
}, [databaseList, datasetInfo]);
|
||||
|
||||
/**
|
||||
* 데이터 그리드 컬럼 생성
|
||||
* @param data
|
||||
*/
|
||||
const createColumns = data => {
|
||||
let target = null;
|
||||
if (data instanceof Array && data.length > 0) {
|
||||
target = data[0];
|
||||
} else if (data instanceof Object) {
|
||||
target = data;
|
||||
}
|
||||
return Object.keys(target).map(key => {
|
||||
return { name: key, header: key, align: key, width: 200, sortable: true };
|
||||
});
|
||||
};
|
||||
|
||||
const addCompleter = () => {
|
||||
const rhymeCompleter = {
|
||||
getCompletions: (editor, session, pos, prefix, callback) => {
|
||||
@@ -305,9 +290,9 @@ const DataSet = () => {
|
||||
>
|
||||
<Select
|
||||
id="databaseId"
|
||||
sx={{ width: '500px' }}
|
||||
sx={{ maxWidth: '500px' }}
|
||||
displayEmpty
|
||||
disabled={isModifyMode}
|
||||
disabled={isModifyMode || !databaseList.length}
|
||||
size="small"
|
||||
value={databaseId ?? ''}
|
||||
// renderValue={selected => {
|
||||
@@ -320,11 +305,15 @@ const DataSet = () => {
|
||||
// }}
|
||||
onChange={onChangeDatabaseId}
|
||||
>
|
||||
{databaseList.map(item => (
|
||||
<MenuItem key={item.id} value={item.id ?? ''}>
|
||||
{item.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
{databaseList.length ? (
|
||||
databaseList.map(item => (
|
||||
<MenuItem key={item.id} value={item.id ?? ''}>
|
||||
{item.name}
|
||||
</MenuItem>
|
||||
))
|
||||
) : (
|
||||
<MenuItem value="">불러올 데이터 소스가 없습니다.</MenuItem>
|
||||
)}
|
||||
</Select>
|
||||
|
||||
<TextField
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useContext, useEffect, useLayoutEffect, useState } from 'react';
|
||||
import { Stack, Typography } from '@mui/material';
|
||||
import PageContainer from '@/components/PageContainer';
|
||||
import PageTitleBox from '@/components/PageTitleBox';
|
||||
import ImgCardList from '@/components/ImgCardList';
|
||||
import ConfirmCancelButton from '@/components/button/ConfirmCancelButton';
|
||||
@@ -292,48 +291,46 @@ function DataSource() {
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageTitleBox
|
||||
upperTitle="데이터"
|
||||
upperTitleLink="/data"
|
||||
title={'데이터 소스 연결'}
|
||||
button={
|
||||
<ConfirmCancelButton
|
||||
confirmProps={{ disabled: !isConnected, onClick: handleSaveClick }}
|
||||
cancelProps={{ onClick: handleCancelClick }}
|
||||
<PageTitleBox
|
||||
upperTitle="데이터"
|
||||
upperTitleLink="/data"
|
||||
title={'데이터 소스 연결'}
|
||||
button={
|
||||
<ConfirmCancelButton
|
||||
confirmProps={{ disabled: !isConnected, onClick: handleSaveClick }}
|
||||
cancelProps={{ onClick: handleCancelClick }}
|
||||
/>
|
||||
}
|
||||
sx={{ p: 0 }}
|
||||
>
|
||||
<Stack sx={{ width: '100%' }}>
|
||||
<Stack sx={{ p: '30px 25px 50px 25px' }}>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
component="span"
|
||||
sx={{ fontWeight: 'bold', fontSize: '18px', color: '#141414', mb: '14px' }}
|
||||
>
|
||||
step.01 타입 설정
|
||||
</Typography>
|
||||
<ImgCardList
|
||||
data={typeList}
|
||||
selectedType={dataType}
|
||||
setSelectedType={setDataType}
|
||||
handleTypeClick={handleTypeClick}
|
||||
/>
|
||||
}
|
||||
sx={{ p: 0 }}
|
||||
>
|
||||
<Stack sx={{ width: '100%' }}>
|
||||
<Stack sx={{ p: '30px 25px 50px 25px' }}>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
component="span"
|
||||
sx={{ fontWeight: 'bold', fontSize: '18px', color: '#141414', mb: '14px' }}
|
||||
>
|
||||
step.01 타입 설정
|
||||
</Typography>
|
||||
<ImgCardList
|
||||
data={typeList}
|
||||
selectedType={dataType}
|
||||
setSelectedType={setDataType}
|
||||
handleTypeClick={handleTypeClick}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack sx={{ p: '30px 25px 50px 25px', bgcolor: '#f5f6f8' }}>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
component="span"
|
||||
sx={{ fontWeight: 'bold', fontSize: '18px', color: '#141414', mb: '14px' }}
|
||||
>
|
||||
step.02 연결 정보 입력
|
||||
</Typography>
|
||||
{dbType()}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</PageTitleBox>
|
||||
</PageContainer>
|
||||
<Stack sx={{ p: '30px 25px 50px 25px', bgcolor: '#f5f6f8' }}>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
component="span"
|
||||
sx={{ fontWeight: 'bold', fontSize: '18px', color: '#141414', mb: '14px' }}
|
||||
>
|
||||
step.02 연결 정보 입력
|
||||
</Typography>
|
||||
{dbType()}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</PageTitleBox>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import PageContainer from '@/components/PageContainer';
|
||||
import DataLayout from './DataLayout';
|
||||
import PageTitleBox from '@/components/PageTitleBox';
|
||||
import Seo from '@/seo/Seo';
|
||||
@@ -13,13 +12,10 @@ const title = '데이터';
|
||||
|
||||
const Data = () => {
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageTitleBox title={title} sx={{ paddingLeft: 0, paddingRight: 0, width: '100%', height: '100%' }}>
|
||||
<Seo title={title} />
|
||||
|
||||
<PageTitleBox title={title} sx={{ paddingLeft: 0, paddingRight: 0, width: '100%', height: '100%' }}>
|
||||
<DataLayout />
|
||||
</PageTitleBox>
|
||||
</PageContainer>
|
||||
<DataLayout />
|
||||
</PageTitleBox>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useAlert } from 'react-alert';
|
||||
import { LoadingContext } from '@/contexts/LoadingContext';
|
||||
import { ReactComponent as Logo } from '@/assets/images/logo.svg';
|
||||
import backgroundImage from '@/assets/images/visual-bg.png';
|
||||
import Copyright from '@/components/Copyright';
|
||||
import { Copyright } from '@/layouts/Footer';
|
||||
import authService from '@/api/authService';
|
||||
import { checkId, checkPwd } from '@/utils/util';
|
||||
import { SnackbarContext } from '@/contexts/AlertContext';
|
||||
@@ -119,14 +119,21 @@ const Login = () => {
|
||||
<RouterLink to="/">
|
||||
<Logo width="223px" height="43px" />
|
||||
</RouterLink>
|
||||
<Typography sx={{ mt: '17px', fontSize: '16px', color: '#043f84' }}>
|
||||
<Typography sx={{ mt: '17px', fontSize: '16px', color: '#043f84', textAlign: 'center' }}>
|
||||
통합 데이터분석을 위한{' '}
|
||||
<Typography component="span" sx={{ fontSize: '16px', fontWeight: 'bold' }}>
|
||||
대시보드 리포팅 솔루션
|
||||
</Typography>
|
||||
</Typography>
|
||||
<Stack component="form" onSubmit={handleLogin} noValidate sx={{ width: '360px', mt: '56px' }} spacing="20px">
|
||||
<Stack
|
||||
component="form"
|
||||
onSubmit={handleLogin}
|
||||
noValidate
|
||||
sx={{ width: { xs: 'calc(100% - 40px)', sm: '360px' }, mt: '56px' }}
|
||||
spacing="20px"
|
||||
>
|
||||
<TextField
|
||||
autoFocus={true}
|
||||
label="User ID"
|
||||
name="userId"
|
||||
value={userInfo.userId}
|
||||
@@ -134,6 +141,11 @@ const Login = () => {
|
||||
margin="normal"
|
||||
required={true}
|
||||
fullWidth
|
||||
sx={{ height: { xs: '44px', sm: '36px' } }}
|
||||
InputLabelProps={{ sx: { pt: { xs: '6px', sm: 0 } } }}
|
||||
InputProps={{
|
||||
sx: { height: { xs: '44px', sm: '36px' }, input: { padding: '12px 14px' } },
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
label="Password"
|
||||
@@ -144,9 +156,19 @@ const Login = () => {
|
||||
margin="normal"
|
||||
required={true}
|
||||
fullWidth
|
||||
sx={{ height: '36px' }}
|
||||
sx={{ height: { xs: '44px', sm: '36px' } }}
|
||||
InputLabelProps={{ sx: { pt: { xs: '6px', sm: 0 } } }}
|
||||
InputProps={{
|
||||
sx: { height: { xs: '44px', sm: '36px' }, input: { padding: '12px 14px' } },
|
||||
}}
|
||||
/>
|
||||
<Button type="submit" size="large" fullWidth variant="contained" sx={{ mt: 3, mb: 2 }}>
|
||||
<Button
|
||||
type="submit"
|
||||
size="large"
|
||||
fullWidth
|
||||
variant="contained"
|
||||
sx={{ height: { xs: '50px', sm: '44px' }, mt: 3, mb: 2 }}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Box, Card, Stack, Typography } from '@mui/material';
|
||||
import { Box, Card, Typography } from '@mui/material';
|
||||
import WidgetWrapper from '@/widget/wrapper/WidgetWrapper';
|
||||
import { useAlert } from 'react-alert';
|
||||
import { Responsive, WidthProvider } from 'react-grid-layout';
|
||||
import '/node_modules/react-grid-layout/css/styles.css';
|
||||
import '/node_modules/react-resizable/css/styles.css';
|
||||
import DashboardTitleBox from '../Dashboard/Components/DashboardTitleBox';
|
||||
import PageViewBox from '../../components/PageViewBox';
|
||||
import { LoadingContext } from '@/contexts/LoadingContext';
|
||||
import shareService from '@/api/shareService';
|
||||
import { LandingLogo } from '@/layouts/Header/Logo';
|
||||
import Copyright from '@/components/Copyright';
|
||||
import Seo from '@/seo/Seo';
|
||||
import { dateData } from '@/utils/util';
|
||||
import { setShareToken } from '@/helpers/shareHelper';
|
||||
@@ -20,7 +18,7 @@ const ResponsiveGridLayout = WidthProvider(Responsive);
|
||||
const Share = () => {
|
||||
const { dashboardUuid } = useParams();
|
||||
const alert = useAlert();
|
||||
const { showLoading, hideLoading } = useContext(LoadingContext);
|
||||
const { loading, showLoading, hideLoading } = useContext(LoadingContext);
|
||||
const [dashboardInfo, setDashboardInfo] = useState({
|
||||
title: '',
|
||||
widgets: [],
|
||||
@@ -109,124 +107,67 @@ const Share = () => {
|
||||
|
||||
// 데이터가 없는 상태별 텍스트
|
||||
const generateInvalidText = () => {
|
||||
if (!isInvalidData) {
|
||||
return <>데이터를 불러오고 있습니다.</>;
|
||||
if (loading) {
|
||||
return '데이터를 불러오고 있습니다.';
|
||||
} else if (isInvalidData === 401) {
|
||||
return (
|
||||
<>
|
||||
대시보드의 공유 기간이 만료되었습니다.
|
||||
<br />
|
||||
대시보드의 공유 상태를 다시 확인해 주세요.
|
||||
</>
|
||||
);
|
||||
return '대시보드의 공유 기간이 만료되었습니다.\n대시보드의 공유 상태를 다시 확인해 주세요.';
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
대시보드가 존재하지 않습니다.
|
||||
<br />
|
||||
대시보드의 URL을 다시 확인해 주세요.
|
||||
</>
|
||||
);
|
||||
return '대시보드가 존재하지 않습니다.\n대시보드의 URL을 다시 확인해 주세요.';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack sx={{ width: '1392px', m: 'auto', mt: '32px' }}>
|
||||
<LandingLogo sx={{ mb: '5px' }} />
|
||||
{!isShareOn ? (
|
||||
// 공유 URL이 유효하지 않을 때 보여줄 화면
|
||||
<DashboardTitleBox>
|
||||
<Box
|
||||
sx={{
|
||||
width: '1390px',
|
||||
minWidth: '1390px',
|
||||
backgroundColor: '#f9f9fa',
|
||||
borderRadius: '0px 0px 6px 6px',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
margin: '200px auto',
|
||||
fontSize: '16px',
|
||||
fontWeight: 600,
|
||||
textAlign: 'center',
|
||||
lineHeight: '1.6',
|
||||
color: '#333',
|
||||
}}
|
||||
>
|
||||
{generateInvalidText()}
|
||||
</Typography>
|
||||
</Box>
|
||||
</DashboardTitleBox>
|
||||
) : (
|
||||
<DashboardTitleBox
|
||||
title={
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
component="span"
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
paddingLeft: '18px',
|
||||
height: '16px',
|
||||
fontFamily: 'Pretendard',
|
||||
fontSize: '18px',
|
||||
fontStretch: 'normal',
|
||||
fontStyle: 'normal',
|
||||
lineHeight: 0.89,
|
||||
letterSpacing: '-0.18px',
|
||||
textAlign: 'left',
|
||||
color: '#141414',
|
||||
}}
|
||||
>
|
||||
{dashboardInfo.title}
|
||||
</Typography>
|
||||
}
|
||||
button={
|
||||
<Stack direction="row" alignItems="center" sx={{ marginRight: '24px' }}>
|
||||
<span
|
||||
style={{
|
||||
height: '16px',
|
||||
fontFamily: 'Pretendard',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
fontStretch: 'normal',
|
||||
fontStyle: 'normal',
|
||||
lineHeight: '1.14',
|
||||
letterSpacing: 'normal',
|
||||
textAlign: 'left',
|
||||
color: '#333333',
|
||||
}}
|
||||
>
|
||||
{`편집일 : ${dateData(dashboardInfo.updatedAt)}`}
|
||||
</span>
|
||||
</Stack>
|
||||
}
|
||||
return !isShareOn ? (
|
||||
<PageViewBox sx={{ borderTop: '1px solid #ddd' }}>
|
||||
<Box
|
||||
sx={{
|
||||
flex: '1 1 auto',
|
||||
backgroundColor: '#f9f9fa',
|
||||
borderRadius: '0px 0px 6px 6px',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
margin: '200px auto',
|
||||
fontSize: '16px',
|
||||
fontWeight: 600,
|
||||
textAlign: 'center',
|
||||
lineHeight: '1.6',
|
||||
whiteSpace: 'pre-wrap',
|
||||
color: '#333',
|
||||
}}
|
||||
>
|
||||
<Seo title={dashboardInfo.title} />
|
||||
<Box
|
||||
sx={{
|
||||
width: '1390px',
|
||||
minWidth: '1390px',
|
||||
minHeight: '1080px',
|
||||
backgroundColor: '#f9f9fa',
|
||||
borderRadius: '0px 0px 6px 6px',
|
||||
}}
|
||||
>
|
||||
<ResponsiveGridLayout
|
||||
rowHeight={88}
|
||||
compactType={null}
|
||||
cols={{ lg: 12 }}
|
||||
layouts={{ lg: layout }}
|
||||
containerPadding={{ lg: [24, 24] }}
|
||||
margin={{ lg: [24, 24] }}
|
||||
>
|
||||
{generateWidget()}
|
||||
</ResponsiveGridLayout>
|
||||
</Box>
|
||||
</DashboardTitleBox>
|
||||
)}
|
||||
<Copyright sx={{ mt: '40px', mb: '75px' }} />
|
||||
</Stack>
|
||||
{generateInvalidText()}
|
||||
</Typography>
|
||||
</Box>
|
||||
</PageViewBox>
|
||||
) : (
|
||||
<PageViewBox
|
||||
title={dashboardInfo.title}
|
||||
date={`${dateData(dashboardInfo.updatedAt)}`}
|
||||
sx={{ borderTop: '1px solid #ddd' }}
|
||||
>
|
||||
<Seo title={dashboardInfo.title} />
|
||||
<Box
|
||||
sx={{
|
||||
flex: '1 1 auto',
|
||||
minHeight: { sm: '1080px' },
|
||||
backgroundColor: '#f9f9fa',
|
||||
borderRadius: '0px 0px 6px 6px',
|
||||
}}
|
||||
>
|
||||
<ResponsiveGridLayout
|
||||
rowHeight={88}
|
||||
compactType={null}
|
||||
breakpoints={{ xs: 0, md: 800, lg: 1000 }}
|
||||
cols={{ xs: 2, md: 8, lg: 12 }}
|
||||
layouts={{ xs: layout, md: layout, lg: layout }}
|
||||
containerPadding={{ xs: [20, 20], md: [20, 20], lg: [24, 24] }}
|
||||
margin={{ xs: [20, 20], md: [20, 20], lg: [24, 24] }}
|
||||
>
|
||||
{generateWidget()}
|
||||
</ResponsiveGridLayout>
|
||||
</Box>
|
||||
</PageViewBox>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ReactComponent as Logo } from '@/assets/images/logo.svg';
|
||||
import { Link as RouterLink, useNavigate } from 'react-router-dom';
|
||||
import { LoadingContext } from '@/contexts/LoadingContext';
|
||||
import { useAlert } from 'react-alert';
|
||||
import Copyright from '@/components/Copyright';
|
||||
import { Copyright } from '@/layouts/Footer';
|
||||
import authService from '@/api/authService';
|
||||
import { SnackbarContext } from '@/contexts/AlertContext';
|
||||
import { checkEmail, checkId, checkPwd } from '@/utils/util';
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import React from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import PageTitleBox from '@/components/PageTitleBox';
|
||||
|
||||
function Status404(props) {
|
||||
return <Box p={3}>404 Error</Box>;
|
||||
function Status404() {
|
||||
return (
|
||||
<PageTitleBox title="404 Error">
|
||||
<Box sx={{ m: 'auto', fontSize: '18px', fontWeight: 600 }}>페이지를 찾을 수 없습니다.</Box>
|
||||
</PageTitleBox>
|
||||
);
|
||||
}
|
||||
|
||||
export default Status404;
|
||||
|
||||
@@ -45,10 +45,10 @@ const WidgetAttributeSelect = props => {
|
||||
|
||||
const getData = () => {
|
||||
const param = isModifyMode
|
||||
? { datasetType: widgetOption.datasetType, datasetId: widgetOption.datasetId }
|
||||
? { ...widgetOption }
|
||||
: dataset.datasetType === 'TABLE'
|
||||
? { databaseId: dataset.databaseId, datasetType: dataset.datasetType, tableName: dataset.tableName }
|
||||
: { databaseId: dataset.databaseId, datasetType: dataset.datasetType, datasetId: dataset.id };
|
||||
? { ...dataset }
|
||||
: { ...dataset, datasetId: dataset.id };
|
||||
console.log('getData param', param);
|
||||
showLoading();
|
||||
DatabaseService.selectData(param)
|
||||
|
||||
@@ -8,6 +8,7 @@ function WidgetTypeSelect(props) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
flex: '1 1 auto',
|
||||
height: '100%',
|
||||
px: '25px',
|
||||
pt: '22px',
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Box, Button, Stack, Step, StepLabel, Stepper, SvgIcon } from '@mui/material';
|
||||
import PageTitleBox from '@/components/PageTitleBox';
|
||||
import PageContainer from '@/components/PageContainer';
|
||||
import WidgetDataSelect from './WidgetDataSelect';
|
||||
import WidgetTypeSelect from './WidgetTypeSelect';
|
||||
import WidgetAttributeSelect from './WidgetAttributeSelect';
|
||||
@@ -110,113 +109,117 @@ const WidgetCreate = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageTitleBox
|
||||
fixed
|
||||
title={title}
|
||||
upperTitle="위젯"
|
||||
upperTitleLink="/widget"
|
||||
sx={{ paddingLeft: 0, paddingRight: 0, width: '100%', height: '100%' }}
|
||||
button={
|
||||
<Stack direction="row" gap="10px">
|
||||
<PageTitleBox
|
||||
fixed
|
||||
title={title}
|
||||
upperTitle="위젯"
|
||||
upperTitleLink="/widget"
|
||||
sx={{ paddingLeft: 0, paddingRight: 0, width: '100%', height: '100%' }}
|
||||
button={
|
||||
<Stack direction="row" gap="10px">
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleBack}
|
||||
disabled={activeStep === 0}
|
||||
startIcon={
|
||||
<SvgIcon
|
||||
component={LeftArrow}
|
||||
sx={{
|
||||
width: '14px',
|
||||
height: '14px',
|
||||
padding: '1px',
|
||||
}}
|
||||
inheritViewBox
|
||||
/>
|
||||
}
|
||||
sx={{
|
||||
color: '#043f84',
|
||||
'&.Mui-disabled &.MuiButton-startIcon': {
|
||||
color: '#fff',
|
||||
},
|
||||
}}
|
||||
>
|
||||
이전
|
||||
</Button>
|
||||
|
||||
{activeStep !== 2 ? (
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleBack}
|
||||
disabled={activeStep === 0}
|
||||
startIcon={
|
||||
variant="contained"
|
||||
type="button"
|
||||
onClick={handleNext}
|
||||
disabled={isNextButtonDisabled}
|
||||
endIcon={
|
||||
<SvgIcon
|
||||
component={LeftArrow}
|
||||
sx={{
|
||||
width: '14px',
|
||||
height: '14px',
|
||||
padding: '1px',
|
||||
}}
|
||||
sx={{ width: '14px', height: '14px', transform: 'rotate(180deg)', padding: '1px' }}
|
||||
inheritViewBox
|
||||
/>
|
||||
}
|
||||
sx={{
|
||||
color: '#043f84',
|
||||
'&.Mui-disabled &.MuiButton-startIcon': {
|
||||
color: '#fff',
|
||||
},
|
||||
}}
|
||||
sx={{ backgroundColor: '#043f84' }}
|
||||
>
|
||||
이전
|
||||
다음
|
||||
</Button>
|
||||
|
||||
{activeStep !== 2 ? (
|
||||
<Button
|
||||
variant="contained"
|
||||
type="button"
|
||||
onClick={handleNext}
|
||||
disabled={isNextButtonDisabled}
|
||||
endIcon={
|
||||
<SvgIcon
|
||||
component={LeftArrow}
|
||||
sx={{ width: '14px', height: '14px', transform: 'rotate(180deg)', padding: '1px' }}
|
||||
inheritViewBox
|
||||
/>
|
||||
}
|
||||
sx={{ backgroundColor: '#043f84' }}
|
||||
>
|
||||
다음
|
||||
</Button>
|
||||
) : (
|
||||
' '
|
||||
)}
|
||||
</Stack>
|
||||
}
|
||||
) : (
|
||||
' '
|
||||
)}
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
zIndex: 1000,
|
||||
width: '100%',
|
||||
mt: '56px',
|
||||
borderBottom: '1px solid #e3e7ea',
|
||||
backgroundColor: '#fff',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
<Stepper
|
||||
activeStep={activeStep}
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
zIndex: 1000,
|
||||
width: '100%',
|
||||
mt: '56px',
|
||||
borderBottom: '1px solid #e3e7ea',
|
||||
backgroundColor: '#fff',
|
||||
width: '50%',
|
||||
minWidth: '450px',
|
||||
maxWidth: '564px',
|
||||
height: '72px',
|
||||
m: 'auto',
|
||||
}}
|
||||
>
|
||||
<Stepper
|
||||
activeStep={activeStep}
|
||||
sx={{
|
||||
width: '50%',
|
||||
minWidth: '450px',
|
||||
maxWidth: '564px',
|
||||
height: '72px',
|
||||
m: 'auto',
|
||||
}}
|
||||
>
|
||||
{steps.map(label => {
|
||||
const stepProps: { completed?: boolean } = {};
|
||||
const labelProps: {
|
||||
optional?: React.ReactNode;
|
||||
} = {};
|
||||
return (
|
||||
<Step key={label} {...stepProps}>
|
||||
<StepLabel {...labelProps}>{label}</StepLabel>
|
||||
</Step>
|
||||
);
|
||||
})}
|
||||
</Stepper>
|
||||
</Box>
|
||||
<Box mt="129px">
|
||||
{activeStep === 0 ? (
|
||||
<WidgetDataSelect setDataSet={setDataset} />
|
||||
) : activeStep === 1 ? (
|
||||
<WidgetTypeSelect widgetType={widgetOption} setWidgetType={setWidgetOption} componentList={componentList} />
|
||||
) : (
|
||||
<WidgetAttributeSelect
|
||||
dataset={dataset}
|
||||
widgetOption={widgetOption}
|
||||
saveWidgetInfo={saveWidgetInfo}
|
||||
widgetTypeName={widgetOption.title}
|
||||
widgetTypeDescription={widgetOption.description}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</PageTitleBox>
|
||||
</PageContainer>
|
||||
{steps.map(label => {
|
||||
const stepProps: { completed?: boolean } = {};
|
||||
const labelProps: {
|
||||
optional?: React.ReactNode;
|
||||
} = {};
|
||||
return (
|
||||
<Step key={label} {...stepProps}>
|
||||
<StepLabel {...labelProps}>{label}</StepLabel>
|
||||
</Step>
|
||||
);
|
||||
})}
|
||||
</Stepper>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flex: '1 1 auto',
|
||||
mt: '129px',
|
||||
}}
|
||||
>
|
||||
{activeStep === 0 ? (
|
||||
<WidgetDataSelect setDataSet={setDataset} />
|
||||
) : activeStep === 1 ? (
|
||||
<WidgetTypeSelect widgetType={widgetOption} setWidgetType={setWidgetOption} componentList={componentList} />
|
||||
) : (
|
||||
<WidgetAttributeSelect
|
||||
dataset={dataset}
|
||||
widgetOption={widgetOption}
|
||||
saveWidgetInfo={saveWidgetInfo}
|
||||
widgetTypeName={widgetOption.title}
|
||||
widgetTypeDescription={widgetOption.description}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</PageTitleBox>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import PageContainer from '@/components/PageContainer';
|
||||
import PageTitleBox from '@/components/PageTitleBox';
|
||||
import { ConfirmButton } from '@/components/button/ConfirmCancelButton';
|
||||
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
@@ -82,33 +81,31 @@ const WidgetModify = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageTitleBox
|
||||
upperTitle="위젯"
|
||||
upperTitleLink="/widget"
|
||||
title="위젯 편집"
|
||||
sx={{ padding: 0 }}
|
||||
button={
|
||||
<ConfirmButton
|
||||
confirmLabel="저장"
|
||||
confirmProps={{
|
||||
form: 'widgetAttribute',
|
||||
type: 'submit',
|
||||
variant: 'contained',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<WidgetAttributeSelect
|
||||
widgetTypeName={widgetInfo.componentTitle}
|
||||
widgetTypeDescription={widgetInfo.componentDescription}
|
||||
isModifyMode={true}
|
||||
dataSetId={widgetInfo.datasetId}
|
||||
widgetOption={widgetInfo}
|
||||
saveWidgetInfo={saveWidgetInfo}
|
||||
<PageTitleBox
|
||||
upperTitle="위젯"
|
||||
upperTitleLink="/widget"
|
||||
title="위젯 편집"
|
||||
sx={{ padding: 0 }}
|
||||
button={
|
||||
<ConfirmButton
|
||||
confirmLabel="저장"
|
||||
confirmProps={{
|
||||
form: 'widgetAttribute',
|
||||
type: 'submit',
|
||||
variant: 'contained',
|
||||
}}
|
||||
/>
|
||||
</PageTitleBox>
|
||||
</PageContainer>
|
||||
}
|
||||
>
|
||||
<WidgetAttributeSelect
|
||||
widgetTypeName={widgetInfo.componentTitle}
|
||||
widgetTypeDescription={widgetInfo.componentDescription}
|
||||
isModifyMode={true}
|
||||
dataSetId={widgetInfo.datasetId}
|
||||
widgetOption={widgetInfo}
|
||||
saveWidgetInfo={saveWidgetInfo}
|
||||
/>
|
||||
</PageTitleBox>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Avatar, Card, Stack, Typography } from '@mui/material';
|
||||
import { Box, Card, Hidden } from '@mui/material';
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import PageTitleBox from '@/components/PageTitleBox';
|
||||
import WidgetService from '@/api/widgetService';
|
||||
@@ -8,7 +8,7 @@ import WidgetWrapper from '@/widget/wrapper/WidgetWrapper';
|
||||
import ReloadButton from '@/components/button/ReloadButton';
|
||||
import ModifyButton from '@/components/button/ModifyButton';
|
||||
import DeleteButton from '@/components/button/DeleteButton';
|
||||
import DashboardTitleBox from '@/pages/Dashboard/Components/DashboardTitleBox';
|
||||
import PageViewBox from '@/components/PageViewBox';
|
||||
import { useAlert } from 'react-alert';
|
||||
import { LoadingContext } from '@/contexts/LoadingContext';
|
||||
import { SnackbarContext } from '@/contexts/AlertContext';
|
||||
@@ -67,85 +67,53 @@ const WidgetView = () => {
|
||||
};
|
||||
|
||||
const removeWidget = () => {
|
||||
alert.success(`${widgetOption.title}\n위젯을 삭제하시겠습니까?`, {
|
||||
title: '위젯 삭제',
|
||||
closeCopy: '취소',
|
||||
actions: [
|
||||
{
|
||||
copy: '삭제',
|
||||
onClick: () => {
|
||||
showLoading();
|
||||
WidgetService.deleteWidget(widgetId)
|
||||
.then(response => {
|
||||
if (response.status === 200) {
|
||||
navigate('/widget', { replace: true });
|
||||
snackbar.success('위젯이 삭제되었습니다.');
|
||||
} else {
|
||||
alert.error('위젯 삭제에 실패했습니다.\n다시 시도해 주세요.');
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
hideLoading();
|
||||
});
|
||||
alert.success(
|
||||
<Box sx={{ span: { fontWeight: 600 } }}>
|
||||
<span>{widgetOption.title}</span>
|
||||
<br />
|
||||
위젯을 삭제하시겠습니까?
|
||||
</Box>,
|
||||
{
|
||||
title: '위젯 삭제',
|
||||
closeCopy: '취소',
|
||||
actions: [
|
||||
{
|
||||
copy: '삭제',
|
||||
onClick: () => {
|
||||
showLoading();
|
||||
WidgetService.deleteWidget(widgetId)
|
||||
.then(response => {
|
||||
if (response.status === 200) {
|
||||
navigate('/widget', { replace: true });
|
||||
snackbar.success('위젯이 삭제되었습니다.');
|
||||
} else {
|
||||
alert.error('위젯 삭제에 실패했습니다.\n다시 시도해 주세요.');
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
hideLoading();
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
],
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageTitleBox upperTitle="위젯" upperTitleLink="/widget" title="위젯 조회" sx={{ width: '100%', marginTop: '22px' }}>
|
||||
<PageTitleBox
|
||||
upperTitle="위젯"
|
||||
upperTitleLink="/widget"
|
||||
title="위젯 조회"
|
||||
sx={{ width: '100%', marginTop: { xs: 0, sm: '22px' }, flex: '1 1 auto', p: { xs: 0 } }}
|
||||
>
|
||||
<Seo title={widgetOption.title} />
|
||||
<DashboardTitleBox
|
||||
sx={{ minWidth: '600px', maxWidth: '1392px', width: '95%' }}
|
||||
title={
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
sx={{
|
||||
pl: '20px',
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
src={`/static/images/${widgetOption.icon}`}
|
||||
sx={{ width: '30px', height: '30px', borderRadius: 0, objectFit: 'contain', backgroundColor: 'transparent' }}
|
||||
/>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
component="span"
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
paddingLeft: '14px',
|
||||
height: '16px',
|
||||
fontSize: '18px',
|
||||
fontStretch: 'normal',
|
||||
fontStyle: 'normal',
|
||||
lineHeight: 0.89,
|
||||
letterSpacing: '-0.18px',
|
||||
textAlign: 'left',
|
||||
color: '#141414',
|
||||
}}
|
||||
>
|
||||
{widgetOption.componentTitle}
|
||||
</Typography>
|
||||
</Stack>
|
||||
}
|
||||
<PageViewBox
|
||||
iconName={widgetOption.icon}
|
||||
title={widgetOption.componentTitle}
|
||||
date={dateData(widgetOption.updatedAt)}
|
||||
button={
|
||||
<Stack direction="row" alignItems="center" sx={{ marginRight: '20px' }}>
|
||||
<span
|
||||
style={{
|
||||
marginRight: '56px',
|
||||
height: '16px',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
lineHeight: '1.14',
|
||||
letterSpacing: 'normal',
|
||||
textAlign: 'left',
|
||||
color: '#333',
|
||||
}}
|
||||
>
|
||||
{dateData(widgetOption.updatedAt)}
|
||||
</span>
|
||||
<Hidden smDown>
|
||||
<ReloadButton
|
||||
size="medium"
|
||||
sx={{ marginRight: '36px', padding: 0 }}
|
||||
@@ -173,16 +141,16 @@ const WidgetView = () => {
|
||||
removeWidget();
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Hidden>
|
||||
}
|
||||
>
|
||||
<Card
|
||||
sx={{
|
||||
width: '60%',
|
||||
width: { xs: 'calc(100% - 40px)', sm: '60%' },
|
||||
height: '50vw',
|
||||
minHeight: '300px',
|
||||
maxHeight: '700px',
|
||||
margin: '54px auto',
|
||||
margin: { xs: '20px auto', sm: '54px auto' },
|
||||
borderRadius: '8px',
|
||||
boxShadow: '2px 2px 9px 0 rgba(42, 50, 62, 0.1), 0 4px 4px 0 rgba(0, 0, 0, 0.02)',
|
||||
border: 'solid 1px #e2e2e2',
|
||||
@@ -191,7 +159,7 @@ const WidgetView = () => {
|
||||
>
|
||||
<WidgetWrapper widgetOption={widgetOption} dataSetId={widgetOption.datasetId} />
|
||||
</Card>
|
||||
</DashboardTitleBox>
|
||||
</PageViewBox>
|
||||
</PageTitleBox>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Link as RouterLink, Outlet, useParams } from 'react-router-dom';
|
||||
import { useAlert } from 'react-alert';
|
||||
import PageContainer from '@/components/PageContainer';
|
||||
import PageTitleBox from '@/components/PageTitleBox';
|
||||
import BoardList from '@/components/BoardList';
|
||||
import WidgetService from '@/api/widgetService';
|
||||
import { LoadingContext } from '@/contexts/LoadingContext';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import { Box, Button, Stack } from '@mui/material';
|
||||
import { Box, Button, Stack, useMediaQuery, useTheme } from '@mui/material';
|
||||
import { styled } from '@mui/system';
|
||||
import { STATUS } from '@/constant';
|
||||
import { SnackbarContext } from '@/contexts/AlertContext';
|
||||
@@ -23,10 +22,12 @@ const Widget = () => {
|
||||
|
||||
const [widgetList, setWidgetList] = useState([]);
|
||||
const [noData, setNoData] = useState(false);
|
||||
const theme = useTheme();
|
||||
const matches = useMediaQuery(theme.breakpoints.up('sm'));
|
||||
|
||||
const GTSpan = styled('span')({
|
||||
fontFamily: 'Pretendard',
|
||||
fontSize: '13px',
|
||||
fontSize: matches ? '13px' : '10px',
|
||||
fontWeight: '500',
|
||||
fontStretch: 'normal',
|
||||
fontStyle: 'normal',
|
||||
@@ -59,31 +60,38 @@ const Widget = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const removeWidget = item => {
|
||||
alert.success(`${item.title}\n위젯을 삭제하시겠습니까?`, {
|
||||
title: '위젯 삭제',
|
||||
closeCopy: '취소',
|
||||
actions: [
|
||||
{
|
||||
copy: '삭제',
|
||||
onClick: () => {
|
||||
showLoading();
|
||||
WidgetService.deleteWidget(item.id)
|
||||
.then(response => {
|
||||
if (response.status === 200) {
|
||||
getWidgetList();
|
||||
snackbar.success('위젯이 삭제되었습니다.');
|
||||
} else {
|
||||
alert.error('위젯 삭제에 실패했습니다.\n다시 시도해 주세요.');
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
hideLoading();
|
||||
});
|
||||
const removeWidget = (id, title) => {
|
||||
alert.success(
|
||||
<Box sx={{ span: { fontWeight: 600 } }}>
|
||||
<span>{title}</span>
|
||||
<br />
|
||||
위젯을 삭제하시겠습니까?
|
||||
</Box>,
|
||||
{
|
||||
title: '위젯 삭제',
|
||||
closeCopy: '취소',
|
||||
actions: [
|
||||
{
|
||||
copy: '삭제',
|
||||
onClick: () => {
|
||||
showLoading();
|
||||
WidgetService.deleteWidget(id)
|
||||
.then(response => {
|
||||
if (response.status === 200) {
|
||||
getWidgetList();
|
||||
snackbar.success('위젯이 삭제되었습니다.');
|
||||
} else {
|
||||
alert.error('위젯 삭제에 실패했습니다.\n다시 시도해 주세요.');
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
hideLoading();
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
],
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
// 목록이 없을때 보여줄 화면
|
||||
@@ -93,18 +101,18 @@ const Widget = () => {
|
||||
<Stack
|
||||
flexDirection="row"
|
||||
justifyContent="space-between"
|
||||
sx={{ paddingLeft: '20px', paddingRight: '217px', marginBottom: '11px', marginTop: '36px' }}
|
||||
sx={{ paddingLeft: '20px', paddingRight: { xs: '20px', sm: '217px' }, marginBottom: '11px', marginTop: '36px' }}
|
||||
>
|
||||
<GTSpan>이름</GTSpan>
|
||||
<GTSpan>수정일</GTSpan>
|
||||
</Stack>
|
||||
<Box
|
||||
sx={{
|
||||
height: '57px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
flexGrow: '0',
|
||||
py: '18px',
|
||||
margin: '0 0 0 0',
|
||||
borderRadius: '6px',
|
||||
border: 'solid 1px #ddd',
|
||||
@@ -113,20 +121,21 @@ const Widget = () => {
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
height: '16px',
|
||||
flexGrow: 0,
|
||||
fontFamily: 'Pretendard',
|
||||
fontSize: '16px',
|
||||
fontSize: matches ? '16px' : '14px',
|
||||
fontWeight: 600,
|
||||
fontStretch: 'normal',
|
||||
fontStyle: 'normal',
|
||||
lineHeight: 1,
|
||||
lineHeight: 1.43,
|
||||
letterSpacing: 'normal',
|
||||
textAlign: 'center',
|
||||
color: '#333333',
|
||||
}}
|
||||
>
|
||||
위젯이 존재하지 않습니다. 위젯을 생성해보세요.
|
||||
생성한 위젯이 없습니다.
|
||||
{matches ? ' ' : <br />}
|
||||
위젯을 생성 후 확인해 보세요.
|
||||
</span>
|
||||
</Box>
|
||||
</>
|
||||
@@ -134,7 +143,7 @@ const Widget = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<Stack sx={{ width: '100%', height: '100%', flex: '1 1 auto' }}>
|
||||
<Seo title={title} />
|
||||
|
||||
{!widgetId ? (
|
||||
@@ -163,7 +172,7 @@ const Widget = () => {
|
||||
) : (
|
||||
<Outlet />
|
||||
)}
|
||||
</PageContainer>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -17,11 +17,11 @@ import { ProtectedRoute } from '@/router/ProtectedRoute';
|
||||
import Layout from '@/layouts/Layout';
|
||||
import Login from '@/pages/Login';
|
||||
import Share from '@/pages/Share';
|
||||
import PublicLayout from '@/layouts/PublicLayout';
|
||||
|
||||
function Router() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/share/:dashboardUuid" element={<Share />} />
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
@@ -58,10 +58,14 @@ function Router() {
|
||||
<Route path="/data/set/modify" element={<DataSet />}>
|
||||
<Route path=":setId" element={<DataSet />} />
|
||||
</Route>
|
||||
<Route path="/*" element={<Status404 />} />
|
||||
</Route>
|
||||
<Route path="/login" element={<Login />} />
|
||||
{/*<Route path="/signup" element={<SignUp />} />*/}
|
||||
<Route path="/*" element={<Status404 />} />
|
||||
<Route path="/" element={<PublicLayout />}>
|
||||
<Route path="/share/:dashboardUuid" element={<Share />} />
|
||||
<Route path="*" element={<Status404 />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import React from 'react';
|
||||
import { ROUTE_URL } from '@/constant';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
|
||||
const Seo = props => {
|
||||
const {
|
||||
title = 'VanillaMeta',
|
||||
description = '최신 엔터프라이즈용 비즈니스 인텔리전스 웹 애플리케이션, VanillaMeta',
|
||||
image = `${ROUTE_URL}/static/images/logo/vanillaMeta-og.jpg`,
|
||||
url = ROUTE_URL,
|
||||
image = `${process.env.PUBLIC_URL}/static/images/logo/vanillaMeta-og.jpg`,
|
||||
url = process.env.PUBLIC_URL,
|
||||
} = props;
|
||||
const titleText = title === 'VanillaMeta' ? 'VanillaMeta' : title + ' - VanillaMeta';
|
||||
|
||||
|
||||
@@ -375,7 +375,7 @@ export default createTheme({
|
||||
'html, body, #root': {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
minWidth: '600px',
|
||||
minWidth: '320px',
|
||||
fontFamily: 'Pretendard',
|
||||
backgroundColor: '#FFFFFF',
|
||||
color: '#1F2123',
|
||||
|
||||
@@ -28,3 +28,19 @@ export const dateData = data => {
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* 데이터 그리드 컬럼 생성
|
||||
* @param data
|
||||
*/
|
||||
export const createColumns = data => {
|
||||
let target = null;
|
||||
if (data instanceof Array && data.length > 0) {
|
||||
target = data[0];
|
||||
} else if (data instanceof Object) {
|
||||
target = data;
|
||||
}
|
||||
return Object.keys(target).map(key => {
|
||||
return { name: key, header: key, minWidth: 200, sortable: true };
|
||||
});
|
||||
};
|
||||
|
||||
@@ -55,9 +55,16 @@ const NumericBoard = props => {
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
display: 'block',
|
||||
maxWidth: '700px', // 글자 최대너비 제한
|
||||
width: '100%',
|
||||
mx: 'auto',
|
||||
fontSize: componentOption.header.fontSize,
|
||||
color: componentOption.header.color,
|
||||
textAlign: 'center',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
{componentOption.header.title}
|
||||
@@ -65,10 +72,17 @@ const NumericBoard = props => {
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
display: 'block',
|
||||
maxWidth: '700px', // 글자 최대너비 제한
|
||||
width: '100%',
|
||||
mx: 'auto',
|
||||
fontSize: componentOption.content.fontSize,
|
||||
color: componentOption.content.color,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
{score}
|
||||
|
||||
@@ -1,23 +1,42 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Stack } from '@mui/material';
|
||||
import DataGrid from '@/components/datagrid';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import DataGrid, { DataGridWrapper } from '@/components/datagrid';
|
||||
import _ from 'lodash';
|
||||
|
||||
const TableBoard = props => {
|
||||
const { option, dataSet } = props;
|
||||
const [columns, setColumns] = useState([]);
|
||||
const [resizeObserver, setResizeObserver] = useState(null);
|
||||
const wrapperRef = useRef(null);
|
||||
|
||||
const defaultComponentOption = {
|
||||
columns: [],
|
||||
};
|
||||
|
||||
const [columns, setColumns] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (option && dataSet) {
|
||||
const newOption = createComponentOption();
|
||||
setColumns([...option.columns]);
|
||||
setColumns([...newOption.columns]);
|
||||
}
|
||||
}, [option, dataSet]);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new ResizeObserver(entries => {
|
||||
throttleResize(entries);
|
||||
});
|
||||
if (wrapperRef.current) {
|
||||
observer.observe(wrapperRef.current);
|
||||
}
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const throttleResize = _.throttle(entries => {
|
||||
entries?.forEach(entry => {
|
||||
setResizeObserver(entry.contentRect);
|
||||
});
|
||||
}, 300);
|
||||
|
||||
/**
|
||||
*
|
||||
* 위젯옵션과 데이터로
|
||||
@@ -31,14 +50,9 @@ const TableBoard = props => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
sx={{
|
||||
height: '100%',
|
||||
minHeight: '100%',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<DataGridWrapper ref={wrapperRef}>
|
||||
<DataGrid
|
||||
resizeObserver={resizeObserver}
|
||||
minBodyHeight={300}
|
||||
bodyHeight={'fitToParent'}
|
||||
data={dataSet}
|
||||
@@ -47,7 +61,7 @@ const TableBoard = props => {
|
||||
resizable: true,
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</DataGridWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -75,7 +75,11 @@ const LineChart = props => {
|
||||
const op = {
|
||||
[axis + 'Axis']: {
|
||||
type: 'category',
|
||||
data: option[axis + 'Field'] ? aggrData.map(item => item[option[axis + 'Field']]) : '',
|
||||
data: option[axis + 'Field']
|
||||
? axis === 'x'
|
||||
? aggrData.map(item => item[option[axis + 'Field']])
|
||||
: aggrData.map(item => item[option[axis + 'Field']]).reverse()
|
||||
: '',
|
||||
},
|
||||
series: newSeries,
|
||||
grid: getGridSize(option.legendPosition),
|
||||
|
||||
@@ -99,7 +99,11 @@ const MixedLinePieChart = props => {
|
||||
const op = {
|
||||
[axis + 'Axis']: {
|
||||
type: 'category',
|
||||
data: option[axis + 'Field'] ? aggrData.map(item => item[option[axis + 'Field']]) : '',
|
||||
data: option[axis + 'Field']
|
||||
? axis === 'x'
|
||||
? aggrData.map(item => item[option[axis + 'Field']])
|
||||
: aggrData.map(item => item[option[axis + 'Field']]).reverse()
|
||||
: '',
|
||||
},
|
||||
series: newSeries,
|
||||
grid: getGridSize(option.legendPosition),
|
||||
|
||||
@@ -113,7 +113,7 @@ export const getAggregationData = (type, data, field) => {
|
||||
if (data.length > 0 && (type === WIDGET_AGGREGATION.MIN || type === WIDGET_AGGREGATION.MAX)) {
|
||||
result = Number(data[0][field]);
|
||||
} else if (data.length > 0 && type === WIDGET_AGGREGATION.SUM && field) {
|
||||
dataList = data.map(row => row[field]);
|
||||
dataList = data.map(row => Number(row[field]));
|
||||
fits = decimalFits(dataList);
|
||||
}
|
||||
switch (type) {
|
||||
|
||||
@@ -55,7 +55,7 @@ export const WidgetEmpty = () => {
|
||||
};
|
||||
|
||||
const WidgetViewer = props => {
|
||||
const { title, widgetType, widgetOption, dataSet, isInvalidData } = props;
|
||||
const { title, widgetType, widgetOption, dataSet, isInvalidData, size } = props;
|
||||
const { showLoading, hideLoading } = useContext(LoadingContext);
|
||||
const [module, setModule] = useState(null);
|
||||
|
||||
@@ -398,9 +398,10 @@ const WidgetViewer = props => {
|
||||
left: 0,
|
||||
right: 0,
|
||||
maxWidth: 'fit-content',
|
||||
height: 0,
|
||||
height: '100%',
|
||||
margin: 'auto',
|
||||
transform: 'translateY(-50%)',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
<DonutChart {...chartProps} />
|
||||
@@ -419,9 +420,10 @@ const WidgetViewer = props => {
|
||||
left: 0,
|
||||
right: 0,
|
||||
maxWidth: 'fit-content',
|
||||
height: 0,
|
||||
height: '100%',
|
||||
margin: 'auto',
|
||||
transform: 'translateY(-50%)',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
<DonutChart
|
||||
@@ -522,17 +524,28 @@ const WidgetViewer = props => {
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: 'calc(100% - 48px)',
|
||||
// position: 'relative',
|
||||
padding: '10px 40px 48px 40px',
|
||||
pt: '10px',
|
||||
pb: { xs: '25px', sm: '48px' },
|
||||
px: { xs: '10px', sm: '40px' },
|
||||
overflowY: 'auto',
|
||||
}}
|
||||
>
|
||||
{isInvalidData ? <WidgetEmpty /> : module}
|
||||
{/*<ReactECharts*/}
|
||||
{/* option={componentOption}*/}
|
||||
{/* style={{ height: '100%', maxHeight: '600px', width: '100%' }}*/}
|
||||
{/* lazyUpdate={true}*/}
|
||||
{/* notMerge={true}*/}
|
||||
{/*/>*/}
|
||||
<Stack
|
||||
sx={{
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
minWidth: { xs: `calc(${size} * 80px)`, sm: 0 },
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
{isInvalidData ? <WidgetEmpty /> : module}
|
||||
{/*<ReactECharts*/}
|
||||
{/* option={componentOption}*/}
|
||||
{/* style={{ height: '100%', maxHeight: '600px', width: '100%' }}*/}
|
||||
{/* lazyUpdate={true}*/}
|
||||
{/* notMerge={true}*/}
|
||||
{/*/>*/}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useAlert } from 'react-alert';
|
||||
import { SnackbarContext } from '@/contexts/AlertContext';
|
||||
|
||||
const WidgetWrapper = props => {
|
||||
const { widgetOption, dataSetId } = props;
|
||||
const { widgetOption, dataSetId, size } = props;
|
||||
const { showLoading, hideLoading } = useContext(LoadingContext);
|
||||
const [dataset, setDataset] = useState(null);
|
||||
const snackbar = useAlert(SnackbarContext);
|
||||
@@ -49,6 +49,7 @@ const WidgetWrapper = props => {
|
||||
widgetOption={widgetOption.option}
|
||||
dataSet={dataset}
|
||||
isInvalidData={isInvalidData}
|
||||
size={size}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user