Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

강제 업데이트 네비게이션 생성 #61

Merged
merged 20 commits into from
Nov 10, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed .yarn/cache/fsevents-patch-21ad2b1333-8.zip
Binary file not shown.
4 changes: 4 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import BenefitPage from 'pages/Services/Benefit';
import NoticeList from 'pages/Services/Notice/NoticeList';
import NoticeDetail from 'pages/Services/Notice/NoticeDetail';
import NoticeWrite from 'pages/Services/Notice/NoticeWrite';
import ForceUpdate from 'pages/Update/ForceUpdate';
import UpdateList from 'pages/Update/UpdateList';

function RequireAuth() {
const location = useLocation();
Expand Down Expand Up @@ -70,6 +72,8 @@ function App() {
<Route path="/notice" element={<NoticeList />} />
<Route path="/notice/:id" element={<NoticeDetail />} />
<Route path="/notice/write" element={<NoticeWrite />} />
<Route path="/force-update" element={<ForceUpdate />} />
<Route path="/update-list" element={<UpdateList />} />
<Route path="*" element={<h1>404</h1>} />
</Route>
</Routes>
Expand Down
7 changes: 6 additions & 1 deletion src/components/common/SideNav/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
HomeOutlined, UserSwitchOutlined,
UsergroupDeleteOutlined, FolderOpenOutlined, ControlOutlined,
UserAddOutlined, BoldOutlined, ApartmentOutlined, SnippetsOutlined, GiftOutlined,
NotificationOutlined,
NotificationOutlined, IssuesCloseOutlined, FormOutlined, UnorderedListOutlined,
} from '@ant-design/icons';
import { Menu, MenuProps } from 'antd';
import { Link, useLocation, useNavigate } from 'react-router-dom';
Expand Down Expand Up @@ -49,6 +49,11 @@ const items: MenuProps['items'] = [
getItem('테스트', 'test', <ControlOutlined />, [
getItem('AB 테스트', '/abtest', <ApartmentOutlined />),
]),

getItem('강제업데이트', 'force-update', <IssuesCloseOutlined />, [
getItem('업데이트 관리', '/force-update', <FormOutlined />),
getItem('목록 관리', '/update-list', <UnorderedListOutlined />),
]),
];

const SideNavConatiner = styled.nav`
Expand Down
1 change: 1 addition & 0 deletions src/constant/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ export const TITLE_MAPPER: Record<string, string> = {
title: '제목',
author: '작성자',
post_date: '게시일',
updatedAt: 'date',
};
11 changes: 11 additions & 0 deletions src/model/forceUpdate.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export type AppType = 'android' | 'ios';

export interface AppVersionResponse {
id: number,
type: AppType,
version: string,
title: string,
content: string,
created_at: string,
updated_at: string,
}
15 changes: 15 additions & 0 deletions src/model/updateList.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { AppType, AppVersionResponse } from 'model/forceUpdate.model';

export interface UpdateListRequest {
page: number,
type: AppType,
limit?: number,
}

export interface UpdateListResponse {
total_count: number,
current_count: number,
total_page: number,
current_page: number,
versions: AppVersionResponse[],
}
54 changes: 54 additions & 0 deletions src/pages/Update/ForceUpdate/ForceUpdate.style.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import styled from 'styled-components';

export * from 'styles/List.style';

export const PageContainer = styled.div`
`;

export const UpdateContainer = styled.div`
display: flex;
flex-direction: column;
gap: 24px;
`;

export const UpdateInfo = styled.div`
display: flex;
flex-direction: column;
gap: 36px;
padding: 36px;
`;

export const Title = styled.div`
font-size: 20px;
font-weight: 700;
`;

export const Content = styled.div`
font-size: 14px;
font-weight: 700;
display: flex;
`;

export const Theme = styled.div`
width: 75px;
flex-shrink: 0;
align-self: center;
`;

export const Detail = styled.div`
`;

export const Input = styled.input`
font-size: 14px;
font-weight: 700;
width: 70%;
height: 48px;
padding: 3px 6px;
`;

export const Button = styled.button`
width: 150px;
height: 50px;
align-self: end;
font-weight: 700;
`;
117 changes: 117 additions & 0 deletions src/pages/Update/ForceUpdate/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { message } from 'antd';
import { useState } from 'react';
import { AppType } from 'model/forceUpdate.model';
import { useGetAppVersionQuery, useUpdateAppVersionMutation } from 'store/api/forceUpdate';
import AppTypeDropdown from 'pages/Update/components/AppTypeDropdown';
import * as S from './ForceUpdate.style';

export default function ForceUpdate() {
const [appType, setAppType] = useState<AppType>('android');

const [appVersion, setAppVersion] = useState('');
const [title, setTitle] = useState('');
const [content, setContent] = useState('');

const versionRegex = /^\d+\.\d+\.\d+$/;

const { data: version } = useGetAppVersionQuery(appType);
const [updateVersion] = useUpdateAppVersionMutation();

const handleAppType = (type: AppType) => {
setAppType(type);
};

const checkForm = (inputArray: string[]) => {
const themes = ['version', 'title', 'content'];
if (!versionRegex.test(inputArray[0]) && inputArray[0] !== '') {
message.error('예시 형식과 맞게 version을 입력해주세요.');
return true;
}
const hasEmptyField = inputArray.some((text, index) => {
if (text === '') {
const theme = themes[index];
message.error(`${theme}는 필수 값입니다. ${theme}값을 입력해주세요.`);
return true;
}
return false;
});
return hasEmptyField;
};

const submit = () => {
const inputArray = [appVersion, title, content];
if (checkForm(inputArray)) return;
updateVersion({
type: appType,
version: appVersion,
title,
content,
})
.then(() => {
setAppVersion('');
setTitle('');
setContent('');
message.success('업데이트 완료');
})
.catch(({ data }) => {
message.error(data.message);
});
};

return (
<S.PageContainer>
<S.Heading>강제 업데이트 관리</S.Heading>
<S.UpdateContainer>
<AppTypeDropdown
appType={appType}
handleAppType={handleAppType}
/>
{version && (
<S.UpdateInfo>
<S.Title>현재 업데이트 상황</S.Title>
<S.Content>
<S.Theme>version :</S.Theme>
{version.version}
</S.Content>
<S.Content>
<S.Theme>title :</S.Theme>
{version.title}
</S.Content>
<S.Content>
<S.Theme>content :</S.Theme>
{version.content}
</S.Content>
</S.UpdateInfo>
)}
<S.UpdateInfo>
<S.Title>수정 문구는 아래에 입력해서 수정해주세요.</S.Title>
<S.Content>
<S.Theme>version :</S.Theme>
<S.Input
placeholder="ex) 3.4.0"
value={appVersion}
onChange={(e) => setAppVersion(e.target.value)}
/>
</S.Content>
Copy link
Member

Choose a reason for hiding this comment

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

이 페이지도 하나의 Form이고, 각각의 요소가 상태이므로 다른 페이지처럼 바꿔서 쓰는게 디자인적 통일 & 시간적 이득을 볼 수 있었을 것 같아요.

<CustomForm.Input /> 으로 구조화하는건 어려웠을까요?

Copy link
Contributor Author

@Gwak-Seungju Gwak-Seungju Nov 8, 2024

Choose a reason for hiding this comment

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

말씀해주신 부분을 반영해서 전체적으로 CustomForm을 사용하여 구조를 변경했습니다!
다만, 주요 변경 사항이 있습니다!

  1. content의 길이가 한 줄 이상으로 길어지는 경우 CustomForm.Input을 사용하면 antd의 Input 특성 때문에? 한 줄로만 보여줘서 모든 text를 보여주지 못 하더라고요. 따라서 CustomForm.TextArea를 사용하고자 했습니다.
    기존 CustomFormCustomTextArea에서는 글자 수를 보여주는 showCount와 행 수에 따른 size를 자동으로 조절해주는 autoSize속성이 없어 추가했습니다. style 속성도 변경하고 싶어 아래와 같이 추가했습니다..!
// src\components\common\CustomForm\index.tsx
interface CustomTextAreaProps {
  maxLength?: number;
  showCount?: boolean;
  **style?: React.CSSProperties;**
  autoSize?: AutoSizeProps;
}

function CusctomTextArea({
  label, name, maxLength, disabled, rules, showCount, autoSize, style,
}: CustomFormItemProps & CustomTextAreaProps) {
  return (
    <S.FormItem label={label} name={name} rules={rules}>
      <Input.TextArea
        showCount={showCount}
        maxLength={maxLength}
        disabled={disabled}
        autoSize={autoSize}
        **style={style}**
      />
    </S.FormItem>
  );
}
  1. UI를 변경하고 싶어서 아래처럼 style 변수를 선언하고 컴포넌트에 추가하는 식으로 했는데 공통 컴포넌트에 style을 줄 수 있는 방법이 이것밖에 생각나지 않아 이런 방식을 사용했습니다. 더 좋은 방법이 있는지 궁금합니다..!
const titleStyle = {
  fontSize: '20px',
  fontWeight: '700',
};

<Divider
  orientation="left"
  orientationMargin="0"
  style={titleStyle}
>

Copy link
Member

Choose a reason for hiding this comment

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

  1. 좋네요!
  2. 선택자 접근으로 하위 요소에 대한 css로 처리도 가능할거같긴한데, 위 방법도 괜찮아보여욤

<S.Content>
<S.Theme>title :</S.Theme>
<S.Input
placeholder="ex) 변경할 코인업데이트 화면 제목 문구를 작성해주세요."
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
</S.Content>
<S.Content>
<S.Theme>content :</S.Theme>
<S.Input
placeholder="ex) 변경할 코인업데이트 화면 콘텐츠 문구를 작성해주세요."
value={content}
onChange={(e) => setContent(e.target.value)}
/>
</S.Content>
</S.UpdateInfo>
<S.Button onClick={submit}>수정 완료</S.Button>
</S.UpdateContainer>
</S.PageContainer>
);
}
8 changes: 8 additions & 0 deletions src/pages/Update/UpdateList/UpdateList.style.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import styled from 'styled-components';

export * from 'styles/List.style';

export const PageContainer = styled.div`
display: flex;
flex-direction: column;
`;
43 changes: 43 additions & 0 deletions src/pages/Update/UpdateList/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import AppTypeDropdown from 'pages/Update/components/AppTypeDropdown';
import { useState } from 'react';
import { AppType } from 'model/forceUpdate.model';
import CustomTable from 'components/common/CustomTable';
import { useGetUpdateListQuery } from 'store/api/updateList';
import * as S from './UpdateList.style';

export default function UpdateList() {
const [appType, setAppType] = useState<AppType>('android');

const [page, setPage] = useState<number>(1);

const { data: updateList } = useGetUpdateListQuery({ page, type: appType });

const handleAppType = (type: AppType) => {
setAppType(type);
};

return (
<S.PageContainer>
<S.Heading>강제 업데이트 목록</S.Heading>
<AppTypeDropdown
appType={appType}
handleAppType={handleAppType}
/>
{updateList
&& (
<CustomTable
data={updateList.versions}
pagination={{
current: page,
onChange: setPage,
total: updateList.total_page,
}}
columnSize={[10, 10, 30, 30, 10]}
hiddenColumns={['id', 'createdAt']}
onClick={() => {}}
/>
)}

</S.PageContainer>
);
}
42 changes: 42 additions & 0 deletions src/pages/Update/components/AppTypeDropdown.style.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import styled from 'styled-components';

export const TypeContainer = styled.div`
position: relative;
width: 150px;
height: 50px;
align-self: end;
`;

export const Type = styled.button`
position: relative;
width: 100%;
height: 100%;
background-color: #fff;
font-weight: 700;
`;

export const Icon = styled.span`
position: absolute;
right: 10px;
`;

export const MenuList = styled.ul`
position: absolute;
top: 50px;
left: 0;
padding: 0;
margin: 0;
font-weight: 700;
list-style-type: none;
z-index: 5;
background-color: #fff;
`;

export const Menu = styled.li`
display: flex;
align-items: center;
justify-content: center;
width: 150px;
height: 50px;
border: 1px solid #000;
`;
47 changes: 47 additions & 0 deletions src/pages/Update/components/AppTypeDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useState } from 'react';
import { AppType } from 'model/forceUpdate.model';
import { DownOutlined, UpOutlined } from '@ant-design/icons';
import * as S from './AppTypeDropdown.style';

interface AppTypeDropdownProps {
appType: AppType,
handleAppType: (type: AppType) => void,
}

export default function AppTypeDropdown({ appType, handleAppType }: AppTypeDropdownProps) {
const [isOpen, setIsOpen] = useState(false);

const typeList: AppType[] = ['android', 'ios'];

const toggle = () => {
setIsOpen((prev) => !prev);
};

const selectAppType = (type: AppType) => {
handleAppType(type);
toggle();
};

return (
<S.TypeContainer>
<S.Type onClick={toggle}>
{appType}
<S.Icon>{isOpen ? <UpOutlined /> : <DownOutlined />}</S.Icon>
</S.Type>
{isOpen && (
<S.MenuList>
{typeList.map((type) => (
appType !== type && (
<S.Menu
onClick={() => selectAppType(type)}
key={type}
>
{type}
</S.Menu>
)
))}
</S.MenuList>
)}
</S.TypeContainer>
);
}
Loading
Loading