-
케이커 웹/앱 - 1 (가게 정보 등록 및 수정, 가게 정보 미리보기)프론트엔드 2022. 8. 14. 10:37
const.js
상수 데이터 모음
default 생략 시, 한 파일에 여러 export 구현 가능
export const SHOP_DATA = [{key: "name",height: "60px",placeholder: " 정확한 상호명을 입력해주세요.",keyword: "가게 이름",},{key: "readme",height: "125px",placeholder: " 자신의 가게를 자유롭게 소개해주세요! (최대 300자)",keyword: "가게 소개",},{key: "phoneNumber",height: "60px",placeholder: " ex. 02-0000-0000",keyword: "전화번호",},{key: "address",height: "60px",placeholder: " ex. 서울특별시 서대문구 대현동 11-11층",keyword: "주소",},{key: "openTime",height: "60px",placeholder: " ex. 매일 12:00~20:00",keyword: "운영 시간",},{key: "kakaoUrl",height: "60px",placeholder: " 운영중인 카카오톡 채널이 있다면 링크를 첨부해주세요.",keyword: "카카오톡 채널",},{key: "instagram",height: "60px",placeholder: " 운영중인 인스타그램이 있다면 링크를 첨부해주세요.",keyword: "인스타그램",},];
export const DEFAULT_STORE_DATA = {name: "", //(string)readme: "", //stringaddress: "", //stringkakaoUrl: "", //stringinstagram: "", //stringcertifyFlag: 0, //가게 인증 여부 (boolean)openTime: "", //stringphoneNumber: "", //stringmainImg: "",};index.jsimport { SHOP_DATA, DEFAULT_STORE_DATA } from "./const";import {WrapBox,ShopRegistering,ShopIntroduceName,ShopIntroduce,RegisterBtn,} from "./styles";스타일 컴포넌트 따로 만들어서 추출해온다.import http from "../../../common/http";import { useNavigate } from "react-router-dom";const token = JSON.parse(localStorage.getItem("token"));카카오에서 받아온 토큰을 localStorage에 저장 후, 가져온다. => 회원 정보 구분JSON.parse() 메소드는 인수로 전달받은 문자열을 자바스크립트 객체로 변환하여 반환한다.
//ui 구현const ShopInformationRegister = () => {const nav = useNavigate();useNavigate() 메소드는 url을 조작할 수 있는 interface를 제공한다.const formData = new FormData();const [storeData, setStoreData] = useState(DEFAULT_STORE_DATA);
const updateStoreData = partialStoreData => {setStoreData(prev => ({ ...prev, ...partialStoreData }));};이전값들은 prev로, 현재 객체 형태의 값은 partialStoreData로 들어간다. => 새로운 State로 반환ES6 spread 연산자 {...} 에 대해 알아보자.
spread 연산자는 배열 및 객체를 간편하게 '복제'한다.
특히, 객체에서 특정한 값만을 변경(update)해야 할 때, 자주 쓰인다.
const object1 = { a:1, b:2, c:3};
const object2 = { ...object1, b:4};
a,c는 그대로 유지되고 b만 바뀐다.const [MainFile, setMainFile] = useState([]);const [MenuFile, setMenuFile] = useState([]);파일 업로드(DND)는 컴포넌트로 따로 분리해 작성해뒀다. => 포스팅 4 참고const MainFileHandler = e => {e.preventDefault();setShopFile(e.tartget.value);};
const MenuFileHandler = e => {e.preventDefault();setMenuFile(e.tartget.value);};
//제출 -> 백엔드로 postconst submitHandler = e => {console.log(storeData);
http.post("/stores/myStore", storeData, {headers: {"Content-type": "application/json",},}).then(res => postImg(res.data)).catch(err => console.log("json 포스트 실패", err));카카오에서 토큰을 받았다면, axios 헤더에 토큰을 추가해 사용자를 구분해냈다.
const postImg = async id => {formData.append("mainImg", MainFile[0].original, "mainImage.png");formData.append("menuImg", MenuFile[0].original, "menuImage.png");formData.append("storeId", id);
for (const keyValue of formData) console.log(keyValue);axiosheaders: {"Content-Type": "multipart/form-data","X-AUTH-TOKEN": token,},}).then(res => console.log("파일 포스트 성공", res)).catch(err => console.log("파일 포스트 실패", err));nav("/caker/mypage");};처음에는 application/json 타입과 multipart/form-data 타입을 formData에 묶어 백엔드로 한 번에 보내려 했으나, 실패했다. (엄청 어려운 작업이라고 들었다,,) 그래서 application/json 타입을 먼저 보내고 얻어온(회원 구분) id를 multipart/form-data에 추가해 보내는 방법을 선택했다.};
return (<WrapBox><TopBar /><div><ShopRegistering>가게 정보 등록</ShopRegistering>{SHOP_DATA.map(data => {return (<React.Fragment key={data.key}><ShopIntroduceName>{data.keyword}</ShopIntroduceName><ShopIntroducetype="text"height={data.height}value={storeData[data.key]} 키에 해당하는 현재 값onChange={e => {e.preventDefault();updateStoreData({ [data.key]: e.target.value }); 객체 형태(키: 현재 인풋 창에 입력된 값)}}placeholder={data.placeholder}/></React.Fragment>);})}SHOP_DATA는 const.js에 저장해두었던 데이터셋으로, map() 메소드를 이용해 뿌려준다.map() 메소드를 알아보자.
반복되는 컴포넌트를 렌더링 하기위해 사용한다.
새로운 배열의 요소를 생성하는 함수로서 세가지 인수를 가질 수 있다.
ㆍcurrenVlaue : 현재 배열(arr) 내의 값들을 의미
ㆍindex : 현재 배열 내 값의 인덱스를 의미
ㆍarray : 현재 배열cf. 이차원 배열로도 응용 가능하다.
const MapEx = () => {
const names = [
["a", 1],
["b", 2],
["c", 3]
]
const nameList = names.map((name) = (<div> {name[0]} {name[1]} </div>}
return(
<>
{nameList}
</>
);
}{/* 드래그앤 드롭 파일 컴포넌트 2개 */}<ShopImgUploadMainFile={MainFile}setMainFile={setMainFile}MainFileHandler={MainFileHandler}/><ShopMenuUploadMenuFile={MenuFile}setMenuFile={setMenuFile}MenuFileHandler={MenuFileHandler}/><RegisterBtn onClick={submitHandler}>등록하기</RegisterBtn></div></WrapBox>);};ShopInformationModify > index.jscf. 위 코드에서 if-else 구문을 추가해 "가게 정보 미리보기" 코드를 구현//메인 이미지const MainImages = styled.div`border-radius: 10px;width: 340px;height: 300px;display: flex;align-items: center;justify-content: center;`;
const MainImg = styled.div`width: 300px;height: 300px;background: url(${props => props.preview});background-repeat: no-repeat;background-size: cover;box-sizing: border-box;background-size: contain;border-radius: 6px;`;배경에 props를 전달받아 이미지를 띄운다.
const [flip, setFlip] = useState(true);
flip의 참/거짓을 통해 미리보기 창의 유무를 선택
const onPreview = e => {e.preventDefault();setFlip(!flip);};
if (flip == true) {return (<WrapBox><TopBar /><div><ShopRegistering>가게 정보 수정</ShopRegistering>{SHOP_DATA.map(data => {return (<React.Fragment key={data.key}><ShopIntroduceName>{data.keyword}</ShopIntroduceName><ShopIntroducetype="text"height={data.height}value={storeData[data.key]}onChange={e => {e.preventDefault();updateStoreData({ [data.key]: e.target.value });}}placeholder={data.placeholder}/></React.Fragment>);})}
{/* 드래그앤 드롭 파일 컴포넌트 2개 */}<ShopImgUploadMainFile={MainFile}setMainFile={setMainFile}MainFileHandler={MainFileHandler}/><ShopMenuUploadMenuFile={MenuFile}setMenuFile={setMenuFile}MenuFileHandler={MenuFileHandler}/>
<BeforeShowBtn onClick={onPreview}>미리보기</BeforeShowBtn>
<ModifyBtn onClick={submitHandler}>수정하기</ModifyBtn>
버튼에 제출 함수를 걸어도 되고, form태그를 이용해 제출해도 된다.</div></WrapBox>);} else {return (<WrapBox><TopBar /><form onSubmit={submitHandler}><ShopRegistering>가게 정보 수정</ShopRegistering><WhiteModal><MainImages>{MainFile.map((img, idx) => {return (<MainImgkey={idx}preview={window.URL.createObjectURL(img)}
Blob 객체를 url로 바꾸어 이미지를 띄운다.></MainImg>);})}</MainImages><ShopDetailHeader><ShopName>{storeData.name}</ShopName><IsRegistered>등록가게</IsRegistered></ShopDetailHeader>{" "}<SubTitle>소개</SubTitle><ShopDesc>{storeData.readme}</ShopDesc><DetailInfoCard><DetailInfoItemcategory="전화번호"content={storeData.phoneNumber}fontSize="14px"/><DetailInfoItemcategory="주소"content={storeData.address}fontSize="14px"/><DetailInfoItemcategory="운영시간"content={storeData.openTime}fontSize="14px"/><DetailInfoItemcategory="문의"content="카카오톡 | 인스타그램"fontSize="14px"/></DetailInfoCard><SubTitle>대표 케이크</SubTitle><CakeImages>{MenuFile.map((img, idx) => {return (<CakeProductImagekey={idx}preview={window.URL.createObjectURL(img)}></CakeProductImage>);})}</CakeImages><CloseButton onClick={onPreview}>닫기</CloseButton></WhiteModal><BeforeShowBtn>미리보기</BeforeShowBtn><ModifyBtn type="submit">수정하기</ModifyBtn></form></WrapBox>);}};컴포넌트에 ctrl+마우스 좌클릭을 하면, 해당 컴포넌트로 이동한다.
DetailInfoItem.js
import styled from "styled-components";
// 세부 정보const ItemBox = styled.div`display: flex;font-size: ${(props) => props.fontSize};& + & {margin-top: 30px;}
부모 선택자 사이의 간격을 30px로 준다는 의미이다.`;const Category = styled.div`width: 100px;font-weight: 600;color: var(--main-pink);`;const Content = styled.div`width: 100%;color: var(--black-text);word-break: keep-all; 어절이 끊기지 않고 줄바꿈한다.`;const DetailInfoItem = ({ category, content, fontSize }) => {return (<ItemBox fontSize={fontSize}><Category>{category}</Category><Content>{content}</Content></ItemBox>);};
export default DetailInfoItem;styles.jsimport styled from "styled-components";
//전체 428px;export const WrapBox = styled.div`width: 100%;height: 1850px;`;반응형 ui로 작업했기에, position이 아닌 margin을 사용했고 전체 ui의 크기를 %로 두었다.
//가게 정보 등록export const ShopRegistering = styled.div`width: 162px;height: 29px;margin: 56px 0px 60px 24px;font-family: "Apple SD Gothic Neo";font-style: normal;font-weight: 700;font-size: 24px;line-height: 29px;z-index: 1;`;
//이름export const ShopIntroduceName = styled.div`width: 120px;height: 22px;margin: 30px 0px 0px 24px;font-family: "Apple SD Gothic Neo";font-style: normal;font-weight: 700;font-size: 18px;line-height: 22px;text-transform: uppercase;color: #202020;z-index: 1;`;
//인풋 칸export const ShopIntroduce = styled.input`width: 90%;height: ${props => props.height};margin: 10px 0px 0px 4.5%;background: var(--sub-lightgray);border-radius: 6px;border: 1px solid var(--sub-lightgray);text-align: justify;`;내용 양에 따라 입력칸의 높이가 변하도록 props를 이용했다 .
//등록하기 버튼export const RegisterBtn = styled.button`margin: 70px 0px 0px 24px;width: 90%;height: 60px;left: 24px;top: 1593.19px;background: var(--main-pink);border-radius: 6px;border: 1px solid var(--main-pink);color: white;font-family: "Apple SD Gothic Neo";font-style: normal;font-weight: 700;font-size: 16px;line-height: 19px;`;'프론트엔드' 카테고리의 다른 글
컴포넌트 성능 최적화(함수형 업데이트, 리덕스) (0) 2022.08.18 케이커 웹/앱 - 5 (제안서) (0) 2022.08.15 케이커 웹/앱 - 4 (파일 업로드, 드래그 앤 드롭) (0) 2022.08.14 케이커 웹/앱 - 3 (사이드바) (0) 2022.08.14 케이커 웹/앱 - 2 (마이페이지) (1) 2022.08.14