이전 게시글 '[PHP] 섬머노트(Summernote) 사용해서 게시글 작성하기' 글의 다음 글입니다.
이전글을 먼저 보고와주세요. (https://stickode.tistory.com/466)
오늘은 이미지, 동영상을 업로드를 해보도록 하겠습니다.
먼저 이미지, 동영상 파일을 붙여넣을 수 있게 newpost.php 부터 수정하겠습니다.
newpost.php
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>글쓰기</title>
<!-- include libraries(jQuery, bootstrap) -->
<link href="http://netdna.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.css" rel="stylesheet">
<script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.js"></script>
<script src="http://netdna.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.js"></script>
<!-- include summernote css/js-->
<link href="http://cdnjs.cloudflare.com/ajax/libs/summernote/0.8.8/summernote.css" rel="stylesheet">
<script src="http://cdnjs.cloudflare.com/ajax/libs/summernote/0.8.8/summernote.js"></script>
<style>
body {
padding: 1rem;
}
h1 {
text-align: center;
}
button {
float: right;
}
#userInfoContainer {
display: flex;
justify-content: flex-end;
gap: 1rem;
}
#inputTitle {
width: 100%;
font-size: xx-large;
}
/*
이미지, 비디오 버튼 삭제. 드래그앤 드랍만 사용하기 위해서 버튼을 삭제했다.
이미지, 동영상 추가 버튼은 이해가 어려웠음.
*/
.note-insert {
display: none
}
</style>
</head>
<body>
<h1>게시글 작성</h1>
<div id="userInfoContainer">
<div>
<label for="inputNickname">
닉네임 :
</label>
<input id="inputNickname" />
</div>
<div>
<label for="inputPassword">
비밀번호 :
</label>
<input id="inputPassword" type='password' />
</div>
</div>
<input id="inputTitle" placeholder="제목을 작성해주세요" />
<div id="summernote"></div>
<button onclick="summit()">완료</button>
<script>
// 메인화면 페이지 로드 함수
$(document).ready(function() {
$('#summernote').summernote({
placeholder: '내용을 작성하세요',
height: 400,
maxHeight: 400,
callbacks: {
// 파일 업로드시 동작하는 코드
// onImageUpload 이지만 비디오 드랍도 동작함.
onImageUpload: function(files) {
setFiles(files);
},
// 클립보드에 있는(윈도우 + 쉬프트 + s) 한 경우에 에디터에서 붙여넣기(컨트롤+v) 하는 경우
// 섬머노트 기본 이미지 붙여넣기 기능을 막는 코드.
// 없으면 이미지 2장씩 들어간다. ( 하나는 setFiles(file 형태) 로 하나는 base64(string 형태) 로 )
onPaste: function(e) {
const clipboardData = e.originalEvent.clipboardData;
if (clipboardData && clipboardData.items && clipboardData.items.length) {
const item = clipboardData.items[0];
// 붙여넣는게 파일이고, 이미지면
if (item.kind === 'file' && item.type.indexOf('image/') !== -1) {
// 이벤트 막음
e.preventDefault();
}
}
}
},
});
});
// summit 함수 만들기
function summit() {
const button = event.srcElement;
button.disabled = true;
// nickname, password, content를 가지고와서 formdata 로 전송
const nickname = document.getElementById('inputNickname').value;
const password = document.getElementById('inputPassword').value;
const title = document.getElementById('inputTitle').value;
let content = $('#summernote').summernote('code');
const formData = new FormData;
// 에디터 내부에 img, iframe 태그가 남아있는지 확인.
const sommernoteWriteArea = document.getElementsByClassName("note-editable")[0];
const srcArray = [];
// getElementsByTagName 가 반환하는 형태는 HTMLCollection 인데 실제 배열이 없어서 forEach() 가 없음..
// 그래서 Array.from 로 array 로 만들어줌.
const iframeTags = Array.from(sommernoteWriteArea.getElementsByTagName('iframe'));
const imgsTags = Array.from(sommernoteWriteArea.getElementsByTagName('img'));
// 람다 사용함. ( 공부해보시면 좋을것 같네요.. )
iframeTags.forEach(iframe => {
srcArray.push(iframe.src);
});
imgsTags.forEach(img => {
srcArray.push(img.src);
});
const filesArrayLenght = filesArray.length;
for (let i = 0; i < filesArrayLenght; i++) {
const itrFile = filesArray[i];
// 에디터 안에 주소가 쓰이고 있으면
if (srcArray.includes(itrFile.name)) {
console.log(itrFile.name);
// 이유는 모르겠는데 서버에서 받는 파일 이름은 스키마나 baseUrl값이 없어져있었다.
// 그래서 여기서 문자열을 변환해주도록 만들었다.
const pathSplitArray = itrFile.name.split('/');
content = content.replace(itrFile.name, pathSplitArray[pathSplitArray.length - 1]);
// 왼쪽부터 (서버에서 받을때 사용할 파일 배열키, 파일)
// 서버에서 항상 배열로 받을려면 키 뒤에 '[]' 필요.
formData.append('files[]', itrFile);
}
// 이제 url 객체는 필요없으니까 메모리 해제
URL.revokeObjectURL(itrFile.name);
}
formData.append("nickname", nickname);
formData.append("password", password);
formData.append("title", title);
formData.append("content", content);
console.log(content);
const httpRequest = new XMLHttpRequest();
httpRequest.onreadystatechange = () => {
if (httpRequest.readyState === XMLHttpRequest.DONE) {
if (httpRequest.status === 200) {
console.log(httpRequest.response);
location.href = "/post.php?id=" + httpRequest.response;
} else {
alert("게시물 등록중 오류가 발생했습니다.");
button.disabled = false;
}
}
}
httpRequest.open('post', '/summitPost.php', true);
httpRequest.send(formData);
}
// filesArray 는 서버로 전송하기 전에 임시로 uri들을 들고 있는 배열이다.
const filesArray = [];
// 드래그앤 드랍시 동작하는 코드
function setFiles(files) {
const filesLenght = files.length;
for (let i = 0; i < filesLenght; i++) {
const file = files[i];
if (file.type.match('image.*')) {
// 임시 url 생성하는 부분
const url = URL.createObjectURL(file);
file.name = url;
// filesArray 이름을 방금 받은 url 로 담아둔다. (나중에 서버로 파일 보낼때 필요)
filesArray.push(new File([file], url, {
type: file.type
}));
// 에디터에 이미지 붙여넣기.
const imgElement = document.createElement("img");
imgElement.src = url;
const sommernoteWriteArea = document.getElementsByClassName("note-editable")[0];
sommernoteWriteArea.appendChild(imgElement);
} else if (file.type.match('video.*')) {
// 임시 url 생성하는 부분
const url = URL.createObjectURL(file);
console.log(file.type);
filesArray.push(new File([file], url, {
type: file.type
}));
const videoIframe = document.createElement("iframe");
videoIframe.src = url;
videoIframe.width = "640px";
videoIframe.height = "480px";
videoIframe.frameBorder = "0";
videoIframe.className = "note-video-clip";
// 에디터에 영상 붙여넣기 note-editable 에 붙여넣으면 됌.
const sommernoteWriteArea = document.getElementsByClassName("note-editable")[0];
sommernoteWriteArea.appendChild(videoIframe);
// 비디오나 이미지가 아니면
} else {
alert('지원하지 않는 파일 타입입니다.');
}
}
}
</script>
</body>
</html>
주석으로 대부분 설명이 되어 있으니 간단하게만 설명하자면 파일을 드래그 앤 드랍했을 때 동작하는 콜백(섬머노트에서 제공)에서 파일을 들고 있도록 코드를 추가했습니다.
setFiles 이벤트에서는 이미지나 비디오를 에디터에 붙여넣고 filesArray 에 담아둡니다.
마지막으로 완료 버튼이 눌리면 에디터 안에 있는 img, iframe 태그에 들어있는 모든 src 값을 가져와서 지금까지 저장하고 있던 파일들을 확인하고, 파일이 있으면 formData 에 넣어 주도록 작성했습니다.( 이 과정이 없으면 백스페이스로 지워진 파일도 서버로 보내짐 )
다음으로 받는쪽 코드 작성해보도록 하겠습니다.
summitPost.php
<?php
$mediaBaseUrl = 'http://localhost/upload/';
$mediaRoot = '/var/www/html/upload/';
// $mediaRoot 경로에 $iam( apm 자동설치 했으면 보통 www-data ) 유저, 그룹 권한과 755 권한을 줘야한다.
$iam = exec('whoami');
$nickname = $_POST['nickname'];
// 비밀번호 암호화
$password = password_hash($_POST['password'], PASSWORD_DEFAULT);
$title = $_POST['title'];
$content = $_POST['content'];
if (isset($_FILES['files'])) {
$files = $_FILES['files'];
$countfiles = count($files['name']);
for ($i = 0; $i < $countfiles; $i++) {
$filename = $files['name'][$i];
// 확장자 가져오기. 보통 사용할때는 크게 문제 없어보임.
// 혹시 확장자가 빈문자열이면 php.ini 업로드 용량 제한 확인해볼 것.
$extension = explode('/', $files['type'][$i])[1];
$filePath = $filename . '.' . $extension;
// 해당 코드는 해킹 위험이 있습니다.
// 관련 블로그 글( https://mytory.net/archives/3011 )
// 파일 업로드 성공했다면
if (move_uploaded_file($files['tmp_name'][$i], $mediaRoot . $filePath)) {
// 기존 경로값을 서버의 파일 경로로 변경.
$content = str_replace($filename, $mediaBaseUrl . $filePath, $content);
// 파일 업로드 실패했다면
} else {
// 에러는 번호로 나옴. 구글 검색해볼 것.
$error = $files['error'][0];
die();
}
}
}
// 비밀번호 hash 후 길이 60( 궁금하면 echo 찍어보면 됨 )
$length = strlen($password);
include("../db.php");
$stmt = $mysqli->prepare("insert into board(nickname,password,title,content) values(?,?,?,?)");
// 닉네임(s), 컨텐츠(s), 패스워드(s) 를 각각 입력(sql 인젝션 방어 코드)
$stmt->bind_param("ssss", $nickname, $password, $title, $content);
$stmt->execute();
// 성공적으로 디비에 들어가면 게시물 id 값을 클라이언트로 보내줌.
echo $stmt->insert_id;
$stmt->close();
DB 정보를 저장 전에 파일을 저장하는 코드가 들어가도록 변경되었습니다.
설명은 주석을 참고해주세요.
다음으로 서버 루트 ( /var/www/html ) 에 upload 파일을 만들고 권한을 변경해주세요.
// 서버 루트로 이동
cd /var/www/html
// upload 파일 생성
mkdir upload
// 아파치 접속 유저(보통 www-data)로 upload 폴더 권한 변경
chown -R www-data:www-data upload
// upload 폴더 사용 권한 변경
chmod -R 755 upload
혹시 파일 업로드가 안되시면 php.ini 파일의 업로드 파일 용량 제한 설정을 변경하시고 아파치를 재시작 해주세요.
php.ini 파일 위치는 phpinfo(); 상단에 Loaded Configuration File 를 확인해보시면 됩니다.
이제 마지막으로 등록한 게시물을 볼 수 있는 화면을 만들어보도록 하겠습니다.
post.php
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>게시물</title>
<style>
body {
padding: 1rem;
}
h1 {
text-align: center;
}
#userInfoContainer {
display: flex;
justify-content: flex-end;
gap: 1rem;
}
#title {
width: 100%;
font-size: xx-large;
}
</style>
</head>
<body>
<?php
$id = $_GET["id"];
include("../db.php");
$stmt = $mysqli->prepare("select * from board where id =?");
$stmt->bind_param("i", $id);
$stmt->execute();
$result = $stmt->get_result();
$post = $result->fetch_assoc();
?>
<h1>게시글 작성</h1>
<div id="userInfoContainer">
<div>
작성자 : <?php echo $post['nickname'] ?>
</div>
</div>
<div id="title">
<?php echo $post['title'] ?>
</div>
<div id="contents">
<?php echo $post['content'] ?>
</div>
</body>
</html>
newpost.php 에서 구조를 가져왔습니다.
특별한건 없고 get 에 들어있는 id 값을 가져와서 DB 검색 후 결과를 html 에 넣어주는 방식으로 구현했습니다.
결과 )
'PHP' 카테고리의 다른 글
[PHP]이미지 URI 주소로 이미지 다운로드 (0) | 2022.06.29 |
---|---|
[PHP] Session 을 이용해서 로그인정보 가져오기 (0) | 2022.06.15 |
[PHP] 섬머노트(Summernote) 사용해서 게시글 작성하기 (0) | 2022.05.24 |
[PHP] 게시판 목록 화면 만들기 (0) | 2022.05.22 |
[PHP] 확장자 없는 파일명 추출하기 (0) | 2022.04.04 |