p.s. 현재 서버를 닫은 관계로 실행이 되지 않습니다
👨👩👦👦Member. Pl2h의 팀원들입니다
한경현(kyunghyun Han):깃허브SNS Frontend
이민주(Minjoo Lee):깃허브SNS Backend
이상민(ssangMin Lee):깃허브Shop Frontend
박준혁(jun hyuk Park):깃허브Shop Backend
기간 : 2021-11-05 ~ 2021-11-22
전반적인 프로젝트 진행절차
1. 프로젝트 파트별로 진행(커플상점 부분과 SNS부분)
2. 오프라인 위주의 팀 작업
3. 오프라인이 불가능할 경우 온라인으로 팀 작업을 진행하며 서로의 코드 상황 브리핑 및 회의
4. 각 파트별 진행 상황을 수시로 공유 - 개발 중 예상치 못한 이슈가 있는지, 다른 파트의 지원이 필요한 부분은 없는지
1주차
-1일차 : 서로의 프로젝트 아이디어 공유 및 정리
-2일차 : 아이디어 세부사항 및 db관계쿼리 정리
-3일차 부터 프로젝트 진행
2주차
-프로젝트 파트별 진행, 중간 에러확인, 파트별 진행률을 확인하면서 앞으로의 진행일정 조율.
3주차
-프로젝트 파트별 마무리 및 배포진행
배포 후
3주간 진행하였던 프로젝트의 결과물에 대해 좋았던 점과 개선해야 할 점을 생각하고 공유합니다.
각자 맡은 부분에 대해 설명하며, 그 외에도 서비스의 가치나 방향성에 대한 의견을 주고 받으며 서로의 생각을 나누었습니다.
지난 프로젝트의 진행과정을 기록으로 남기며 정리합니다.
DB 구조 (ERD)
전체 흐름도
프로젝트 설명 PPT
최근 싸이월드 출시소식관련 하여, 싸이월드 감성과 네이버 커플페이지를 접목시키고, 더불어 SNS를 이용하는 커플들을 대상으로 아이템들을 판매하는 상점를 통한 복합서비스를 구상해보았습니다.
커플상점과 SNS에서 사용하는 DB를 MongoDB와 MySql로 분류하여 더욱더 많은 공부가 될 수 있게 하였고,
관련자료
위의 자료들을 바탕으로 감성과 기능이 함께하는 서비스를 제공하고자 노력했습니다.
커플 전용 SNS 및 SHOP 활성화
1. 커플 기능 구현활용 SNS 활성화
2. RDS(Mysql),Atlas(MongoDB) 동시 활용 프로젝트 활용
3. 결제서비스 구현 숙달
4. sns 페이지 구성
5. 배포 서비스 구현 공부 (ec2,s3,nginx)
기존 서비스와의 차별점
웹 화면 구성 : REACT
DB 액션 처리 : Sequelize
mongoose
DBMS : MySQL
Mongo
개발 Tool :PostMan
Visual Studio Code``github
AWS 배포 : EC2
RDS
Atlas
로그인 구현 : REDUX
프레임워크 : NodeJs
REACT
결제: PAY PAL
프로젝트 관리 Tool : Google Drive
GitHub
notion
사용 모듈(Client) : `
"@material-ui/core": "^4.12.3",
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.60",
"@mui/icons-material": "^5.1.0",
"@mui/styles": "^5.1.0",
"antd": "^3.26.20",
"axios": "^0.19.2",
"axios-mock-adapter": "^1.20.0",
"bootstrap": "^5.1.3",
"components": "^0.1.0",
"core-js": "^3.6.4",
"d3": "^7.1.1",
"formik": "^1.5.8",
"html-react-parser": "^1.4.0",
"postcss-loader": "^6.2.0",
"react": "^16.8.6",
"react-app-polyfill": "^1.0.6",
"react-carousel-minimal": "^1.4.1",
"react-copy-to-clipboard": "^5.0.4",
"react-dom": "^16.8.6",
"react-dropzone": "^11.4.2",
"react-elastic-carousel": "^0.11.5",
"react-flip-move": "^3.0.4",
"react-hook-form": "^7.19.0",
"react-icons": "^3.7.0",
"react-image-gallery": "^1.2.7",
"react-image-shadow": "^1.1.3",
"react-infinite-scroll-component": "^6.1.0",
"react-instagram-zoom-slider": "^1.4.0",
"react-material-ui-carousel": "^3.0.4",
"react-moment": "^1.1.1",
"react-paypal-express-checkout": "^1.0.5",
"react-redux": "^7.1.0-rc.1",
"react-responsive-carousel": "^3.2.22",
"react-router-dom": "^5.0.1",
"react-scripts": "3.4.1",
"react-show-more-text": "^1.5.0",
"react-slick": "^0.28.1",
"react-spring": "^9.3.0",
"react-swift-slider": "^7.0.1",
"react-use-gesture": "^9.1.3",
"redux": "^4.0.0",
"redux-promise": "^0.6.0",
"redux-thunk": "^2.3.0",
"styled-reset": "^4.3.4",
"webpack": "^4.42.0",
"yarn": "^1.22.17",
"yup": "^0.27.0"
사용 모듈(Server) :`
"async": "^3.2.2",
"bcrypt": "^5.0.1",
"body-parser": "^1.18.3",
"cookie-parser": "^1.4.5",
"cors": "^2.8.5",
"debug": "^4.1.1",
"dotenv": "^10.0.0",
"express": "^4.17.1",
"express-session": "^1.17.2",
"fluent-ffmpeg": "^2.1.2",
"jsonwebtoken": "^8.5.1",
"mongoose": "^5.4.20",
"morgan": "^1.10.0",
"multer": "^1.4.3",
"mysql2": "^2.3.3",
"passport": "^0.5.0",
"passport-local": "^1.0.0",
"promise": "^8.1.0",
"sequelize": "^6.9.0",
"sequelize-cli": "^6.3.0"
`
Sequelize
passport
mysql
REDUX
Sequelize
passport
mysql
mongo
mongoose
REDUX
Sequelize
mysql
mongo
mongoose
Sequelize
mysql
mongo
mongoose
Sequelize
passport
PAYTAL
mongo
mongoose
Sequelize
mysql
RDS
Sequelize
mysql
RDS
Sequelize
mysql
RDS
Sequelize
mysql
RDS
Sequelize
mysql
RDS
styled-componets
mongo
mongoose
mongo
mongoose
Sequelize
mysql
RDS
코드 병합시 오류를 최소화 하기위해 깃브런치
✔ main - 개발 완료하고 최종 코드 올리는 브랜치
✔ SNS sns 파트 깃브런치
- diaryhyun (sns 파트 프론트 브런치)
- diarymin (sns 파트 깃 브런치)
✔ SHOP shop 파트 깃브런치
- ssangmin (shop 파트 프론트 브런치)
- parkjh (shop 파트 깃 브런치)
[상황]
[문제]
서버는 포트가 5000 번이고,
클라이언트는 포트가 3000 번으로 다른데 서로 주고 받으려고 하면,
CORS 정책에 의해 막혀버린다.
[해결]
npm i http-proxy-middleware 모듈을 설치하고
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function(app) {
app.use(
'/api',
createProxyMiddleware({
target: 'http://localhost:5000', // 노드 서버가 5000 번이므로 target 도 같게한다.
changeOrigin: true,
})
);
};
프록시 서버를 이용하여 에러를 해결했다.
[상황] 2 가지(MongoDB와 MySqlDB) DB를 동시에 사용하기 때문에, 2 가지 DB를 충돌없이 사용할 수 있는 방안
[문제]
SNS(MySqlDB)로 로그인 하여도 커플상점(MongoDB)를 이용할 수 있도록 하여야 함.
일부 공통된 DB가 필요한 상황이 발생하여, 2 가지 DB의 충돌없이 사용할 수 있도록 고민.
[해결]
Auth.js에서 if문을 사용하여 MySql와 Monggo 이용에 조건을 부여함.
[상황] gitignore에 올라가 있는 env파일이 없어 DB접속 불가
[문제]
GitHub사용할 때 Git branch 전환 시에
gitignore에 올라가 있는 env파일이 없어 DB접속 불가
[해결]
env파일의 존재를 인지하고 env파일을 추가하여 DB접속 성공.
[상황] MusicPlayer 및 auth에러발생
[문제]
auth 와 musicplyer간 충돌로 인한 음악 재생 및 로그인 에러발생
[해결]
음악재생을 유튜브 url로 변경하여 충돌방지
[상황] gitignore에 올라가 있는 env파일이 없어 DB접속 불가
[문제]
[해결]
{Products.length === 0 ? (
<div className="no_item">
<h2>등록된 아이템이 없읍니다</h2>
</div>
) : (
<div className="shop-main-content-item_list">{renderCards}</div>
)}
상품이 없을 경우를 추가했다
[상황] React 렌더링중 에러발생
[문제]
[해결]
const userInfo = useSelector(state => state.user);
const [User, setUser] = useState({});
useEffect(() => {
if (userInfo) {
if (userInfo.userData) {
setUser(userInfo.userData);
if (userInfo.userData.couple_code) {
setCoupleCode(userInfo.userData.couple_code);
}
}
}
}, [userInfo.userData]);
useEffect 를 사용하여 유저정보를 state담아 문제를
[상황] 배포도중 에러발생
[문제]
Error: error:0308010C:digital envelope routines::unsupported
at new Hash (node:internal/crypto/hash:67:19)
at Object.createHash (node:crypto:130:10)
at module.exports (/Users/user/Programming Documents/WebServer/untitled/node_modules/webpack/lib/util/createHash.js:135:53)
at NormalModule._initBuildHash (/Users/user/Programming Documents/WebServer/untitled/node_modules/webpack/lib/NormalModule.js:417:16)
at handleParseError (/Users/user/Programming Documents/WebServer/untitled/node_modules/webpack/lib/NormalModule.js:471:10)
at /Users/user/Programming Documents/WebServer/untitled/node_modules/webpack/lib/NormalModule.js:503:5
at /Users/user/Programming Documents/WebServer/untitled/node_modules/webpack/lib/NormalModule.js:358:12
at /Users/user/Programming Documents/WebServer/untitled/node_modules/loader-runner/lib/LoaderRunner.js:373:3
at iterateNormalLoaders (/Users/user/Programming Documents/WebServer/untitled/node_modules/loader-runner/lib/LoaderRunner.js:214:10)
at iterateNormalLoaders (/Users/user/Programming Documents/WebServer/untitled/node_modules/loader-runner/lib/LoaderRunner.js:221:10)
/Users/user/Programming Documents/WebServer/untitled/node_modules/react-scripts/scripts/start.js:19
throw err;
^
react 배포 중 에러발생. 이는 nodejs 버전이 높아서 생기는 문제다.
[해결] export 옵션
export NODE_OPTIONS=--openssl-legacy-provider
사용하여 문제해결
또는 pakage json 에서
// 변경 전
"start": "react-scripts start"
// 변경 후
"start": "react-scripts --openssl-legacy-provider start"
[상황] 빌드도중 에러 발생
[문제]
FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
빌드시 자바스크립트 에러발생
[해결] export 옵션
-노드의 기본 메모리 사이즈는 512MB다.
과도한 메모리 점유로 인하여 js 엔진이 폭발해버린 것이다.
Java와 마찬가지로 VM에서 Heap메모리 최대한도가 정해져있기 때문이며. 물론 가장 좋은것은 메모리를 적게 쓰도록 로직을 수정하는 것이지만,
그것이 어려울 경우 ‘—max-old-space-size’ 옵션을 주면 힙메모리 영역 한도를 키울수가 있다
로컬에서 빌드 후 git pull후 실행
빌드시 메모리 관련 에러 발생 및 알 수 없는 에러가 발생햇었다.
배포시 배포 관련 숙달이 부족하였고, 빌드하는데서 에러가 발생하여 애를 먹엇다 .
좀 더 숙달하여 이러한 에러가 발생하지 않도록 하며 , 또한 베포시 node js 버전 문제로 인하여 에러가 발생햇엇다
NGINX 서버로 하는 법을 숙달하여 시도를 해보면 좋을 것 같다
데이터베이스 AWS EC2방식 체택 인스턴스 생성후 키값생성 후
서버 연결
sudo su 입력하여 관리모드로 변경
sudo apt update 하여 업데이트
sudo apt install git 하여 깃 다운로드
curl -o-https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash 하여 nvm설치
.~/.nvm/nvm.sh 하여 NVM활성화
nvm install node nvm을 이용하여 Node.js를 설치
git clone 및 git pull 하여 깃의 전체 코드 및 새로운 버전 최신화
server 폴더에서 concurrently 를 사용한 npm run dev 실행하여 server client 동시실행
AWS의 RDS 및 Altars 데이터베이스를 공유하여 협업하여 프로젝트를 진행하였고, AWS의 EC2를 활용하여 배포를 진행 server client 동시 실행
$ sudo apt-get install screen 후 screen 실행 후 npm run dev 실행
스크린 종료 -
screen -ls 서버 확인
kill [screen 번호] 스크린 종료
전체 코드 : 코드
- 폴더 설명 (client 와 server 폴더 나누어서 폴더 진행)
server : 서버 폴더 설정
- config : config 파일 : rds연결 주소
- mongo : mongo db 라우터 및 모델
-configmongo : 몽고 연결 주소
- middleware : 몽고 auth
- routes : 몽고 라우터
- schmas : 몽고 스키마
- mysql : mysql db 라우터 및 모델
- middleware : mysql auth
- models : myslq model
- passport : mysql 로그인 관련
- routes : mysql 라우터
client (view): 클라이언트 폴더 설정
- public : css 이미지 등
- resources
- img
- result-image
- src : 소스모음
- Common
- Setting
- _actions : auth redux 관련
- _reducers :react reducer
- components : 컴포넌트
- hoc : react -hook
- img : 이미지 모음
- log : 유저 댓글 관련 img
- minime : minime 관련 캐릭터
- room : 방설정 이미지
- miniroom : home.js 에 미니룸 관련 js
- SNS
- Jam : 놀거리 관련
- Latter: 편지기능 관련 폴더
- Layout : 전체 레이아웃 관룐
- Login : 로그인 관련
- Main : SNS 로그인 관련
- Menu : 메인 메뉴관련
- MyPage : 상태변경 관련 페이지
- Pic : 사진첩
- Profile : SNS 관련폴더
- RegisterPage : 회원가입 관련 폴더
- rightSideBox: 우측 사이드 뮤직플레이어 관련
- SHOP
- CartPage : 장바구니 관련
- DatilProductPage : 물품 디테일페이지 관련
- HistoryPage :
- LoginMainPage : 로그인페이지 관련
- MainForm : SHOP전체 레이아웃 및 뮤직플레이어 관련
- ShopMain : Shop 메인관련
- UploadProductPage : 이미지 및 동여상 업로드 관련
- images : 이미지 모음
서버(app.js) : 코드
-> 각 페이지별 라우터 설정
passport 5000번사용
mongo db mysql db 분류하여 따로 로그인 가능 -> auth 사용하여 로그인 및 회원가입후 유지기능
mongo db: shop
mysql db: sns
export default function (ComposedClass, reload, homePage, adminRoute = null) {
function AuthenticationCheck(props) {
const user = useSelector(state => state.user);
const dispatch = useDispatch();
useEffect(() => {
if (homePage) { // SNS PAGE
dispatch(auth(true)).then((response) => {
if (!response.payload.isAuth) {
if (reload) {
props.history.push("/sns");
}
} else {
if (adminRoute && !response.payload.isAdmin) {
props.history.push("/sns/admin");
} else {
if (reload === false) {
props.history.push("/sns/main");
}
}
}
});
} else { // SHOP PAGE
{/* SNS */}
<Route exact path="/sns/main" component={Auth(Home, true, true)} />
로그아웃 하기 버튼 누르게 되면 /API/mysql/users/logout으로 데이터 전송
destroy 사 유저 로그인 정보 삭제
const express = require("express");
const router = express.Router();
router.get("/", (req, res) => {
req.logout();
req.session.destroy();
res.send('ok');
});
module.exports = router;
useSelector를 사용하여 redux의 store에 저장된 user정보를 끌고온 후, useState를 사용하여 끌고 온 유저 정보를 변경합니다.
//useSelector를 사용하여 리덕스의 유저정보를 활용
const userInfo = useSelector((state) => state.user);
//useState 를 사용하여 값 변경
const [ChangeMyImage, setChangeMyImage] = useState("");
const [ChangeCoupleCode, setChangeCoupleCode] = useState("");
const [ChangeMessage, setChangeMessage] = useState("");
const [ChangePhone, setChangePhone] = useState("");
const [ChangeAddress, setChangeAddress] = useState("");
const updateImages = (newImages) => {
setChangeMyImage(newImages);
};
const onChangeCoupleCode = (event) => {
setChangeCoupleCode(event.currentTarget.value);
};
const onChangeMessage = (event) => {
setChangeMessage(event.currentTarget.value);
};
const onChangePhone = (event) => {
setChangePhone(event.currentTarget.value);
};
const onChangeAddress = (event) => {
setChangeAddress(event.currentTarget.value);
};
const onSubmitUserInfo = () => {
const variables = {
id: userInfo.userData.id,
image: ChangeMyImage,
couple_code: ChangeCoupleCode,
message: ChangeMessage,
phone: ChangePhone,
address: ChangeAddress,
};
Axios.post("/api/mysql/conditions/update", variables).then((response) => {
if (response.data.success) {
alert("정보가 변경되었습니다.");
props.history.push("/sns/Main");
} else {
alert("정보 변경에 실패하였습니다.");
}
});
};
let userImg;
if (props.user.userData) {
userImg = props.user.userData.image;
}
let postImage = {
updateImages,
};
Math.random() 와 fromCharCode() 를 이용해
랜덤한 숫자에 해당하는 아스키코드를 찾아
알파벳 소문자, 대문자, 숫자를 한자씩 for문으로 지정한 길이만큼 만들어낸다
const [CodeCode, setCodeCode] = useState("");
const [Length, setLength] = useState("8");
const [LowerCheck, setLowerCheck] = useState(true);
const [UpperCheck, setUpperCheck] = useState(true);
const [NumberCheck, setNumberCheck] = useState(true);
const [SymbolCheck, setSymbolCheck] = useState(false);
// 랜덤 코드 만드는 옵션별 함수를 담아둠
const randomFunc = {
lower: getRandomLower,
upper: getRandomUpper,
number: getRandomNumber,
symbol: getRandomSymbol,
};
// crypto.getRandomValues() : 함수는 암호학적으로 강력한 수준의 임의의(random) 값을 생성.
// 매개 변수로 제공된 배열은 임의의 숫자로 채움 (암호화의 의미는 무작위)
function secureMathRandom() {
return (
window.crypto.getRandomValues(new Uint32Array(1))[0] /
(Math.pow(2, 32) - 1)
);
}
// Math.random() : 0에서 1 소수점 랜덤 반환
// Math.floor() : 소수점 버림
// fromCharCode() : 메서드는 UTF-16 코드 유닛의 시퀀스로부터 문자열을 생성해 반환
// 소문자 랜덤생성
function getRandomLower() {
// 해석 : 랜덤 소수점을 알파벳(26자)만큼 곱하고 97을 더한 값을 소수점을 버린 정수에
// 해당하는 알파벳을 반환
// 아스키코드 97~122번, 대문자는 65~90번, 숫자는 48~57번이기 때문
return String.fromCharCode(Math.floor(Math.random() * 26) + 97);
}
// 대문자 랜덤생성
function getRandomUpper() {
return String.fromCharCode(Math.floor(Math.random() * 26) + 65);
}
// 숫자 랜덤생성
function getRandomNumber() {
return String.fromCharCode(Math.floor(secureMathRandom() * 10) + 48);
}
// 특수문자 랜덤생성
function getRandomSymbol() {
const symbols = '~!@#$%^&*()_+{}":?><;.,';
return symbols[Math.floor(Math.random() * symbols.length)];
}
// 옵션대로 코드 만들어낼 함수
function generateCode(length, lower, upper, number, symbol) {
// 결과물의 초기값은 빈값 ""
let generatedCode = "";
// filter 거쳐서 typesArr 에 배열로 담기
const typesArr = [{ lower }, { upper }, { number }, { symbol }].filter(
// filter 로 각 옵션(객체)들의 배열 인덱스0 에서 false 거르기
(item) => Object.values(item)[0]
);
// 입력한 코드 길이만큼 for문 돌리기
for (let i = 0; i < length; i++) {
// 옵션 수 만큼 forEach 돌리기
typesArr.forEach((type) => {
// forEach 해당회차의 옵션을 funcName에 담음
const funcName = Object.keys(type)[0];
// randomFunc 함수에서 해당회차 옵션명에 해당하는 함수 돌려서 나온 값을 결과에 추가 //
generatedCode += randomFunc[funcName]();
});
}
// 결과물 배열 잘라서 반환
return generatedCode.slice(0, length);
} ////////////////////////////////////////////////함수 분석
// 입력한 길이만큼 소문자, 대문자, 숫자, 특수문자 순으로 결과를 뱉어낸다
// 그럼 (true인 옵션 수) * (입력한 길이) 의 결과물이 나오고
// 결과물에서 입력했던 길이만큼만 잘라낸다 ((옵션수*길이)-길이) 만큼의 코드는 버리게 됨
function CodeCodeCreator() {
if (Length < 8) {
alert("코드가 너무 적어요 8자 이상으로 해주세요");
} else if (LowerCheck || UpperCheck || NumberCheck || SymbolCheck) {
setCodeCode(
generateCode(Length, LowerCheck, UpperCheck, NumberCheck, SymbolCheck)
);
} else {
alert("선택된 옵션이 없어요");
}
}
sns 홈 화면에는 방문자가 글을 남길 수 있는 방명록 기능을 두어 싸이월드의 감성을 높였습니다. useState를 사용하여 미니미를 선택할 수 있습니다.
const onSubmit = (event) => {
// event.preventDefault(); // antd 자체 적용
if (
!ProfileContent
) {
return alert("fill all the fields first!");
}
const variables = {
content: ProfileContent,
};
console.log(variables);
Axios.post("/api/mysql/profiles/write", variables)
.then((response) => {
console.log('props.userInfo 는 : ', response);
if (response.data.success) {
alert("Product Successfully Uploaded");
window.location.replace("/sns/main");
} else {
console.log(response.data)
alert("Failed to upload Product");
}
});
};
<Form onSubmit={onSubmit}>
<Container>
<Wrap className="VisitMsgBox">
<ShowMiniMe className="VisitMsgBox-ImageBox">
<img src={mArr[minime]} alt={"미니미"} />
<SettingMinime onClick={() => setIsShow(!isShow)}>
미니미
</SettingMinime>
<DropdownMinime
isShow={isShow}
setIsShow={setIsShow}
setMinime={setMinime}
mArr={mArr}
/>
</ShowMiniMe>
<VisitMsg onChange={onProfileContent} value={ProfileContent} />
<Button onClick={onSubmit}>남기기</Button>
</Wrap>
</Container>
</Form>
커플인 경우 커플코드를 입력받아서, 같은 커플코드를 가진 경우 커플로 매칭하고 sns 메인 화면에 커플을 상징하는 이미지와 커플들의 이름을 띄워줍니다.
const getCouple = () => {
Axios.post("/api/mysql/couples/read")
.then((response) => {
const arr = response.data.allUser;
for (let i = 0; i < arr.length; i++) {
if (arr[i].couple_code === CoupleCode) {
if (arr[i].name !== userInfo.userData.name && userInfo.userData.couple_code !== "9999") {
setCC1(userInfo.userData.name)
setCC2(arr[i].name)
break;
}
} else {
}
}
});
}
const couplelove = () => {
if (CoupleCode === "9999" && CoupleCode === "9999") {
return <LinkTitle><p>빛이나는 솔로</p></LinkTitle>
} else if (CoupleCode !== "9999" && CoupleCode !== "9999") {
return <div className="couple">
<img src={img} />
<p>{CC1}♥️{CC2}</p>
</div>
}
}
useState를 이용하여 게시글을 DB에 저장하였습니다.
const [profilecontent, setPostTitle] = useState("");
const onSubmit = (event) => {
if (!profilecontent) {
return alert("fill all the fields first!");
}
const variables = {
content: profilecontent,
};
Axios.post("/api/mysql/posts/write", variables)
.then((response) => {
console.log('props.user 는 : ', response);
if (response.data.success) {
alert("Post Successfully Uploaded");
props.history.push("/sns");
} else {
console.log(response.data)
alert("Failed to upload Post");
}
});
};
모달 내부에서 DELETE사용 SERVER로 정보를 가게하여 삭제할수있게 기능 추가
const Delete = (props) => {
const [postData, setPostData] = useState([]);
let index = (props.modal.index);
useEffect(() => {
setPostData(props.modal.modal);
}, []);
const onSubmit = (event) => {
event.preventDefault();
Axios.delete(`/api/mysql/posts/write/delete/${postData[index].id}`)
.then(response => {
if (response.data.success) {
window.location.replace('./profile')
} else {
alert('댓글을 저장하지 못했습니다.');
}
})
}
return (
<form style={{ display:'flex' }} onSubmit={onSubmit}>
<button style={{ width: '20%', height: '37px' }} onClick={onSubmit} >삭제</button>
</form>
)
};
SNS Modal
사진첩 Modal-
onsubmit 을 통해 클릭시 모달창 렌더링
modal 에 props를 전달 하여 데이터 및 이미지 렌더링
onSubmit={onSubmit} onDoubleClick={() => {
setIndex(index);
setOpenModal(true);
<Modal
index={Index}
modal={PostArray}
setOpenModal={setOpenModal}
openModal={openModal}
/>
const Modal = (props) => {
const { setOpenModal } = props;
const data = props.modal
disablekb 를 1로 설정하여 플레이어가 키보드 컨트롤에 응답하지 않게 했고,
autoplay 를 1로 설정하여 자동 재생되게 하였습니다
<iframe
width="240"
height="100"
src="https://www.youtube.com/embed/_3A-jr1xC9o?loop=1&controls=1&autoplay=1&mute=0&disablekb=1&fs=0"
title="YouTube video player"
allow="accelerometer; autoplay; clipboard-write; encrypted-media;
gyroscope; picture-in-picture;"
></iframe>
선택된 카데고리 값을 e.currentTarget.innerText를 이용하여 back으로 전달 합니다. back에서는 일치하는 값을 찾아서 front로 보내고, front는 전달 받은 data를 map함수를 이용하여 화면에 출력하였습니다.
export default () => {
const [posts, setPosts] = useState([]);
const [mood, setMood] = useState('');
const moods = ['영화', '공연', '축제', '여행', '맛집', '기타'];
const onMoodChange = (e) => {
setMood(e.currentTarget.innerText);
setPosts([]);
const variables = {
mood: e.currentTarget.innerText,
}
//back으로 선택된 카데고리 값을 전달합니다.
Axios.post("/api/mysql/jams/read/mood", variables)
.then((response) => {
if (response.data.success) {
setPosts(response.data.jams);
} else {
alert("읽어드리는 데 실패하였습니다.");
}
});
};
return (
<MarginContainer>
<HeaderContainer>
<Title>🎪🎉🎏</Title>
<MoodList>
{moods.map((moodText) => (
<Mood
key={moodText}
onClick={onMoodChange}
// 선택된 카테고리가 활성화 됩니다.
active={moodText === mood ? true : false}
>
{moodText}
</Mood>
))}
</MoodList>
</HeaderContainer>
<Container>
{posts.map((post) => (
<Picture
//컴포넌트로 props를 전달합니다.
id={post.id}
key={post.id}
imageUrl={post.images[0]}
mood={post.mood}
review={post.review}
title={post.title}
/>
))}
</Container>
</MarginContainer>
);
};
router.post('/mood', async (req, res, next) => {
try {
let keyWord = req.body.mood
const fulljam = await Jam.findAll({
include: {
model: User,
attribute: ["id", "name"],
},
where: {
mood: keyWord,
},
order: [['id', 'DESC']],
});
useSelector와 useState를 사용하여 연인에게 편지를 보낼 수 있는 기능입니다.
const LetterWrite = (props) => {
const userInfo = useSelector(state => state.user);
const [LetterTo, setLetterTo] = useState("");
const [LetterTitle, setLetterTitle] = useState("");
const [LetterContent, setLetterContent] = useState("");
const [LetterFrom, setLetterFrom] = useState("");
const [LetterPs, setLetterPs] = useState("");
const [Code, setCode] = useState("");
const [Name, setName] = useState("");
useEffect(() => {
if (userInfo) {
if (userInfo.userData) {
if (userInfo.userData.couple_code) {
setCode(userInfo.userData.couple_code);
}
if (userInfo.userData.name) {
setName(userInfo.userData.name);
}
}
}
}, [userInfo])
const onLetterTo= (event) => {
setLetterTo(event.currentTarget.value);
};
const onLetterTitle= (event) => {
setLetterTitle(event.currentTarget.value);
};
const onLetterContent= (event) => {
setLetterContent(event.currentTarget.value);
};
const onLetterFrom= (event) => {
setLetterFrom(event.currentTarget.value);
};
const onLetterPs= (event) => {
setLetterPs(event.currentTarget.value);
};
const user = useSelector(state => state.user);
const onSubmit = (event) => {
if (
!LetterTo ||
!LetterTitle ||
!LetterContent ||
!LetterFrom ||
!LetterPs
) {
return alert("fill all the fields first!");
}
const variables = {
to: LetterTo,
title:LetterTitle,
content:LetterContent,
from:LetterFrom,
ps:LetterPs,
};
Axios.post("/api/mysql/letters/write", variables)
.then((response) => {
console.log('props.user 는 : ', response);
if (response.data.success) {
alert("Letter Successfully Uploaded");
props.history.push("/sns/Latter");
} else {
console.log(response.data)
alert("Failed to upload Letter");
}
});
};
if (Code !== null && Code !== undefined) {
return <div id="wrap">
<div id='form_wrap'>
<Form onSubmit={onSubmit}>
{/* DropZone */}
<label>to</label>
<Input onChange={onLetterTo} value={LetterTo} />
<label>제목</label>
<Input onChange={onLetterTitle} value={LetterTitle} />
<label>내용</label>
<Input onChange={onLetterContent} value={LetterContent} />
<label>From</label>
<Input onChange={onLetterFrom} value={LetterFrom} />
<label>추신</label>
<Input onChange={onLetterPs} value={LetterPs} />
<br />
<Button onClick={onSubmit}>편지보내기</Button>
</Form>
</div>
</div>
} else {
return <div><p>편지를 못써요</p></div>
}
};
물품등록 페이지에서 기입한 내용과 사진,영상에 대한 정보가 DB에 들어간다
물품등록
Axios.post("/api/mongo/product/uploadProduct", variables).then(
(response) => {
if (response.data.success) {
alert("아이템이 거래소에 등록되었습니다.");
props.history.push("/shop/main");
} else {
alert("아이템 등록이 실패하였습니다");
}
}
);
router.post("/", (req, res) => {
console.log(req.body);
const product = new Product(req.body);
product.save((err, doc) => {
if (err) return res.json({ success: false, err });
return res.status(200).json({ success: true });
});
});
const [Checked, setChecked] = useState([]);
const handleToggle = (value) => {
const currentIndex = Checked.indexOf(value);
const newChecked = [...Checked];
if (currentIndex === -1) {
newChecked.push(value);
} else {
newChecked.splice(currentIndex, 1);
}
setChecked(newChecked);
props.handleFilters(newChecked);
};
const renderCheckboxLists = () =>
props.list &&
props.list.map((value, index) => {
return (
<React.Fragment key={index}>
<div>
<Checkbox
onChange={() => handleToggle(value._id)}
checked={Checked.indexOf(value._id) === -1 ? false : true}
/>
<span>{value.name}</span>
</div>
</React.Fragment>
);
});
const itemNumber = [
{
_id: 1,
name: "의류",
},
{
_id: 2,
name: "신발",
},
// .......중략
{
_id: 12,
name: "기타",
},
];
export { itemNumber };
카테고리 분류 추가
체크박스와 라디오박스를 선택하면
물품목록과 가격대목록의 _id 값을 찾아서
해당되는 물품들 새로 출력
const addToCarthandler = () => {
props.addToCart(props.detail._id);
};
const dispatch = useDispatch();
const addToCartHandler = (pd_id) => {
dispatch(addToCart(pd_id));
alert("장바구니에 담겼습니다.");
};
export function addToCart(_id) {
const request = axios
.get(`${MONGO_USER_SERVER}/addToCart?productId=${_id}`)
.then((response) => response.data);
return {
type: ADD_TO_CART_USER,
payload: request,
};
}
장바구니에 담으면 물품정보를 디스패치 액션을 통해 User DB에 저장한다
export function getHistory(data) {
let request = axios
.get(`${MONGO_USER_SERVER}/payment/getHistory`, data)
.then((response) => response.data);
return {
type: AUTH_USER,
payload: request,
};
}
결제되면 물품에 대한 구매이력 DB에 저장
댓글을 작성하면 작성자, 게시글, 댓글내용이 DB에 저장이되고 답글을 달면 댓글정보도 추가로 DB에 저장된다.
댓글은 Comment.js 에서 State 에 저장되고 refreshFunction 로 지정된 props 로 최상위 부모인 DetailProductPage.js 에 Comment State 에 저장된다.
또한 Comment.js 에서는 SingleComment.js 의 props 를 이용해서 첫번째 댓글이 정해지며, ReplyComment.js 의 props 를 이용해서 첫댓글의 댓글을 불러온다.
위의 과정을 반복하여 무한한 대댓글을 달 수 있다.
const user = useSelector((state) => state.user);
const [Comment, setComment] = useState("");
const [Writer, setWriter] = useState("");
const handleChange = (event) => {
setComment(event.currentTarget.value);
};
useEffect(() => {
const userData = { userInfo : user.userData };
if (user.userData) {
if (user.userData.gender) {
axios.get('/api/mongo/users/sns/getMongo', userData)
.then(response => {
if (response.data.success) {
console.log(response);
setWriter(response.data.user[0]._id);
} else {
alert('Failed')
}
})
} else {
if (user.userData) {
setWriter(user.userData._id);
}
}
}
}, [user.userData])
const onSubmit = (event) => {
event.preventDefault();
const variables = {
content: Comment,
writer: Writer,
postId: props.postId,
};
axios.post("/api/mongo/product/saveComment", variables).then((response) => {
if (response.data.success) {
setComment("");
props.refreshFunction(response.data.result);
} else {
alert("Failed to save Comment");
}
});
};
return (
<div className="postPage-comment">
<p className="comment-text">댓글</p>
<hr />
{props.commentLists &&
props.commentLists.map(
(comment, index) =>
!comment.responseTo && (
<React.Fragment>
<SingleComment
comment={comment}
postId={props.postId}
refreshFunction={props.refreshFunction}
/>
<ReplyComment
CommentLists={props.commentLists}
postId={props.postId}
parentCommentId={comment._id}
refreshFunction={props.refreshFunction}
/>
</React.Fragment>
)
)}
const user = useSelector(state => state.user);
const [CommentValue, setCommentValue] = useState("")
const [Writer, setWriter] = useState("");
// 생략
const onSubmit = (event) => {
event.preventDefault();
const variables = {
writer: Writer,
postId: props.postId,
responseTo: props.comment._id,
content: CommentValue
}
Axios.post('/api/mongo/product/saveComment', variables)
.then(response => {
if (response.data.success) {
setCommentValue("")
setOpenReply(!OpenReply)
props.refreshFunction(response.data.result)
} else {
alert('Failed to save Comment')
}
})
}
const actions = [
<span onClick={openReply} key="comment-basic-reply-to">답글 달기</span>
]
if (props.comment) {
return (
<div>
<Comment
actions={actions}
author={props.comment.writer.name}
avatar={
<Avatar
src={props.comment.writer.image}
alt="image"
/>
}
content={
<p>
{props.comment.content}
</p>
}
/>
const [ChildCommentNumber, setChildCommentNumber] = useState(0)
const [OpenReplyComments, setOpenReplyComments] = useState(false)
useEffect(() => {
let commentNumber = 0;
props.CommentLists.map((comment) => {
if (comment.responseTo === props.parentCommentId) {
commentNumber++
}
})
setChildCommentNumber(commentNumber)
}, [props.CommentLists, props.parentCommentId])
let renderReplyComment = (parentCommentId) =>
props.CommentLists.map((comment, index) => (
<React.Fragment>
{comment.responseTo === parentCommentId &&
<div style={{ width: '80%', marginLeft: '40px' }}>
<SingleComment comment={comment} postId={props.postId} refreshFunction={props.refreshFunction} />
<ReplyComment CommentLists={props.CommentLists} parentCommentId={comment._id} postId={props.postId} refreshFunction={props.refreshFunction} />
</div>
}
</React.Fragment>
))
React랑 React+Redux의 결정적 차이
React는 React 컴포넌트 자신이 개별적으로 상태관리를 한다.
React+Redux는 상태관리를 하는 전용 장소(store)에서 상태를 관리하고, React 컴포넌트는 그걸 보여주기만 하는 용도로 쓰인다.
Store
상태는 기본적으로 전부 여기서 집중관리 됩니다. 커다란 JSON의 결정체정도의 이미지입니다.
Action 및 Action Creator
Store 및 Store에 존재하는 State는 아주 신성한 것이라고 할 수 있습니다. React 컴포넌트같은 하등한 것이 직접 접근하려고 하면 안 되는 것이죠. 직접 접근하기 위해 Action이라는 의식을 거쳐야 합니다. 이벤트 드리븐과 같은 개념입니다.
Reducer
앞에 ‘Store의 문지기’라고 쓴 적이 있습니다만, 그 개념과 비슷한 역할을 하는 것이 Reducer입니다.
함수형 프로그래밍에서 Reducer라는 용어는 합성곱을 의미합니다만, Redux에 한해서는 아래와 같이 이전 상태와 Action을 합쳐, 새로운 state를 만드는 조작을 말합니다.
각 관계에 맞게 관계쿼리 생성
image의 경우 sns 및 사진첩 에서 동시에 사용하기 때문에 관계를 엮어 사용
구글 프레젠테이션 파일
구글 프리젠테이션 파일 기획안 2