데이터베이스를 배우면서 웹 프로그래밍도 같이 진행을 하면서 만들게 된 프로젝트이다.
프로젝트의 주제는 구매자와 판매자가 중개자 없이 직접 거래를 할 수 있는 스마트팜 거래 플랫폼이다.
XAMPP를 사용하여 Apache를 사용하였고 HTML, Javascript, PHP를 사용하여 만들었다.
DB설계
데이터베이스 EERD이다.
user는 기본적인 정보들을 가지고 있으며 특이한 점은 로그인을 할 때 farm을 가지고 있다면 farmer 페이지로 이동되고 farm을 가지고 있지 않다면 user 페이지로 이동한다는 것이다. 그리고 주문을 하면 바로 주문을 한 농장의 화분에 할당이 된다.
UI
User 전용 페이지
로그인 페이지에서 user의 계정으로 로그인 하면 나오는 화면이다.
핵심 기능은 최근 한달간 가장 많이 주문된 작물을 3가지 보여주는 부분과 신규로 추가된 작물을 보여주는 것이다.
다른 쇼핑몰과 비슷한 구성이며 가격순, 판매량순으로 정렬을 할 수 있다.
작물에 대한 상세 정보를 알수있는 페이지이며 장바구니에 담을 수 있다.
카카오 지도 API를 사용하여 지도를 표시하였고 아래에는 농장의 위치를 텍스트로 볼 수 있다.
장바구니에 담은 작물의 수량을 증감하면 데이터베이스에 바로 업데이트 되며 페이지를 종료하고 다시 켜도 저장되어있다.
앞에서 주문하려는 당근과 토마토의 재배 기간을 고려하여 재배 기간이 긴 작물을 기준으로 재배 기간 + 배송기간을 고려하여 그 전에는 선택할수 없게 만들었고 위 사진에서처럼 토마토는 20일이 걸리므로 배송일3일을 고려하여 28일 이후로 달력에서 선택할 수 있다.
배송지는 user의 입력된 주소중에서 선택하여 배송을 받을 수 있다.
어떤 농장에 주문을 넣을지 선택하는 페이지이다.
최종 결제 페이지로 이전의 페이지들에서 선택한 정보들이 맞는지 확인할 수 있고 맞다면 최종 결제를 하는 페이지이다.
최종 결제가 완료되면 내 정보 페이지로 이동되면서 구매목록에서 확인할 수 있다.
Farmer 전용 페이지
로그인 페이지에서 Farmer계정으로 로그인 하면 처음 나오는 메인 페이지로 상단에는 알림으로 주문과 화분의 장비 고장등을 알 수 있으며 상대적으로 중요한 장비 고장 문구는 항상 위에 있도록 만들었다.
농장별로 선택하여 화분이 제대로 동작하고 있는지 한눈에 확인할 수 있다. 고장난 화분은 빨간 테두리로 표시한다.
메인 페이지에서 화분을 눌렀을때 해당하는 농장의 상세 페이지로 이동하며 클릭한 화분의 정보를 바로 보여준다.
각각 화분의 장비 상태와 재배중인 작물의 정보를 보여준다.
가지고 있는 농장에 들어온 주문을 한눈에 볼 수 있고 들어온 주문을 취소할 수 도 있고 완료된 주문만 따로 모아서 볼 수도있다.
알림들을 전체적으로 볼 수 있고 처리해야할 업무도 나온다.
핵심 기능
로그인 해시처리 및 user & farmer 구분
// post 방식
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$userID = $_POST['userID'];
$password = $_POST['pw'];
// 유저가 존재하는지 확인하기
$query = "SELECT * FROM user WHERE userID = ? LIMIT 1";
$stmt = $con->prepare($query);
$stmt->bind_param("s", $userID);
$stmt->execute();
$result = $stmt->get_result();
$user = $result->fetch_assoc();
// 해시를 사용하여 저장되어 있는 데이터베이스의 비밀번호와 비교
if ($user && hash_equals($user['pw'], hash('sha256', $password))) {
// 유저가 가진 농장
$query = "SELECT * FROM farm WHERE User_userID = ? LIMIT 1";
$stmt = $con->prepare($query);
$stmt->bind_param("s", $userID);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows > 0) {
// 유저가 농장을 가지고 있다면, farmerMain.php으로 이동
$_SESSION['userID'] = $userID;
header("Location: farmerMain.php");
exit();
} else {
// 유저가 농장이 없다면, userMain.php으로 이동
$_SESSION['userID'] = $userID;
header("Location: userMain.php");
exit();
}
} else {
// Login 실패시
echo "Invalid userID or password";
}
}
비밀번호가 해시 처리되어 데이터베이스에 저장되어 있는데 이것을 가져와서 입력한 비밀번호와 비교하고 일치한다면 농장을 가지고 있는지 데이터베이스에 조회를 한다. 조회해서 결과가 있다면 farmerMain 페이지로 이동하고 없다면 userMain 페이지로 이동한다.
작물 정렬
<script>
// 높은 가격순으로 정렬
function sortByPriceDescending() {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
document.querySelector('.container').innerHTML = this.responseText;
}
};
xhttp.open("GET", "sort_by_dsc.php", true);
xhttp.send();
}
</script>
<script>
// 판매량 순으로 정렬
function manyorderdescending() {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
document.querySelector('.container').innerHTML = this.responseText;
}
};
xhttp.open("GET", "manyorder.php", true);
xhttp.send();
}
</script>
<script>
// 낮은 가격순으로 정렬
function sortByPriceAscending() {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
document.querySelector('.container').innerHTML = this.responseText;
}
};
xhttp.open("GET", "sort_by_asc.php", true);
xhttp.send();
}
document.addEventListener("DOMContentLoaded", function() {
document.getElementById("sortAscending").addEventListener("click", function() {
sortByPriceAscending(); // 버튼 클릭 시 낮은 가격순으로 정렬 함수 호출
});
document.getElementById("sortDescending").addEventListener("click", function() {
sortByPriceDescending(); // 버튼 클릭시 높은 가격순으로 정렬 함수 호출
});
document.getElementById("manyordering").addEventListener("click", function() {
manyorderdescending(); // 버튼 클릭시 판매량순으로 정렬 함수 호출
});
});
</script>
버튼 클릭시 높은 가격순, 낮은 가격순, 판매량 순으로 작물을 정렬한다.
달력에 주문 가능한 날짜만 선택할 수 있게 하기
// 세션에서 사용자 ID 가져오기
$userID = $_SESSION['userID'];
$availableDate = "";
$deliveryDate = "";
$today = date('Y-m-d'); // 현재 날짜 가져오기
$harvestPeriod = 0;
// MainCart.php에서 여러 작물을 주문하는 경우
else if (isset($_POST['cropIDs']) && is_array($_POST['cropIDs'])) {
$cropIDs = $_POST['cropIDs'];
foreach ($cropIDs as $cropID){
$stmt = mysqli_prepare($con, "SELECT harvsestPeriod FROM crop WHERE cropID = ?");
mysqli_stmt_bind_param($stmt, "i", $cropID);
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
$row = mysqli_fetch_assoc($result);
$tempHarvestPeriod = $row['harvsestPeriod'];
if ($tempHarvestPeriod > $harvestPeriod) {
$harvestPeriod = $tempHarvestPeriod;
}
}
}
// 배송기간 설정
$deliveryPeriod = 3; // 예시로, 3일로 설정
// 현재 날짜에 재배기간 및 배송기간 더하기
$availableDate = date('Y-m-d', strtotime("+" . ($harvestPeriod + $deliveryPeriod) . " days"));
echo "<script>var deliveryDate = new Date('" . $availableDate . "');</script>";
function drawMonth(year, month) {
tblMonth.innerHTML = '';
var weekdays = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']; // Array of weekdays
// Create the header row with weekday names
var headerRow = document.createElement('tr');
for (var i = 0; i < 7; i++) {
var dayHeader = document.createElement('th');
dayHeader.textContent = weekdays[i];
headerRow.appendChild(dayHeader);
}
tblMonth.appendChild(headerRow);
var firstDay = new Date(year, month - 1, 1).getDay(); // Get the day of the week for the 1st of the month
var lastDate = new Date(year, month, 0).getDate(); // Get the last day of the month
monthThis.textContent = year + '.' + ('0' + month).slice(-2);
var row = document.createElement('tr');
var day = 1;
for (var i = 0; i < firstDay; i++) {
var cell = document.createElement('td');
cell.textContent = '';
row.appendChild(cell);
}
while (day <= lastDate) {
if (row.children.length === 7) {
tblMonth.appendChild(row);
row = document.createElement('tr');
}
var cell = document.createElement('td');
cell.textContent = day;
var currentDate = new Date(year, month - 1, day);
if (currentDate.getTime() >= deliveryDate.getTime()) {
cell.classList.add('selectable'); // 선택 가능한 날짜에 'selectable' 클래스 추가
} else {
cell.classList.add('disabled-date'); // 선택 불가능한 날짜에 'disabled-date' 클래스 추가
}
row.appendChild(cell);
day++;
}
tblMonth.appendChild(row);
}
여러 개의 작물이 있는 경우 작물의 기간이 가장 긴 작물에 배송일 3일을 더한다.
JavaScript에서 deliveryDate라는 새로운 변수를 선언하고, PHP에서 계산된 날짜($availableDate)를 사용하여 Date 객체를 초기화한다.
drawMonth 함수에서 사용자가 날짜를 선택할 수 있는 달력을 만들고 위에서 계산된 날짜 이후의 날짜는 선택가능하고 이전의 날짜는 선택이 불가능하다.
농장 관리 (높은 가시성)
function showPots(farmID) {
// AJAX 요청을 통해 서버에 farmID 전달 및 데이터 요청
fetch('http://localhost/FarmLink/includes/ajax_fmain.php', {
method: 'POST',
body: JSON.stringify({ farmID: farmID }),
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
const pots = data.pots;
let failedPots = data.failedPots;
// 객체를 배열로 변환 (필요한 경우)
if (!Array.isArray(failedPots)) {
failedPots = Object.values(failedPots);
}
console.log("Received data:", data); // 서버로부터 받은 데이터 확인
updatePotGallery(pots, failedPots); // 데이터를 사용하여 pot-gallery 업데이트
})
.catch(error => console.error('Error:', error));
}
function updatePotGallery(pots, failedPots) {
const gallery = document.getElementById('pot-gallery');
gallery.innerHTML = ''; // 기존 내용을 비우고 새로운 내용으로 채움
pots.forEach(pot => {
console.log(`Pot ID: ${pot.potID}, Failed Pots: ${failedPots}`);
const img = document.createElement('img');
img.setAttribute('data-pot-id', pot.potID);
img.className = 'pot-item';
img.src = 'http://localhost/FarmLink/assets/images/plants.png';
img.alt = 'Pot Image';
img.style.width = '150px';
img.style.padding = '5px';
img.style.margin = '45px';
// 고장난 장비가 있는 경우, 테두리 색상을 빨간색으로 설정
if (failedPots.includes(Number(pot.potID))) {
img.style.border = '5px solid red';
} else {
img.style.border = '1px solid #ddd';
}
gallery.appendChild(img);
});
addPotClickEvents(); // 화분 갤러리 업데이트 후 클릭 이벤트 추가
}
동적인 동작이 필요하여 ajax 요청을 통해서 데이터베이스에서 화분의 정보를 조회하여 화분을 글이 아닌 이미지로 보여줌으로서 가시성을 높였고 고장났는지 정상인지 한눈에 파악할 수 있다.
재배 진행률
const today = new Date();
potDataArray.crop.forEach(cropInfo => { // 배열의 각 요소에 대해 반복
const startDate = new Date(cropInfo.latestOrderDate);
const harvestDate = new Date(startDate);
harvestDate.setDate(harvestDate.getDate() + cropInfo.harvestPeriod);
const harvestDateString = harvestDate.toISOString().split('T')[0];
const progressTime = today - startDate;
const totalTime = harvestDate - startDate;
let progressPercent = (progressTime / totalTime) * 100;
progressPercent = Math.min(Math.max(progressPercent, 0), 100);
progressElement.textContent = `${progressPercent.toFixed(2)}%`;
수확 예상날짜 뿐만 아니라 재배가 어느정도 진행이 되었는지 계산하여 퍼센트로 보여준다.
구현하면서 어려웠던 점 & 직면한 문제들
- 데이터베이스를 만들 때 처음 만들어봐서 관계 설정들과 필요 없는 테이블들이 많아서 여러 번 수정을 하였다. 예를 들어 user와 farmer를 각각의 테이블로 나누어서 만들었다가 user로 통합하고 farm을 가지고 있다면 farmer, 없다면 user로 구분하였고 management 테이블 하나만 있었는데 deviceID 만으로 어떤 장비인지 이름을 정의할 수 없어서 equipment 테이블을 추가하였다.
- CSS를 적용할 때 F5를 눌러도 새롭게 짠 CSS가 적용이 안될 때가 있었다. 이럴 때 내가 CSS를 잘못 만진 건지 적용이 아직 안 된 건지 구분이 잘 안되었는데 CTRL + F5를 사용하여 Cache 메모리를 강제로 삭제 시킨 후에 새로고침 하여 사이트를 마치 처음 접속한 것처럼 새롭게 가져와서 해결하였다.
- 이전 페이지에서 사용하였던 정보들을 다른 페이지에서 사용하고 싶었다. 예를들면 결제 페이지에서 여러 단계를 거쳐 진행되는 경우. Session storage에 저장하여 원할 때 추출해서 사용하였다.
아쉬운 점
- DB 수업이라서 DB 설계를 먼저 하고 UI와 웹을 구성하여 초기에 생각했던 웹 페이지를 만들지 못하여 아쉽다. 웹을 만들 때는 UI를 먼저 구성을 하고 DB를 넣거나 혹은 같이 진행을 하면 좋을 것 같다는 생각이 든다.
- 농장주인이 농장을 추가로 증설하게 되면 직접 웹에서 농장과 화분을 추가할 수 있는 부분이 필요하다. (사용자가 스스로 만드는 콘텐츠가 부족)
- 프로젝트 기간이 촉박하여 여러 추가 기능들을 구현하지 못하였고 좀 더 사용자 편의성을 높였으면 좋을 것 같다. (카카오 지도 API를 사용하긴 했지만 클러스터로 농장을 표시하지는 못한점 등)