안경잡이개발자

728x90
반응형

1. Style 적용을 위한 라이브러리 추가하기


  yarn add style-loader

  yarn add css-loader


2. 웹팩(Webpack) conf 파일 수정하기


'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'
}, {
test: /\.css$/,
loader: "style-loader!css-loader"
}]
},
plugins: [
new CopyWebpackPlugin([{
context: './public',
from: '*.*'
}]),
],
devServer: {
contentBase: './public',
host: 'localhost',
port: 8080
}
}


▶ ./src/index.css


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

div {
font-family: 'Noto Sans KR' !important;
}


▶ ./src/main.js


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

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

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


  이후에 모든 컴포넌트 JS 파일에 import '../index.css';를 넣어주시면 됩니다.


※ 실행 결과 ※



728x90
반응형

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
반응형