Compare commits

...

10 Commits

21 changed files with 2336 additions and 1162 deletions

304
README.md
View File

@@ -1,100 +1,31 @@
# CoreUI Free React Admin Template [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social&logo=twitter)](https://twitter.com/intent/tweet?text=CoreUI%20-%20Free%React%204%20Admin%20Template%20&url=https://coreui.io&hashtags=bootstrap,admin,template,dashboard,panel,free,angular,react,vue) # sample.admin.react
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT) 신규 프로젝트 초기 구성 참고용 Sample Admin React
[![@coreui coreui](https://img.shields.io/badge/@coreui%20-coreui-lightgrey.svg?style=flat-square)](https://github.com/coreui/coreui)
[![npm package][npm-coreui-badge]][npm-coreui]
[![NPM downloads][npm-coreui-download]][npm-coreui]
[![@coreui react](https://img.shields.io/badge/@coreui%20-react-lightgrey.svg?style=flat-square)](https://github.com/coreui/react)
[![npm package][npm-coreui-react-badge]][npm-coreui-react]
[![NPM downloads][npm-coreui-react-download]][npm-coreui-react]
[npm-coreui]: https://www.npmjs.com/package/@coreui/coreui ## Tech Stack
[npm-coreui-badge]: https://img.shields.io/npm/v/@coreui/coreui.png?style=flat-square - Typescript
[npm-coreui-download]: https://img.shields.io/npm/dm/@coreui/coreui.svg?style=flat-square - CoreUI Free React Admin Template
[npm-coreui-react]: https://www.npmjs.com/package/@coreui/react
[npm-coreui-react-badge]: https://img.shields.io/npm/v/@coreui/react.png?style=flat-square
[npm-coreui-react-download]: https://img.shields.io/npm/dm/@coreui/react.svg?style=flat-square
[npm]: https://www.npmjs.com/package/@coreui/react
[![Bootstrap Admin Template](https://assets.coreui.io/products/coreui-free-bootstrap-admin-template-light-dark.webp)](https://coreui.io/product/free-react-admin-template/)
CoreUI is meant to be the UX game changer. Pure & transparent code is devoid of redundant components, so the app is light enough to offer ultimate user experience. This means mobile devices also, where the navigation is just as easy and intuitive as on a desktop or laptop. The CoreUI Layout API lets you customize your project for almost any device be it Mobile, Web or WebApp CoreUI covers them all! ## Build/Run
## Table of Contents
* [Versions](#versions)
* [CoreUI PRO](#coreui-pro)
* [CoreUI PRO React Admin Templates](#coreui-pro-react-admin-templates)
* [Quick Start](#quick-start)
* [Installation](#installation)
* [Basic usage](#basic-usage)
* [What's included](#whats-included)
* [Documentation](#documentation)
* [Components](#components)
* [Versioning](#versioning)
* [Creators](#creators)
* [Community](#community)
* [Support CoreUI Development](#support-coreui-development)
* [Copyright and License](#copyright-and-license)
## Versions
* [CoreUI Free Bootstrap Admin Template](https://github.com/coreui/coreui-free-bootstrap-admin-template)
* [CoreUI Free Angular Admin Template](https://github.com/coreui/coreui-free-angular-admin-template)
* [CoreUI Free React.js Admin Template (Vite)](https://github.com/coreui/coreui-free-react-admin-template)
* [CoreUI Free React.js Admin Template (Create React App)](https://github.com/coreui/coreui-free-react-admin-template-cra)
* [CoreUI Free Vue.js Admin Template](https://github.com/coreui/coreui-free-vue-admin-template)
## CoreUI PRO
* 💪 [CoreUI PRO Angular Admin Template](https://coreui.io/product/angular-dashboard-template/)
* 💪 [CoreUI PRO Bootstrap Admin Template](https://coreui.io/product/bootstrap-dashboard-template/)
* 💪 [CoreUI PRO Next.js Admin Template](https://coreui.io/product/next-js-dashboard-template/)
* 💪 [CoreUI PRO React Admin Template](https://coreui.io/product/react-dashboard-template/)
* 💪 [CoreUI PRO Vue Admin Template](https://coreui.io/product/vue-dashboard-template/)
## CoreUI PRO React Admin Templates
| Default Theme | Light Theme |
| --- | --- |
| [![CoreUI PRO React Admin Template](https://coreui.io/images/templates/coreui_pro_default_light_dark.webp)](https://coreui.io/product/react-dashboard-template/?theme=default) | [![CoreUI PRO React Admin Template](https://coreui.io/images/templates/coreui_pro_light_light_dark.webp)](https://coreui.io/product/react-dashboard-template/?theme=light)|
| Modern Theme | Bright Theme |
| --- | --- |
| [![CoreUI PRO React Admin Template](https://coreui.io/images/templates/coreui_pro_default_v3_light_dark.webp)](https://coreui.io/product/react-dashboard-template/?theme=modern) | [![CoreUI PRO React Admin Template](https://coreui.io/images/templates/coreui_pro_light_v3_light_dark.webp)](https://coreui.io/product/react-dashboard-template/?theme=bright)|
## Quick Start
- [Download the latest release](https://github.com/coreui/coreui-free-react-admin-template/archive/refs/heads/main.zip)
- Clone the repo: `git clone https://github.com/coreui/coreui-free-react-admin-template.git`
### Installation ### Installation
``` bash ```bash
$ npm install $ npm install
``` ```
or
``` bash
$ yarn install
```
### Basic usage ### Basic usage
``` bash ```bash
# dev server with hot reload at http://localhost:3000 # dev server with hot reload at http://localhost:3000
$ npm start $ npm start
``` ```
or vite.config.ts 에서 포트 변경 가능함
``` bash vscode 계열 IDE 에서 F5 로 debug 실행 가능
# dev server with hot reload at http://localhost:3000
$ yarn start
```
Navigate to [http://localhost:3000](http://localhost:3000). The app will automatically reload if you change any of the source files.
#### Build #### Build
@@ -105,139 +36,100 @@ Run `build` to build the project. The build artifacts will be stored in the `bui
$ npm run build $ npm run build
``` ```
or build 결과물 테스트
```bash
# build for production with minification
$ yarn build
``` ```
npx serve -s build
```
--listen 5000 옵션등으로 포트 지정하지 않으면 3000 포트 사용
## What's included
Within the download you'll find the following directories and files, logically grouping common assets and providing both compiled and minified variations. You'll see something like this: ### local test url
- 단독 접속 테스트
- http://localhost:3000/
- vmware nginx 통해서 frontend + backend 통합 접속
- http://192.168.70.101/
- vmware nginx 설정 필요
## 프로젝트 구조
### 상위구조
``` ```
coreui-free-react-admin-template sample.admin.react
├── public/ # static files ├─ .vscode : VS Code 설정 경로
├── favicon.ico └─ launch.json : 디버그 실행 설정
│ └── manifest.json ├─ build : 빌드 결과물 경로 (npm run build 실행 시 생성)
├─ node_modules : npm 패키지 설치 경로
├── src/ # project root ├─ public : 정적 파일 경로 (빌드 시 그대로 복사)
│ ├── assets/ # images, icons, etc. │ ├─ favicon.ico : 파비콘 이미지
├── components/ # common components - header, footer, sidebar, etc. └─ manifest.json : PWA 매니페스트 설정
├─ layouts/ # layout containers ├─ src : React 소스 경로
├─ scss/ # scss styles ├─ .browserslistrc : 지원 브라우저 목록 설정
├─ views/ # application views ├─ .editorconfig : 에디터 공통 설정 (들여쓰기, 인코딩 등)
├─ _nav.js # sidebar navigation config ├─ .gitattributes : Git 파일 속성 설정
│ ├── App.js ├─ .gitignore : Git 제외 파일 목록
├─ index.js ├─ .prettierignore : Prettier 제외 파일 목록
├─ routes.js # routes config ├─ .prettierrc.js : Prettier 코드 포맷팅 설정
│ └── store.js # template state example ├─ eslint.config.mjs : ESLint 코드 검사 설정
├─ index.html : HTML 템플릿 (Vite 진입점)
├── index.html # html template ├─ LICENSE : 라이선스 파일
├── ... ├─ package.json : 프로젝트 설정 및 의존성 정의
├── package.json ─ package-lock.json : 의존성 버전 잠금 파일
├── ... ├─ README.md : 프로젝트 설명 문서
└── vite.config.mjs # vite config ├─ tsconfig.json : TypeScript 컴파일러 설정
└─ vite.config.mjs : Vite 빌드 도구 설정
``` ```
## Documentation ### React 소스 구조
The documentation for the CoreUI Admin Template is hosted at our website [CoreUI for React](https://coreui.io/react/docs/templates/installation/) ```
src
├─ assets : 이미지, 브랜드 로고 등 정적 자원 경로
├─ axios : API 통신 관련 설정 경로
│ ├─ authService.ts : 인증 관련 API (로그인, 로그아웃, 토큰 관리)
│ └─ axios.ts : Axios 인스턴스 및 인터셉터 설정
├─ components : 재사용 가능한 공통 컴포넌트 경로
│ ├─ header
│ │ └─ AppHeaderDropdown.tsx : 헤더 우측 사용자 드롭다운 메뉴
│ ├─ AppBreadcrumb.tsx : 브레드크럼 네비게이션 컴포넌트
│ ├─ AppContent.tsx : 메인 콘텐츠 영역 (라우트 렌더링)
│ ├─ AppFooter.tsx : 하단 푸터 컴포넌트
│ ├─ AppHeader.tsx : 상단 헤더 컴포넌트
│ ├─ AppSidebar.tsx : 좌측 사이드바 컴포넌트
│ ├─ AppSidebarNav.tsx : 사이드바 네비게이션 메뉴 컴포넌트
│ └─ index.ts : 컴포넌트 export 모음
├─ context : React Context 경로
│ └─ AuthContext.tsx : 인증 상태 관리 Context (로그인 상태, 사용자 정보)
├─ hooks : 커스텀 훅 경로
│ └─ useAuth.ts : 인증 Context 사용을 위한 커스텀 훅
├─ layout : 레이아웃 컴포넌트 경로
│ └─ DefaultLayout.tsx : 기본 레이아웃 (사이드바 + 헤더 + 콘텐츠 + 푸터)
├─ routes : 라우팅 관련 경로
│ ├─ ProtectedRoute.tsx : 인증 필요 라우트 보호 컴포넌트
│ └─ routes.ts : 라우트 정의 (경로, 컴포넌트 매핑)
├─ scss : 스타일시트 경로
│ └─ style.scss : 전역 스타일 정의
├─ services : 도메인별 API 서비스 경로
│ ├─ adminMemberService.ts : 어드민 회원 관리 API
│ └─ adminMenuService.ts : 어드민 메뉴 관리 API
├─ views : 화면(페이지) 컴포넌트 경로
│ ├─ admin : 어드민 관리 화면 경로
│ │ ├─ AdminMemberManagement.tsx : 어드민 회원 관리 화면
│ │ └─ AdminMenuManagement.tsx : 어드민 메뉴 관리 화면
│ ├─ dashboard : 대시보드 화면 경로
│ │ └─ Dashboard.tsx : 대시보드 메인 화면
│ └─ pages : 인증 외 페이지 경로
│ ├─ login
│ │ └─ Login.tsx : 로그인 화면
│ ├─ register
│ │ └─ Register.tsx : 어드민 회원가입 신청 화면
│ ├─ page404
│ │ └─ Page404.tsx : 404 에러 화면
│ └─ page500
│ └─ Page500.tsx : 500 에러 화면
├─ _nav.tsx : 사이드바 네비게이션 메뉴 정의 (정적 메뉴)
├─ App.tsx : 애플리케이션 루트 컴포넌트 (라우터 설정)
├─ index.tsx : 애플리케이션 진입점 (ReactDOM 렌더링)
└─ store.tsx : Redux 스토어 설정 (사이드바 상태 관리)
```
## Components
CoreUI React.js Admin Templates are built on top of CoreUI and CoreUI PRO UI components libraries, including all of these components.
- [React Accordion](https://coreui.io/react/docs/components/accordion/)
- [React Alert](https://coreui.io/react/docs/components/alert/)
- [React Autocomplete](https://coreui.io/react/docs/forms/autocomplete/) **PRO**
- [React Avatar](https://coreui.io/react/docs/components/avatar/)
- [React Badge](https://coreui.io/react/docs/components/badge/)
- [React Breadcrumb](https://coreui.io/react/docs/components/breadcrumb/)
- [React Button](https://coreui.io/react/docs/components/button/)
- [React Button Group](https://coreui.io/react/docs/components/button-group/)
- [React Callout](https://coreui.io/react/docs/components/callout/)
- [React Card](https://coreui.io/react/docs/components/card/)
- [React Carousel](https://coreui.io/react/docs/components/carousel/)
- [React Checkbox](https://coreui.io/react/docs/forms/checkbox/)
- [React Close Button](https://coreui.io/react/docs/components/close-button/)
- [React Collapse](https://coreui.io/react/docs/components/collapse/)
- [React Date Picker](https://coreui.io/react/docs/forms/date-picker/) **PRO**
- [React Date Range Picker](https://coreui.io/react/docs/forms/date-range-picker/) **PRO**
- [React Dropdown](https://coreui.io/react/docs/components/dropdown/)
- [React Floating Labels](https://coreui.io/react/docs/forms/floating-labels/)
- [React Footer](https://coreui.io/react/docs/components/footer/)
- [React Header](https://coreui.io/react/docs/components/header/)
- [React Image](https://coreui.io/react/docs/components/image/)
- [React Input](https://coreui.io/react/docs/forms/input/)
- [React Input Group](https://coreui.io/react/docs/forms/input-group/)
- [React List Group](https://coreui.io/react/docs/components/list-group/)
- [React Loading Button](https://coreui.io/react/docs/components/loading-button/) **PRO**
- [React Modal](https://coreui.io/react/docs/components/modal/)
- [React Multi Select](https://coreui.io/react/docs/forms/multi-select/) **PRO**
- [React Navs & Tabs](https://coreui.io/react/docs/components/navs-tabs/)
- [React Navbar](https://coreui.io/react/docs/components/navbar/)
- [React Offcanvas](https://coreui.io/react/docs/components/offcanvas/)
- [React One Time Password Input](https://coreui.io/react/docs/forms/one-time-password-input/) **PRO**
- [React Pagination](https://coreui.io/react/docs/components/pagination/)
- [React Password Input](https://coreui.io/react/docs/forms/password-input/) **PRO**
- [React Placeholder](https://coreui.io/react/docs/components/placeholder/)
- [React Popover](https://coreui.io/react/docs/components/popover/)
- [React Progress](https://coreui.io/react/docs/components/progress/)
- [React Radio](https://coreui.io/react/docs/forms/radio/)
- [React Range](https://coreui.io/react/docs/forms/range/)
- [React Range Slider](https://coreui.io/react/docs/forms/range-slider/) **PRO**
- [React Rating](https://coreui.io/react/docs/forms/rating/)
- [React Select](https://coreui.io/react/docs/forms/select/)
- [React Sidebar](https://coreui.io/react/docs/components/sidebar/)
- [React Smart Pagination](https://coreui.io/react/docs/components/smart-pagination/) **PRO**
- [React Smart Table](https://coreui.io/react/docs/components/smart-table/) **PRO**
- [React Spinner](https://coreui.io/react/docs/components/spinner/)
- [React Stepper](https://coreui.io/react/docs/forms/stepper/) **PRO**
- [React Switch](https://coreui.io/react/docs/forms/switch/)
- [React Table](https://coreui.io/react/docs/components/table/)
- [React Textarea](https://coreui.io/react/docs/forms/textarea/)
- [React Time Picker](https://coreui.io/react/docs/forms/time-picker/) **PRO**
- [React Toast](https://coreui.io/react/docs/components/toast/)
- [React Tooltip](https://coreui.io/react/docs/components/tooltip/)
## Versioning
For transparency into our release cycle and in striving to maintain backward compatibility, CoreUI Free Admin Template is maintained under [the Semantic Versioning guidelines](http://semver.org/).
See [the Releases section of our project](https://github.com/coreui/coreui-free-react-admin-template/releases) for changelogs for each release version.
## Creators
**Łukasz Holeczek**
* <https://twitter.com/lukaszholeczek>
* <https://github.com/mrholek>
**Andrzej Kopański**
* <https://github.com/xidedix>
**CoreUI Team**
* <https://twitter.com/core_ui>
* <https://github.com/coreui>
* <https://github.com/orgs/coreui/people>
## Community
Get updates on CoreUI's development and chat with the project maintainers and community members.
- Follow [@core_ui on Twitter](https://twitter.com/core_ui).
- Read and subscribe to [CoreUI Blog](https://coreui.ui/blog/).
## Support CoreUI Development
CoreUI is an MIT-licensed open source project and is completely free to use. However, the amount of effort needed to maintain and develop new features for the project is not sustainable without proper financial backing. You can support development by buying the [CoreUI PRO](https://coreui.io/pricing/?framework=react&src=github-coreui-free-react-admin-template) or by becoming a sponsor via [Open Collective](https://opencollective.com/coreui/).
## Copyright and License
copyright 2025 creativeLabs Łukasz Holeczek.
Code released under [the MIT license](https://github.com/coreui/coreui-free-react-admin-template/blob/main/LICENSE).

View File

@@ -73,7 +73,7 @@ const App = () => {
> >
<Routes> <Routes>
{/* 1. 로그인 여부와 관계없이 항상 독립적으로 표시되는 페이지 */} {/* 1. 로그인 여부와 관계없이 항상 독립적으로 표시되는 페이지 */}
<Route path="/register" element={<Register />} /> <Route path="/admin/member/register" element={<Register />} />
<Route path="/404" element={<Page404 />} /> <Route path="/404" element={<Page404 />} />
<Route path="/500" element={<Page500 />} /> <Route path="/500" element={<Page500 />} />

View File

@@ -1,21 +1,7 @@
import React from 'react' import React from 'react'
import CIcon from '@coreui/icons-react' import CIcon from '@coreui/icons-react'
import { import {
cilBell,
cilBrowser,
cilBoatAlt,
cilCalculator,
cilChartPie,
cilCursor,
cilDescription,
cilDrop,
cilExternalLink,
cilNotes,
cilPencil,
cilPuzzle,
cilSpeedometer, cilSpeedometer,
cilStar,
cilMenu,
} from '@coreui/icons' } from '@coreui/icons'
import { CNavGroup, CNavItem, CNavTitle } from '@coreui/react' import { CNavGroup, CNavItem, CNavTitle } from '@coreui/react'
@@ -32,60 +18,7 @@ const _nav = [
}, },
{ {
component: CNavTitle, component: CNavTitle,
name: 'Theme', name: 'Menu',
},
{
component: CNavItem,
name: 'Colors',
to: '/theme/colors',
icon: <CIcon icon={cilDrop} customClassName="nav-icon" />,
},
{
component: CNavItem,
name: 'Typography',
to: '/theme/typography',
icon: <CIcon icon={cilPencil} customClassName="nav-icon" />,
},
{
component: CNavTitle,
name: 'Admin',
},
{
component: CNavItem,
name: 'Menu Management',
to: '/admin/menu',
icon: <CIcon icon={cilMenu} customClassName="nav-icon" />,
},
{
component: CNavTitle,
name: 'Extras',
},
{
component: CNavGroup,
name: 'Pages',
icon: <CIcon icon={cilStar} customClassName="nav-icon" />,
items: [
{
component: CNavItem,
name: 'Login',
to: '/login',
},
{
component: CNavItem,
name: 'Register',
to: '/register',
},
{
component: CNavItem,
name: 'Error 404',
to: '/404',
},
{
component: CNavItem,
name: 'Error 500',
to: '/500',
},
],
}, },
] ]

View File

@@ -1,18 +1,4 @@
export const logo = [ export const logo = [
'599 116', '180 32',
`<g> `<text x="50%" y="22" text-anchor="middle" font-family="Arial, sans-serif" font-size="18" font-weight="bold" fill="currentColor">Admin Sample</text>`,
<g fill="none" fill-rule="nonzero">
<g style="fill:#80d0ff;">
<path d="m358.773 79.151-8.768-20.736a.25.25 0 0 0-.255-.191h-9.985a.226.226 0 0 0-.256.255v20.543a.566.566 0 0 1-.64.641h-1.216a.565.565 0 0 1-.64-.64v-43.52a.566.566 0 0 1 .64-.64h12.544a9.979 9.979 0 0 1 7.744 3.23 12.204 12.204 0 0 1 2.944 8.546 12.439 12.439 0 0 1-2.24 7.584 9.37 9.37 0 0 1-6.08 3.744c-.17.086-.214.191-.127.32l8.704 20.608.064.255c0 .342-.192.512-.576.512h-1.152a.703.703 0 0 1-.705-.51Zm-19.264-41.793v18.496a.226.226 0 0 0 .256.257h10.304a7.669 7.669 0 0 0 6.017-2.592 9.878 9.878 0 0 0 2.303-6.816 10.286 10.286 0 0 0-2.272-6.976 7.601 7.601 0 0 0-6.048-2.624h-10.304a.226.226 0 0 0-.256.255ZM398.082 37.102H378.05a.226.226 0 0 0-.256.256v18.496a.226.226 0 0 0 .256.257h13.824a.566.566 0 0 1 .64.64v.96a.566.566 0 0 1-.64.64H378.05a.226.226 0 0 0-.256.256v18.56a.226.226 0 0 0 .256.256h20.032a.567.567 0 0 1 .64.64v.96a.566.566 0 0 1-.64.64h-22.144a.566.566 0 0 1-.64-.64v-43.52a.566.566 0 0 1 .64-.64h22.144a.566.566 0 0 1 .64.64v.96a.566.566 0 0 1-.64.64ZM435.802 79.151l-2.431-8.832a.296.296 0 0 0-.32-.192h-16.768a.295.295 0 0 0-.32.192l-2.368 8.768a.658.658 0 0 1-.704.576h-1.216a.588.588 0 0 1-.48-.191.582.582 0 0 1-.096-.513l12.031-43.584a.644.644 0 0 1 .704-.512h1.6a.644.644 0 0 1 .704.512l12.16 43.584.065.192c0 .342-.214.512-.64.512h-1.217a.643.643 0 0 1-.704-.512ZM416.7 67.92a.303.303 0 0 0 .223.096h15.489a.304.304 0 0 0 .223-.096c.065-.065.075-.117.033-.16l-7.873-28.928c-.043-.085-.085-.128-.127-.128-.042 0-.086.043-.128.128l-7.872 28.928c-.043.043-.033.095.032.16ZM453.357 76.911a11.637 11.637 0 0 1-3.328-8.704V46.19a11.414 11.414 0 0 1 3.36-8.575 12.09 12.09 0 0 1 8.8-3.265 12.253 12.253 0 0 1 8.865 3.233 11.39 11.39 0 0 1 3.36 8.607v.64a.566.566 0 0 1-.641.641l-1.28.064c-.427 0-.64-.192-.64-.576v-.833a9.287 9.287 0 0 0-2.656-6.912 10.67 10.67 0 0 0-14.016 0 9.284 9.284 0 0 0-2.656 6.913v22.272a9.282 9.282 0 0 0 2.656 6.912 10.673 10.673 0 0 0 14.016 0 9.286 9.286 0 0 0 2.656-6.912v-.768c0-.384.213-.576.64-.575l1.28.063a.566.566 0 0 1 .64.64v.511a11.498 11.498 0 0 1-3.36 8.64 13.626 13.626 0 0 1-17.696 0v.001ZM511.193 35.503v.96a.565.565 0 0 1-.64.64H499.8a.226.226 0 0 0-.256.256v41.663a.566.566 0 0 1-.64.641h-1.216a.565.565 0 0 1-.64-.64V37.357a.227.227 0 0 0-.256-.255h-10.176a.565.565 0 0 1-.64-.64v-.96a.566.566 0 0 1 .64-.64h23.936a.566.566 0 0 1 .64.64ZM518.822 78.51a2.835 2.835 0 0 1-.8-2.047 2.923 2.923 0 0 1 .8-2.112c.544-.56 1.3-.862 2.08-.832a2.847 2.847 0 0 1 2.944 2.944c.03.78-.273 1.536-.832 2.08a2.921 2.921 0 0 1-2.112.8 2.754 2.754 0 0 1-2.08-.832ZM539.16 77.007a11.31 11.31 0 0 1-3.2-8.416v-5.44a.566.566 0 0 1 .64-.64h1.217a.567.567 0 0 1 .64.64v5.504a9.144 9.144 0 0 0 2.528 6.72 8.974 8.974 0 0 0 6.687 2.56 8.79 8.79 0 0 0 9.28-9.28V35.504a.565.565 0 0 1 .64-.64h1.217a.566.566 0 0 1 .64.64V68.59a11.252 11.252 0 0 1-3.233 8.416 13.062 13.062 0 0 1-17.055 0ZM577.106 77.102a10.482 10.482 0 0 1-3.36-8.127v-1.792a.565.565 0 0 1 .64-.64h1.088a.566.566 0 0 1 .64.64v1.6a8.544 8.544 0 0 0 2.752 6.655 10.536 10.536 0 0 0 7.36 2.496 9.876 9.876 0 0 0 6.976-2.367 8.215 8.215 0 0 0 2.56-6.336 8.397 8.397 0 0 0-1.12-4.416 11.383 11.383 0 0 0-3.328-3.392 71.626 71.626 0 0 0-6.176-3.712 71.302 71.302 0 0 1-6.24-3.84 12.174 12.174 0 0 1-3.424-3.68 10.257 10.257 0 0 1-1.28-5.345 9.86 9.86 0 0 1 3.072-7.744 12.012 12.012 0 0 1 8.32-2.752c3.796 0 6.783 1.035 8.96 3.105a10.823 10.823 0 0 1 3.264 8.224v1.6a.566.566 0 0 1-.64.64h-1.152a.565.565 0 0 1-.64-.64v-1.471a8.865 8.865 0 0 0-2.624-6.689 9.994 9.994 0 0 0-7.232-2.528 9.365 9.365 0 0 0-6.528 2.144 7.822 7.822 0 0 0-2.368 6.112 7.8 7.8 0 0 0 1.024 4.16 10.376 10.376 0 0 0 3.008 3.04 62.829 62.829 0 0 0 5.952 3.488 71.058 71.058 0 0 1 6.72 4.256 13.454 13.454 0 0 1 3.648 3.936 10.049 10.049 0 0 1 1.28 5.184 10.714 10.714 0 0 1-3.264 8.191c-2.175 2.05-5.12 3.073-8.832 3.073-3.798 0-6.817-1.024-9.057-3.073Z"/>
</g>
<g style="fill:currentColor;">
<path d="m96.59 25.058-39-22.517a12 12 0 0 0-12 0l-39 22.517a12.034 12.034 0 0 0-6 10.392v45.033a12.033 12.033 0 0 0 6 10.393l39 22.516a12 12 0 0 0 12 0l39-22.516a12.033 12.033 0 0 0 6-10.393V35.45a12.034 12.034 0 0 0-6-10.392Zm-2 55.425a4 4 0 0 1-2 3.464l-39 22.517a4 4 0 0 1-4 0l-39-22.517a4 4 0 0 1-2-3.464V35.45a4 4 0 0 1 2-3.464l39-22.517a4 4 0 0 1 4 0l39 22.517a4 4 0 0 1 2 3.464v45.033Z"/>
<path d="M74.612 71.005h-2.866c-.673 0-1.335.17-1.925.493l-17.28 9.485L32.59 69.465V46.487L52.54 34.97l17.29 9.455a4 4 0 0 0 1.919.49h2.863a2 2 0 0 0 2-2v-2.712a2 2 0 0 0-1.04-1.754L56.383 27.952a8.039 8.039 0 0 0-7.842.09L28.59 39.56a8.025 8.025 0 0 0-4 6.929v22.976a8 8 0 0 0 4 6.928l19.95 11.519a8.043 8.043 0 0 0 7.843.087l19.19-10.53a2 2 0 0 0 1.038-1.754v-2.71a2 2 0 0 0-1.999-2Z"/>
<g transform="translate(118 34)">
<path d="M51.335.362c-8.28.009-14.99 6.719-15 15v17.277c0 8.284 6.716 15 15 15 8.284 0 15-6.716 15-15V15.36c-.01-8.28-6.72-14.99-15-15Zm7 32.277a7 7 0 0 1-14 0V15.36a7 7 0 0 1 14 0V32.64ZM14.67 8.421a7.01 7.01 0 0 1 7.867 6.075.99.99 0 0 0 .985.865h6.03a1.01 1.01 0 0 0 .998-1.097C29.945 6.14 22.971-.02 14.834.381 6.751.932.504 7.696.59 15.796v16.407C.503 40.305 6.752 47.068 14.835 47.62c8.137.401 15.11-5.76 15.716-13.884a1.01 1.01 0 0 0-.999-1.097h-6.03a.99.99 0 0 0-.984.865 7.01 7.01 0 0 1-7.868 6.075 7.164 7.164 0 0 1-6.08-7.184v-16.79a7.164 7.164 0 0 1 6.08-7.184ZM97.513 27.928a12.158 12.158 0 0 0 7.184-11.077v-3.702C104.697 6.44 99.257 1 92.547 1H75.59a1 1 0 0 0-1 1v44a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V29h6.621l7.916 17.414a1 1 0 0 0 .91.586h6.591a1 1 0 0 0 .91-1.414l-8.025-17.658Zm-.816-11.077A4.154 4.154 0 0 1 92.547 21h-9.85V9h9.85a4.154 4.154 0 0 1 4.15 4.15v3.7ZM139.59 1h-26a1 1 0 0 0-1 1v44a1 1 0 0 0 1 1h26a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1h-19V27h13a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1h-13V9h19a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1ZM177.59 1h-6a1 1 0 0 0-1 1v22.648a7.007 7.007 0 1 1-14 0V2a1 1 0 0 0-1-1h-6a1 1 0 0 0-1 1v22.648a15.003 15.003 0 1 0 30 0V2a1 1 0 0 0-1-1Z"/>
<rect width="8" height="38" x="186.59" y="1" rx="1"/>
</g>
</g>
</g>
</g>`,
] ]

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -36,6 +36,7 @@ const AppContent = () => {
})} })}
<Route path="/" element={<Navigate to="/dashboard" replace />} /> <Route path="/" element={<Navigate to="/dashboard" replace />} />
<Route path="/login" element={<Navigate to="/dashboard" replace />} /> <Route path="/login" element={<Navigate to="/dashboard" replace />} />
<Route path="*" element={<Navigate to="/404" replace />} />
</Routes> </Routes>
)} )}
</Suspense> </Suspense>

View File

@@ -5,14 +5,7 @@ const AppFooter = () => {
return ( return (
<CFooter className="px-4"> <CFooter className="px-4">
<div> <div>
<a href="https://coreui.io" target="_blank" rel="noopener noreferrer"> <span className="m-2">CoreUI React Referrence :</span>
CoreUI
</a>
<span className="ms-1">&copy; 2025 creativeLabs.</span>
</div>
<div>
<span className="m-2">Referrence :</span>
<a href="https://coreui.io/demos/react/5.5/free/?theme=light#/dashboard" target="_blank" rel="noopener noreferrer"> <a href="https://coreui.io/demos/react/5.5/free/?theme=light#/dashboard" target="_blank" rel="noopener noreferrer">
Demo Demo
</a> </a>

View File

@@ -69,17 +69,6 @@ const AppHeader = () => {
{member.memberId} {member.memberId}
</span> </span>
)} )}
<CIcon icon={cilBell} size="lg" />
</CNavLink>
</CNavItem>
<CNavItem>
<CNavLink href="#">
<CIcon icon={cilList} size="lg" />
</CNavLink>
</CNavItem>
<CNavItem>
<CNavLink href="#">
<CIcon icon={cilEnvelopeOpen} size="lg" />
</CNavLink> </CNavLink>
</CNavItem> </CNavItem>
</CHeaderNav> </CHeaderNav>

View File

@@ -1,10 +1,13 @@
import React from 'react' import React, { useEffect, useState, useMemo } from 'react'
import { NavLink } from 'react-router-dom' import { NavLink } from 'react-router-dom'
import { useSelector, useDispatch } from 'react-redux' import { useSelector, useDispatch } from 'react-redux'
import { RootState } from 'src/store' import { RootState } from 'src/store'
import { import {
CCloseButton, CCloseButton,
CNavGroup,
CNavItem,
CNavTitle,
CSidebar, CSidebar,
CSidebarBrand, CSidebarBrand,
CSidebarFooter, CSidebarFooter,
@@ -12,6 +15,8 @@ import {
CSidebarToggler, CSidebarToggler,
} from '@coreui/react' } from '@coreui/react'
import CIcon from '@coreui/icons-react' import CIcon from '@coreui/icons-react'
import * as icons from '@coreui/icons'
import { cilMinus } from '@coreui/icons'
import { AppSidebarNav } from 'src/components/AppSidebarNav' import { AppSidebarNav } from 'src/components/AppSidebarNav'
@@ -20,11 +25,98 @@ import { sygnet } from 'src/assets/brand/sygnet'
// sidebar nav config // sidebar nav config
import navigation from 'src/_nav' import navigation from 'src/_nav'
import { getAdminMenuTree, AdminMenuTree } from 'src/services/adminMenuService'
// 아이콘 이름으로 아이콘 컴포넌트 가져오기 (최상위 메뉴용)
const getIconByName = (iconName: string) => {
const icon = (icons as Record<string, unknown>)[iconName]
if (icon) {
return <CIcon icon={icon as string[]} customClassName="nav-icon" />
}
// 아이콘이 없으면 기본 아이콘 표시
return <CIcon icon={cilMinus} customClassName="nav-icon" />
}
// 하위 메뉴용 아이콘 (깊이에 따른 들여쓰기 포함)
const getChildIcon = (iconName: string, depth: number) => {
const icon = (icons as Record<string, unknown>)[iconName]
const iconElement = icon ? (
<CIcon icon={icon as string[]} style={{ width: '1.25rem', height: '1.25rem' }} />
) : (
<CIcon icon={cilMinus} style={{ width: '1.25rem', height: '1.25rem' }} />
)
// 깊이에 따라 들여쓰기 증가 (2단계: 1rem, 3단계: 2rem, ...)
const paddingLeft = `${depth}rem`
return (
<span className="nav-icon" style={{ paddingLeft }}>
{iconElement}
</span>
)
}
// API 응답을 CoreUI 네비게이션 형식으로 변환
const convertMenuTreeToNav = (menuTree: AdminMenuTree[], depth = 0): any[] => {
return menuTree.map((menu) => {
// 깊이가 0이면 최상위, 1 이상이면 하위 메뉴
const icon = depth === 0 ? getIconByName(menu.iconName) : getChildIcon(menu.iconName, depth)
if (menu.childMenuList && menu.childMenuList.length > 0) {
// 하위 메뉴가 있는 경우 CNavGroup 사용
return {
component: CNavGroup,
name: menu.menuName,
icon: icon,
items: convertMenuTreeToNav(menu.childMenuList, depth + 1),
}
} else {
// 하위 메뉴가 없는 경우 CNavItem 사용
const navItem: any = {
component: CNavItem,
name: menu.menuName,
icon: icon,
indent: depth > 0,
}
// URL이 있는 경우에만 to 속성 추가
if (menu.menuUrl) {
navItem.to = menu.menuUrl
}
return navItem
}
})
}
const AppSidebar = () => { const AppSidebar = () => {
const dispatch = useDispatch() const dispatch = useDispatch()
const unfoldable = useSelector((state: RootState) => state.sidebarUnfoldable) const unfoldable = useSelector((state: RootState) => state.sidebarUnfoldable)
const sidebarShow = useSelector((state: RootState) => state.sidebarShow) const sidebarShow = useSelector((state: RootState) => state.sidebarShow)
const [dynamicMenus, setDynamicMenus] = useState<AdminMenuTree[]>([])
// API에서 메뉴 트리 조회
useEffect(() => {
const fetchMenuTree = async () => {
try {
const menuTree = await getAdminMenuTree()
setDynamicMenus(menuTree)
} catch (error) {
console.error('메뉴 트리 조회 실패:', error)
}
}
fetchMenuTree()
}, [])
// 동적 메뉴를 포함한 전체 네비게이션 생성
const fullNavigation = useMemo(() => {
const navItems = [...navigation]
// 'Menu' 타이틀 바로 다음에 동적 메뉴 삽입
const menuTitleIndex = navItems.findIndex(
(item) => item.name === 'Menu' && item.component === CNavTitle
)
if (menuTitleIndex !== -1 && dynamicMenus.length > 0) {
const dynamicNavItems = convertMenuTreeToNav(dynamicMenus)
navItems.splice(menuTitleIndex + 1, 0, ...dynamicNavItems)
}
return navItems
}, [dynamicMenus])
return ( return (
<CSidebar <CSidebar
@@ -48,7 +140,7 @@ const AppSidebar = () => {
onClick={() => dispatch({ type: 'set', sidebarShow: false })} onClick={() => dispatch({ type: 'set', sidebarShow: false })}
/> />
</CSidebarHeader> </CSidebarHeader>
<AppSidebarNav items={navigation} /> <AppSidebarNav items={fullNavigation} />
<CSidebarFooter className="border-top d-none d-lg-flex"> <CSidebarFooter className="border-top d-none d-lg-flex">
<CSidebarToggler <CSidebarToggler
onClick={() => dispatch({ type: 'set', sidebarUnfoldable: !unfoldable })} onClick={() => dispatch({ type: 'set', sidebarUnfoldable: !unfoldable })}

View File

@@ -32,14 +32,9 @@ export const AppSidebarNav = ({ items }: AppSidebarNavProps) => {
const navLink = (name?: string, icon?: React.ReactNode, badge?: Badge, indent = false) => { const navLink = (name?: string, icon?: React.ReactNode, badge?: Badge, indent = false) => {
return ( return (
<> <>
{icon {icon && icon}
? icon {indent && !icon && <span style={{ marginLeft: '1rem' }} />}
: indent && ( <span style={{ whiteSpace: 'nowrap' }}>{name && name}</span>
<span className="nav-icon">
<span className="nav-icon-bullet"></span>
</span>
)}
{name && name}
{badge && ( {badge && (
<CBadge color={badge.color} className="ms-auto" size="sm"> <CBadge color={badge.color} className="ms-auto" size="sm">
{badge.text} {badge.text}
@@ -50,21 +45,17 @@ export const AppSidebarNav = ({ items }: AppSidebarNavProps) => {
} }
const navItem = (item: NavItem, index: number, indent = false) => { const navItem = (item: NavItem, index: number, indent = false) => {
const { component, name, badge, icon, ...rest } = item const { component, name, badge, icon, indent: itemIndent, ...rest } = item
const Component = component const Component = component
return ( return (
<Component as="div" key={index}> <Component as="div" key={index}>
{rest.to || rest.href ? (
<CNavLink <CNavLink
{...(rest.to && { as: NavLink })} {...(rest.to && { as: NavLink })}
{...(rest.href && { target: '_blank', rel: 'noopener noreferrer' })} {...(rest.href && { target: '_blank', rel: 'noopener noreferrer' })}
{...rest} {...rest}
> >
{navLink(name, icon, badge, indent)} {navLink(name, icon, badge, itemIndent || indent)}
</CNavLink> </CNavLink>
) : (
navLink(name, icon, badge, indent)
)}
</Component> </Component>
) )
} }

View File

@@ -1,7 +1,6 @@
import React from 'react' import React, { useState, useEffect } from 'react'
import { import {
CAvatar, CAvatar,
CBadge,
CDropdown, CDropdown,
CDropdownDivider, CDropdownDivider,
CDropdownHeader, CDropdownHeader,
@@ -10,24 +9,64 @@ import {
CDropdownToggle, CDropdownToggle,
} from '@coreui/react' } from '@coreui/react'
import { import {
cilBell,
cilCreditCard,
cilCommentSquare,
cilEnvelopeOpen,
cilFile,
cilLockLocked,
cilSettings,
cilTask,
cilUser, cilUser,
cilAccountLogout, cilAccountLogout,
cilClock,
} from '@coreui/icons' } from '@coreui/icons'
import { useAuth } from 'src/hooks/useAuth' import { useAuth } from 'src/hooks/useAuth'
import { getAccessTokenFromCookie, getRefreshTokenFromCookie, getUserFromToken } from 'src/axios/authService'
import CIcon from '@coreui/icons-react' import CIcon from '@coreui/icons-react'
import avatar8 from 'src/assets/images/avatars/8.jpg' import avatar8 from 'src/assets/images/avatars/default.png'
const AppHeaderDropdown = () => { const AppHeaderDropdown = () => {
const { logout } = useAuth() const { logout, state } = useAuth()
const [accessTokenExpiry, setAccessTokenExpiry] = useState<string>('')
const [refreshTokenExpiry, setRefreshTokenExpiry] = useState<string>('')
useEffect(() => {
const formatExpiry = (diff: number): string => {
if (diff <= 0) return '만료됨'
const days = Math.floor(diff / (24 * 60 * 60 * 1000))
const hours = Math.floor((diff % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000))
const minutes = Math.floor((diff % (60 * 60 * 1000)) / 60000)
const seconds = Math.floor((diff % 60000) / 1000)
if (days > 0) return `${days}${hours}시간 후 만료`
if (hours > 0) return `${hours}시간 ${minutes}분 후 만료`
return `${minutes}${seconds}초 후 만료`
}
const updateExpiry = () => {
const now = new Date()
// Access Token
const accessToken = getAccessTokenFromCookie() || localStorage.getItem('accessToken')
if (accessToken) {
const decoded = getUserFromToken(accessToken)
if (decoded?.exp) {
const diff = decoded.exp * 1000 - now.getTime()
setAccessTokenExpiry(formatExpiry(diff))
}
}
// Refresh Token
const refreshToken = getRefreshTokenFromCookie() || localStorage.getItem('refreshToken')
if (refreshToken) {
const decoded = getUserFromToken(refreshToken)
if (decoded?.exp) {
const diff = decoded.exp * 1000 - now.getTime()
setRefreshTokenExpiry(formatExpiry(diff))
}
} else {
setRefreshTokenExpiry('HttpOnly (접근 불가)')
}
}
updateExpiry()
const interval = setInterval(updateExpiry, 1000)
return () => clearInterval(interval)
}, [])
return ( return (
<CDropdown variant="nav-item"> <CDropdown variant="nav-item">
@@ -36,56 +75,21 @@ const AppHeaderDropdown = () => {
</CDropdownToggle> </CDropdownToggle>
<CDropdownMenu className="pt-0"> <CDropdownMenu className="pt-0">
<CDropdownHeader className="bg-body-secondary fw-semibold mb-2">Account</CDropdownHeader> <CDropdownHeader className="bg-body-secondary fw-semibold mb-2">Account</CDropdownHeader>
<CDropdownItem href="#"> <CDropdownItem className="disabled">
<CIcon icon={cilBell} className="me-2" />
Updates
<CBadge color="info" className="ms-2">
42
</CBadge>
</CDropdownItem>
<CDropdownItem href="#">
<CIcon icon={cilEnvelopeOpen} className="me-2" />
Messages
<CBadge color="success" className="ms-2">
42
</CBadge>
</CDropdownItem>
<CDropdownItem href="#">
<CIcon icon={cilTask} className="me-2" />
Tasks
<CBadge color="danger" className="ms-2">
42
</CBadge>
</CDropdownItem>
<CDropdownItem href="#">
<CIcon icon={cilCommentSquare} className="me-2" />
Comments
<CBadge color="warning" className="ms-2">
42
</CBadge>
</CDropdownItem>
<CDropdownHeader className="bg-body-secondary fw-semibold my-2">Settings</CDropdownHeader>
<CDropdownItem href="#">
<CIcon icon={cilUser} className="me-2" /> <CIcon icon={cilUser} className="me-2" />
Profile ID: {state.member?.memberId || '-'}
</CDropdownItem> </CDropdownItem>
<CDropdownItem href="#"> <CDropdownItem className="disabled">
<CIcon icon={cilSettings} className="me-2" /> <CIcon icon={cilUser} className="me-2" />
Settings : {state.member?.memberName || '-'}
</CDropdownItem> </CDropdownItem>
<CDropdownItem href="#"> <CDropdownItem className="disabled">
<CIcon icon={cilCreditCard} className="me-2" /> <CIcon icon={cilClock} className="me-2" />
Payments Access: {accessTokenExpiry}
<CBadge color="secondary" className="ms-2">
42
</CBadge>
</CDropdownItem> </CDropdownItem>
<CDropdownItem href="#"> <CDropdownItem className="disabled">
<CIcon icon={cilFile} className="me-2" /> <CIcon icon={cilClock} className="me-2" />
Projects Refresh: {refreshTokenExpiry}
<CBadge color="primary" className="ms-2">
42
</CBadge>
</CDropdownItem> </CDropdownItem>
<CDropdownDivider /> <CDropdownDivider />
<CDropdownItem onClick={logout} style={{ cursor: 'pointer' }}> <CDropdownItem onClick={logout} style={{ cursor: 'pointer' }}>

View File

@@ -1,17 +1,14 @@
import React from 'react' import React from 'react'
const Dashboard = React.lazy(() => import('src/views/dashboard/Dashboard')) const Dashboard = React.lazy(() => import('src/views/dashboard/Dashboard'))
const Colors = React.lazy(() => import('src/views/theme/colors/Colors'))
const Typography = React.lazy(() => import('src/views/theme/typography/Typography'))
const AdminMenuManagement = React.lazy(() => import('src/views/admin/AdminMenuManagement')) const AdminMenuManagement = React.lazy(() => import('src/views/admin/AdminMenuManagement'))
const AdminMemberManagement = React.lazy(() => import('src/views/admin/AdminMemberManagement'))
const routes = [ const routes = [
{ path: '/', exact: true, name: 'Home' }, { path: '/', exact: true, name: 'Home' },
{ path: '/dashboard', name: 'Dashboard', element: Dashboard }, { path: '/dashboard', name: 'Dashboard', element: Dashboard },
{ path: '/theme', name: 'Theme', element: Colors, exact: true },
{ path: '/theme/colors', name: 'Colors', element: Colors },
{ path: '/theme/typography', name: 'Typography', element: Typography },
{ path: '/admin/menu', name: 'Admin Menu Management', element: AdminMenuManagement }, { path: '/admin/menu', name: 'Admin Menu Management', element: AdminMenuManagement },
{ path: '/admin/member', name: 'Admin Member Management', element: AdminMemberManagement },
] ]
export default routes export default routes

View File

@@ -0,0 +1,70 @@
import axios from 'src/axios/axios';
// 회원 정보 인터페이스
export interface AdminMember {
memberSeq?: number;
memberId: string;
memberName: string;
password?: string;
level: number;
status: number;
lastLoginDatetime?: string;
insertDatetime?: string;
updateDatetime?: string;
}
// 페이징 정보 인터페이스
export interface PageInfo {
pageNum: number;
pageSize: number;
totalContent: number;
totalPage: number;
isFirstPage: boolean;
isLastPage: boolean;
}
// API 응답 인터페이스
export interface AdminMemberResponse {
resultCode: string;
resultMessage: string;
resultData?: any;
pageInfo?: PageInfo;
}
// 회원 목록 조회
export const getAdminMemberList = async (pageNum: number = 1): Promise<AdminMemberResponse> => {
const response = await axios.get<AdminMemberResponse>('/admin/member/list', {
params: { pageNum }
});
return response.data;
};
// 회원 상세 조회
export const getAdminMember = async (memberId: string): Promise<AdminMember> => {
const response = await axios.get<AdminMemberResponse>(`/admin/member/${memberId}`);
return response.data.resultData;
};
// 회원 추가
export const addAdminMember = async (member: Partial<AdminMember>): Promise<AdminMemberResponse> => {
const response = await axios.post<AdminMemberResponse>('/admin/member/add', member);
return response.data;
};
// 회원 수정
export const updateAdminMember = async (member: Partial<AdminMember>): Promise<AdminMemberResponse> => {
const response = await axios.post<AdminMemberResponse>('/admin/member/update', member);
return response.data;
};
// 회원 삭제
export const deleteAdminMember = async (memberSeq: number): Promise<AdminMemberResponse> => {
const response = await axios.post<AdminMemberResponse>('/admin/member/delete', { memberSeq });
return response.data;
};
// 회원 가입 (비로그인 상태에서 회원가입)
export const registerAdminMember = async (member: Partial<AdminMember>): Promise<AdminMemberResponse> => {
const response = await axios.post<AdminMemberResponse>('/admin/member/register', member);
return response.data;
};

View File

@@ -8,6 +8,19 @@ export interface AdminMenu {
menuName: string; menuName: string;
iconName?: string; iconName?: string;
menuUrl?: string; menuUrl?: string;
level: number;
}
// 트리 구조 메뉴 인터페이스
export interface AdminMenuTree {
adminMenuSeq: number;
parentSeq: number;
menuOrder: number;
menuName: string;
iconName: string;
menuUrl: string;
level: number;
childMenuList: AdminMenuTree[] | null;
} }
// 페이징 정보 인터페이스 // 페이징 정보 인터페이스
@@ -67,21 +80,6 @@ export const getAdminMenuListByParentSeq = async (parentSeq: number, pageNum: nu
return { content: [], pageNum: 1, pageSize: 0, totalContent: 0, totalPage: 0, isFirstPage: true, isLastPage: true }; return { content: [], pageNum: 1, pageSize: 0, totalContent: 0, totalPage: 0, isFirstPage: true, isLastPage: true };
}; };
// parentSeq로 어드민 메뉴 목록 조회 (페이징 없이 전체 목록)
export const getAdminMenuListByParentSeqAll = async (parentSeq: number): Promise<AdminMenu[]> => {
const response = await axios.get<AdminMenuResponse>(`/admin/menu/listByParentSeq/${parentSeq}`);
const resultData = response.data.resultData;
// 페이징 형태의 응답인 경우 content 배열 반환
if (resultData && resultData.content && Array.isArray(resultData.content)) {
return resultData.content;
}
// 배열 형태의 응답인 경우 그대로 반환
if (Array.isArray(resultData)) {
return resultData;
}
return [];
};
// adminMenuSeq로 어드민 메뉴 조회 // adminMenuSeq로 어드민 메뉴 조회
export const getAdminMenu = async (adminMenuSeq: number): Promise<AdminMenu> => { export const getAdminMenu = async (adminMenuSeq: number): Promise<AdminMenu> => {
const response = await axios.get<AdminMenuResponse>(`/admin/menu/${adminMenuSeq}`); const response = await axios.get<AdminMenuResponse>(`/admin/menu/${adminMenuSeq}`);
@@ -105,3 +103,9 @@ export const deleteAdminMenu = async (adminMenuSeq: number): Promise<AdminMenuRe
const response = await axios.post<AdminMenuResponse>('/admin/menu/delete', { adminMenuSeq }); const response = await axios.post<AdminMenuResponse>('/admin/menu/delete', { adminMenuSeq });
return response.data; return response.data;
}; };
// 어드민 메뉴 트리 조회 (로그인 회원 레벨 기준)
export const getAdminMenuTree = async (): Promise<AdminMenuTree[]> => {
const response = await axios.get<AdminMenuResponse>('/admin/menu/tree');
return response.data.resultData || [];
};

View File

@@ -0,0 +1,549 @@
import React, { useEffect, useState, useCallback } from 'react';
import {
CCard,
CCardBody,
CCardHeader,
CTable,
CTableHead,
CTableRow,
CTableHeaderCell,
CTableBody,
CTableDataCell,
CButton,
CModal,
CModalHeader,
CModalTitle,
CModalBody,
CModalFooter,
CForm,
CFormLabel,
CFormInput,
CFormSelect,
CPagination,
CPaginationItem,
CSpinner,
CBadge,
} from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilPencil, cilTrash, cilPlus, cilWarning } from '@coreui/icons';
import {
AdminMember,
PageInfo,
getAdminMemberList,
addAdminMember,
updateAdminMember,
deleteAdminMember,
} from 'src/services/adminMemberService';
// 상태 옵션
const statusOptions = [
{ value: 0, label: '가입신청', color: 'warning' },
{ value: 1, label: '정상', color: 'success' },
{ value: 2, label: '휴면', color: 'secondary' },
{ value: 3, label: '정지', color: 'danger' },
{ value: 4, label: '탈퇴', color: 'dark' },
];
const AdminMemberManagement: React.FC = () => {
const [members, setMembers] = useState<AdminMember[]>([]);
const [pageInfo, setPageInfo] = useState<PageInfo | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [loading, setLoading] = useState(false);
// 반응형 상태 (화면 너비에 따라 열 표시/숨김)
const [isCompact, setIsCompact] = useState(false);
// 화면 너비 변경 감지
const handleResize = useCallback(() => {
setIsCompact(window.innerWidth < 992);
}, []);
useEffect(() => {
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [handleResize]);
// 모달 상태
const [modalVisible, setModalVisible] = useState(false);
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
const [isEditMode, setIsEditMode] = useState(false);
const [selectedMember, setSelectedMember] = useState<AdminMember | null>(null);
// 폼 상태
const [formData, setFormData] = useState({
memberId: '',
memberName: '',
password: '',
passwordConfirm: '',
level: 1,
status: 0,
});
// 회원 목록 조회
const fetchMembers = useCallback(async (page: number) => {
setLoading(true);
try {
const response = await getAdminMemberList(page);
if (response.resultCode === '200' && response.resultData) {
const data = response.resultData;
// resultData.content에 회원 목록, 나머지는 페이지 정보
setMembers(Array.isArray(data.content) ? data.content : []);
setPageInfo({
pageNum: data.pageNum,
pageSize: data.pageSize,
totalContent: data.totalContent,
totalPage: data.totalPage,
isFirstPage: data.isFirstPage,
isLastPage: data.isLastPage,
});
}
} catch (error) {
console.error('회원 목록 조회 실패:', error);
setMembers([]);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchMembers(currentPage);
}, [currentPage, fetchMembers]);
// 상태 뱃지 가져오기
const getStatusBadge = (status: number) => {
const statusInfo = statusOptions.find(s => s.value === status);
return statusInfo ? (
<CBadge color={statusInfo.color}>{statusInfo.label}</CBadge>
) : (
<CBadge color="secondary"> </CBadge>
);
};
// 날짜 포맷 (날짜와 시간을 줄바꿈으로 구분)
const formatDate = (dateString?: string) => {
if (!dateString) return '-';
const date = new Date(dateString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return (
<>
{`${year}/${month}/${day}`}
<br />
{`${hours}:${minutes}`}
</>
);
};
// 모달 열기 (추가)
const openAddModal = () => {
setIsEditMode(false);
setFormData({
memberId: '',
memberName: '',
password: '',
passwordConfirm: '',
level: 1,
status: 0,
});
setModalVisible(true);
};
// 모달 열기 (수정)
const openEditModal = (member: AdminMember) => {
setIsEditMode(true);
setSelectedMember(member);
setFormData({
memberId: member.memberId,
memberName: member.memberName,
password: '',
passwordConfirm: '',
level: member.level,
status: member.status,
});
setModalVisible(true);
};
// 삭제 모달 열기
const openDeleteModal = (member: AdminMember) => {
setSelectedMember(member);
setDeleteModalVisible(true);
};
// 폼 입력 핸들러
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: name === 'level' || name === 'status' ? parseInt(value) : value,
}));
};
// 저장
const handleSave = async () => {
// 유효성 검사
if (!formData.memberId.trim()) {
alert('어드민 회원 ID를 입력해주세요.');
return;
}
if (!formData.memberName.trim()) {
alert('회원 이름을 입력해주세요.');
return;
}
if (!isEditMode && !formData.password) {
alert('비밀번호를 입력해주세요.');
return;
}
if (formData.password && formData.password !== formData.passwordConfirm) {
alert('비밀번호가 일치하지 않습니다.');
return;
}
try {
if (isEditMode && selectedMember) {
const updateData: Partial<AdminMember> = {
memberSeq: selectedMember.memberSeq,
memberName: formData.memberName,
level: formData.level,
status: formData.status,
};
if (formData.password) {
updateData.password = formData.password;
}
const response = await updateAdminMember(updateData);
if (response.resultCode === '200') {
alert('회원 정보가 수정되었습니다.');
setModalVisible(false);
fetchMembers(currentPage);
} else {
alert(response.resultMessage || '수정에 실패했습니다.');
}
} else {
const response = await addAdminMember({
memberId: formData.memberId,
memberName: formData.memberName,
password: formData.password,
level: formData.level,
status: formData.status,
});
if (response.resultCode === '200') {
alert('회원이 추가되었습니다.');
setModalVisible(false);
fetchMembers(1);
setCurrentPage(1);
} else {
alert(response.resultMessage || '추가에 실패했습니다.');
}
}
} catch (error) {
console.error('저장 실패:', error);
alert('저장에 실패했습니다.');
}
};
// 삭제
const handleDelete = async () => {
if (!selectedMember?.memberSeq) return;
try {
const response = await deleteAdminMember(selectedMember.memberSeq);
if (response.resultCode === '200') {
alert('회원이 삭제되었습니다.');
setDeleteModalVisible(false);
fetchMembers(currentPage);
} else {
alert(response.resultMessage || '삭제에 실패했습니다.');
}
} catch (error) {
console.error('삭제 실패:', error);
alert('삭제에 실패했습니다.');
}
};
// 페이지 변경
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
// 페이지네이션 렌더링
const renderPagination = () => {
if (!pageInfo || pageInfo.totalPage <= 1) return null;
const pages = [];
const maxVisiblePages = 5;
let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
let endPage = Math.min(pageInfo.totalPage, startPage + maxVisiblePages - 1);
if (endPage - startPage + 1 < maxVisiblePages) {
startPage = Math.max(1, endPage - maxVisiblePages + 1);
}
for (let i = startPage; i <= endPage; i++) {
pages.push(i);
}
return (
<CPagination className="justify-content-center">
<CPaginationItem
disabled={pageInfo.isFirstPage}
onClick={() => handlePageChange(1)}
>
{'<<'}
</CPaginationItem>
<CPaginationItem
disabled={pageInfo.isFirstPage}
onClick={() => handlePageChange(currentPage - 1)}
>
{'<'}
</CPaginationItem>
{pages.map(page => (
<CPaginationItem
key={page}
active={page === currentPage}
onClick={() => handlePageChange(page)}
>
{page}
</CPaginationItem>
))}
<CPaginationItem
disabled={pageInfo.isLastPage}
onClick={() => handlePageChange(currentPage + 1)}
>
{'>'}
</CPaginationItem>
<CPaginationItem
disabled={pageInfo.isLastPage}
onClick={() => handlePageChange(pageInfo.totalPage)}
>
{'>>'}
</CPaginationItem>
</CPagination>
);
};
return (
<>
<CCard className="mb-4">
<CCardHeader className="d-flex justify-content-between align-items-center">
<strong> </strong>
<CButton color="primary" size="sm" onClick={openAddModal}>
<CIcon icon={cilPlus} className="me-1" />
</CButton>
</CCardHeader>
<CCardBody>
{loading ? (
<div className="text-center py-5">
<CSpinner color="primary" />
</div>
) : (
<>
<CTable hover responsive style={{ tableLayout: 'fixed', minWidth: isCompact ? 'auto' : '800px' }}>
<CTableHead>
<CTableRow>
<CTableHeaderCell scope="col" className="bg-body-tertiary text-center" style={{ width: '80px' }}></CTableHeaderCell>
<CTableHeaderCell scope="col" className="bg-body-tertiary text-center"> ID</CTableHeaderCell>
{!isCompact && (
<CTableHeaderCell scope="col" className="bg-body-tertiary text-center"></CTableHeaderCell>
)}
{!isCompact && (
<CTableHeaderCell scope="col" className="bg-body-tertiary text-center" style={{ width: '80px' }}></CTableHeaderCell>
)}
{!isCompact && (
<CTableHeaderCell scope="col" className="bg-body-tertiary text-center" style={{ width: '80px' }}></CTableHeaderCell>
)}
{!isCompact && (
<CTableHeaderCell scope="col" className="bg-body-tertiary text-center" style={{ width: '110px' }}> </CTableHeaderCell>
)}
{!isCompact && (
<CTableHeaderCell scope="col" className="bg-body-tertiary text-center" style={{ width: '110px' }}></CTableHeaderCell>
)}
<CTableHeaderCell scope="col" className="bg-body-tertiary text-center" style={{ width: '100px' }}></CTableHeaderCell>
</CTableRow>
</CTableHead>
<CTableBody>
{members.length === 0 ? (
<CTableRow>
<CTableDataCell colSpan={isCompact ? 3 : 8} className="text-center py-4">
.
</CTableDataCell>
</CTableRow>
) : (
members.map((member) => (
<CTableRow key={member.memberSeq}>
<CTableDataCell className="text-center">{member.memberSeq}</CTableDataCell>
<CTableDataCell className="text-center">{member.memberId}</CTableDataCell>
{!isCompact && (
<CTableDataCell className="text-center">{member.memberName}</CTableDataCell>
)}
{!isCompact && (
<CTableDataCell className="text-center">{member.level}</CTableDataCell>
)}
{!isCompact && (
<CTableDataCell className="text-center">{getStatusBadge(member.status)}</CTableDataCell>
)}
{!isCompact && (
<CTableDataCell className="text-center">{formatDate(member.lastLoginDatetime)}</CTableDataCell>
)}
{!isCompact && (
<CTableDataCell className="text-center">{formatDate(member.insertDatetime)}</CTableDataCell>
)}
<CTableDataCell className="text-center">
<CButton
color="info"
size="sm"
className="me-1"
onClick={() => openEditModal(member)}
title="수정"
>
<CIcon icon={cilPencil} />
</CButton>
<CButton
color="danger"
size="sm"
onClick={() => openDeleteModal(member)}
title="삭제"
>
<CIcon icon={cilTrash} />
</CButton>
</CTableDataCell>
</CTableRow>
))
)}
</CTableBody>
</CTable>
{pageInfo && pageInfo.totalPage > 1 && (
<div className="mt-3">
{renderPagination()}
</div>
)}
</>
)}
</CCardBody>
</CCard>
{/* 추가/수정 모달 */}
<CModal visible={modalVisible} onClose={() => setModalVisible(false)} backdrop="static">
<CModalHeader>
<CModalTitle>{isEditMode ? '어드민 회원 수정' : '어드민 회원 추가'}</CModalTitle>
</CModalHeader>
<CModalBody>
<CForm>
<div className="mb-3">
<CFormLabel htmlFor="memberId"> ID *</CFormLabel>
<CFormInput
type="text"
id="memberId"
name="memberId"
value={formData.memberId}
onChange={handleInputChange}
disabled={isEditMode}
placeholder="어드민 회원 ID를 입력하세요"
/>
</div>
<div className="mb-3">
<CFormLabel htmlFor="memberName"> *</CFormLabel>
<CFormInput
type="text"
id="memberName"
name="memberName"
value={formData.memberName}
onChange={handleInputChange}
placeholder="이름을 입력하세요"
/>
</div>
<div className="mb-3">
<CFormLabel htmlFor="password">
{isEditMode ? '(변경 시에만 입력)' : '*'}
</CFormLabel>
<CFormInput
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleInputChange}
placeholder="비밀번호를 입력하세요"
/>
</div>
<div className="mb-3">
<CFormLabel htmlFor="passwordConfirm"> </CFormLabel>
<CFormInput
type="password"
id="passwordConfirm"
name="passwordConfirm"
value={formData.passwordConfirm}
onChange={handleInputChange}
placeholder="비밀번호를 다시 입력하세요"
/>
</div>
<div className="mb-3">
<CFormLabel htmlFor="level"></CFormLabel>
<CFormInput
type="number"
id="level"
name="level"
value={formData.level}
onChange={handleInputChange}
min={0}
placeholder="레벨을 입력하세요"
/>
</div>
<div className="mb-3">
<CFormLabel htmlFor="status"></CFormLabel>
<CFormSelect
id="status"
name="status"
value={formData.status}
onChange={handleInputChange}
>
{statusOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</CFormSelect>
</div>
</CForm>
</CModalBody>
<CModalFooter>
<CButton color="secondary" onClick={() => setModalVisible(false)}>
</CButton>
<CButton color="primary" onClick={handleSave}>
{isEditMode ? '수정' : '추가'}
</CButton>
</CModalFooter>
</CModal>
{/* 삭제 확인 모달 */}
<CModal visible={deleteModalVisible} onClose={() => setDeleteModalVisible(false)} backdrop="static">
<CModalHeader>
<CModalTitle> </CModalTitle>
</CModalHeader>
<CModalBody>
<div className="text-center">
<CIcon icon={cilWarning} size="3xl" className="text-danger mb-3" />
<p>
<strong>{selectedMember?.memberName}</strong> ({selectedMember?.memberId}) ?
</p>
<p className="text-muted small"> .</p>
</div>
</CModalBody>
<CModalFooter>
<CButton color="secondary" onClick={() => setDeleteModalVisible(false)}>
</CButton>
<CButton color="danger" onClick={handleDelete}>
</CButton>
</CModalFooter>
</CModal>
</>
);
};
export default AdminMemberManagement;

File diff suppressed because it is too large Load Diff

View File

@@ -1,210 +1,10 @@
import React from 'react' import React from 'react'
import classNames from 'classnames'
import {
CAvatar,
CButton,
CButtonGroup,
CCard,
CCardBody,
CCardFooter,
CCardHeader,
CCol,
CProgress,
CRow,
CTable,
CTableBody,
CTableDataCell,
CTableHead,
CTableHeaderCell,
CTableRow,
} from '@coreui/react'
import CIcon from '@coreui/icons-react'
import {
cibCcAmex,
cibCcApplePay,
cibCcMastercard,
cibCcPaypal,
cibCcStripe,
cibCcVisa,
cibGoogle,
cibFacebook,
cibLinkedin,
cifBr,
cifEs,
cifFr,
cifIn,
cifPl,
cifUs,
cibTwitter,
cilCloudDownload,
cilPeople,
cilUser,
cilUserFemale,
} from '@coreui/icons'
import avatar1 from 'src/assets/images/avatars/1.jpg'
import avatar2 from 'src/assets/images/avatars/2.jpg'
import avatar3 from 'src/assets/images/avatars/3.jpg'
import avatar4 from 'src/assets/images/avatars/4.jpg'
import avatar5 from 'src/assets/images/avatars/5.jpg'
import avatar6 from 'src/assets/images/avatars/6.jpg'
import MainChart from 'src/views/dashboard/MainChart'
const Dashboard = () => { const Dashboard = () => {
const tableExample = [
{
avatar: { src: avatar1, status: 'success' },
user: {
name: 'Yiorgos Avraamu',
new: true,
registered: 'Jan 1, 2023',
},
country: { name: 'USA', flag: cifUs },
usage: {
value: 50,
period: 'Jun 11, 2023 - Jul 10, 2023',
color: 'success',
},
payment: { name: 'Mastercard', icon: cibCcMastercard },
activity: '10 sec ago',
},
{
avatar: { src: avatar2, status: 'danger' },
user: {
name: 'Avram Tarasios',
new: false,
registered: 'Jan 1, 2023',
},
country: { name: 'Brazil', flag: cifBr },
usage: {
value: 22,
period: 'Jun 11, 2023 - Jul 10, 2023',
color: 'info',
},
payment: { name: 'Visa', icon: cibCcVisa },
activity: '5 minutes ago',
},
{
avatar: { src: avatar3, status: 'warning' },
user: { name: 'Quintin Ed', new: true, registered: 'Jan 1, 2023' },
country: { name: 'India', flag: cifIn },
usage: {
value: 74,
period: 'Jun 11, 2023 - Jul 10, 2023',
color: 'warning',
},
payment: { name: 'Stripe', icon: cibCcStripe },
activity: '1 hour ago',
},
{
avatar: { src: avatar4, status: 'secondary' },
user: { name: 'Enéas Kwadwo', new: true, registered: 'Jan 1, 2023' },
country: { name: 'France', flag: cifFr },
usage: {
value: 98,
period: 'Jun 11, 2023 - Jul 10, 2023',
color: 'danger',
},
payment: { name: 'PayPal', icon: cibCcPaypal },
activity: 'Last month',
},
{
avatar: { src: avatar5, status: 'success' },
user: {
name: 'Agapetus Tadeáš',
new: true,
registered: 'Jan 1, 2023',
},
country: { name: 'Spain', flag: cifEs },
usage: {
value: 22,
period: 'Jun 11, 2023 - Jul 10, 2023',
color: 'primary',
},
payment: { name: 'Google Wallet', icon: cibCcApplePay },
activity: 'Last week',
},
{
avatar: { src: avatar6, status: 'danger' },
user: {
name: 'Friderik Dávid',
new: true,
registered: 'Jan 1, 2023',
},
country: { name: 'Poland', flag: cifPl },
usage: {
value: 43,
period: 'Jun 11, 2023 - Jul 10, 2023',
color: 'success',
},
payment: { name: 'Amex', icon: cibCcAmex },
activity: 'Last week',
},
]
return ( return (
<> <div className="d-flex justify-content-center align-items-center" style={{ minHeight: '60vh' }}>
<CCard className="mb-4"> <h1 className="text-body-secondary">Welcome to Admin Sample</h1>
<CCardHeader>Traffic {' & '} Sales</CCardHeader>
<CCardBody>
<CTable align="middle" className="mb-0 border" hover responsive>
<CTableHead className="text-nowrap">
<CTableRow>
<CTableHeaderCell className="bg-body-tertiary text-center">
<CIcon icon={cilPeople} />
</CTableHeaderCell>
<CTableHeaderCell className="bg-body-tertiary">User</CTableHeaderCell>
<CTableHeaderCell className="bg-body-tertiary text-center">
Country
</CTableHeaderCell>
<CTableHeaderCell className="bg-body-tertiary">Usage</CTableHeaderCell>
<CTableHeaderCell className="bg-body-tertiary text-center">
Payment Method
</CTableHeaderCell>
<CTableHeaderCell className="bg-body-tertiary">Activity</CTableHeaderCell>
</CTableRow>
</CTableHead>
<CTableBody>
{tableExample.map((item, index) => (
<CTableRow v-for="item in tableItems" key={index}>
<CTableDataCell className="text-center">
<CAvatar size="md" src={item.avatar.src} status={item.avatar.status} />
</CTableDataCell>
<CTableDataCell>
<div>{item.user.name}</div>
<div className="small text-body-secondary text-nowrap">
<span>{item.user.new ? 'New' : 'Recurring'}</span> | Registered:{' '}
{item.user.registered}
</div> </div>
</CTableDataCell>
<CTableDataCell className="text-center">
<CIcon size="xl" icon={item.country.flag} title={item.country.name} />
</CTableDataCell>
<CTableDataCell>
<div className="d-flex justify-content-between text-nowrap">
<div className="fw-semibold">{item.usage.value}%</div>
<div className="ms-3">
<small className="text-body-secondary">{item.usage.period}</small>
</div>
</div>
<CProgress thin color={item.usage.color} value={item.usage.value} />
</CTableDataCell>
<CTableDataCell className="text-center">
<CIcon size="xl" icon={item.payment.icon} />
</CTableDataCell>
<CTableDataCell>
<div className="small text-body-secondary text-nowrap">Last login</div>
<div className="fw-semibold text-nowrap">{item.activity}</div>
</CTableDataCell>
</CTableRow>
))}
</CTableBody>
</CTable>
</CCardBody>
</CCard>
</>
) )
} }

View File

@@ -53,7 +53,11 @@ const Login = () => {
setError('쿠키에서 accessToken을 찾을 수 없습니다.'); setError('쿠키에서 accessToken을 찾을 수 없습니다.');
} }
} else { } else {
setError(response.resultMessage || '로그인에 실패했습니다.'); let errorMsg = response.resultMessage || '로그인에 실패했습니다.'
if (response.resultData && typeof response.resultData === 'string') {
errorMsg += `\n${response.resultData}`
}
setError(errorMsg)
} }
} catch (err: any) { } catch (err: any) {
setError(err.response?.data?.message || '로그인 중 오류가 발생했습니다.'); setError(err.response?.data?.message || '로그인 중 오류가 발생했습니다.');
@@ -65,16 +69,16 @@ const Login = () => {
return ( return (
<div className="bg-body-tertiary min-vh-100 d-flex flex-row align-items-center"> <div className="bg-body-tertiary min-vh-100 d-flex flex-row align-items-center">
<CContainer> <CContainer fluid>
<CRow className="justify-content-center"> <CRow className="justify-content-center">
<CCol md={8}> <CCol md={8}>
<CCardGroup> <CCardGroup style={{ minWidth: '300px' }}>
<CCard className="p-4"> <CCard className="p-4">
<CCardBody> <CCardBody>
<CForm onSubmit={handleSubmit}> <CForm onSubmit={handleSubmit}>
<h1>Login</h1> <h1>Login</h1>
<p className="text-body-secondary">Sign In to your account</p> <p className="text-body-secondary">Sign In to your account</p>
{error && <div className="text-danger mb-3">{error}</div>} {error && <div className="text-danger mb-3" style={{ whiteSpace: 'pre-line' }}>{error}</div>}
<CInputGroup className="mb-3"> <CInputGroup className="mb-3">
<CInputGroupText> <CInputGroupText>
<CIcon icon={cilUser} /> <CIcon icon={cilUser} />
@@ -108,16 +112,19 @@ const Login = () => {
</CForm> </CForm>
</CCardBody> </CCardBody>
</CCard> </CCard>
<CCard className="text-white bg-primary py-5"> <CCard className="text-white bg-primary py-5" style={{ minWidth: '300px' }}>
<CCardBody className="text-center"> <CCardBody className="text-center">
<div> <div>
<h2>Sign up</h2> <h2></h2>
<br/>
<p> <p>
, . <br/>
<br/>
.
</p> </p>
<Link to="/register"> <Link to="/admin/member/register">
<CButton color="primary" className="mt-3" active tabIndex={-1}> <CButton color="primary" className="mt-3" active tabIndex={-1}>
Register Now!
</CButton> </CButton>
</Link> </Link>
</div> </div>

View File

@@ -1,4 +1,5 @@
import React from 'react' import React, { useState, useEffect } from 'react'
import { useNavigate, Link } from 'react-router-dom'
import { import {
CButton, CButton,
CCard, CCard,
@@ -7,14 +8,79 @@ import {
CContainer, CContainer,
CForm, CForm,
CFormInput, CFormInput,
CInputGroup, CFormLabel,
CInputGroupText,
CRow, CRow,
CSpinner,
} from '@coreui/react' } from '@coreui/react'
import CIcon from '@coreui/icons-react' import { registerAdminMember } from 'src/services/adminMemberService'
import { cilLockLocked, cilUser } from '@coreui/icons' import { logout } from 'src/axios/authService'
const Register = () => { const Register = () => {
const navigate = useNavigate()
const [loading, setLoading] = useState(false)
// 회원가입 페이지 진입 시 기존 토큰 제거
useEffect(() => {
logout()
}, [])
const [formData, setFormData] = useState({
memberId: '',
memberName: '',
password: '',
passwordConfirm: '',
})
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target
setFormData(prev => ({
...prev,
[name]: value,
}))
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
// 유효성 검사
if (!formData.memberId.trim()) {
alert('어드민 회원 ID를 입력해주세요.')
return
}
if (!formData.memberName.trim()) {
alert('이름을 입력해주세요.')
return
}
if (!formData.password) {
alert('비밀번호를 입력해주세요.')
return
}
if (formData.password !== formData.passwordConfirm) {
alert('비밀번호가 일치하지 않습니다.')
return
}
setLoading(true)
try {
const response = await registerAdminMember({
memberId: formData.memberId,
memberName: formData.memberName,
password: formData.password,
})
if (response.resultCode === '200') {
alert('회원가입이 완료되었습니다. 관리자 승인 후 로그인이 가능합니다.')
navigate('/login')
} else {
alert(response.resultMessage || '회원가입에 실패했습니다.')
}
} catch (error) {
console.error('회원가입 실패:', error)
alert('회원가입에 실패했습니다.')
} finally {
setLoading(false)
}
}
return ( return (
<div className="bg-body-tertiary min-vh-100 d-flex flex-row align-items-center"> <div className="bg-body-tertiary min-vh-100 d-flex flex-row align-items-center">
<CContainer> <CContainer>
@@ -22,41 +88,78 @@ const Register = () => {
<CCol md={9} lg={7} xl={6}> <CCol md={9} lg={7} xl={6}>
<CCard className="mx-4"> <CCard className="mx-4">
<CCardBody className="p-4"> <CCardBody className="p-4">
<CForm> <CForm onSubmit={handleSubmit}>
<h1>Register</h1> <h1> </h1>
<p className="text-body-secondary">Create your account</p> <p className="text-body-secondary"> </p>
<CInputGroup className="mb-3">
<CInputGroupText> <div className="mb-3">
<CIcon icon={cilUser} /> <CFormLabel htmlFor="memberId"> ID *</CFormLabel>
</CInputGroupText> <CFormInput
<CFormInput placeholder="Username" autoComplete="username" /> type="text"
</CInputGroup> id="memberId"
<CInputGroup className="mb-3"> name="memberId"
<CInputGroupText>@</CInputGroupText> value={formData.memberId}
<CFormInput placeholder="Email" autoComplete="email" /> onChange={handleInputChange}
</CInputGroup> placeholder="어드민 회원 ID를 입력하세요"
<CInputGroup className="mb-3"> autoComplete="username"
<CInputGroupText> />
<CIcon icon={cilLockLocked} /> </div>
</CInputGroupText>
<div className="mb-3">
<CFormLabel htmlFor="memberName"> *</CFormLabel>
<CFormInput
type="text"
id="memberName"
name="memberName"
value={formData.memberName}
onChange={handleInputChange}
placeholder="이름을 입력하세요"
autoComplete="name"
/>
</div>
<div className="mb-3">
<CFormLabel htmlFor="password"> *</CFormLabel>
<CFormInput <CFormInput
type="password" type="password"
placeholder="Password" id="password"
name="password"
value={formData.password}
onChange={handleInputChange}
placeholder="비밀번호를 입력하세요"
autoComplete="new-password" autoComplete="new-password"
/> />
</CInputGroup> </div>
<CInputGroup className="mb-4">
<CInputGroupText> <div className="mb-3">
<CIcon icon={cilLockLocked} /> <CFormLabel htmlFor="passwordConfirm"> *</CFormLabel>
</CInputGroupText>
<CFormInput <CFormInput
type="password" type="password"
placeholder="Repeat password" id="passwordConfirm"
name="passwordConfirm"
value={formData.passwordConfirm}
onChange={handleInputChange}
placeholder="비밀번호를 다시 입력하세요"
autoComplete="new-password" autoComplete="new-password"
/> />
</CInputGroup> </div>
<div className="d-grid"> <div className="d-grid">
<CButton color="success">Create Account</CButton> <CButton color="success" type="submit" disabled={loading}>
{loading ? (
<>
<CSpinner size="sm" className="me-2" />
...
</>
) : (
'회원가입'
)}
</CButton>
</div>
<div className="text-center mt-3">
<span className="text-body-secondary"> ? </span>
<Link to="/login"></Link>
</div> </div>
</CForm> </CForm>
</CCardBody> </CCardBody>

View File

@@ -1,91 +0,0 @@
import React, { useEffect, useState, createRef } from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'
import { CRow, CCol, CCard, CCardHeader, CCardBody } from '@coreui/react'
import { rgbToHex } from '@coreui/utils'
import { DocsLink } from 'src/components'
const ThemeView = () => {
const [color, setColor] = useState('rgb(255, 255, 255)')
const ref = createRef()
useEffect(() => {
const el = ref.current.parentNode.firstChild
const varColor = window.getComputedStyle(el).getPropertyValue('background-color')
setColor(varColor)
}, [ref])
return (
<table className="table w-100" ref={ref}>
<tbody>
<tr>
<td className="text-body-secondary">HEX:</td>
<td className="font-weight-bold">{rgbToHex(color)}</td>
</tr>
<tr>
<td className="text-body-secondary">RGB:</td>
<td className="font-weight-bold">{color}</td>
</tr>
</tbody>
</table>
)
}
const ThemeColor = ({ className, children }) => {
const classes = classNames(className, 'theme-color w-75 rounded mb-3')
return (
<CCol xs={12} sm={6} md={4} xl={2} className="mb-4">
<div className={classes} style={{ paddingTop: '75%' }}></div>
{children}
<ThemeView />
</CCol>
)
}
ThemeColor.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
}
const Colors = () => {
return (
<>
<CCard className="mb-4">
<CCardHeader>
Theme colors
<DocsLink href="https://coreui.io/docs/utilities/colors/" />
</CCardHeader>
<CCardBody>
<CRow>
<ThemeColor className="bg-primary">
<h6>Brand Primary Color</h6>
</ThemeColor>
<ThemeColor className="bg-secondary">
<h6>Brand Secondary Color</h6>
</ThemeColor>
<ThemeColor className="bg-success">
<h6>Brand Success Color</h6>
</ThemeColor>
<ThemeColor className="bg-danger">
<h6>Brand Danger Color</h6>
</ThemeColor>
<ThemeColor className="bg-warning">
<h6>Brand Warning Color</h6>
</ThemeColor>
<ThemeColor className="bg-info">
<h6>Brand Info Color</h6>
</ThemeColor>
<ThemeColor className="bg-light">
<h6>Brand Light Color</h6>
</ThemeColor>
<ThemeColor className="bg-dark">
<h6>Brand Dark Color</h6>
</ThemeColor>
</CRow>
</CCardBody>
</CCard>
</>
)
}
export default Colors

View File

@@ -1,229 +0,0 @@
import React from 'react'
import { CCard, CCardHeader, CCardBody } from '@coreui/react'
import { DocsLink } from 'src/components'
const Typography = () => {
return (
<>
<CCard className="mb-4">
<CCardHeader>
Headings
<DocsLink href="https://coreui.io/docs/content/typography/" />
</CCardHeader>
<CCardBody>
<p>
Documentation and examples for Bootstrap typography, including global settings,
headings, body text, lists, and more.
</p>
<table className="table">
<thead>
<tr>
<th>Heading</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<p>
<code className="highlighter-rouge">&lt;h1&gt;&lt;/h1&gt;</code>
</p>
</td>
<td>
<span className="h1">h1. Bootstrap heading</span>
</td>
</tr>
<tr>
<td>
<p>
<code className="highlighter-rouge">&lt;h2&gt;&lt;/h2&gt;</code>
</p>
</td>
<td>
<span className="h2">h2. Bootstrap heading</span>
</td>
</tr>
<tr>
<td>
<p>
<code className="highlighter-rouge">&lt;h3&gt;&lt;/h3&gt;</code>
</p>
</td>
<td>
<span className="h3">h3. Bootstrap heading</span>
</td>
</tr>
<tr>
<td>
<p>
<code className="highlighter-rouge">&lt;h4&gt;&lt;/h4&gt;</code>
</p>
</td>
<td>
<span className="h4">h4. Bootstrap heading</span>
</td>
</tr>
<tr>
<td>
<p>
<code className="highlighter-rouge">&lt;h5&gt;&lt;/h5&gt;</code>
</p>
</td>
<td>
<span className="h5">h5. Bootstrap heading</span>
</td>
</tr>
<tr>
<td>
<p>
<code className="highlighter-rouge">&lt;h6&gt;&lt;/h6&gt;</code>
</p>
</td>
<td>
<span className="h6">h6. Bootstrap heading</span>
</td>
</tr>
</tbody>
</table>
</CCardBody>
</CCard>
<CCard className="mb-4">
<CCardHeader>Headings</CCardHeader>
<CCardBody>
<p>
<code className="highlighter-rouge">.h1</code> through
<code className="highlighter-rouge">.h6</code>
classes are also available, for when you want to match the font styling of a heading but
cannot use the associated HTML element.
</p>
<div className="bd-example">
<p className="h1">h1. Bootstrap heading</p>
<p className="h2">h2. Bootstrap heading</p>
<p className="h3">h3. Bootstrap heading</p>
<p className="h4">h4. Bootstrap heading</p>
<p className="h5">h5. Bootstrap heading</p>
<p className="h6">h6. Bootstrap heading</p>
</div>
</CCardBody>
</CCard>
<CCard className="mb-4">
<div className="card-header">Display headings</div>
<div className="card-body">
<p>
Traditional heading elements are designed to work best in the meat of your page content.
When you need a heading to stand out, consider using a <strong>display heading</strong>
a larger, slightly more opinionated heading style.
</p>
<div className="bd-example bd-example-type">
<table className="table">
<tbody>
<tr>
<td>
<span className="display-1">Display 1</span>
</td>
</tr>
<tr>
<td>
<span className="display-2">Display 2</span>
</td>
</tr>
<tr>
<td>
<span className="display-3">Display 3</span>
</td>
</tr>
<tr>
<td>
<span className="display-4">Display 4</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</CCard>
<CCard className="mb-4">
<CCardHeader>Inline text elements</CCardHeader>
<CCardBody>
<p>
Traditional heading elements are designed to work best in the meat of your page content.
When you need a heading to stand out, consider using a <strong>display heading</strong>
a larger, slightly more opinionated heading style.
</p>
<div className="bd-example">
<p>
You can use the mark tag to <mark>highlight</mark> text.
</p>
<p>
<del>This line of text is meant to be treated as deleted text.</del>
</p>
<p>
<s>This line of text is meant to be treated as no longer accurate.</s>
</p>
<p>
<ins>This line of text is meant to be treated as an addition to the document.</ins>
</p>
<p>
<u>This line of text will render as underlined</u>
</p>
<p>
<small>This line of text is meant to be treated as fine print.</small>
</p>
<p>
<strong>This line rendered as bold text.</strong>
</p>
<p>
<em>This line rendered as italicized text.</em>
</p>
</div>
</CCardBody>
</CCard>
<CCard className="mb-4">
<CCardHeader>Description list alignment</CCardHeader>
<CCardBody>
<p>
Align terms and descriptions horizontally by using our grid systems predefined classes
(or semantic mixins). For longer terms, you can optionally add a{' '}
<code className="highlighter-rouge">.text-truncate</code> class to truncate the text
with an ellipsis.
</p>
<div className="bd-example">
<dl className="row">
<dt className="col-sm-3">Description lists</dt>
<dd className="col-sm-9">A description list is perfect for defining terms.</dd>
<dt className="col-sm-3">Euismod</dt>
<dd className="col-sm-9">
<p>
Vestibulum id ligula porta felis euismod semper eget lacinia odio sem nec elit.
</p>
<p>Donec id elit non mi porta gravida at eget metus.</p>
</dd>
<dt className="col-sm-3">Malesuada porta</dt>
<dd className="col-sm-9">Etiam porta sem malesuada magna mollis euismod.</dd>
<dt className="col-sm-3 text-truncate">Truncated term is truncated</dt>
<dd className="col-sm-9">
Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut
fermentum massa justo sit amet risus.
</dd>
<dt className="col-sm-3">Nesting</dt>
<dd className="col-sm-9">
<dl className="row">
<dt className="col-sm-4">Nested definition list</dt>
<dd className="col-sm-8">
Aenean posuere, tortor sed cursus feugiat, nunc augue blandit nunc.
</dd>
</dl>
</dd>
</dl>
</div>
</CCardBody>
</CCard>
</>
)
}
export default Typography