240213 기록
npx create-react-app my-app --template typescript
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
▶️ tailwind.config.js 랑 postcss.config.js 파일 생성
tailwind.config.js 수정하기
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
src/index.css
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
App.tsx
import React from 'react';
import logo from './logo.svg';
import './App.css';
function App() {
return (
<div className="App">
<h1 className="text-3xl font-bold underline">
Hello world!
</h1>
{/* <header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header> */}
</div>
);
}
export default App;
npm run start
create-react-app은 PostCSS를 지원해주지만, 재정의를 할 수 없기에 TailwindCSS를 이용하기에 다양한 제약이 따릅니다. 따라서 CRACO를 설치하고, craco.config.js파일을 추가해서 쉽고 다양하게 커스터마이징을 할 수 있도록 설정해줍니다.
yarn add @craco/craco
npm install @craco/craco
craco.config.js
module.exports = {
style: {
postcssOptions: {
plugins: [require('tailwindcss'), require('autoprefixer')],
},
},
};
package.json
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
}
▶️ 위 코드를 아래 코드와 같이 수정
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "react-scripts eject"
}
패키지 설치
npm install classnames
npm install react-router-dom
npm install react-router-dom@latest
npm install sass
패키지 설치
npm install swiper
240215 기록
패키지 설치
# react-slick 사용하기
npm install react-slick
# react-slick에서 css 수정하고 싶다면
npm install slick-carousel
npm install --save-dev @types/react-slick
240219 기록
240220 기록
npm install tw-elements
npm install @tosspayments/payment-widget-sdk
npm install nanoid
240227 기록
npm install @emotion/react
npm install @emotion/styled
[참고링크]
240301 기록
npm install react-icons
npm install react-daum-postcode
MyRestore.tsx
import "../../../Components_scss/MyRestore.scss"
import { useImage } from "../../common/hooks/useImage";
import { useEffect, useRef, useState } from "react"
import {Form, useActionData} from "react-router-dom";
import ModalBase from '../../../Components/ModalBase';
import CardModal from '../../../Components/CardModal';
import {FormMessage} from "../../../common/FormMessage";
import {getApi} from "../../../api/ApiWrapper";
import {PageOrderResDto, ProductDto} from "../../../api/Api";
interface State {
id: string;
value: string;
label: string;
desc: string;
}
const stateList: State[] = [
{ id: '1', value: 'S', label: 'S - 가장 낮았던 판매가격의 50%', desc: '흠집이 없으며 새 것과 동일한 상태'},
{ id: '2', value: 'A', label: 'A - 가장 낮았던 판매가격의 40%', desc: '경미한 흠집이 있으나 전반적으로 양호한 상태'},
{ id: '3', value: 'B', label: 'B - 가장 낮았던 판매가격의 30%', desc: '흠집 다소 있으며 사용감이 있는 상태'},
];
const MyRestore = () => {
const image = useImage()
const [orderHistoryItem, setOrderHistoryItem] = useState<ProductDto[]>([])
const error = useActionData() as FormMessage
const formRef = useRef<HTMLFormElement | null>(null);
// 리스토어 항목 불러오기
useEffect(() => {
async function fetchOrderHistory() {
try {
const products: ProductDto[] = []
const api = await getApi()
const myOrders = (await api.getOrders({page: 0, pageSize: 5}, {})).data as PageOrderResDto
if (myOrders.content !== undefined) {
for (let p of myOrders.content) {
if (p.products === undefined) continue
for (let product of p.products) {
try {
// @ts-ignore
const res = (await api.getProduct1(product.productId)).data as ProductDto
products.push(res)
} catch (e) {
}
}
}
setOrderHistoryItem(products)
}
} catch (e) {
}
}
fetchOrderHistory().then()
}, []);
// 모달 기능
const [isActive, setIsActive] = useState(false);
const onClickModalOn = () => {
setIsActive(true);
};
const onClickModalOff = () => {
setIsActive(false);
};
const onClickCardConfirm = () => {
// 모달을 닫고 캐시 비우기
onClickModalOff();
setImgFile(undefined);
setRestoreImgPath("");
if (imgRef.current) {
imgRef.current.value = "";
}
alert('리스토어가 신청되었습니다.');
};
const handleFormSubmit = (event: React.FormEvent) => {
event.preventDefault();
console.log("Restoration Data:");
console.log("Selected State:", restoreGrade);
console.log("Image File:", imgFile);
console.log("Restore Description:", restoreDesc);
console.log("리스토어 이미지 경로:", restoreImgPath);
};
// 상태 선택 기능
const [restoreGrade, setRestoreGrade] = useState<string>();
// console.log(`Selected state: ${restoreGrade}`);
// 사진 첨부 기능
const [imgFile, setImgFile] = useState<File>();
const [restoreImgPath, setRestoreImgPath] = useState<string>();
const imgRef = useRef<HTMLInputElement>(null);
const MAX_IMAGE_SIZE_BYTES = 1024 * 1024 * 2;
// console.log(restoreImgPath);
const previewImage = () => {
if (imgRef.current && imgRef.current.files) {
const img = imgRef.current.files[0];
setImgFile(img);
//이미지 미리보기 기능
const reader = new FileReader();
reader.readAsDataURL(img);
reader.onload = () => {
setRestoreImgPath(reader.result as string);
};
}
};
// 상품 설명
const [restoreDesc, setRestoreDesc] = useState<String>();
// console.log(restoreDesc);
return (
<div className="MyRestore">
<div className="MyRestoreWrapper mb-3">
<div className="MyRestoreWrapperTitle">리스토어 신청</div>
<div className="MyRestoreSearchWrapper">
<div className="MyRestoreSearch relative mb-4 flex w-full flex-wrap items-stretch">
<input
type="search"
className="MyRestoreSearchInput ㅌrelative m-0 -mr-0.5 block min-w-0 flex-auto rounded-l border border-solid border-neutral-300 bg-transparent bg-clip-padding px-3 py-[0.25rem] text-base font-normal leading-[1.6] text-neutral-700 outline-none transition duration-200 ease-in-out focus:z-[3] focus:border-primary focus:text-neutral-700 focus:shadow-[inset_0_0_0_1px_rgb(59,113,202)] focus:outline-none dark:border-neutral-600 dark:text-neutral-200 dark:placeholder:text-neutral-200 dark:focus:border-primary"
placeholder="검색어를 입력하세요."
aria-label="Search"
aria-describedby="button-addon3" />
<button
className="MyRestoreSearchBtn relative z-[2] rounded-r border-2 border-primary px-6 py-2 text-xs font-medium uppercase text-primary transition duration-150 ease-in-out hover:bg-black hover:bg-opacity-5 focus:outline-none focus:ring-0"
type="button"
id="button-addon3"
data-te-ripple-init>
검색
</button>
</div>
</div>
<div className="MyRestoreContent">
<div className="MyRestoreContentItem">
<img className="MyRestoreContentItemImg" src={image("베스트1.jpeg")} title="pic"></img>
<div className="MyRestoreContentItemText">
<div className="MyRestoreContentItemBrand">코이</div>
<div className="MyRestoreContentItemName">코이 6단 이동식 책상세트</div>
<div className="MyRestoreContentItemInfo">사이즈: 1200 / 색상: 화이트</div>
</div>
<button onClick={onClickModalOn} className="MyRestoreContentItemBtn" title="신청하기">신청하기</button>
<ModalBase active={isActive} closeEvent={onClickModalOff}>
<Form className="RestoreForm" method={"post"}>
<CardModal closeEvent={onClickModalOff} title="리스토어 신청하기" actionMsg="확인" actionEvent={onClickCardConfirm}>
<div className="RestoreModal">
{/* 상품상태 */}
<div className="RestoreModalState">
<div className="RestoreModalStateTitle">제품상태</div>
<div className="RestoreModalStateContent">
<ul className="PreferenceTestRightAnswer space-y-4 mb-4">
{stateList.map((state) => (
<li key={state.id}>
<input
type="radio"
id={state.id}
name="job"
value={state.value}
className="hidden peer"
onChange={() => setRestoreGrade(state.value)}
required
/>
<label
htmlFor={state.id}
className="inline-flex items-center justify-between w-full p-5 text-gray-900 bg-white border border-gray-200 rounded-lg cursor-pointer dark:hover:text-gray-300 dark:border-gray-500 dark:peer-checked:text-blue-500 peer-checked:border-blue-600 peer-checked:text-blue-600 hover:text-gray-900 hover:bg-gray-100 dark:text-white dark:bg-gray-600 dark:hover:bg-gray-500"
>
<div className="block">
<div className="w-full text-lg font-semibold">{state.label}</div>
<div className="w-full text-lg font-semibold">{state.desc}</div>
</div>
</label>
</li>
))}
</ul>
</div>
</div>
{/* 상품사진 */}
<div className="RestoreModalPic">
<div className="RestoreModalPicTitle">상품사진</div>
<div className="RestoreModalPicContent">
<div className="RestoreModalPicContentText">사진을 첨부해주세요.</div>
<div className="RestoreModalPicContentButton">
<label className="RestoreModalPicUploadPreviewLabel" htmlFor="photo">
<img
//사용자가 이미지 파일을 업로드하면 해당 이미지를 보여주고, 없으면 기본 이미지를 보여준다.
className="RestoreModalPicUploadPreviewLImg"
src={restoreImgPath ? restoreImgPath : image("upload.png")}
alt="사진 첨부하기"
/>
</label>
<label className="RestoreModalPicUploadInputLabel" htmlFor="photo">
사진 첨부하기
<input
className="RestoreModalPicUploadInput"
type="file"
id="photo"
name="photo"
accept=".png, .jpeg, .jpg"
onChange={previewImage}
ref={imgRef}
/>
<input type="hidden" name="restoreImgPath"
onChange={() => setRestoreGrade(restoreImgPath)} />
</label>
</div>
</div>
</div>
{/* 상품설명 */}
<div className="RestoreModalDescription">
<div className="RestoreModalDescriptionTitle">상품설명</div>
<textarea
className="RestoreModalDescriptionContent"
placeholder="상품상태를 간략하게 설명해주세요."
title="상품설명"
onChange={(e) => setRestoreDesc(e.target.value)}
/>
</div>
{/* 안내사항 */}
<div className="RestoreModalInfo">
<div className="RestoreModalInfoTitle">위의 조건을 충족하지 못하나요?</div>
<div className="RestoreModalInfoContent">조건에 맞지 않는 제품은 리스토어 판매가 어렵습니다.
<br/>H.Livv 리스토어 서비스는 가구에 제2의 삶을 불어 넣을 수 있는 선택 중 하나일 뿐입니다. 가구를 폐기할 때가 되었다면 다른 재활용 방법을 고려해보세요.</div>
</div>
</div>
</CardModal>
</Form>
</ModalBase>
</div>
</div>
</div>
</div>
);
}
export default MyRestore;
MyRestoreRouter.ts
import {FormMessage} from "../../../common/FormMessage";
import {Api} from "../../../api/ApiWrapper";
import {getAuthToken} from "../../../api/auth/Token";
// @ts-ignore
export async function myRestoreAction({request, params}) {
const formData = await request.formData()
const formDataObj = Object.fromEntries(formData.entries());
// 프론트에서 입력 받아올 값
const {
requestGrade,
restoreDesc,
restoreImageUrls
} = formDataObj;
// 입력값 유효성 확인
const validationResult = validateRestoreInput(requestGrade, restoreDesc, restoreImageUrls)
if (validationResult !== null){
return validationResult
}
// 프론트에서 입력하지 않은 변수들을 임의의 값으로 채워넣기
const filledProductId = 0;
const filledPickUpDate = "2024-03-09T10:34:33.145Z";
const filledWhenRejected = true;
try {
const api = Api
const headers = {
Authorization: `Bearer ${getAuthToken()}`,
// 다른 필요한 헤더도 추가할 수 있음
};
const result = await api.restoreRegister({
productId: filledProductId,
pickUpDate: filledPickUpDate,
requestGrade: requestGrade ? requestGrade.toString() : '',
restoreDesc: restoreDesc ? restoreDesc.toString() : '',
whenRejected: filledWhenRejected,
restoreImageUrls: restoreImageUrls ? restoreImageUrls.split(',') : []
}, { headers })
// 콘솔에 성공적인 응답을 출력
console.log('Successful response:', result);
return FormMessage.createFormMessage("리스토어 신청 성공", 200)
} catch(e) {
// 실패한 경우 콘솔에 에러 메시지 출력
console.error('Error:', (e as Error).message);
return FormMessage.createFormMessage(`${(e as Error).message}`, 500)
}
}
const validateRestoreInput = (requestGrade: string, restoreDesc: string, restoreImageUrls:string) => {
if (requestGrade === null) return FormMessage.createFormMessage("등급을 선택해주세요", 400)
if (restoreDesc === null) return FormMessage.createFormMessage("상풍설명을 입력해주세요", 400)
if (restoreImageUrls === null) return FormMessage.createFormMessage("상품사진을 등록해주세요", 400)
return null
}