Mi Lugarcito

React와 Firebase로 앱 개발하기 - 워드 클라우드 API 상세 수치 설정하기 본문

React & Next.js

React와 Firebase로 앱 개발하기 - 워드 클라우드 API 상세 수치 설정하기

selene park 2021. 3. 16. 21:38

word clould.py

# 단어구름에 필요한 라이브러리를 불러옵니다.
from wordcloud import WordCloud
# 한국어 자연어 처리 라이브러리를 불러옵니다.
from konlpy.tag import Twitter
# 명사의 출현 빈도를 세는 라이브러리를 불러옵니다.
from collections import Counter
# 그래프 생성에 필요한 라이브러리를 불러옵니다.
import matplotlib.pyplot as plt
# Flask 웹 서버 구축에 필요한 라이브러리를 불러옵니다.
from flask import Flask, request, jsonify
# 테스트를 위하여 CORS를 처리합니다.
from flask_cors import CORS
# 파일에 접근하기 위한 라이브러리를 불러옵니다.
import os

# 플라스크 웹 서버 객체를 생성합니다.
app = Flask(__name__, static_folder='outputs')
CORS(app)

# 폰트 경로 설정
font_path = 'NanumGothic.ttf'


def get_tags(text, max_count, min_length):
    # 명사만 추출합니다.
    t = Twitter()
    nouns = t.nouns(text)
    processed = [n for n in nouns if len(n) >= min_length]
    # 모든 명사의 출현 빈도를 계산합니다.
    count = Counter(processed)
    result = {}
    # 출현 빈도가 높은 max_count 개의 명사만을 추출합니다.
    for n, c in count.most_common(max_count):
        result[n] = c
    # 추출된 단어가 하나도 없는 경우 '내용이 없습니다.'를 화면에 보여줍니다.
    if len(result) == 0:
        result["내용이 없습니다."] = 1
    return result


def make_cloud_image(tags, file_name):
    # 만들고자 하는 워드 클라우드의 기본 설정을 진행합니다.
    word_cloud = WordCloud(
        font_path=font_path,
        width=800,
        height=800,
        background_color="white",
    )
    # 추출된 단어 빈도수 목록을 이용해 워드 클라우드 객체를 초기화 합니다.
    word_cloud = word_cloud.generate_from_frequencies(tags)
    # 워드 클라우드를 이미지로 그립니다.
    fig = plt.figure(figsize=(10, 10))
    plt.imshow(word_cloud)
    plt.axis("off")
    # 만들어진 이미지 객체를 파일 형태로 저장합니다.
    path = "outputs/{0}.png".format(file_name)
    # 이미 파일이 존재하는 경우 덮어쓰기합니다.
    if os.path.isfile(path):
        os.remove(path)
    fig.savefig(path)


def process_from_text(text, max_count, min_length, words, file_name):
    # 최대 max_count 개의 단어 및 등장 횟수를 추출합니다.
    tags = get_tags(text, int(max_count), int(min_length))
    # 단어 가중치를 적용합니다.
    for n, c in words.items():
        if n in tags:
            tags[n] = tags[n] * float(words[n])
    # 명사의 출현 빈도 정보를 통해 워드 클라우드 이미지를 생성합니다.
    make_cloud_image(tags, file_name)


@app.route("/process", methods=['GET', 'POST'])
def process():
    content = request.json
    words = {}
    if content['words'] is not None:
        for data in content['words'].values():
            words[data['word']] = data['weight']
    process_from_text(content['text'], content['maxCount'], content['minLength'], words, content['textID'])
    result = {'result': True}
    return jsonify(result)


@app.route('/outputs', methods=['GET', 'POST'])
def output():
    text_id = request.args.get('textID')
    return app.send_static_file(text_id + '.png')


@app.route('/validate', methods=['GET',' POST'])
def validate():
    text_id = request.args.get('textID')
    path = "outputs/{0}.png".format(text_id)
    result = {}
    # 해당 이미지 파일이 존재하는지 확인합니다.
    if os.path.isfile(path):
        result['result'] = True
    else:
        result['result'] = False
    return jsonify(result)


if __name__ == '__main__':
    app.run('0.0.0.0', port=5000, threaded=True) # 처리 속도 향상을 위해 쓰레드를 적용합니다.

Detail.js

import React from 'react';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import '../index.css';
import { withStyles } from '@material-ui/core/styles';
import Fab from '@material-ui/core/Fab';
import UpdateIcon from '@material-ui/icons/Update';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogTitle from '@material-ui/core/DialogTitle';
import Button from '@material-ui/core/Button';
import DialogContent from '@material-ui/core/DialogContent';
import TextField from '@material-ui/core/TextField';

const databaseURL = "https://word-cloud-1e33e-default-rtdb.firebaseio.com";
const apiURL = "http://localhost:5000";

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


class Detail extends React.Component {

    constructor(props) {
        super(props);
        this.state = {
            dialog: false,
            textContent: '',
            words: {},
            imageUrl: null,
            maxCount: 30,
            minLength: 1
        }
    }

    componentDidMount() {
        this._getImage();
        this._getText();
        this._getWords();
    }

    _getText() {
        fetch(`${databaseURL}/texts/${this.props.match.params.textID}.json`).then(res => {
            if (res.status != 200) {
                throw new Error(res.statusText);
            }
            return res.json();
        }).then(text => this.setState({ textContent: text['textContent'] }));
    }

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

    _getImage() {
        fetch(`${apiURL}/validate?textID=${this.props.match.params.textID}`).then(res => {
            if (res.status != 200) {
                throw new Error(res.statusText);
            }
            return res.json();
        }).then(data => {
            if (data['result'] == true) {
                this.setState({ imageUrl: apiURL + "/outputs?textID=" + this.props.match.params.textID })
            } else {
                this.setState({ imageUrl: 'NONE' });
            }
        });
    }

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

    handleSubmit = () => {
        this.setState({ imageUrl: 'READY' });
        const wordCloud = {
            textID: this.props.match.params.textID,
            text: this.state.textContent,
            maxCount: this.state.maxCount,
            minLength: this.state.minLength,
            words: this.state.words
        }
        this.handleDialogToggle();
        if (!wordCloud.textID ||
            !wordCloud.text ||
            !wordCloud.maxCount ||
            !wordCloud.minLength ||
            !wordCloud.words) {
            return;
        }
        this._post(wordCloud);
    }

    _post = (wordCloud) => {
        return fetch(`${apiURL}/process`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(wordCloud)
        }).then(res => {
            if (res.status != 200) {
                throw new Error(res.statusText);
            }
            return res.json();
        }).then(data => {
            this.setState({ imageUrl: apiURL + "/outputs?textID=" + this.props.match.params.textID })
        });
    }

    handleValueChange = (e) => {
        let nextState = {};
        if (e.target.value % 1 === 0) {
            if (e.target.value < 1) {
                nextState[e.target.name] = 1;
            } else {
                nextState[e.target.name] = e.target.value;
            }
        }
        this.setState(nextState);
    }

    render() {
        const { classes } = this.props;
        return (
            <div>
                <Card>
                    <CardContent>
                        {
                            (this.state.imageUrl) ?
                                ((this.state.imageUrl == 'READY') ?
                                    '워드 클라우드 이미지를 불러오고 있습니다.' :
                                    ((this.state.imageUrl == 'NONE') ?
                                        '해당 텍스트에 대한 워드 클라우드를 만들어 주세요.' :
                                        <img key={Math.random()} src={this.state.imageUrl + '&random=' + Math.random()} style={{ width: '100%' }} />)) :
                                ''
                        }
                    </CardContent>
                </Card>
                <Fab color="primary" className={classes.fab} onClick={this.handleDialogToggle}>
                    <UpdateIcon />
                </Fab>
                <Dialog open={this.state.dialog} onClose={this.handleDialogToggle}>
                    <DialogTitle>워드 클라우드 생성</DialogTitle>
                    <DialogContent>
                        <TextField label="최대 단어 개수" type="number" name="maxCount" value={this.state.maxCount} onChange={this.handleValueChange} /><br />
                        <TextField label="최소 단어 길이" type="number" name="minLength" value={this.state.minLength} onChange={this.handleValueChange} /><br />
                    </DialogContent>
                    <DialogActions>
                        <Button variant="contained" color="primary" onClick={this.handleSubmit}>
                            {(this.state.imageUrl == 'NONE') ? '만들기' : '다시 만들기'}
                        </Button>
                        <Button variant="outlined" color="primary" onClick={this.handleDialogToggle}>닫기</Button>
                    </DialogActions>
                </Dialog>
            </div>
        );
    }
}

export default withStyles(styles)(Detail);

 

Word.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';
import '../index.css';

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

const databaseURL = "https://word-cloud-1e33e-default-rtdb.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);
        if (e.target.value < 1) {
            this.setState({ weight: 1 });
        }
        else if (e.target.value > 9) {
            this.setState({ weight: 9 });
        }
    }

    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="가중치(1부터 9까지)" 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);