JS FULL STACK 14장 팀프로젝트 회고
뭘만들었지?
1. 맛집 리뷰 Crud
팀원 총 : 2명
진행기간 프로젝트 1개당 7일 총 14일
함께 강의를 듣던 같은 대학교 친구와 만들었다.
프론트-백엔드 교체 개발
먼저 맛집 리뷰 crud때는 백엔드 개발을 담당했다.
GitHub - TeambMakzip/makzip_back
Contribute to TeambMakzip/makzip_back development by creating an account on GitHub.
github.com
또한 레시피 제작 crud때는 프론트엔드 개발을 담당했다.
GitHub - MWTeamB/Recipe_Frontend-
Contribute to MWTeamB/Recipe_Frontend- development by creating an account on GitHub.
github.com
프로젝트 주요 기능 및 내가 한것
1. 맛집 리뷰 사이트
첫날 Figjam을 통해 팀원과함께 업무를 파악하기 위해 기획서를 그려보며 대화를 나누었다.
기존에 학교 프로젝트로 맛집정보제공사이트를 제작하고있었는데 영감을 얻어 이러한 음식에대한 정보를 주고받는 사이트를 만들어보자는 취지에서
간단한 맛집 리뷰사이트를 제작하였다.
그리고 기획서를 중심으로 하나의 큰덩어리인 Entity를 뽑아보려했는데
내가 생각했을때 해당 crud를 빠르게 구현하기 위해서 단 하나의 Entity 즉 맛집(식당)이라는 녀석만 있으면될거같다는 생각이 들었다.
놀랍게도 이게 전부다..
그리고 클라이언트측에서 각각 맛집을 조회하고, 등록하고 삭제하고, 수정 요청할수있도록 API를 만들어 fly.io로 배포하였다.
import cors from "cors";
import express from "express";
import pkg from "pg";
const { Pool } = pkg;
const pool = new Pool({
user: "postgres",
password: "VIsperTaGPPlEG6",
host: "makzip-teamb.internal",
database: "postgres",
port: 5432,
});
const app = express();
const corsOptions = {
origin: "*",
};
app.use(cors(corsOptions));
app.use(express.json());
const port = 3000;
app.get("/", (req, res) => {
res.send("Hello World!");
});
// 다건조회
app.get("/api/v1/review", async (req, res) => {
try {
const result = await pool.query("SELECT * FROM Restaurant");
const listrows = result.rows;
res.json({
resultCode: "S-1",
msg: "성공",
data: listrows,
});
} catch (error) {
console.error(error);
res.status(500).json({
resultCode: "F-1",
msg: "에러 발생",
error: error.toString(),
});
}
});
// 단건조회
app.get("/api/v1/review/:id", async (req, res) => {
try {
const id = req.params.id;
const result = await pool.query("SELECT * FROM Restaurant WHERE id = $1", [
id,
]);
const listrow = result.rows[0];
res.json({
resultCode: "S-1",
msg: "성공",
data: listrow,
});
} catch (error) {
console.error(error);
res.status(500).json({
resultCode: "F-1",
msg: "에러 발생",
error: error.toString(),
});
}
});
// 생성
app.post("/api/v1/review", async (req, res) => {
try {
const { title, contents, is_checked = false } = req.body;
if (!title) {
res.status(400).json({
resultCode: "F-1",
msg: "title required",
});
return;
}
if (!contents) {
res.status(400).json({
resultCode: "F-1",
msg: "contents required",
});
return;
}
const result = await pool.query(
"INSERT INTO Restaurant (title, contents, created_at, updated_at, is_checked) VALUES ($1, $2, NOW(), NOW(), $3) RETURNING *",
[title, contents, is_checked]
);
const recordrow = result.rows[0];
res.json({
resultCode: "S-1",
msg: "성공",
data: recordrow,
});
} catch (error) {
console.error(error);
res.status(500).json({
resultCode: "F-1",
msg: "에러 발생",
error: error.toString(),
});
}
});
// 수정
app.patch("/api/v1/review/:id", async (req, res) => {
const { id } = req.params;
const { title, contents, is_checked = 0 } = req.body;
try {
const checkResult = await pool.query(
"SELECT * FROM Restaurant WHERE id = $1",
[id]
);
const listrow = checkResult.rows[0];
if (listrow === undefined) {
res.status(404).json({
resultCode: "F-1",
msg: "not found",
});
return;
}
await pool.query(
"UPDATE Restaurant SET title = $1, contents = $2, updated_at = NOW(), is_checked = $3 WHERE id = $4",
[title, contents, is_checked, id]
);
// 수정된 데이터를 다시 조회하여 클라이언트로 전송
const updatedResult = await pool.query(
"SELECT * FROM Restaurant WHERE id = $1",
[id]
);
const updatedListrow = updatedResult.rows[0];
res.json({
resultCode: "S-1",
msg: "성공",
data: updatedListrow, // 수정된 데이터를 클라이언트로 전송
});
} catch (error) {
console.error(error);
res.status(500).json({
resultCode: "F-1",
msg: "에러 발생",
error: error.toString(),
});
}
});
//삭제
app.delete("/api/v1/review/:id", async (req, res) => {
const { id } = req.params;
const checkResult = await pool.query(
"SELECT * FROM Restaurant WHERE id = $1",
[id]
);
const listrow = checkResult.rows[0];
if (listrow === undefined) {
res.status(404).json({
resultCode: "F-1",
msg: "not found",
});
return;
}
try {
await pool.query("DELETE FROM Restaurant WHERE id = $1", [id]);
res.json({
resultCode: "S-1",
msg: `${id}번 리뷰가 삭제 되었습니다`,
});
} catch (error) {
console.error(error);
res.status(500).json({
resultCode: "F-1",
msg: "에러 발생",
error: error.toString(),
});
}
});
app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
});
트러블슈팅(문제및 해결방법)
사실 너무 간단하게 만들어놓으니 문제가없었던게 문제였다.
물론 fly.io로 배포해야되기때문에 mysql -> postgresql db서버를 하나 연다음 연결시켜서 따로 서버를 배포했어야된다는걸 깜빡했는데.
프론트측에서 곧바로 말해줘서 fly.io로 배포한뒤에 다시 서버주소를 넘겨주었다.
실현 가능성을 통해서 crud만 가능하도록 아주 간단한게 개발하고자한것이 딱히 어떤 큰 문제이슈나 개발능력을 키워주는데 도움을주지 못해서 아쉬웠다. 또한 팀원과 적극적으로 피드백이 오고가지않아 아쉬웠다.
2. 음식 제작 레시피 crud
역시 첫날 figjam으로 함께 기획서를 그려보며 업무파악을 진행하였다
사람이라면 누구나 음식을 사랑하지않을까 그렇기 때문에 음식과 관련된 카테고리를 이어서 레시피를 만들고 기록해보자는 취지에서
해당 crud웹 서비스를 제작해보았다.
.
이전보다는 확실히 더많은 기능들을 구현해보고자 대화를 나누어 기획서를 제작해보았고
나는 보다 동적으로 화면구성이 어떻게 진행되었으면 할까라는 궁금증에서 HTML , CSS를 통해서 화면 구성을 먼저해보았다.
심플하고 간단하게 오고갈수있는 버튼과 레시피를 카드에 담는 담아 정보를 제공한다는 컨셉은
내가 사진자료를 많이 얻고하는 핀터레스트로부터 영감을 얻었다.
핀터레스트의 이러한 레이아웃과 카드에 컨텐츠를 담아 표시하는 masonry레이아웃은 꽤나 큰 인기를 끌었다고 한다.
동적인 화면을 먼저 구상하다보니 팀원이 API개발을 2일만에 모두만들어 배포를 해줬고
나는 해당 요청에 맞는 화면을 이쁘게 만들어보고자 tailwind css 라이브러리를 사용하며 Ui를 만들어보았다.
Tailwind 라이브러리로 원하는 ui를 만들어볼때 구글에 Tailwind card와같은 이름으로 검색해보며 필요한 클래스명들을 가져와 제작했고
cheet sheet를 참고하며 속성들을 조절하며 만들어보았다.
각각의 페이지로 이동하는 것은 React-router 라이브러리를 활용하였다.
트러블슈팅(문제및 해결방법)
<div className="block max-w-sm p-6 rounded-lg shadow hover:bg-gray-800 bg-gray-400 dark:border-gray-700 dark:hover:bg-gray-700" onClick={handleCardClick}>
{isEditing ? (
<>
<input
type="text"
value={editedField}
onChange={(e) => setEditedField(e.target.value)}
className="mb-2 px-3 py-2 rounded-lg border border-gray-300"
/>
<textarea
value={editedDescription}
onChange={(e) => setEditedDescription(e.target.value)}
className="mb-2 px-3 py-2 rounded-lg border border-gray-300"
></textarea>
<input
type="text"
value={editedCookingTime}
onChange={(e) => setEditedCookingTime(e.target.value)}
className="mb-2 px-3 py-2 rounded-lg border border-gray-300"
/>
</>
) : (
<>
<h5 className="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">
{list.field}
</h5>
<p className="font-normal text-white dark:text-gray-400">{list.description}</p>
<p className="font-normal text-white dark:text-gray-400">조리시간 : {list.cooking_time}</p>
</>
)}
<button
className="inline-flex items-center px-4 py-2 text-sm font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
onClick={isEditing ? handleSaveEdit : handleEditClick}
>
{isEditing ? '저장' : '레시피 수정'}
</button>
{isEditing && (
<button
className="inline-flex items-center px-4 py-2 text-sm font-medium text-center text-gray-900 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-700 dark:focus:ring-gray-700 ms-3"
onClick={handleCancelEdit}
>
취소
</button>
)}
</div>
저 레시피 목록을 담아두고있는 card가 바로 코드 상단의 div태그로 카드를 클릭하면 삭제안내창을 띄우도록 하는게 내 목적이였는데
문제는 레시피수정버튼이나 재료 추가 버튼을 눌러도 저 안내메시지가 계속해서 뜨는 문제였다.
코드상으로 div태그 안에 존재하기 때문에 onclick event 함수가 전파가 된 문제였다.
아주 다행히도 stopPropagation함수를 통해 이 문제를 해결 할 수 있었다.
또한
https://velog.io/@seoyul0203/REACT-%EA%B2%80%EC%83%89%EA%B8%B0%EB%8A%A5%EA%B5%AC%ED%98%84
velog
velog.io
해당 블로그글을 통해 쉽게 문제를 해결할수 있었다.
아쉬운점 ?
첫번째프로젝트의경우 너무 간단해서 아쉬웠고
두번째프로젝트는 반응형으로 구현하지 못한 것이 아쉬웠다.
실제로 내 mac air화면에서 깔끔하게 나오지만 폰이나 태블릿으로 보면 화면이 전부 끌어 당겨져서 잘 보이지 않고 어떤 버튼은 사라지기도한다.
그래서 다음프로젝트에서는 꼭 반응형 웹에 맞게 UI를 만들어보고싶다는 소망이 생겼다.