본문 바로가기

레퍼런스

TIL 2022.05.12 프로젝트 구현 내용 추가 정리 (이미지 업로드, 드래그 앤 드랍, ajax 비동기식 유효성검사)

이미지 업로드

profile.html

<div class="my_info">
    <img id="user_profile_img" src="/static/profile_images/{{ users.profile_img }}" alt="">
    <div class="info_box">
        <div class="nick_name">
            {{ users.nickname }}
            <button class="profile_btn" type="button" onclick="change_profile()">프로필 사진 변경</button>
        </div>
        ...
   </div>
   ...
</div>

프로필 사진을 출력하는 공간 <img id="user_profule_img" ...>

프로필 사진 변경 버튼 클릭 시 change_profile() 함수 호출하도록 설정

function change_profile() {
    var url = '/change_profile';
    var name = '프로필 사진 변경';
    var option = 'width = 800, height = 800, top = 100, left = 200, location = no'
    window.open(url, name, option);
}

팝업창을 띄우기 위해서 window.open(url, name, option) 함수 사용 (변수로 선언해서 내용 지정한 뒤 파라미터로 전달)

app.py

@app.route('/change_profile')
def change_profile():
    # 현재 컴퓨터에 저장 된 'mytoken'인 쿠키 확인
    token_receive = request.cookies.get('mytoken')
    # 암호화되어있는 token의 값 디코딩(암호화 풀기)
    payload = jwt.decode(token_receive, SECRET_KEY, algorithms=['HS256'])

    my_info = db.USER.find_one({'id': payload['id']})

    return render_template('/profile/change_profile.html', my_info=my_info)

팝업창에 change_profile.html 화면을 띄우고 로그인되어 있는 유저의 정보도 전달해줘야하기 때문에 해당 정보를 토큰에서 가져와서 그 유저의 정보를 my_info에 담아서 리턴

change_profile.html

<div class="file-upload">
    <input type="hidden" id="nickname" value="{{ my_info.nickname }}">
    <input type="file" id="file">
    <button onclick="posting('{{ my_info.nickname }}')">업로드</button>
</div>
<hr>
<div>
    <img src="/static/profile_images/{{ my_info.profile_img }}" id="preview">
</div>

body 부분 html 리턴 받은 my_info에서 nickname을 가져오고 기존 이미지를 미리보기 해주기 위해서 jinja 이용해서 사진 출력 해주는 코드 그리고 업로드 버튼 눌렀을 때 posting() 함수가 호출되고 마찬가지로 유저의 닉네임 값을 전달 해주기 위해서 파라미터로 줬다.

$(document).ready(function () {
    $('#file').change(function () {
        setImageFromFile(this, '#preview');
    });

    function setImageFromFile(input, expression) {
        if (input.files && input.files[0]) {
            var reader = new FileReader();
            reader.onload = function (e) {
                $(expression).attr('src', e.target.result);
            }
            reader.readAsDataURL(input.files[0]);
        }
    }
})

변경할 사진을 선택할 경우 업로드한 사진을 미리보기 할 수 있도록 하는 코드

this를 이용해서 업로드된 파일을 감지해서 정확히는 변경점과 이미지 태그의 id 값(#preview)을

setImageFromFile() 함수의 인자를 주고 해당 함수에서 첫번째 input 받은 값이 파일인지 아닌지 판별한 뒤

파일이 맞다면 해당 파일을 읽기 위해서 FileReader() 클래스의 인스턴스 생성하여 onload() 함수를 이용해서

파일이 읽혀지면 id="preview" 의 태그의 src="" 내용을 onload한 대상의 결과 즉, 읽은 파일의 내용으로 바꾸도록 attr 함수를 사용했다. 마지막에 reder.readAsDataURL(input.files[0]) 경우는 이미지 파일을 base64 형태로 인코딩해서 이미지를 다루기 좋도록 만들어 주는 코드입니다. 이 코드를 사용하지 않으면 흔히 아는 경로값이 String으로 나오는게 아니라 인코딩 되기전에 전혀 알 수 없는 이미지 값의 코드가 나온다고 생각하면된다.

 

function posting(nickname) {
    alert('사진 변경 시작!')
    let nick = nickname
    let file = $('#file')[0].files[0]
    let form_data = new FormData()

    form_data.append("nick_give", nick)
    form_data.append("file_give", file)

    $.ajax({
        type: "POST",
        url: "/fileupload",
        data: form_data,
        cache: false,
        contentType: false,
        processData: false,
        success: function (response) {
            alert(response["result"])
            opener.parent.location.reload();
            window.close();
        }
    });
}

사진 전송을 위해서는 파일을 담아야하기 때문에 일반 변수들 처럼 val()해서 가져오는 것이 아닌 .files[0]을 해서 가져와야한다. 데이터를 담기 위해서도 FormData() 클래스의 인스턴스를 생성해서 해당 인스턴스에 담아서 request 해야한다.

 

form_data에 다양한 데이터들을 담았기 때문에 반드시 cache , contentType, processData 설정을 false해줘야 문제 없이 작동한다. 성공 시 즉, 이미지파일이 업로드 되고 나서 팝업 창을 닫고 부모창을 리로드하도록 코드를 작성했다.

app.py

# 방식1 : DB에는 파일 이름만 넣어놓고, 이미지 자체는 서버 컴퓨터에 저장하는 방식
@app.route('/fileupload', methods=['POST'])
def file_upload():
    print('file_upload() 도착')
    nick_receive = request.form['nick_give']
    file = request.files['file_give']
    # 해당 파일에서 확장자명만 추출
    extension = file.filename.split('.')[-1]
    # 파일 이름이 중복되면 안되므로, 지금 시간을 해당 파일 이름으로 만들어서 중복이 되지 않게 함!
    today = datetime.datetime.utcnow()
    mytime = today.strftime('%Y-%m-%d-%H-%M-%S')
    filename = f'{nick_receive}-{mytime}'
    # 파일 저장 경로 설정 (파일은 db가 아니라, 서버 컴퓨터 자체에 저장됨)
    save_to = f'static/profile_images/{filename}.{extension}'
    # 파일 저장!
    file.save(save_to)

    db.USER.update_one({'nickname': nick_receive}, {'$set': {'profile_img': f'{filename}.{extension}'}})
    feed = list(db.FEED.find({'nickname': nick_receive}))  

    db.FEED.update_many({'nickname': nick_receive}, {'$set': {'profile_img': f'{filename}.{extension}'}})
    for nick in feed:
        print(nick)
    return jsonify({'result': 'success'})

파일을 받을 때는 request.file['지정한 이름'] 으로 받아와야한다. 파일을 저장하기 위해서 확장자명 따로 추출하고 파일 이름의 중복을 막기 위해서 현재시간을 파일 이름으로 만들어서 중복되지 않도록 datetime 클래스의 함수 사용

srftime()으로 시간 형식을 지정하고 파일이름은 닉네임 - 시간 으로 지정해서 최종으로 실제 저장할 경로 값까지 추가해서 file.save(save_to) 함수로 컴퓨터에 저장하고

DB에 이미지 이름을 저장하는 것으로 완료 이를 출력하기 위해서 src="/static/경로/{{ 리턴 받은 값 }}" 형태로 출력

<img id="user_profile_img" src="/static/profile_images/{{ users.profile_img }}" alt="">

 

 

이미지 드래그 앤 드랍으로 업로드 후 게시글 작성

<div class="mdbox-post">
    <div class="post-title">새 게시물 만들기</div>
    <img alt=img-upload class="post-img" src="{{url_for('static', filename = 'nav-img/posting.png')}}">
    <div class="post-info">사진과 동영상을 여기에 끌어다 놓으세요</div>
</div>
<div>
    <textarea id="input_content" cols="12" rows="12"></textarea>
</div>
<div>
    <button type="button" id="write_feed_btn">공유하기</button>
</div>
$(document).ready(function () {
    // 이미지 드래그 앤 드랍으로 업로드
    $('.mdbox-post')
        .on("dragover", dragOver)
        .on("dragleave", dragOver)
        .on("drop", uploadFiles);

    function dragOver(e) {
        e.stopPropagation();
        e.preventDefault();

        if (e.type == "dragover") {
            console.log('dragover')
            $(e.target).css({
                "background-color": "black",
                "outline-offset": "-20px"
            });
        } else {
            console.log('dragleave')
            $(e.target).css({
                "background-color": "white",
                "outline-offset": "-10px"
            });
        }
    } 
    {# 파일을 담을 변수 선언 이후 게시판 작성하기 할때 해당 함수에 담겨져있는 파일 가져가기 위함#}
    let files; 
   
    function uploadFiles(e) {
    	{# drop했을 때 해당 함수 실행되며 파라미터로 drop한 데이터 가져옴 #}
        e.stopPropagation();
        e.preventDefault();
        e.dataTransfer = e.originalEvent.dataTransfer;
        files = e.dataTransfer.files; {# drop받은 데이터 파일형태로 files에 저장 #}

        if (files.length > 1) {
            alert('하나만');
            return;
        }
        console.log('하나 판별 완료')
        if (files[0].type.match(/image.*/)) {
            $('#input_image').css({
                "background-image": "url(" + window.URL.createObjectURL(files[0]) + ")",
                "outline": "none",
                "background-size": "contain",
                "background-repeat": "no-repeat",
                "background-position": "center"
            });
        } else {
            alert('이미지가 아닙니다.');
            return;
        }
    }
// 게시글 공유하기 버튼 클릭했을 때
    $('#write_feed_btn').on('click', () => {
        const content = $('#input_content').val();
        const profile_image = $('#input_profile_image').attr('src');
        const user_nick = $('#user_nickname').text();
        const file = files[0];

        let fd = new FormData();

        fd.append('file', file);
        fd.append('content', content);
        fd.append('profile_image', profile_image);
        fd.append('user_nick', user_nick);

        if (image.length <= 0) {
            alert("이미지가 비어있습니다.");
        } else if (content.length <= 0) {
            alert("설명을 입력하세요");
        } else if (profile_image.length <= 0) {
            alert("프로필 이미지가 비어있습니다.");
        } else if (user_nick.length <= 0) {
            alert("사용자 id가 없습니다.");
        } else {
            alert('writeFeed()로 왓다')
            $.ajax({
                url: '/api/feed/upload',
                data: fd,
                method: "POST",
                processData: false,
                contentType: false,
                success: function (data) {
                    alert('게시물 작성 성공!')
                },
                error: function (request, status, error) {
                    alert('게시물 작성 실패!')
                }
            })
        }
    });
});
@app.route('/api/feed/upload', methods=['POST'])
def feed_upload():
    token_receive = request.cookies.get('mytoken')
    payload = jwt.decode(token_receive, SECRET_KEY, algorithms=['HS256'])
    my_follow = db.USER.find_one({'id': payload['id']})

    file = request.files['file']
    content = request.form['content']
    user_nick = request.form['user_nick']

    # 해당 파일에서 확장자명만 추출
    extension = file.filename.split('.')[-1]
    # 파일 이름이 중복되면 안되므로, 지금 시간을 해당 파일 이름으로 만들어서 중복이 되지 않게 함!
    today = datetime.datetime.utcnow()
    mytime = today.strftime('%Y-%m-%d-%H-%M-%S')
    filename = f'{user_nick}-{mytime}'
    # 파일 저장 경로 설정 (파일은 db가 아니라, 서버 컴퓨터 자체에 저장됨)
    save_to = f'static/upload/{filename}.{extension}'
    # 파일 저장!
    file.save(save_to)

    count_list = [0]
    feed_list = list(db.FEED.find({}, {'num': True, '_id': False}))
    for number in feed_list:
        print(number['num'])
        count_list.append(int(number['num']))

    count = max(count_list) + 1

    # 아래와 같이 입력하면 db에 추가 가능!
    doc = {
        'num': str(count),
        'date': mytime,
        'nickname': user_nick,
        'feed_images': [f'{filename}.{extension}'],
        'profile_img': my_follow['profile_img'],
        'content': content,
        'like': [],
        'bookmark': [],
        'reply': []
    }
    db.FEED.insert_one(doc)

    return jsonify({'result': 'success'})

 ajax 비동기식 유효성검사

join.html

<div className="box-input"> <!-- input box -->
    <input className="input-join" type="text" id="id" placeholder="휴대폰 번호 또는 이메일 주소">
    <span id="id_check"></span><!-- input id -->
    <input className="input-join" type="text" id="name" placeholder="성명">
    <span id="name_check"></span><!-- input name -->
    <input className="input-join" type="text" id="nickname" placeholder="사용자 이름">
    <span id="nick_check"></span><!-- input nickname -->
    <input className="input-join" type="password" id="pwd" placeholder="비밀번호">
    <span id="pwd_check"></span><!-- input pw -->
</div>
<!--입력 값이 전부 올바르게 입력되었는지 판별을 위한 boolean값을 저장하기 위한 type="hidden"태그 들-->
<input type="hidden" id="hidden-id" value="false">
<input type="hidden" id="hidden-name" value="false">
<input type="hidden" id="hidden-nick" value="false">
<input type="hidden" id="hidden-pwd" value="false">
<div class="box-button"> <!-- button box -->
    <button class="button" type="button" onclick="join()">가입</button> <!-- button -->
</div>
// id 값을 입력했을 때
$('#id').blur(function () {
    let id = $('#id').val()
    // 입력받은 값 중 입력하지 않은 값이 있을 경우
    if (id == "") {
        $('#id_check').html('<i class="fa-solid fa-circle-exclamation"></i>')
        $('#hidden-id').val('false')
    } else {
        // 아이디 중복 확인
        $.ajax({
            type: "POST",
            url: '/api/id_dup',
            data: {'id_give': id},
            success: function (response) {
                if (response['duplicate']) {
                    $('#id_check').html('<i class="fa-regular fa-circle-xmark"></i>')
                    $('#hidden-id').val('false')
                } else {
                    // 정규 표현식을 이용한 아이디 형식 제한 영문 소문자, 대문자, 숫자, 4-30자
                    let id_regExp = /^[a-zA-Z0-9]([a-zA-Z0-9]*)(@)([a-zA-Z0-9]*)(\.)([a-zA-Z]*){4,40}$/;
                    if (!id_regExp.test(id)) {
                        $('#id_check').html('<i class="fa-regular fa-circle-xmark"></i>')
                        $('#hidden-id').val('false')
                    } else {
                        $('#id_check').html('<i class=\"fa-regular fa-circle-check\"></i>')
                        $('#hidden-id').val('true')
                    }
                }
            }
        })
    }
})

function join() {
    if ($('#hidden-id').val() == 'true') {
        if ($('#hidden-nick').val() == 'true') {
            if ($('#hidden-name').val() == 'true') {
                if ($('#hidden-pwd').val() == 'true') {
                    // 입력한 값들 가져와 변수에 저장
                    let id = $('#id').val()
                    let name = $('#name').val()
                    let nick = $('#nickname').val()
                    let pwd = $('#pwd').val()
                    $.ajax({
                        type: "POST",
                        url: "/api/join",
                        data: {'id_give': id, 'name_give': name, 'nick_give': nick, 'pwd_give': pwd},
                        success: function (response) {
                            alert(response["msg"])
                            window.location.href = '/login'
                        }
                    })
                } else {
                    alert('패스워드는 영대소문자, 숫자 특수문자 8-20자 입니다.!')
                }
            } else {
                alert('이름은 한글, 영문 2-20자입니다.')
            }
        } else {
          alert('닉네임이 중복이거나 영대소문자,한글,숫자, 4-12자에 맞지않습니다.')
        }
    } else {
        alert('아이디가 중복 또는 이메일 형식이 아닙니다.')
    }
}

app.py

# ajax에서 비동기식으로 아이디 중복 확인을 위해 따로 함수 정의
@app.route("/api/id_dup", methods=["POST"])
def id_dup():
    id_receive = request.form['id_give']
    id_dup = bool(db.USER.find_one({'id': id_receive}))
    return jsonify({'duplicate': id_dup})


# ajax에서 비동기식으로 닉네임 중복 확인을 위해 따로 함수 정의
@app.route("/api/nick_dup", methods=["POST"])
def nick_dup():
    nick_receive = request.form['nick_give']
    nick_dup = bool(db.USER.find_one({'nickname': nick_receive}))
    return jsonify({'duplicate': nick_dup})
    
    
# 회원가입 입력받은 값을 받아 DB에 추가하기
@app.route("/api/join", methods=["POST"])
def api_join():
    # data = json.loads(request.data) request.data로 get()으로 받아오는 방식으로 사용가능
    # id_receive = data.get('id_give')
    id_receive = request.form['id_give']
    name_receive = request.form['name_give']
    nick_receive = request.form['nick_give']
    pwd_receive = request.form['pwd_give']
    # 입력받은 패스워드 값 해싱하여 암호화
    hashed_pw = hashlib.sha256(pwd_receive.encode('utf-8')).hexdigest()

    doc = {
        'id': id_receive,
        'pwd': hashed_pw,
        'name': name_receive,
        'nickname': nick_receive,
        'follower': [],
        'following': [],
        'like': [],
        'bookmark': [],
        'profile_img': 'profile_default.png'
    }
    db.USER.insert_one(doc)
    return jsonify({'result': 'success', 'msg': '회원 가입 완료'})