RestFul API 개발 5

j0yy00n0·2025년 6월 10일
post-thumbnail

2025.05.15 ~ 05.16

RestFul API 개발

  • 프론트엔드

Main 화면

로그인

<!-- 로그인 버튼 클릭시 디스패처 실행 및 메인 페이지로 이동 -->
const onClickLoginHandler = () => { 
    dispatch(callLoginAPI({	// 로그인
        form: form
    }));
}

callLoginAPI

<!-- 매개변수 : 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 });
	};
};

  • dispatch({ type: POST_LOGIN, payload: result }) 부분의 result 안에 json 형식으로 들어가 있게 된다

MemberModule.js

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;

SecurityConfig 백엔드

@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]
);

callProductListAPI

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 });
		}
	};
};

ProductModule.js

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
        }));            
    }
    ,[]
);

callProductDetailAPI

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 });
		}
	};
};

ProductModule.js

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>
	</>
);

callSearchProductAPI

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 });
}

tokenUtils.js

import {jwtDecode} from "jwt-decode";

export function decodeJwt(token) {

    if(token === null) return null;

    return jwtDecode(token);
};

TokenProvider 백엔드

<!-- 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 });        
};

callPurchaseAPI

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
            }));            
        }
    }
    ,[]
);

callPurchaseListAPI

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!!");
};

callReviewWriteAPI

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
}

callReviewUpdateAPI

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]);

callProductListForAdminAPI

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();
};

callProductRegistAPI

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

  • 백엔드에서 build.gradle 부분
  • 관련 의존성 확인

프론트는 코드 조금 더 분석 해볼 필요가 있다

profile
잔디 속 새싹 하나

0개의 댓글