2025.05.15 ~ 05.16

<!-- 로그인 버튼 클릭시 디스패처 실행 및 메인 페이지로 이동 -->
const onClickLoginHandler = () => {
dispatch(callLoginAPI({ // 로그인
form: form
}));
}
<!-- 매개변수 : Login 컴포넌트에서 관리되는 ID, PWD -->
export const callLoginAPI = ({ form }) => {
<!-- .env : 민감한 정보를 관리하는 파일 -->
const requestURL = `http://${import.meta.env.VITE_APP_RESTAPI_IP}:8080/auth/login`;
return async (dispatch, getState) => {
<!-- 클라이언트 fetch mode : no-cors 사용시 application/json 방식으로 요청이 불가능
서버에서 cors 허용을 해주어야 함 -->
<!-- headers에 Access-Control-Allow-Origin을
*로 해서 모든 도메인에 대해 허용한다. -->
<!-- promise 객체임 -->
const result = await fetch(requestURL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: '*/*',
<!-- EX. 'Access-Control-Allow-Origin': 'localhost:8080'
'Access-Control-Allow-Origin': '*'
},
<!-- 직렬화 -->
body: JSON.stringify({
memberId: form.memberId,
memberPassword: form.memberPassword
}) <!-- 여기까지 백엔드 서버로 로그인 정보를 보내는 과정 -->
}).then((response) => response.json());
console.log('[MemberAPICalls] callLoginAPI RESULT : ', result);
if (result.status === 200) {
window.localStorage.setItem('accessToken', result.data.accessToken);
} else if (result.status === 400) {
alert(result.message); <!-- 로그인 실패 시 메시지를 alert로 표시 -->
}
dispatch({ type: POST_LOGIN, payload: result });
};
};
import { createActions, handleActions } from 'redux-actions';
<!-- 초기값 -->
const initialState = [];
<!-- 액션 -->
export const GET_MEMBER = 'member/GET_MEMBER';
export const POST_LOGIN = 'member/POST_LOGIN';
export const POST_REGISTER = 'member/POST_REGISTER';
<!-- payload 가 비어있는 액션 함수를 생성 -->
const actions = createActions({
[GET_MEMBER]: () => {},
[POST_LOGIN]: () => {},
[POST_REGISTER]: () => {}
});
<!-- 리듀서 -->
const memberReducer = handleActions(
{
[GET_MEMBER]: (state, { payload }) => {
return payload;
},
[POST_LOGIN]: (state, { payload }) => {
return payload;
},
[POST_REGISTER]: (state, { payload }) => {
return payload;
}
},
initialState
);
export default memberReducer;
@Bean
CorsConfigurationSource corsConfigurationSource() {
<!-- CORS 관련 설정을 진행할 객체 생성 -->
CorsConfiguration configuration = new CorsConfiguration();
<!-- 허용할 도메인 -->
configuration.setAllowedOrigins(Arrays.asList("http://localhost:5173"));
<!-- 허용할 메서드 -->
configuration.setAllowedMethods(Arrays.asList("GET", "PUT", "POST", "DELETE"));
<!-- 허용할 헤더 -->
configuration.setAllowedHeaders(
Arrays.asList(
<!-- 서버에서 응답할 때, 어떤 출처(origin)에서 요청을 허용할지를 결정하는 헤더 -->
"Access-Control-Allow-Origin",
<!-- 요청 또는 응답의 콘텐츠 유형(미디어 타입) -->
"Content-type",
<!-- CORS 요청을 통해 허용되는 HTTP 요청 헤더 -->
"Access-Control-Allow-Headers",
<!-- 클라이언트가 서버로 인증 정보(여기서는 JWT)를 보내기 위해 사용되는 헤더 -->
"Authorization",
<!-- XMLHttpRequest로 요청이 이루어졌는지를 나타내기 위해 사용 -->
"X-Requested-With"
)
);
<!-- CORS 설정을 적용할 URL 패턴 설정 -->
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
const pageNumber = [];
if(pageInfo){
for(let i = 1; i <= pageInfo.pageEnd ; i++){
pageNumber.push(i);
}
}
useEffect(
() => {
dispatch(callProductListAPI({
currentPage: currentPage
}));
}
,[currentPage]
);
export const callProductListAPI = ({ currentPage }) => {
let requestURL;
if (currentPage !== undefined || currentPage !== null) {
requestURL = `http://${import.meta.env.VITE_APP_RESTAPI_IP}:8080/api/v1/products?offset=${currentPage}`;
} else {
requestURL = `http://${import.meta.env.VITE_APP_RESTAPI_IP}:8080/api/v1/products`;
}
console.log('[ProduceAPICalls] requestURL : ', requestURL);
return async (dispatch, getState) => {
const result = await fetch(requestURL, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Accept: '*/*'
}
}).then((response) => response.json());
if (result.status === 200) {
console.log('[ProduceAPICalls] callProductAPI RESULT : ', result);
dispatch({ type: GET_PRODUCTS, payload: result.data });
}
};
};
import { createActions, handleActions } from 'redux-actions';
<!-- 초기값 -->
const initialState = [];
<!-- 액션 -->
export const GET_PRODUCT = 'product/GET_PRODUCT';
export const GET_PRODUCTS = 'product/GET_PRODUCTS';
export const GET_PRODUCTS_MEAL = 'product/GET_PRODUCTS_MEAL';
export const GET_PRODUCTS_DESSERT = 'product/GET_PRODUCTS_DESSERT';
export const GET_PRODUCTS_BEVERAGE = 'product/GET_PRODUCTS_BEVERAGE';
export const POST_PRODUCT = 'product/POST_PRODUCT';
export const PUT_PRODUCT = 'product/PUT_PRODUCT';
const actions = createActions({
[GET_PRODUCT]: () => {},
[GET_PRODUCTS]: () => {},
[GET_PRODUCTS_MEAL]: () => {},
[GET_PRODUCTS_DESSERT]: () => {},
[GET_PRODUCTS_BEVERAGE]: () => {},
[POST_PRODUCT]: () => {},
[PUT_PRODUCT]: () => {}
});
<!-- 리듀서 -->
const productReducer = handleActions(
{
[GET_PRODUCT]: (state, { payload }) => {
return payload;
},
[GET_PRODUCTS]: (state, { payload }) => {
return payload;
},
[GET_PRODUCTS_MEAL]: (state, { payload }) => {
return payload;
},
[GET_PRODUCTS_DESSERT]: (state, { payload }) => {
return payload;
},
[GET_PRODUCTS_BEVERAGE]: (state, { payload }) => {
return payload;
},
[POST_PRODUCT]: (state, { payload }) => {
return payload;
},
[PUT_PRODUCT]: (state, { payload }) => {
return payload;
}
},
initialState
);
export default productReducer;
useEffect(
() => {
dispatch(callProductDetailAPI({ <!-- 상품 상세 정보 조회 -->
productCode: params.productCode
}));
}
,[]
);
export const callProductDetailAPI = ({ productCode }) => {
const requestURL = `http://${import.meta.env.VITE_APP_RESTAPI_IP}:8080/api/v1/products/${productCode}`;
return async (dispatch, getState) => {
const result = await fetch(requestURL, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Accept: '*/*'
}
}).then((response) => response.json());
console.log('[ProduceAPICalls] callProductDetailAPI RESULT : ', result);
if (result.status === 200) {
console.log('[ProduceAPICalls] callProductDetailAPI SUCCESS');
dispatch({ type: GET_PRODUCT, payload: result.data });
}
};
};
import { createActions, handleActions } from 'redux-actions';
/* 초기값 */
const initialState = [];
/* 액션 */
export const GET_PRODUCT = 'product/GET_PRODUCT';
export const GET_PRODUCTS = 'product/GET_PRODUCTS';
export const GET_PRODUCTS_MEAL = 'product/GET_PRODUCTS_MEAL';
export const GET_PRODUCTS_DESSERT = 'product/GET_PRODUCTS_DESSERT';
export const GET_PRODUCTS_BEVERAGE = 'product/GET_PRODUCTS_BEVERAGE';
export const POST_PRODUCT = 'product/POST_PRODUCT';
export const PUT_PRODUCT = 'product/PUT_PRODUCT';
const actions = createActions({
[GET_PRODUCT]: () => {},
[GET_PRODUCTS]: () => {},
[GET_PRODUCTS_MEAL]: () => {},
[GET_PRODUCTS_DESSERT]: () => {},
[GET_PRODUCTS_BEVERAGE]: () => {},
[POST_PRODUCT]: () => {},
[PUT_PRODUCT]: () => {}
});
/* 리듀서 */
const productReducer = handleActions(
{
[GET_PRODUCT]: (state, { payload }) => {
return payload;
},
[GET_PRODUCTS]: (state, { payload }) => {
return payload;
},
[GET_PRODUCTS_MEAL]: (state, { payload }) => {
return payload;
},
[GET_PRODUCTS_DESSERT]: (state, { payload }) => {
return payload;
},
[GET_PRODUCTS_BEVERAGE]: (state, { payload }) => {
return payload;
},
[POST_PRODUCT]: (state, { payload }) => {
return payload;
},
[PUT_PRODUCT]: (state, { payload }) => {
return payload;
}
},
initialState
);
export default productReducer;
const [search, setSearch] = useState('');
const onSearchChangeHandler = (e) => {
setSearch(e.target.value);
};
const onEnterkeyHandler = (e) => {
if (e.key == 'Enter') {
console.log('Enter key', search);
navigate(`/search?value=${search}`, { replace: false });
dispatch(callSearchProductAPI({
search: search
}));
window.location.reload();
}
};
return (
<>
{loginModal ? <LoginModal setLoginModal={setLoginModal} /> : null}
<div className={HeaderCSS.HeaderDiv}>
<button
className={HeaderCSS.LogoBtn}
onClick={onClickLogoHandler}
>
OHGIRAFFERS
</button>
<input
className={HeaderCSS.InputStyle}
type="text"
placeholder="검색"
<!-- value 안에 검색한 내용이 들어가게 된다 -->
value={search}
onKeyUp={onEnterkeyHandler}
onChange={onSearchChangeHandler}
/>
<!-- 로그인 상태에 따라 다른 컴포넌트 랜더링 -->
{isLogin == null || isLogin === undefined ? (
<BeforeLogin />
) : (
<AfterLogin />
)}
</div>
</>
);
export const callSearchProductAPI = ({ search }) => {
console.log('[ProduceAPICalls] callSearchProductAPI Call');
const requestURL = `http://${import.meta.env.VITE_APP_RESTAPI_IP}:8080/api/v1/products/search?s=${search}`;
return async (dispatch, getState) => {
const result = await fetch(requestURL, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Accept: '*/*'
}
}).then((response) => response.json());
console.log('[ProduceAPICalls] callSearchProductAPI RESULT : ', result);
dispatch({ type: GET_PRODUCTS, payload: result.data });
};
};
회원만 구매가 가능하게 설정
const onClickPurchaseHandler = () => {
<!-- 로그인 상태인지 확인 -->
const token = decodeJwt(window.localStorage.getItem("accessToken"));
console.log('[onClickPurchaseHandler] token : ', token);
if(token === undefined || token === null) {
alert('로그인을 먼저해주세요');
setLoginModal(true);
return;
}
<!-- 토큰이 만료되었을때 다시 로그인 -->
if (token.exp * 1000 < Date.now()) {
setLoginModal(true);
return;
}
<!-- 구매 가능 수량 확인 -->
if(amount > product.productStock) {
alert('구매 가능 수량을 확인해주세요');
return;
}
navigate(`/purchase?amount=${amount}`, { replace: false });
}
import {jwtDecode} from "jwt-decode";
export function decodeJwt(token) {
if(token === null) return null;
return jwtDecode(token);
};
<!-- JWT 토큰 생성 -->
String accessToken = Jwts.builder()
<!-- 회원 아이디를 "sub"이라는 클레임으로 토큰에 추가 -->
.setSubject(member.getMemberId())
<!-- 회원의 권한들을 "auth"라는 클레임으로 토큰에 추가 -->
.claim(AUTHORITIES_KEY, roles)
<!-- 만료 시간 설정 -->
.setExpiration(accessTokenExpiresIn)
<!-- 서명 및 알고리즘 설정 -->
.signWith(key, SignatureAlgorithm.HS512)
<!-- 압축 = header + payload + signature -->
.compact();
System.out.println("조립된 accessToken 확인 = " + accessToken);
const product = useSelector(state => state.productReducer);
<!-- 폼 데이터 한번에 변경 및 State에 저장 -->
const [form, setForm] = useState({
productCode: product.productCode,
orderMemberId: token.sub,
orderPhone: '',
orderEmail: '',
orderReceiver: '',
orderAddress: '',
orderAmount: parseInt(amount)
});
const onChangeHandler = (e) => {
setForm({
...form,
[e.target.name]: e.target.value
});
};
const onClickPurchaseHandler = () => {
console.log('[Purchase] Purchase event Started!!');
console.log('form', form);
if(form.orderPhone === '' || form.orderEmail === ''
|| form.orderReceiver === '' || form.orderAddress === ''){
alert('정보를 다 입력해주세요.');
return ;
}
dispatch(callPurchaseAPI({
form: form
}));
alert('결제 정보 페이지로 이동합니다.');
navigate("/mypage/payment", { replace: true });
};
export const callPurchaseAPI = ({ form }) => {
const requestURL = `http://${import.meta.env.VITE_APP_RESTAPI_IP}:8080/api/v1/purchase`;
return async (dispatch, getState) => {
const result = await fetch(requestURL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: '*/*',
Authorization:
'Bearer ' + window.localStorage.getItem('accessToken')
},
body: JSON.stringify({
productCode: form.productCode,
memberId: form.orderMemberId,
orderPhone: form.orderPhone,
orderEmail: form.orderEmail,
orderReceiver: form.orderReceiver,
orderAddress: form.orderAddress,
orderAmount: form.orderAmount
})
}).then((response) => response.json());
console.log('[PurchaseAPICalls] callPurchaseAPI RESULT : ', result);
dispatch({ type: POST_PURCHASE, payload: result });
};
};
const purchase = useSelector(state => state.purchaseReducer);
useEffect(
() => {
if(token !== null) {
dispatch(callPurchaseListAPI({ <!-- 구매 정보 조회 -->
memberId: token.sub
}));
}
}
,[]
);
export const callPurchaseListAPI = ({ memberId }) => {
const requestURL = `http://${import.meta.env.VITE_APP_RESTAPI_IP}:8080/api/v1/purchase/${memberId}`;
return async (dispatch, getState) => {
const result = await fetch(requestURL, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Accept: '*/*',
Authorization:
'Bearer ' + window.localStorage.getItem('accessToken')
}
}).then((response) => response.json());
console.log('[PurchaseAPICalls] callPurchaseListAPI RESULT : ', result);
dispatch({ type: GET_PURCHASE, payload: result });
};
};
const onClickReviewHandler = (productFromTable) => {
setProductCode(productFromTable.product.productCode);
setMemberCode(productFromTable.orderMember);
setProductReviewModal(true);
};
const onClickProductReviewHandler = () => {
console.log("[ProductReviewModal] onClickProductReviewHandler Start!!");
dispatch(
callReviewWriteAPI({
<!-- 리뷰 작성 -->
form: form,
})
);
setProductReviewModal(false);
alert("리뷰 등록이 완료되었습니다.");
navigate(`/review/${productCode}`, { replace: true });
window.location.reload();
console.log("[ProductReviewModal] onClickProductReviewHandler End!!");
};
export const callReviewWriteAPI = ({ form }) => {
console.log('[ReviewAPICalls] callReviewWriteAPI Call');
const requestURL = `http://${import.meta.env.VITE_APP_RESTAPI_IP}:8080/api/v1/reviews`;
return async (dispatch, getState) => {
const result = await fetch(requestURL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: '*/*',
Authorization:
'Bearer ' + window.localStorage.getItem('accessToken')
},
body: JSON.stringify({
productCode: form.productCode,
memberCode: form.memberCode,
reviewTitle: form.reviewTitle,
reviewContent: form.reviewContent
})
}).then((response) => response.json());
console.log('[ReviewAPICalls] callReviewWriteAPI RESULT : ', result);
dispatch({ type: POST_REVIEW, payload: result });
};
};
const onClickModifyModeHandler = () => {
setModifyMode(true);
setForm({
reviewCode: reviewDetail.reviewCode,
reviewTitle: reviewDetail.reviewTitle,
reviewContent: reviewDetail.reviewContent
});
}
const onClickReviewUpdateHandler = () => {
dispatch(callReviewUpdateAPI({ <!-- 리뷰 정보 업데이트 -->
form: form
}));
navigate(`/review/${reviewDetail.productCode}`, { replace: true});
window.location.reload();
}
{ token &&
(token.sub === reviewDetail.member?.memberId)
?
<div>{!modifyMode &&
<button
className={ ReviewDetailCSS.backBtn }
onClick={ onClickModifyModeHandler }
>
수정모드
</button>
}
{modifyMode &&
<button
className={ ReviewDetailCSS.backBtn }
onClick={ onClickReviewUpdateHandler }
>
리뷰 수정 저장하기
</button>
}
</div>
: null
}
export const callReviewUpdateAPI = ({ form }) => {
console.log('[ReviewAPICalls] callReviewUpdateAPI Call');
const requestURL = `http://${import.meta.env.VITE_APP_RESTAPI_IP}:8080/api/v1/reviews`;
return async (dispatch, getState) => {
const result = await fetch(requestURL, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Accept: '*/*',
Authorization:
'Bearer ' + window.localStorage.getItem('accessToken')
},
body: JSON.stringify({
reviewCode: form.reviewCode,
reviewTitle: form.reviewTitle,
reviewContent: form.reviewContent
})
}).then((response) => response.json());
console.log('[ReviewAPICalls] callReviewUpdateAPI RESULT : ', result);
dispatch({ type: PUT_REVIEW, payload: result });
};
};
관리자 로그인 시 상품 관리 버튼 활성화
if (isLogin !== undefined && isLogin !== null) {
const temp = decodeJwt(window.localStorage.getItem('accessToken'));
console.log(temp);
decoded = temp.auth[0];
}
{decoded === 'ROLE_ADMIN' && (
<li>
<NavLink to="/product-management">상품관리</NavLink>
</li>
)}
useEffect(() => {
setStart((currentPage - 1) * 5);
dispatch(
callProductListForAdminAPI({
currentPage: currentPage
})
);
}, [currentPage]);
export const callProductListForAdminAPI = ({ currentPage }) => {
let requestURL;
if (currentPage !== undefined || currentPage !== null) {
requestURL = `http://${import.meta.env.VITE_APP_RESTAPI_IP}:8080/api/v1/products-management?offset=${currentPage}`;
} else {
requestURL = `http://${import.meta.env.VITE_APP_RESTAPI_IP}:8080/api/v1/products-management`;
}
console.log('[ProduceAPICalls] requestURL : ', requestURL);
return async (dispatch, getState) => {
const result = await fetch(requestURL, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Accept: '*/*',
Authorization:
'Bearer ' + window.localStorage.getItem('accessToken')
}
}).then((response) => response.json());
if (result.status === 200) {
console.log(
'[ProduceAPICalls] callProductListForAdminAPI RESULT : ',
result
);
dispatch({ type: GET_PRODUCTS, payload: result.data });
}
};
};
const onClickProductInsert = () => {
console.log('[ProductManagement] onClickProductInsert');
navigate('/product-registration', { replace: false });
};
<!-- 이미지와 form 태그는 따로 관리하고 있다 -->
const [image, setImage] = useState(null);
const [imageUrl, setImageUrl] = useState();
const [form, setForm] = useState({
productName: '',
productPrice: 0,
productOrderable: '',
categoryCode: '',
productStock: 0,
productDescription: ''
});
useEffect(() => {
<!-- 이미지 업로드시 미리보기 세팅 -->
if (image) {
const fileReader = new FileReader();
fileReader.onload = (e) => {
const { result } = e.target;
if (result) {
setImageUrl(result);
}
};
fileReader.readAsDataURL(image);
}
}, [image]);
const onChangeImageUpload = (e) => {
const image = e.target.files[0];
setImage(image);
};
const onClickProductRegistrationHandler = () => {
console.log('[ProductRegistration] onClickProductRegistrationHandler');
const formData = new FormData();
formData.append('productName', form.productName);
formData.append('productPrice', form.productPrice);
formData.append('productOrderable', form.productOrderable);
formData.append('categoryCode', form.categoryCode);
formData.append('productStock', form.productStock);
formData.append('productDescription', form.productDescription);
if (image) {
formData.append('productImage', image);
}
dispatch(
callProductRegistAPI({
<!-- 상품 상세 정보 조회 -->
form: formData
})
);
alert('상품 리스트로 이동합니다.');
navigate('/product-management', { replace: true });
window.location.reload();
};
export const callProductRegistAPI = ({ form }) => {
console.log('[ProduceAPICalls] callProductRegistAPI Call');
const requestURL = `http://${import.meta.env.VITE_APP_RESTAPI_IP}:8080/api/v1/products`;
return async (dispatch, getState) => {
const result = await fetch(requestURL, {
method: 'POST',
headers: {
Accept: '*/*',
Authorization:
'Bearer ' + window.localStorage.getItem('accessToken')
},
body: form
}).then((response) => response.json());
console.log('[ProduceAPICalls] callProductRegistAPI RESULT : ', result);
dispatch({ type: POST_PRODUCT, payload: result });
};
};
package.json
프론트는 코드 조금 더 분석 해볼 필요가 있다