안경잡이개발자

728x90
반응형

1. 텍스트 가지치기 모듈 다운로드


  yarn add react-text-truncate


  (설치가 안 된다면, 개발 환경을 관리자 권한으로 실행해보세요.)



2. 소스코드 작성하기


▶ ./src/components/Texts.js


import TextTruncate from 'react-text-truncate';
import React from 'react';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import { withStyles } from '@material-ui/core/styles';
import Grid from '@material-ui/core/Grid';
import Button from '@material-ui/core/Button';
import Typography from '@material-ui/core/Typography';
import Fab from '@material-ui/core/Fab';
import AddIcon from '@material-ui/icons/Add';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle';
import TextField from '@material-ui/core/TextField';
import { Link as RouterLink } from 'react-router-dom';
import Link from '@material-ui/core/Link';

const styles = theme => ({
hidden: {
display: 'none'
},
fab: {
position: 'fixed',
bottom: '20px',
right: '20px'
},
});

const databaseURL = "https://react-example-55161.firebaseio.com/";

class Texts extends React.Component {

constructor(props) {
super(props);
this.state = {
fileName: '',
fileContent: null,
texts: '',
textName: '',
dialog: false
}
}

_get() {
fetch(`${databaseURL}/texts.json`).then(res => {
if(res.status != 200) {
throw new Error(res.statusText);
}
return res.json();
}).then(texts => this.setState({texts: (texts == null) ? {} : texts}));
}

_post(text) {
return fetch(`${databaseURL}/texts.json`, {
method: 'POST',
body: JSON.stringify(text)
}).then(res => {
if(res.status != 200) {
throw new Error(res.statusText);
}
return res.json();
}).then(data => {
let nextState = this.state.texts;
nextState[data.name] = text;
this.setState({texts: nextState});
});
}

_delete(id) {
return fetch(`${databaseURL}/texts/${id}.json`, {
method: 'DELETE'
}).then(res => {
if(res.status != 200) {
throw new Error(res.statusText);
}
return res.json();
}).then(() => {
let nextState = this.state.texts;
delete nextState[id];
this.setState({texts: nextState});
});
}

componentDidMount() {
this._get();
}

handleDialogToggle = () => this.setState({
dialog: !this.state.dialog,
fileName: '',
fileContent: '',
textName: ''
})

handleValueChange = (e) => {
let nextState = {};
nextState[e.target.name] = e.target.value;
this.setState(nextState);
}

handleSubmit = () => {
const text = {
textName: this.state.textName,
textContent: this.state.fileContent
}
this.handleDialogToggle();
if (!text.textName || !text.textContent) {
return;
}
this._post(text);
}

handleDelete = (id) => {
this._delete(id);
}

handleFileChange = (e) => {
let reader = new FileReader();
reader.onload = () => {
let text = reader.result;
this.setState({
fileContent: text
})
}
reader.readAsText(e.target.files[0], "EUC-KR");
this.setState({
fileName: e.target.value
});
}

render() {
const { classes } = this.props;
return (
<div>
{Object.keys(this.state.texts).map(id => {
const text = this.state.texts[id];
return (
<Card key={id}>
<CardContent>
<Typography color="textSecondary" gutterBottom>
내용: {text.textContent.substring(0, 24) + '...'}
</Typography>
<Grid container>
<Grid item xs={6}>
<Typography variant="h5" component="h2">
{text.textName.substring(0, 14) + '...'}
</Typography>
</Grid>
<Grid item xs={3}>
<Typography variant="h5" component="h2">
<Link component={RouterLink} to={"detail/" + id}>
<Button variant="contained" color="primary">보기</Button>
</Link>
</Typography>
</Grid>
<Grid item xs={3}>
<Button variant="contained" color="primary" onClick={() => this.handleDelete(id)}>삭제</Button>
</Grid>
</Grid>
</CardContent>
</Card>
);
})}
<Fab color="primary" className={classes.fab} onClick={this.handleDialogToggle}>
<AddIcon />
</Fab>
<Dialog open={this.state.dialog} onClose={this.handleDialogToggle}>
<DialogTitle>텍스트 추가</DialogTitle>
<DialogContent>
<TextField label="텍스트 이름" type="text" name="textName" value={this.state.textName} onChange={this.handleValueChange} /><br /><br />
<input className={classes.hidden} accept="text/plain" id="raised-button-file" type="file" file={this.state.file} value={this.state.fileName} onChange={this.handleFileChange} />
<label htmlFor="raised-button-file">
<Button variant="contained" color="primary" component="span" name="file">
{this.state.fileName === ''? ".txt 파일 선택" : this.state.fileName}
</Button>
</label>
<TextTruncate
line={1}
truncateText="..."
text={this.state.fileContent}
/>
</DialogContent>
<DialogActions>
<Button variant="contained" color="primary" onClick={this.handleSubmit}>추가</Button>
<Button variant="outlined" color="primary" onClick={this.handleDialogToggle}>닫기</Button>
</DialogActions>
</Dialog>
</div>
);
}
}

export default withStyles(styles)(Texts);


▶ ./src/components/Detail.js


import React from 'react';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';

class Detail extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<Card>
<CardContent>
{this.props.match.params.textID}
</CardContent>
</Card>
);
}
}

export default Detail;


▶ ./src/components/App.js


import React from 'react';
import { HashRouter as Router, Route } from 'react-router-dom';
import AppShell from './AppShell';
import Home from './Home';
import Texts from './Texts';
import Words from './Words';
import Detail from './Detail';

class App extends React.Component {
render() {
return (
<Router>
<AppShell>
<div>
<Route exact path="/" component={Home}/>
<Route exact path="/texts" component={Texts}/>
<Route exact path="/words" component={Words}/>
<Route exact path="/detail/:textID" component={Detail}/>
</div>
</AppShell>
</Router>
);
}
}

export default App;


※ 실행 결과 ※


  텍스트 페이지에서 텍스트 파일을 올려서 내용을 확인할 수 있습니다.





  [보기] 버튼을 누르면 상세 페이지로 이동됩니다. 현재는 ID 값만 보내도록 처리했습니다.



728x90
반응형

728x90
반응형

1. 빌드(Build)를 위한 웹팩 설정



'use strict'
const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = {
entry: {
main: ['./src/main.js']
},
output: {
path: path.resolve(__dirname, './build'),
filename: '[name].js'
},
module: {
rules: [{
test: /\.js$/,
include: path.resolve(__dirname, './src'),
loaders: 'babel-loader'
}]
},
plugins: [
new CopyWebpackPlugin([{
context: './public',
from: '*.*'
}]),
],
devServer: {
contentBase: './public',
host: 'localhost',
port: 8080
}
}


2. 각종 모듈 재설치


  (필요할 시 수행하세요.)




3. 파이어베이스(Firebase) 패키지 설치 및 설정하기


  yarn add firebase-tools 명령으로 설치할 수 있습니다.



  이 때 firebase init 명령이 동작하지 않으면, 관리자 권한으로 에디터를 실행하세요. 또한 firebase 계정을 변경하고 싶을 때는 firebase logout 및 firebase login 명령을 이용하시면 됩니다.





4. 기존의 build 폴더를 삭제한 뒤에 yarn build으로 재빌드하기



5. 파이어베이스 호스팅 서비스를 이용해 배포하기



6. .gitignore 파일 수정 및 푸시(Push)하기


node_modules
build
.firebase/
firebase.json
.firebaserc


※ 배포 완료 ※



728x90
반응형

728x90
반응형

1. 지난 소스코드 복습하기


2. 소스코드 수정하기


▶ AppShell.js


<MenuItem onClick={this.handleDrawerToggle}>
<Link component={RouterLink} to="/">
홈 화면
</Link>
</MenuItem>
<MenuItem onClick={this.handleDrawerToggle}>
<Link component={RouterLink} to="/texts">
텍스트 관리
</Link>
</MenuItem>
<MenuItem onClick={this.handleDrawerToggle}>
<Link component={RouterLink} to="/words">
단어 관리
</Link>
</MenuItem>


▶ Words.js


import React from 'react';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import Typography from '@material-ui/core/Typography';
import { withStyles } from '@material-ui/core/styles';
import Grid from '@material-ui/core/Grid';
import Button from '@material-ui/core/Button';
import Fab from '@material-ui/core/Fab';
import AddIcon from '@material-ui/icons/Add';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle';
import TextField from '@material-ui/core/TextField';

const styles = theme => ({
fab: {
position: 'fixed',
bottom: '20px',
right: '20px'
},
});

const databaseURL = "https://react-example-55161.firebaseio.com/";

class Words extends React.Component {
constructor() {
super();
this.state = {
words: {},
dialog: false,
word: '',
weight: ''
};
}

_get() {
fetch(`${databaseURL}/words.json`).then(res => {
if(res.status != 200) {
throw new Error(res.statusText);
}
return res.json();
}).then(words => this.setState({words: words}));
}

_post(word) {
return fetch(`${databaseURL}/words.json`, {
method: 'POST',
body: JSON.stringify(word)
}).then(res => {
if(res.status != 200) {
throw new Error(res.statusText);
}
return res.json();
}).then(data => {
let nextState = this.state.words;
nextState[data.name] = word;
this.setState({words: nextState});
});
}

_delete(id) {
return fetch(`${databaseURL}/words/${id}.json`, {
method: 'DELETE'
}).then(res => {
if(res.status != 200) {
throw new Error(res.statusText);
}
return res.json();
}).then(() => {
let nextState = this.state.words;
delete nextState[id];
this.setState({words: nextState});
});
}

componentDidMount() {
this._get();
}

handleDialogToggle = () => this.setState({
dialog: !this.state.dialog
})

handleValueChange = (e) => {
let nextState = {};
nextState[e.target.name] = e.target.value;
this.setState(nextState);
}

handleSubmit = () => {
const word = {
word: this.state.word,
weight: this.state.weight
}
this.handleDialogToggle();
if (!word.word && !word.weight) {
return;
}
this._post(word);
}

handleDelete = (id) => {
this._delete(id);
}

render() {
const { classes } = this.props;
return (
<div>
{Object.keys(this.state.words).map(id => {
const word = this.state.words[id];
return (
<div key={id}>
<Card>
<CardContent>
<Typography color="textSecondary" gutterBottom>
가중치: {word.weight}
</Typography>
<Grid container>
<Grid item xs={6}>
<Typography variant="h5" component="h2">
{word.word}
</Typography>
</Grid>
<Grid item xs={6}>
<Button variant="contained" color="primary" onClick={() => this.handleDelete(id)}>삭제</Button>
</Grid>
</Grid>
</CardContent>
</Card>
<br />
</div>
);
})}
<Fab color="primary" className={classes.fab} onClick={this.handleDialogToggle}>
<AddIcon />
</Fab>
<Dialog open={this.state.dialog} onClose={this.handleDialogToggle}>
<DialogTitle>단어 추가</DialogTitle>
<DialogContent>
<TextField label="단어" type="text" name="word" value={this.state.word} onChange={this.handleValueChange}/><br/>
<TextField label="가중치" type="number" name="weight" value={this.state.weight} onChange={this.handleValueChange}/><br/>
</DialogContent>
<DialogActions>
<Button variant="contained" color="primary" onClick={this.handleSubmit}>추가</Button>
<Button variant="outlined" color="primary" onClick={this.handleDialogToggle}>닫기</Button>
</DialogActions>
</Dialog>
</div>
);
}
}

export default withStyles(styles)(Words);


※ 실행 결과 ※





728x90
반응형

728x90
반응형

▶ 구글 파이어베이스 콘솔: https://console.firebase.google.com/


1. 프로젝트 생성하기




2. 데이터베이스 페이지 확인



3. 데이터베이스에 단어(Word) 데이터 구축


  (단, 원래는 ID 값으로 0, 1, 2와 같은 단순한 숫자를 넣지 않습니다. 이는 예시 데이터를 구성하기 위해 넣은 것이며, 실제 DB 연동 작업을 마쳤을 때에는 이러한 예시 데이터를 삭제해야 오류가 발생하지 않습니다.)



4. 규칙(Rule) 설정을 통한 외부 접속 허용



5. 단어(Word) API 호출 테스트



▶ ./src/components/Words.js


import React from 'react';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import Typography from '@material-ui/core/Typography';

const databaseURL = "https://react-example-55161.firebaseio.com/";

class Words extends React.Component {
constructor() {
super();
this.state = {
words: {}
};
}

_get() {
fetch(`${databaseURL}/words.json`).then(res => {
if(res.status != 200) {
throw new Error(res.statusText);
}
return res.json();
}).then(words => this.setState({words: words}));
}

shouldComponentUpdate(nextProps, nextState) {
return nextState.words != this.state.words
}

componentDidMount() {
this._get();
}

render() {
return (
<div>
{Object.keys(this.state.words).map(id => {
const word = this.state.words[id];
return (
<div key={id}>
<Card>
<CardContent>
<Typography color="textSecondary" gutterBottom>
가중치: {word.weight}
</Typography>
<Typography variant="h5" component="h2">
{word.word}
</Typography>
</CardContent>
</Card>
<br/>
</div>
);
})}
</div>

);
}
}

export default Words;


※ 실행 결과 ※


728x90
반응형

728x90
반응형

  가장 먼저 라우팅 기능을 위해 라이브러리를 설치합니다.



▶ ./src/components/Home.js


import React from 'react';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';

class Home extends React.Component {
render() {
return (
<Card>
<CardContent>
React 및 Firebase 기반의 워드 클라우드 프로젝트
</CardContent>
</Card>
);
}
}

export default Home;


▶ ./src/components/Texts.js


import React from 'react';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';

class Texts extends React.Component {
render() {
return (
<Card>
<CardContent>
Texts 페이지
</CardContent>
</Card>
);
}
}

export default Texts;


▶ ./src/components/Words.js


import React from 'react';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';

class Words extends React.Component {
render() {
return (
<Card>
<CardContent>
Words 페이지
</CardContent>
</Card>
);
}
}

export default Words;


▶ ./src/components/App.js


import React from 'react';
import { HashRouter as Router, Route } from 'react-router-dom';
import AppShell from './AppShell';
import Home from './Home';
import Texts from './Texts';
import Words from './Words';

class App extends React.Component {
render() {
return (
<Router>
<AppShell>
<div>
<Route exact path="/" component={Home}/>
<Route exact path="/texts" component={Texts}/>
<Route exact path="/words" component={Words}/>
</div>
</AppShell>
</Router>
);
}
}

export default App;


▶ ./src/components/AppShell.js


import React from 'react';
import { Link as RouterLink } from 'react-router-dom';
import Link from '@material-ui/core/Link';
import { withStyles } from '@material-ui/core/styles';
import AppBar from '@material-ui/core/AppBar';
import Drawer from '@material-ui/core/Drawer';
import MenuItem from '@material-ui/core/MenuItem';
import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu';

const styles = {
root: {
flexGrow: 1,
},
menuButton: {
marginRight: 'auto'
},
};

class AppShell extends React.Component {
constructor(props) {
super(props);
this.state = {
toggle: false
};
}
handleDrawerToggle = () => this.setState({toggle: !this.state.toggle})
render() {
const { classes } = this.props;
return (
<div>
<div className={classes.root}>
<AppBar position="static">
<IconButton className={classes.menuButton} color="inherit" onClick={this.handleDrawerToggle}>
<MenuIcon/>
</IconButton>
</AppBar>
<Drawer open={this.state.toggle}>
<MenuItem onClick={this.handleDrawerToggle}>
<Link component={RouterLink} to="/">
Home
</Link>
</MenuItem>
<MenuItem onClick={this.handleDrawerToggle}>
<Link component={RouterLink} to="/texts">
Texts
</Link>
</MenuItem>
<MenuItem onClick={this.handleDrawerToggle}>
<Link component={RouterLink} to="/words">
Words
</Link>
</MenuItem>
</Drawer>
</div>
<div id="content" style={{margin: 'auto', marginTop: '20px'}}>
{React.cloneElement(this.props.children)}
</div>
</div>
);
}
}

export default withStyles(styles)(AppShell);


※ 실행 결과 ※





728x90
반응형

728x90
반응형

  가장 먼저 material-ui 라이브러리를 설치합니다.


  yarn add @material-ui/core

  yarn add @material-ui/icons


  이후에 내비게이션 바의 틀을 만들 수 있습니다.


▶ ./src/components/Appshell.js


import React from 'react';
import { withStyles } from '@material-ui/core/styles';
import AppBar from '@material-ui/core/AppBar';
import Drawer from '@material-ui/core/Drawer';
import MenuItem from '@material-ui/core/MenuItem';
import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu';

const styles = {
root: {
flexGrow: 1,
},
menuButton: {
marginRight: 'auto'
},
};

class AppShell extends React.Component {
constructor(props) {
super(props);
this.state = {
toggle: false
};
}
handleDrawerToggle = () => this.setState({toggle: !this.state.toggle})
render() {
const { classes } = this.props;
return (
<div className={classes.root}>
<AppBar position="static">
<IconButton className={classes.menuButton} color="inherit" onClick={this.handleDrawerToggle}>
<MenuIcon/>
</IconButton>
</AppBar>
<Drawer open={this.state.toggle}>
<MenuItem onClick={this.handleDrawerToggle}>Home</MenuItem>
</Drawer>
</div>
);
}
}

export default withStyles(styles)(AppShell);


▶ ./src/components/App.js


import React from 'react';
import AppShell from './AppShell';

class App extends React.Component {
render() {
return (
<AppShell/>
);
}
}

export default App;


※ 실행 결과 ※


1. 내비게이션 바(Close)



2. 내비게이션 바(Open)



728x90
반응형

728x90
반응형

1.  VSC(Visual Studio Code) 개발 환경을 열어서 특정한 폴더(Folder)를 열어 줍니다.


  (이 때 가능하면, 관리자 권한으로 개발환경을 열 수 있도록 합니다.)



2. yarn init 명령어를 통해 패키지(Package) JSON 파일을 생성합니다.



3. yarn add 명령어를 통해 리액트 라이브러리를 설치합니다.


  리액트 개발을 위하여 react와 react-dom이 모두 필요합니다.



4. yarn add 명령어를 통해 webpack을 설치합니다.


  webpack을 이용해 추후에 앱을 배포할 수 있어요. 또한 webpack-dev-server를 이용해 실시간 로딩으로 빠르게 개발할 수 있습니다.



5. yarn add 명령어를 통해 바벨(Babel)을 설치합니다.


  바벨 또한 개발 환경에서 필요하기 때문에 --dev 옵션으로 설치합니다.



6. yarn add 명령어를 통해 웹팩(Webpack) 클라이언트 모듈을 설치합니다.


  yarn add --dev webpack-cli


  (--dev 혹은 -D라고 옵션을 붙여서 개발 종속성으로 설치합니다.)



7. 소스코드를 작성하기 위해 다음과 같이 프로젝트를 구성합니다.



▶ index.html


<!DOCTYPE html>
<html>
<head>
<title>Word Cloud Project</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#E2E2E2">
</head>
<body>
<div id="app"></div>
<script type="text/javascript" src="main.js"></script>
</body>
</html>


▶ App.js


import React from 'react';

class App extends React.Component {
render() {
return (
<div>
<h3>Hello World</h3>
</div>
);
}
}

export default App;


▶ main.js


import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';

ReactDOM.render(<App/>, document.getElementById('app'));


▶ webpack.config.js


'use strict'
const path = require('path');

module.exports = {
entry: {
main: ['./src/main.js']
},
output: {
path: path.resolve(__dirname, './build'),
filename: '[name].js'
},
module: {
rules: [{
test: /\.js$/,
include: path.resolve(__dirname, './src'),
loaders: 'babel-loader'
}]
},
plugins: [],
devServer: {
contentBase: './public',
host: 'localhost',
port: 8080
}
}


▶ .babelrc


{
"presets": ["react-app"]
}


▶ package.json


{
"name": "ReactProject",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"react": "^16.8.6",
"react-dom": "^16.8.6"
},
"scripts": {
"start": "NODE_ENV=development webpack-dev-server",
"build": "NODE_ENV=production webpack -p"
},
"devDependencies": {
"babel-core": "^6.26.3",
"babel-loader": "^8.0.5",
"babel-preset-react-app": "^7.0.2",
"webpack": "^4.30.0",
"webpack-cli": "^3.3.0",
"webpack-dev-server": "^3.3.1"
}
}


윈도우의 경우에는 다음과 같은 방식으로 start 및 build에 대한 옵션을 설정합니다.


"scripts": {
"start": "set NODE_ENV=development&&webpack-dev-server",
"build": "set NODE_ENV=production&&webpack -p"
},


8. yarn start 명령어를 이용해 프로젝트를 동작시킵니다.



9. 실행 결과를 통해 구성된 프로젝트 확인



10. 구성된 프로젝트를 깃 허브(Git Hub)에 올리기


▶ .gitignore 파일 생성 및 소스코드 작성


node_modules
build


11. 깃 허브(Git Hub)에 리포지터리 생성



12. 생성된 리포지터리의 주소 복사



13. 터미널(Terminal)에서 Git 명령어로 소스코드 업로드


git init

git add .

git commit -m "Initialize React Project"

git remote add origin https://github.com/ndb796/ReactWordCloudWebApp-React.git

git push --set-upstream origin master


728x90
반응형

728x90
반응형

  이번 시간에는 안정적인 UI/UX를 위해 AppBar을 적용하는 시간을 가져보도록 하겠습니다. AppBar는 검색 바, 내비게이션 바 등의 목적으로 사용됩니다. 따라서 가장 먼저 client 폴더로 이동해서 다음과 같은 명령어를 입력해서 icons 라이브러리를 받으면 됩니다.


  ▶ Material UI icons 다운로드 명령어: npm i @material-ui/icons


  이제 코딩을 진행해보도록 하겠습니다. 우리가 적용할 AppBar는 React.js의 Material UI 공식 사이트에서 제공하고 있는 기본 예제입니다. 말 그대로 기본 예제라는 점에서 한글 웹 폰트(Web Font) 등은 적용되어 있지 않습니다. 따라서 우리가 직접 웹 폰트 등을 적용해야 합니다.


  ▶ Material UI AppBar 예제: https://material-ui.com/demos/app-bar/


  우리는 아래에 보이는 App Bar with search field 예제를 넣어보도록 하겠습니다.



※ AppBar 적용하기 ※


▶ App.js


import React, { Component } from 'react';
import Customer from './components/Customer'
import './App.css';
import Paper from '@material-ui/core/Paper';
import Table from '@material-ui/core/Table';
import TableHead from '@material-ui/core/TableHead';
import TableBody from '@material-ui/core/TableBody';
import TableRow from '@material-ui/core/TableRow';
import TableCell from '@material-ui/core/TableCell';
import CircularProgress from '@material-ui/core/CircularProgress';
import { withStyles } from '@material-ui/core/styles';
import CustomerAdd from './components/CustomerAdd';
import MenuIcon from '@material-ui/icons/Menu';
import SearchIcon from '@material-ui/icons/Search';
import InputBase from '@material-ui/core/InputBase';
import { fade } from '@material-ui/core/styles/colorManipulator';
import Typography from '@material-ui/core/Typography';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import IconButton from '@material-ui/core/IconButton';

const styles = theme => ({
root: {
width: "100%",
minWidth: 1080
},
menu: {
marginTop: 15,
marginBottom: 15,
display: 'flex',
justifyContent: 'center'
},
paper: {
marginLeft: 18,
marginRight: 18
},
progress: {
margin: theme.spacing.unit * 2
},
grow: {
flexGrow: 1,
},
tableHead: {
fontSize: '1.0rem'
},
menuButton: {
marginLeft: -12,
marginRight: 20,
},
title: {
display: 'none',
[theme.breakpoints.up('sm')]: {
display: 'block',
},
},
search: {
position: 'relative',
borderRadius: theme.shape.borderRadius,
backgroundColor: fade(theme.palette.common.white, 0.15),
'&:hover': {
backgroundColor: fade(theme.palette.common.white, 0.25),
},
marginLeft: 0,
width: '100%',
[theme.breakpoints.up('sm')]: {
marginLeft: theme.spacing.unit,
width: 'auto',
},
},
searchIcon: {
width: theme.spacing.unit * 9,
height: '100%',
position: 'absolute',
pointerEvents: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
inputRoot: {
color: 'inherit',
width: '100%',
},
inputInput: {
paddingTop: theme.spacing.unit,
paddingRight: theme.spacing.unit,
paddingBottom: theme.spacing.unit,
paddingLeft: theme.spacing.unit * 10,
transition: theme.transitions.create('width'),
width: '100%',
[theme.breakpoints.up('sm')]: {
width: 120,
'&:focus': {
width: 200,
},
},
}
});

class App extends Component {

constructor(props) {
super(props);
this.state = {
customers: '',
completed: 0
}
this.stateRefresh = this.stateRefresh.bind(this);
}

stateRefresh() {
this.setState({
customers: '',
completed: 0
});
this.callApi()
.then(res => this.setState({customers: res}))
.catch(err => console.log(err));
}

componentDidMount() {
this.timer = setInterval(this.progress, 20);
this.callApi()
.then(res => this.setState({customers: res}))
.catch(err => console.log(err));
}

componentWillUnmount() {
clearInterval(this.timer);
}

callApi = async () => {
const response = await fetch('/api/customers');
const body = await response.json();
return body;
}

progress = () => {
const { completed } = this.state;
this.setState({ completed: completed >= 100 ? 0 : completed + 1 });
};

render() {
const { classes } = this.props;
const cellList = ["번호", "프로필 이미지", "이름", "생년월일", "성별", "직업", "설정"]
return (
<div className={classes.root}>
<AppBar position="static">
<Toolbar>
<IconButton className={classes.menuButton} color="inherit" aria-label="Open drawer">
<MenuIcon />
</IconButton>
<Typography className={classes.title} variant="h6" color="inherit" noWrap>
고객 관리 시스템
</Typography>
<div className={classes.grow} />
<div className={classes.search}>
<div className={classes.searchIcon}>
<SearchIcon />
</div>
<InputBase
placeholder="검색하기"
classes={{
root: classes.inputRoot,
input: classes.inputInput,
}}
/>
</div>
</Toolbar>
</AppBar>
<div className={classes.menu}>
<CustomerAdd stateRefresh={this.stateRefresh} />
</div>
<Paper className={classes.paper}>
<Table>
<TableHead>
<TableRow>
{cellList.map(c => {
return <TableCell className={classes.tableHead}>{c}</TableCell>
})}
</TableRow>
</TableHead>
<TableBody>
{this.state.customers ?
this.state.customers.map(c => {
return <Customer stateRefresh={this.stateRefresh} key={c.id} id={c.id} image={c.image} name={c.name} birthday={c.birthday} gender={c.gender} job={c.job} />
}) :
<TableRow>
<TableCell colSpan="6" align="center">
<CircularProgress className={classes.progress} variant="determinate" value={this.state.completed} />
</TableCell>
</TableRow>
}
</TableBody>
</Table>
</Paper>
</div>
);
}
}

export default withStyles(styles)(App);


▶ index.css


  index.css에서는 Note Sans KR 글씨체를 웹 폰트 형태로 불러오게 됩니다.


@import url(http://fonts.googleapis.com/earlyaccess/notosanskr.css);

body {
margin: 0;
padding: 0;
font-family: "Noto Sans KR", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}


▶ index.js


  이제 index.js에서는 Material UI의 테마 폰트로 "Note Sans KR"을 적용주면 됩니다. 이렇게 해주지 않으면 곳곳에 사용된 Material UI에 전체적인 폰트 적용이 안 될 수도 있습니다.


import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles';

const theme = createMuiTheme({
typography: {
fontFamily: '"Noto Sans KR", serif',
},
});

ReactDOM.render(<MuiThemeProvider theme={theme}><App /></MuiThemeProvider>, document.getElementById('root'));

serviceWorker.unregister();


※ 실행 결과 ※


  결과적으로 다음과 같이 전체적인 UI가 Material한 형태로 업데이트 되었습니다.



728x90
반응형

728x90
반응형

  이번 시간에는 모달(Modal) 기능을 이용해서 고객 추가(Customer Add) 기능을 모달 창에서 띄우는 방법에 대해서 알아보도록 하겠습니다. 따라서 Material UI의 Dialog를 import하여 디자인 요소를 활용해야 합니다. 우리는 모달 중에서 다이얼로그(Dialog)를 활용할 것이며 이를 위해서 Dialog 컴포넌트를 사용해야 합니다.


▶ CustomerAdd.js


import React from 'react'

import { post } from 'axios';

import Dialog from '@material-ui/core/Dialog';

import DialogActions from '@material-ui/core/DialogActions';

import DialogContent from '@material-ui/core/DialogContent';

import DialogTitle from '@material-ui/core/DialogTitle';

import TextField from '@material-ui/core/TextField';

import Button from '@material-ui/core/Button';

import { withStyles } from '@material-ui/core/styles';


const styles = theme => ({

hidden: {

display: 'none'

}

});


class CustomerAdd extends React.Component {


constructor(props) {

super(props);

this.state = {

file: null,

userName: '',

birthday: '',

gender: '',

job: '',

fileName: '',

open: false

}

this.handleFormSubmit = this.handleFormSubmit.bind(this)

this.handleFileChange = this.handleFileChange.bind(this)

this.handleValueChange = this.handleValueChange.bind(this)

this.addCustomer = this.addCustomer.bind(this)

this.handleClickOpen = this.handleClickOpen.bind(this)

this.handleClose = this.handleClose.bind(this);

}


handleFormSubmit(e) {

e.preventDefault()

this.addCustomer()

.then((response) => {

console.log(response.data);

this.props.stateRefresh();

})

this.setState({

file: null,

userName: '',

birthday: '',

gender: '',

job: '',

fileName: '',

open: false

})

}


handleFileChange(e) {

this.setState({

file: e.target.files[0],

fileName: e.target.value

});

}

handleValueChange(e) {

let nextState = {};

nextState[e.target.name] = e.target.value;

this.setState(nextState);

}


addCustomer(){

const url = '/api/customers';

const formData = new FormData();

formData.append('image', this.state.file)

formData.append('name', this.state.userName)

formData.append('birthday', this.state.birthday)

formData.append('gender', this.state.gender)

formData.append('job', this.state.job)

const config = {

headers: {

'content-type': 'multipart/form-data'

}

}

return post(url, formData, config)

}


handleClickOpen() {

this.setState({

open: true

});

}


handleClose() {

this.setState({

file: null,

userName: '',

birthday: '',

gender: '',

job: '',

fileName: '',

open: false

})

}


render() {

const { classes } = this.props;

return (

<div>

<Button variant="contained" color="primary" onClick={this.handleClickOpen}>

고객 추가하기

</Button>

<Dialog open={this.state.open} onClose={this.handleClose}>

<DialogTitle>고객 추가</DialogTitle>

<DialogContent>

<input className={classes.hidden} accept="image/*" id="raised-button-file" type="file" file={this.state.file} value={this.state.fileName} onChange={this.handleFileChange} />

<label htmlFor="raised-button-file">

<Button variant="contained" color="primary" component="span" name="file">

{this.state.fileName === ''? "프로필 이미지 선택" : this.state.fileName}

</Button>

</label><br/>

<TextField label="이름" type="text" name="userName" value={this.state.userName} onChange={this.handleValueChange} /><br/>

<TextField label="생년월일" type="text" name="birthday" value={this.state.birthday} onChange={this.handleValueChange} /><br/>

<TextField label="성별" type="text" name="gender" value={this.state.gender} onChange={this.handleValueChange} /><br/>

<TextField label="직업" type="text" name="job" value={this.state.job} onChange={this.handleValueChange} /><br/>

</DialogContent>

<DialogActions>

<Button variant="contained" color="primary" onClick={this.handleFormSubmit}>추가</Button>

<Button variant="outlined" color="primary" onClick={this.handleClose}>닫기</Button>

</DialogActions>

</Dialog>

</div>

)

}

}


export default withStyles(styles)(CustomerAdd)


▶ CustomerDelete.js


import React from 'react';

import Button from '@material-ui/core/Button';

import Dialog from '@material-ui/core/Dialog';

import DialogTitle from '@material-ui/core/DialogTitle';

import DialogContent from '@material-ui/core/DialogContent';

import DialogActions from '@material-ui/core/DialogActions';

import Typography from '@material-ui/core/Typography';


class CustomerDelete extends React.Component {


constructor(props) {

super(props);

this.state = {

open: false

}

this.handleClickOpen = this.handleClickOpen.bind(this)

this.handleClose = this.handleClose.bind(this);

}


handleClickOpen() {

this.setState({

open: true

});

}

handleClose() {

this.setState({

open: false

})

}


deleteCustomer(id){

const url = '/api/customers/' + id;

fetch(url, {

method: 'DELETE'

});

this.props.stateRefresh();

}


render() {

return (

<div>

<Button variant="contained" color="secondary" onClick={this.handleClickOpen}>

삭제

</Button>

<Dialog onClose={this.handleClose} open={this.state.open}>

<DialogTitle onClose={this.handleClose}>

삭제 경고

</DialogTitle>

<DialogContent>

<Typography gutterBottom>

선택한 고객 정보가 삭제됩니다.

</Typography>

</DialogContent>

<DialogActions>

<Button variant="contained" color="primary" onClick={(e) => {this.deleteCustomer(this.props.id)}}>삭제</Button>

<Button variant="outlined" color="primary" onClick={this.handleClose}>닫기</Button>

</DialogActions>

</Dialog>

</div>

)

}

}


export default CustomerDelete;


※ 실행결과 ※



  고객을 추가합시다.



  이어서 삭제도 해봅시다.



  삭제도 잘 되네요.


728x90
반응형

728x90
반응형

  이번 시간에는 고객(Customer) 정보를 삭제하는 기능을 구현하는 시간을 가져보도록 하겠습니다. 특정한 데이터의 삭제 기능을 구현하는 방법은 굉장히 다양합니다. 몇 가지 예제를 살펴보자면 ① 삭제를 수행했을 때 실제 테이블에서 데이터를 삭제하는 방식이 있으며 ② 삭제를 수행했을 때 삭제 체크만 한 뒤에 실제 데이터베이스에는 남겨 놓는 방식이 있습니다.


  우리는 두 번째 방식으로 구현하겠습니다. 따라서 Customer 테이블의 정보부터 수정하도록 하겠습니다.


USE management;

ALTER TABLE CUSTOMER ADD createdDate DATETIME;

ALTER TABLE CUSTOMER ADD isDeleted INT;


  또한 기본적으로 현재 들어있던 데이터에는 새로운 속성(Property)에 대한 데이터가 없으므로 이를 채워넣어줄 수 있도록 합니다.


USE management;

UPDATE CUSTOMER SET createdDate = now();

UPDATE CUSTOMER SET isDeleted = 0;


  위와 같이 MySQL 테이블 설정 명령어를 수행한 이후에 DESC 명령어로 테이블(Table) 정보를 확인해 보시면 다음과 같습니다.



  따라서 모든 데이터는 방금 생성이 된 것으로 처리 되었고, 현재 삭제 되지 않은 상태로 남아있게 되었습니다. 이제 새롭게 CustomerDelete.js를 생성해주도록 합니다.



▶ CustomerDelete.js


import React from 'react';

class CustomerDelete extends React.Component {

deleteCustomer(id){
const url = '/api/customers/' + id;
fetch(url, {
method: 'DELETE'
});
this.props.stateRefresh();
}

render() {
return (
<button onClick={(e) => {this.deleteCustomer(this.props.id)}}>삭제</button>
)
}
}

export default CustomerDelete;


▶ Customer.js


  이제 위에서 만든 Customer Delete 뷰(View)를 한 명의 고객 정보를 출력할 때 함께 보여줄 수 있도록 처리하면 됩니다.


import React from 'react';
import TableRow from '@material-ui/core/TableRow';
import TableCell from '@material-ui/core/TableCell';
import CustomerDelete from './CustomerDelete'

class Customer extends React.Component {
render() {
return (
<TableRow>
<TableCell>{this.props.id}</TableCell>
<TableCell><img src={this.props.image} alt="profile"/></TableCell>
<TableCell>{this.props.name}</TableCell>
<TableCell>{this.props.birthday}</TableCell>
<TableCell>{this.props.gender}</TableCell>
<TableCell>{this.props.job}</TableCell>
<TableCell><CustomerDelete stateRefresh={this.props.stateRefresh} id={this.props.id}/></TableCell>
</TableRow>
)
}
}

export default Customer;


▶ App.js


  이후에 다음과 같이 App.js에서 각 고객 컴포넌트에게 stateRefresh 함수를 넘겨 줄 수 있도록 합니다.


<div>
<Paper className={classes.root}>
<Table className={classes.table}>
<TableHead>
<TableRow>
<TableCell>번호</TableCell>
<TableCell>이미지</TableCell>
<TableCell>이름</TableCell>
<TableCell>생년월일</TableCell>
<TableCell>성별</TableCell>
<TableCell>직업</TableCell>
<TableCell>설정</TableCell>
</TableRow>
</TableHead>
<TableBody>
{this.state.customers ?
this.state.customers.map(c => {
return <Customer stateRefresh={this.stateRefresh} key={c.id} id={c.id} image={c.image} name={c.name} birthday={c.birthday} gender={c.gender} job={c.job} />
}) :
<TableRow>
<TableCell colSpan="6" align="center">
<CircularProgress className={classes.progress} variant="determinate" value={this.state.completed} />
</TableCell>
</TableRow>
}
</TableBody>
</Table>
</Paper>
<CustomerAdd stateRefresh={this.stateRefresh} />
</div>


▶ server.js


  이제 실제로 Express 서버에 고객 데이터 삭제 모듈을 만들어주면 됩니다. 이 때 기존에 존재하던 고객 정보 삽입 모듈과 고객 목록 불러오기 모듈도 조금씩 바꾸어주시면 됩니다.


const fs = require('fs');
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
const port = process.env.PORT || 5000;
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

const data = fs.readFileSync('./database.json');
const conf = JSON.parse(data);
const mysql = require('mysql');

const multer = require('multer');
const upload = multer({dest: './upload'})

const connection = mysql.createConnection({
host: conf.host,
user: conf.user,
password: conf.password,
port: conf.port,
database: conf.database
});
connection.connect();

app.get('/api/customers', (req, res) => {
connection.query(
'SELECT * FROM CUSTOMER WHERE isDeleted = 0',
(err, rows, fields) => {
res.send(rows);
}
)
});

app.use('/image', express.static('./upload'));

app.post('/api/customers', upload.single('image'), (req, res) => {
let sql = 'INSERT INTO CUSTOMER VALUES (null, ?, ?, ?, ?, ?, now(), 0)';
let image = '/image/' + req.file.filename;
let name = req.body.name;
let birthday = req.body.birthday;
let gender = req.body.gender;
let job = req.body.job;
let params = [image, name, birthday, gender, job];
connection.query(sql, params,
(err, rows, fields) => {
res.send(rows);
}
)
});

app.delete('/api/customers/:id', (req, res) => {
let sql = 'UPDATE CUSTOMER SET isDeleted = 1 WHERE id = ?';
let params = [req.params.id];
connection.query(sql, params,
(err, rows, fields) => {
res.send(rows);
}
)
});

app.listen(port, () => console.log(`Listening on port ${port}`));


  실행 결과는 다음과 같습니다. 다음과 같이 가장 오른쪽에 [삭제] 버튼이 추가된 것을 확인할 수 있습니다.



  그래서 결과적으로 다음과 같이 삭제 요청(Request)이 전송되는 것을 확인할 수 있습니다. REST API의 성격에 맞게 DELETE 메소드(Method)로 삭제 요청이 보내졌으며 성공적으로 삭제가 완료된 것입니다.



  결과적으로 다음과 같이 고객 목록이 갱신된 것을 확인할 수 있습니다.




728x90
반응형