컴포넌트 관련 변경
이미지 업로드 한글명칭 불가 처리 유저 조회 부분 수정
This commit is contained in:
@@ -12,7 +12,7 @@ export const UserView = async (token, searchType, searchKey) => {
|
|||||||
{ headers: { Authorization: `Bearer ${token}` } },
|
{ headers: { Authorization: `Bearer ${token}` } },
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.data.data.result;
|
return res.data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
throw new Error('UserView Error', e);
|
throw new Error('UserView Error', e);
|
||||||
|
|||||||
@@ -93,12 +93,6 @@ const UserInfoTable = styled.table`
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
tr:first-child {
|
|
||||||
th,
|
|
||||||
td {
|
|
||||||
border-top: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
th,
|
th,
|
||||||
td {
|
td {
|
||||||
height: 36px;
|
height: 36px;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useState, useEffect } from 'react';
|
|||||||
import { UserTattooView } from '../../apis/Users';
|
import { UserTattooView } from '../../apis/Users';
|
||||||
import { TableSkeleton } from '../Skeleton/TableSkeleton';
|
import { TableSkeleton } from '../Skeleton/TableSkeleton';
|
||||||
|
|
||||||
const UserTatttooInfo = ({ userInfo }) => {
|
const UserTattooInfo = ({ userInfo }) => {
|
||||||
const [dataList, setDataList] = useState();
|
const [dataList, setDataList] = useState();
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ const UserTatttooInfo = ({ userInfo }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default UserTatttooInfo;
|
export default UserTattooInfo;
|
||||||
|
|
||||||
const UserDefaultTable = styled.table`
|
const UserDefaultTable = styled.table`
|
||||||
border: 1px solid #e8eaec;
|
border: 1px solid #e8eaec;
|
||||||
|
|||||||
@@ -32,6 +32,18 @@ const ImageUploadBtn = ({ disabled,
|
|||||||
|
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
|
const koreanRegex = /[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/;
|
||||||
|
if (koreanRegex.test(file.name)) {
|
||||||
|
showToast('FILE_KOREAN_NAME_WARNING', {
|
||||||
|
type: alertTypes.warning
|
||||||
|
});
|
||||||
|
if (document.querySelector('#fileinput')) {
|
||||||
|
document.querySelector('#fileinput').value = '';
|
||||||
|
}
|
||||||
|
onFileDelete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 이미지 파일 확장자 체크
|
// 이미지 파일 확장자 체크
|
||||||
const fileExt = file.name.split('.').pop().toLowerCase();
|
const fileExt = file.name.split('.').pop().toLowerCase();
|
||||||
if (fileExt !== 'png' && fileExt !== 'jpg' && fileExt !== 'jpeg') {
|
if (fileExt !== 'png' && fileExt !== 'jpg' && fileExt !== 'jpeg') {
|
||||||
|
|||||||
@@ -1,79 +1,29 @@
|
|||||||
import React from 'react';
|
import { DatePicker } from 'antd';
|
||||||
import DatePickerComponent from './DatePickerComponent';
|
import dayjs from 'dayjs';
|
||||||
import { DatePickerWrapper } from '../../../styles/Components';
|
|
||||||
import {
|
const { RangePicker } = DatePicker;
|
||||||
FormRowGroup,
|
|
||||||
FormLabel,
|
|
||||||
DateContainer,
|
|
||||||
DateTimeWrapper,
|
|
||||||
DateTimeGroup,
|
|
||||||
} from '../../../styles/ModuleComponents';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
const DateRangePicker = ({
|
const DateRangePicker = ({
|
||||||
label,
|
value,
|
||||||
startDate,
|
onChange,
|
||||||
endDate,
|
format,
|
||||||
onStartDateChange,
|
showTime = true,
|
||||||
onEndDateChange,
|
size = 'middle',
|
||||||
pastDate = new Date(),
|
...props
|
||||||
disabled,
|
|
||||||
startLabel = '시작 일자',
|
|
||||||
endLabel = '종료 일자',
|
|
||||||
setAlert,
|
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const handleStartDate = (date) => {
|
|
||||||
const newDate = new Date(date);
|
|
||||||
onStartDateChange(newDate);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEndDate = (date) => {
|
|
||||||
let newDate = new Date(date);
|
|
||||||
|
|
||||||
if (startDate && newDate < startDate) {
|
|
||||||
setAlert(t('DATE_START_DIFF_END'));
|
|
||||||
newDate = new Date(startDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
onEndDateChange(newDate);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormRowGroup>
|
<RangePicker
|
||||||
<FormLabel>{label}</FormLabel>
|
showTime={showTime}
|
||||||
<DateTimeWrapper>
|
value={value ? [dayjs(value[0]), dayjs(value[1])] : [null, null]}
|
||||||
<DateTimeGroup>
|
format={format || 'YYYY-MM-DD HH:mm:ss'}
|
||||||
<DateContainer>
|
onChange={onChange}
|
||||||
<DatePickerWrapper>
|
placeholder={['시작 일시', '종료 일시']}
|
||||||
<DatePickerComponent
|
size={size}
|
||||||
name={startLabel}
|
allowClear={false}
|
||||||
handleSelectedDate={handleStartDate}
|
{...props}
|
||||||
selectedDate={startDate}
|
/>
|
||||||
pastDate={pastDate}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
</DatePickerWrapper>
|
|
||||||
</DateContainer>
|
|
||||||
</DateTimeGroup>
|
|
||||||
|
|
||||||
<DateTimeGroup>
|
|
||||||
<DateContainer>
|
|
||||||
<DatePickerWrapper>
|
|
||||||
<DatePickerComponent
|
|
||||||
name={endLabel}
|
|
||||||
handleSelectedDate={handleEndDate}
|
|
||||||
selectedDate={endDate}
|
|
||||||
pastDate={pastDate}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
</DatePickerWrapper>
|
|
||||||
</DateContainer>
|
|
||||||
</DateTimeGroup>
|
|
||||||
</DateTimeWrapper>
|
|
||||||
</FormRowGroup>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default DateRangePicker;
|
export default DateRangePicker;
|
||||||
205
src/components/common/Header/Navi_bak.js
Normal file
205
src/components/common/Header/Navi_bak.js
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import { NavLink, useNavigate } from 'react-router-dom';
|
||||||
|
import arrowIcon from '../../../assets/img/icon/icon-tab.png';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { authList } from '../../../store/authList';
|
||||||
|
import Modal from '../modal/Modal';
|
||||||
|
import { BtnWrapper, ButtonClose, ModalText } from '../../../styles/Components';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import Button from '../button/Button';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import { AuthInfo } from '../../../apis';
|
||||||
|
import { getMenuConfig } from '../../../utils';
|
||||||
|
import { adminAuthLevel } from '../../../assets/data/types';
|
||||||
|
|
||||||
|
const Navi = () => {
|
||||||
|
const token = sessionStorage.getItem('token');
|
||||||
|
const userInfo = useRecoilValue(authList);
|
||||||
|
const menu = getMenuConfig(userInfo);
|
||||||
|
|
||||||
|
const [modalClose, setModalClose] = useState('hidden');
|
||||||
|
const [logoutModalClose, setLogoutModalClose] = useState('hidden');
|
||||||
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleToken = async () => {
|
||||||
|
const tokenStatus = await AuthInfo(token);
|
||||||
|
|
||||||
|
tokenStatus.message === '잘못된 타입의 토큰입니다.' && setLogoutModalClose('view');
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleToken();
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
|
const handleTopMenu = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.target.classList.toggle('active');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLink = e => {
|
||||||
|
let topActive = document.querySelectorAll('nav .active');
|
||||||
|
let currentTopMenu = e.target.closest('ul').previousSibling;
|
||||||
|
for (let i = 0; i < topActive.length; i++) {
|
||||||
|
if (topActive[i] !== currentTopMenu) {
|
||||||
|
topActive[i].classList.remove('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleToken();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 등록 완료 모달
|
||||||
|
const handleModalClose = () => {
|
||||||
|
if (modalClose === 'hidden') {
|
||||||
|
setModalClose('view');
|
||||||
|
} else {
|
||||||
|
setModalClose('hidden');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 로그아웃 안내 모달
|
||||||
|
const handleConfirmClose = () => {
|
||||||
|
if (logoutModalClose === 'hidden') {
|
||||||
|
setLogoutModalClose('view');
|
||||||
|
} else {
|
||||||
|
setLogoutModalClose('hidden');
|
||||||
|
sessionStorage.removeItem('token');
|
||||||
|
|
||||||
|
navigate('/');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isClickable = (submenu) => {
|
||||||
|
switch (userInfo.auth_level_type) {
|
||||||
|
case adminAuthLevel.DEVELOPER:
|
||||||
|
case adminAuthLevel.READER:
|
||||||
|
case adminAuthLevel.MASTER:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return submenu.authLevel === adminAuthLevel.NONE && userInfo.auth_list && userInfo.auth_list.some(auth => auth.id === submenu.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
{menu.map((item, idx) => {
|
||||||
|
return (
|
||||||
|
<li key={idx}>
|
||||||
|
{item.access && (
|
||||||
|
<TopMenu to={item.link} onClick={handleTopMenu}>
|
||||||
|
{item.title}
|
||||||
|
</TopMenu>
|
||||||
|
)}
|
||||||
|
<SubMenu>
|
||||||
|
{item.submenu && userInfo &&
|
||||||
|
item.submenu.map((submenu, idx) => {
|
||||||
|
return (
|
||||||
|
<SubMenuItem key={idx} $isclickable={isClickable(submenu) ? 'true' : 'false'}>
|
||||||
|
<NavLink
|
||||||
|
to={isClickable(submenu) ? submenu.link : location.pathname}
|
||||||
|
onClick={e => {
|
||||||
|
isClickable(submenu) ? handleLink(e) : handleModalClose();
|
||||||
|
}}>
|
||||||
|
{submenu.title}
|
||||||
|
</NavLink>
|
||||||
|
</SubMenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</SubMenu>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{/* 접근 불가 모달 */}
|
||||||
|
<Modal min="440px" $padding="40px" $bgcolor="transparent" $view={modalClose}>
|
||||||
|
<BtnWrapper $justify="flex-end">
|
||||||
|
<ButtonClose onClick={handleModalClose} />
|
||||||
|
</BtnWrapper>
|
||||||
|
<ModalText $align="center">
|
||||||
|
해당 메뉴에 대한 조회 권한이 없습니다.
|
||||||
|
<br />
|
||||||
|
권한 등급을 변경 후 다시 이용해주세요.
|
||||||
|
</ModalText>
|
||||||
|
<BtnWrapper $gap="10px">
|
||||||
|
<Button text="확인" theme="primary" type="submit" size="large" width="100%" handleClick={handleModalClose} />
|
||||||
|
</BtnWrapper>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* 로그아웃 안내 모달 */}
|
||||||
|
<Modal min="440px" $padding="40px" $bgcolor="transparent" $view={logoutModalClose}>
|
||||||
|
<BtnWrapper $justify="flex-end">
|
||||||
|
<ButtonClose onClick={handleConfirmClose} />
|
||||||
|
</BtnWrapper>
|
||||||
|
<ModalText $align="center">로그아웃 되었습니다.</ModalText>
|
||||||
|
<BtnWrapper $gap="10px">
|
||||||
|
<Button text="확인" theme="primary" type="submit" size="large" width="100%" handleClick={handleConfirmClose} />
|
||||||
|
</BtnWrapper>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Navi;
|
||||||
|
|
||||||
|
const TopMenu = styled(NavLink)`
|
||||||
|
padding: 16px 30px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #888;
|
||||||
|
position: relative;
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
position: absolute;
|
||||||
|
right: 30px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(0, -50%);
|
||||||
|
background: url('${arrowIcon}') -12px 0 no-repeat;
|
||||||
|
}
|
||||||
|
&:hover,
|
||||||
|
&.active {
|
||||||
|
background: #444;
|
||||||
|
}
|
||||||
|
&.active ~ ul {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
&.active:before {
|
||||||
|
background: url('${arrowIcon}') 0 0 no-repeat;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SubMenu = styled.ul`
|
||||||
|
display: none;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SubMenuItem = styled.li`
|
||||||
|
background: #eee;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
color: #2c2c2c;
|
||||||
|
a {
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px 30px;
|
||||||
|
color: ${props => (props.$isclickable === 'false' ? '#818181' : '#2c2c2c')};
|
||||||
|
text-align: left;
|
||||||
|
&:hover,
|
||||||
|
&.active {
|
||||||
|
color: ${props => (props.$isclickable === 'false' ? '#818181' : '#2c2c2c')};
|
||||||
|
font-weight: ${props => (props.$isclickable === 'false' ? 400 : 600)};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const BackGround = styled.div`
|
||||||
|
background: #eee2;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 100;
|
||||||
|
`;
|
||||||
@@ -26,7 +26,7 @@ const SearchBarLayout = ({ firstColumnData, secondColumnData, filter, direction,
|
|||||||
</SearchRow>
|
</SearchRow>
|
||||||
)}
|
)}
|
||||||
{isSearch &&
|
{isSearch &&
|
||||||
<SearchRow>
|
<SearchRow direction={direction}>
|
||||||
<BtnWrapper $gap="8px">
|
<BtnWrapper $gap="8px">
|
||||||
<Button theme="search" text="검색" handleClick={handleSubmit} type="button" />
|
<Button theme="search" text="검색" handleClick={handleSubmit} type="button" />
|
||||||
<Button theme="reset" handleClick={onReset} type="button" />
|
<Button theme="reset" handleClick={onReset} type="button" />
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
const DotsButton = styled.button`
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
border: none;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: #e0e0e0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 점 스타일링 */
|
|
||||||
.dot {
|
|
||||||
width: 3px;
|
|
||||||
height: 3px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: #333;
|
|
||||||
margin: 2px 0;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const VerticalDotsButton = ({ text, type = 'button', errorMessage, handleClick, size, width, height, borderColor, disabled, name }) => {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DotsButton
|
|
||||||
onSubmit={e => e.preventDefault()}
|
|
||||||
type={type}
|
|
||||||
disabled={disabled}
|
|
||||||
onClick={handleClick}
|
|
||||||
size={size}
|
|
||||||
bordercolor={borderColor}
|
|
||||||
width={width}
|
|
||||||
height={height}
|
|
||||||
name={name}
|
|
||||||
>
|
|
||||||
<div className="dot"></div>
|
|
||||||
<div className="dot"></div>
|
|
||||||
<div className="dot"></div>
|
|
||||||
</DotsButton>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default VerticalDotsButton;
|
|
||||||
@@ -1,231 +0,0 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
const AIMessageInput = ({ onSendMessage }) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const [message, setMessage] = useState('');
|
|
||||||
const [isSending, setIsSending] = useState(false);
|
|
||||||
const textareaRef = useRef(null);
|
|
||||||
const modalRef = useRef(null);
|
|
||||||
|
|
||||||
// 텍스트 영역 높이 자동 조절
|
|
||||||
useEffect(() => {
|
|
||||||
if (textareaRef.current && isOpen) {
|
|
||||||
textareaRef.current.style.height = 'auto';
|
|
||||||
textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 200)}px`;
|
|
||||||
}
|
|
||||||
}, [message, isOpen]);
|
|
||||||
|
|
||||||
// 모달 외부 클릭시 닫기
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (event) => {
|
|
||||||
if (modalRef.current && !modalRef.current.contains(event.target)) {
|
|
||||||
closeModal();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isOpen) {
|
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
|
||||||
};
|
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
// 모달 열기
|
|
||||||
const openModal = () => {
|
|
||||||
setIsOpen(true);
|
|
||||||
// 모달이 열린 후 텍스트 영역에 포커스
|
|
||||||
setTimeout(() => {
|
|
||||||
if (textareaRef.current) {
|
|
||||||
textareaRef.current.focus();
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 모달 닫기
|
|
||||||
const closeModal = () => {
|
|
||||||
setIsOpen(false);
|
|
||||||
setMessage('');
|
|
||||||
};
|
|
||||||
|
|
||||||
// 메시지 전송 처리
|
|
||||||
const handleSendMessage = () => {
|
|
||||||
if (message.trim() && !isSending) {
|
|
||||||
setIsSending(true);
|
|
||||||
|
|
||||||
// 메시지 전송 처리
|
|
||||||
if (onSendMessage) {
|
|
||||||
onSendMessage(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 입력 초기화 및 상태 업데이트
|
|
||||||
setMessage('');
|
|
||||||
setIsSending(false);
|
|
||||||
|
|
||||||
// 모달 닫기
|
|
||||||
closeModal();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 엔터 키 처리 (Shift+Enter 줄바꿈, Enter 전송)
|
|
||||||
const handleKeyDown = (e) => {
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
handleSendMessage();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* 메뉴 버튼 */}
|
|
||||||
<MenuButton onClick={openModal}>
|
|
||||||
<div className="dot"></div>
|
|
||||||
<div className="dot"></div>
|
|
||||||
<div className="dot"></div>
|
|
||||||
</MenuButton>
|
|
||||||
|
|
||||||
{/* 모달 오버레이 */}
|
|
||||||
<ModalOverlay isOpen={isOpen}>
|
|
||||||
<InputContainer ref={modalRef} isOpen={isOpen}>
|
|
||||||
<MessageInput
|
|
||||||
ref={textareaRef}
|
|
||||||
value={message}
|
|
||||||
onChange={(e) => setMessage(e.target.value)}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
placeholder="메시지를 입력하세요..."
|
|
||||||
rows={1}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SendButton
|
|
||||||
onClick={handleSendMessage}
|
|
||||||
disabled={!message.trim() || isSending}
|
|
||||||
>
|
|
||||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" />
|
|
||||||
</svg>
|
|
||||||
</SendButton>
|
|
||||||
</InputContainer>
|
|
||||||
</ModalOverlay>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AIMessageInput;
|
|
||||||
|
|
||||||
const ModalOverlay = styled.div`
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
display: ${props => props.isOpen ? 'flex' : 'none'};
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
z-index: 1000;
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 메인 컨테이너
|
|
||||||
const InputContainer = styled.div`
|
|
||||||
width: 90%;
|
|
||||||
max-width: 600px;
|
|
||||||
border: 1px solid #e0e0e0;
|
|
||||||
border-radius: 12px;
|
|
||||||
background-color: #ffffff;
|
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
animation: ${props => props.isOpen ? 'slideUp 0.3s ease-out' : 'none'};
|
|
||||||
|
|
||||||
@keyframes slideUp {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(20px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 메시지 입력 영역
|
|
||||||
const MessageInput = styled.textarea`
|
|
||||||
width: 100%;
|
|
||||||
min-height: 60px;
|
|
||||||
max-height: 200px;
|
|
||||||
padding: 16px 60px 16px 16px;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
resize: none;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 1.5;
|
|
||||||
background: transparent;
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: #9e9ea7;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 전송 버튼
|
|
||||||
const SendButton = styled.button`
|
|
||||||
position: absolute;
|
|
||||||
bottom: 12px;
|
|
||||||
right: 12px;
|
|
||||||
width: 38px;
|
|
||||||
height: 38px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: #5436DA;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: #4527D0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
background-color: #DADCE0;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
fill: white;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 메뉴 버튼 (세로 점 세개)
|
|
||||||
const MenuButton = styled.button`
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
border: none;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: #e0e0e0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot {
|
|
||||||
width: 4px;
|
|
||||||
height: 4px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: #666;
|
|
||||||
margin: 2px 0;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
@@ -1,57 +1,124 @@
|
|||||||
import { styled } from 'styled-components';
|
import { TextInput, SelectInput, InputGroup } from '../../styles/Components';
|
||||||
import Button from '../common/button/Button';
|
import { SearchBarLayout } from '../common/SearchBar';
|
||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import {
|
||||||
|
userSearchType2,
|
||||||
|
} from '../../assets/data/options';
|
||||||
|
import { useAlert } from '../../context/AlertProvider';
|
||||||
|
import { alertTypes } from '../../assets/data/types';
|
||||||
|
import { UserView } from '../../apis';
|
||||||
|
|
||||||
import { FormWrapper, InputLabel, TextInput, SelectInput, BtnWrapper } from '../../styles/Components';
|
export const useUserSearch = (token) => {
|
||||||
|
const {showToast} = useAlert();
|
||||||
|
|
||||||
const UserSearchBar = () => {
|
const [searchParams, setSearchParams] = useState({
|
||||||
return (
|
search_type: 'GUID',
|
||||||
<>
|
search_data: ''
|
||||||
<FormWrapper>
|
});
|
||||||
<SearchbarStyle>
|
|
||||||
<SearchItem>
|
const [loading, setLoading] = useState(false);
|
||||||
<InputLabel>유저 조회</InputLabel>
|
const [data, setData] = useState(null);
|
||||||
<TextInput type="text" width="300px" placeholder="조회 대상 유저의 GUID를 입력하세요." />
|
|
||||||
</SearchItem>
|
const fetchData = useCallback(async (params) => {
|
||||||
<BtnWrapper $gap="8px">
|
if (!token) return;
|
||||||
<Button theme="reset" />
|
|
||||||
<Button
|
try {
|
||||||
theme="primary"
|
setLoading(true);
|
||||||
text="검색"
|
const result = await UserView(
|
||||||
handleClick={e => {
|
token,
|
||||||
e.preventDefault();
|
params.search_type,
|
||||||
}}
|
params.search_data
|
||||||
/>
|
);
|
||||||
</BtnWrapper>
|
if(result.result === "NOT_USER"){
|
||||||
</SearchbarStyle>
|
showToast(result.result, {type: alertTypes.warning});
|
||||||
</FormWrapper>
|
return;
|
||||||
</>
|
}
|
||||||
);
|
setData(result.data.result);
|
||||||
|
return result.data.result;
|
||||||
|
} catch (error) {
|
||||||
|
showToast('error', {type: alertTypes.error});
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
|
const updateSearchParams = useCallback((newParams) => {
|
||||||
|
setSearchParams(prev => ({
|
||||||
|
...prev,
|
||||||
|
...newParams
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSearch = useCallback(async (newParams = {}, executeSearch = true) => {
|
||||||
|
const updatedParams = {
|
||||||
|
...searchParams,
|
||||||
|
...newParams
|
||||||
|
};
|
||||||
|
updateSearchParams(updatedParams);
|
||||||
|
|
||||||
|
if (executeSearch) {
|
||||||
|
return await fetchData(updatedParams);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [searchParams, fetchData]);
|
||||||
|
|
||||||
|
const handleReset = useCallback(async () => {
|
||||||
|
const now = new Date();
|
||||||
|
now.setDate(now.getDate() - 1);
|
||||||
|
const resetParams = {
|
||||||
|
search_type: 'GUID',
|
||||||
|
search_data: ''
|
||||||
|
};
|
||||||
|
setSearchParams(resetParams);
|
||||||
|
return await fetchData(resetParams);
|
||||||
|
}, [fetchData]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
searchParams,
|
||||||
|
loading,
|
||||||
|
data,
|
||||||
|
handleSearch,
|
||||||
|
handleReset,
|
||||||
|
updateSearchParams
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default UserSearchBar;
|
const UserSearchBar = ({ searchParams, onSearch, onReset }) => {
|
||||||
|
const handleSubmit = event => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
const SearchbarStyle = styled.div`
|
onSearch(searchParams, true);
|
||||||
width: 100%;
|
};
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 20px 0;
|
|
||||||
font-size: 14px;
|
|
||||||
padding: 20px;
|
|
||||||
border-top: 1px solid #000;
|
|
||||||
border-bottom: 1px solid #000;
|
|
||||||
margin-bottom: 40px;
|
|
||||||
`;
|
|
||||||
const SearchItem = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 20px;
|
|
||||||
margin-right: 50px;
|
|
||||||
|
|
||||||
${TextInput}, ${SelectInput} {
|
const searchList = [
|
||||||
height: 35px;
|
<>
|
||||||
}
|
<InputGroup>
|
||||||
${TextInput} {
|
<SelectInput value={searchParams.search_type} onChange={e => onSearch({search_type: e.target.value }, false)}>
|
||||||
padding: 0 10px;
|
{userSearchType2.map((data, index) => (
|
||||||
max-width: 400px;
|
<option key={index} value={data.value}>
|
||||||
}
|
{data.name}
|
||||||
`;
|
</option>
|
||||||
|
))}
|
||||||
|
</SelectInput>
|
||||||
|
<TextInput
|
||||||
|
type="text"
|
||||||
|
placeholder={searchParams.search_type === 'GUID' ? 'GUID 입력' : searchParams.search_type === 'ACCOUNT' ? 'ACCOUNT 입력' : '닉네임 입력'}
|
||||||
|
value={searchParams.search_data}
|
||||||
|
width="260px"
|
||||||
|
onChange={e => onSearch({ search_data: e.target.value }, false)}
|
||||||
|
onKeyDown={e => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
onSearch({ search_data: e.target.value }, true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
</>
|
||||||
|
];
|
||||||
|
|
||||||
|
return <SearchBarLayout firstColumnData={searchList} onReset={onReset} handleSubmit={handleSubmit} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserSearchBar;
|
||||||
@@ -1,130 +1,149 @@
|
|||||||
import { useState, Fragment } from 'react';
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
import { TabScroll, Title } from '../../styles/Components';
|
import { TabItem, TabScroll, TabWrapper, Title } from '../../styles/Components';
|
||||||
import { AnimatedPageWrapper } from '../../components/common/Layout'
|
import { AnimatedPageWrapper } from '../../components/common/Layout'
|
||||||
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import UserViewSearchBar from '../../components/searchBar/UserViewSearchBar';
|
import {
|
||||||
import UserDefaultInfo from '../../components/DataManage/UserDefaultInfo';
|
UserDefaultInfo,
|
||||||
import UserAvatarInfo from '../../components/DataManage/UserAvatarInfo';
|
UserAvatarInfo,
|
||||||
import UserDressInfo from '../../components/DataManage/UserDressInfo';
|
UserDressInfo,
|
||||||
import UserToolInfo from '../../components/DataManage/UserToolInfo';
|
UserToolInfo,
|
||||||
import UserInventoryInfo from '../../components/DataManage/UserInventoryInfo';
|
UserInventoryInfo,
|
||||||
import UserMailInfo from '../../components/DataManage/UserMailInfo';
|
UserMailInfo,
|
||||||
import UserMyHomeInfo from '../../components/DataManage/UserMyHomeInfo';
|
UserMyHomeInfo,
|
||||||
import UserFriendInfo from '../../components/DataManage/UserFriendInfo';
|
UserFriendInfo,
|
||||||
import UserTatttooInfo from '../../components/DataManage/UserTattooInfo';
|
UserTattooInfo,
|
||||||
import UserQuestInfo from '../../components/DataManage/UserQuestInfo';
|
UserQuestInfo,
|
||||||
import UserClaimInfo from '../../components/DataManage/UserClaimInfo';
|
UserClaimInfo
|
||||||
|
} from '../../components/DataManage';
|
||||||
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { authList } from '../../store/authList';
|
import { authList } from '../../store/authList';
|
||||||
import { useRecoilValue } from 'recoil';
|
|
||||||
import AuthModal from '../../components/common/modal/AuthModal';
|
|
||||||
import { authType, TabUserList } from '../../assets/data';
|
import { authType, TabUserList } from '../../assets/data';
|
||||||
|
import { UserSearchBar, useUserSearch } from '../../components/searchBar';
|
||||||
|
import { withAuth } from '../../hooks/hook';
|
||||||
|
import { AnimatedTabs } from '../../components/common';
|
||||||
|
|
||||||
const UserView = () => {
|
const UserView = () => {
|
||||||
const userInfo = useRecoilValue(authList);
|
const token = sessionStorage.getItem('token');
|
||||||
|
|
||||||
const [infoView, setInfoView] = useState('none');
|
const [infoView, setInfoView] = useState('none');
|
||||||
const [activeContent, setActiveContent] = useState('기본정보');
|
const [activeTab, setActiveTab] = useState('BASIC');
|
||||||
const [resultData, setResultData] = useState([]);
|
const [resultData, setResultData] = useState();
|
||||||
|
|
||||||
const handleTab = title => {
|
const {
|
||||||
setActiveContent(title);
|
searchParams,
|
||||||
|
loading: dataLoading,
|
||||||
|
data,
|
||||||
|
handleSearch,
|
||||||
|
handleReset,
|
||||||
|
updateSearchParams
|
||||||
|
} = useUserSearch(token);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(data) {
|
||||||
|
setResultData(data);
|
||||||
|
setInfoView('flex');
|
||||||
|
} else {
|
||||||
|
setInfoView('none');
|
||||||
|
}
|
||||||
|
}, [data])
|
||||||
|
|
||||||
|
const tabItems = useMemo(() => {
|
||||||
|
return TabUserList.map(el => ({
|
||||||
|
key: el.value,
|
||||||
|
label: el.name,
|
||||||
|
children: (() => {
|
||||||
|
switch(el.value) {
|
||||||
|
case 'BASIC': return <UserDefaultInfo userInfo={resultData} />;
|
||||||
|
case 'AVATAR': return <UserAvatarInfo userInfo={resultData} />;
|
||||||
|
case 'CLOTH': return <UserDressInfo userInfo={resultData} />;
|
||||||
|
case 'TOOL': return <UserToolInfo userInfo={resultData} />;
|
||||||
|
case 'INVENTORY': return <UserInventoryInfo userInfo={resultData} />;
|
||||||
|
case 'MAIL': return <UserMailInfo userInfo={resultData} />;
|
||||||
|
case 'MYHOME': return <UserMyHomeInfo userInfo={resultData} />;
|
||||||
|
case 'FRIEND': return <UserFriendInfo userInfo={resultData} />;
|
||||||
|
case 'TATTOO': return <UserTattooInfo userInfo={resultData} />;
|
||||||
|
case 'QUEST': return <UserQuestInfo userInfo={resultData} />;
|
||||||
|
case 'CLAIM': return <UserClaimInfo userInfo={resultData} />;
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
}));
|
||||||
|
}, [resultData]);
|
||||||
|
|
||||||
|
const handleTabChange = (key) => {
|
||||||
|
setActiveTab(key);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// const handleTab = (e, content) => {
|
||||||
|
// e.preventDefault();
|
||||||
|
// setActiveTab(content);
|
||||||
|
// };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<AnimatedPageWrapper>
|
||||||
{userInfo.auth_list && !userInfo.auth_list.some(auth => auth.id === authType.userSearchRead) ? (
|
<Title>유저조회</Title>
|
||||||
<AuthModal />
|
<UserSearchBar
|
||||||
) : (
|
searchParams={searchParams}
|
||||||
<AnimatedPageWrapper>
|
onSearch={(newParams, executeSearch = true) => {
|
||||||
<Title>유저조회</Title>
|
if (executeSearch) {
|
||||||
<UserViewSearchBar setInfoView={setInfoView} resultData={resultData} handleTab={handleTab} setResultData={setResultData} />
|
handleSearch(newParams);
|
||||||
<UserWrapper display={infoView}>
|
} else {
|
||||||
<TabScroll>
|
updateSearchParams(newParams);
|
||||||
<UserTabWrapper>
|
setResultData();
|
||||||
{TabUserList.map((el, idx) => {
|
}
|
||||||
return (
|
}}
|
||||||
<UserTab key={idx} $state={el.title === activeContent ? 'active' : 'unactive'} onClick={() => handleTab(el.title)}>
|
onReset={handleReset}
|
||||||
{el.title}
|
/>
|
||||||
</UserTab>
|
<UserWrapper display={infoView}>
|
||||||
);
|
<AnimatedTabs
|
||||||
})}
|
items={tabItems}
|
||||||
</UserTabWrapper>
|
activeKey={activeTab}
|
||||||
</TabScroll>
|
onChange={handleTabChange}
|
||||||
<UserTabInfo>
|
tabPosition="center"
|
||||||
{activeContent === '기본정보' && <UserDefaultInfo userInfo={resultData && resultData} />}
|
/>
|
||||||
{activeContent === '아바타' && <UserAvatarInfo userInfo={resultData && resultData} />}
|
|
||||||
{activeContent === '의상' && <UserDressInfo userInfo={resultData && resultData} />}
|
{/*<TabScroll>*/}
|
||||||
{activeContent === '도구' && <UserToolInfo userInfo={resultData && resultData} />}
|
{/* <TabWrapper>*/}
|
||||||
{activeContent === '인벤토리' && <UserInventoryInfo userInfo={resultData && resultData} />}
|
{/* {TabUserList.map((el, idx) => {*/}
|
||||||
{activeContent === '우편' && <UserMailInfo userInfo={resultData && resultData} />}
|
{/* return (*/}
|
||||||
{activeContent === '마이홈' && <UserMyHomeInfo userInfo={resultData && resultData} />}
|
{/* <li key={idx}>*/}
|
||||||
{activeContent === '친구목록' && <UserFriendInfo userInfo={resultData && resultData} />}
|
{/* <TabItem $state={activeTab === el.value ? 'active' : 'none'} onClick={e => handleTab(e, el.value)}>*/}
|
||||||
{activeContent === '타투' && <UserTatttooInfo userInfo={resultData && resultData} />}
|
{/* {el.name}*/}
|
||||||
{activeContent === '퀘스트' && <UserQuestInfo userInfo={resultData && resultData} />}
|
{/* </TabItem>*/}
|
||||||
{activeContent === '클레임' && <UserClaimInfo userInfo={resultData && resultData} />}
|
{/* </li>*/}
|
||||||
</UserTabInfo>
|
{/* )*/}
|
||||||
</UserWrapper>
|
{/* })}*/}
|
||||||
</AnimatedPageWrapper>
|
{/* </TabWrapper>*/}
|
||||||
)}
|
{/*</TabScroll>*/}
|
||||||
</>
|
{/*<UserTabInfo>*/}
|
||||||
|
{/* {activeTab === 'BASIC' && <UserDefaultInfo userInfo={resultData && resultData} />}*/}
|
||||||
|
{/* {activeTab === 'AVATAR' && <UserAvatarInfo userInfo={resultData && resultData} />}*/}
|
||||||
|
{/* {activeTab === 'CLOTH' && <UserDressInfo userInfo={resultData && resultData} />}*/}
|
||||||
|
{/* {activeTab === 'TOOL' && <UserToolInfo userInfo={resultData && resultData} />}*/}
|
||||||
|
{/* {activeTab === 'INVENTORY' && <UserInventoryInfo userInfo={resultData && resultData} />}*/}
|
||||||
|
{/* {activeTab === 'MAIL' && <UserMailInfo userInfo={resultData && resultData} />}*/}
|
||||||
|
{/* {activeTab === 'MYHOME' && <UserMyHomeInfo userInfo={resultData && resultData} />}*/}
|
||||||
|
{/* {activeTab === 'FRIEND' && <UserFriendInfo userInfo={resultData && resultData} />}*/}
|
||||||
|
{/* {activeTab === 'TATTOO' && <UserTattooInfo userInfo={resultData && resultData} />}*/}
|
||||||
|
{/* {activeTab === 'QUEST' && <UserQuestInfo userInfo={resultData && resultData} />}*/}
|
||||||
|
{/* {activeTab === 'CLAIM' && <UserClaimInfo userInfo={resultData && resultData} />}*/}
|
||||||
|
{/*</UserTabInfo>*/}
|
||||||
|
</UserWrapper>
|
||||||
|
</AnimatedPageWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default UserView;
|
export default withAuth(authType.userSearchRead)(UserView);
|
||||||
|
|
||||||
const UserTab = styled.li`
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 120px;
|
|
||||||
height: 35px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-top-left-radius: 5px;
|
|
||||||
border-top-right-radius: 5px;
|
|
||||||
color: #888;
|
|
||||||
text-align: center;
|
|
||||||
cursor: pointer;
|
|
||||||
&:hover {
|
|
||||||
font-weight: 700;
|
|
||||||
color: #fff;
|
|
||||||
background: #888;
|
|
||||||
}
|
|
||||||
${props =>
|
|
||||||
props.$state === 'active' &&
|
|
||||||
`font-weight: 700;
|
|
||||||
color: #fff;
|
|
||||||
background: #888;`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const UserTabWrapper = styled.ul`
|
|
||||||
border-bottom: 1px solid #888888;
|
|
||||||
display: flex;
|
|
||||||
background: #fff;
|
|
||||||
`;
|
|
||||||
const UserWrapper = styled.div`
|
const UserWrapper = styled.div`
|
||||||
display: ${props => props.display};
|
display: ${props => props.display};
|
||||||
${props => props.display && `flex-flow:column;`}
|
${props => props.display && `flex-flow:column;`}
|
||||||
min-height: calc(100vh - 345px);
|
min-height: calc(100vh - 345px);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #f9f9f9;
|
padding-left: 40px;
|
||||||
`;
|
padding-right: 40px;
|
||||||
|
//background: #f9f9f9;
|
||||||
const UserTabInfo = styled.div`
|
`;
|
||||||
padding: 50px;
|
|
||||||
|
|
||||||
overflow: auto;
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 4px;
|
|
||||||
}
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background: #666666;
|
|
||||||
}
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
background: #d9d9d9;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
Reference in New Issue
Block a user