프로젝트 기록 - 마켓컬리 클론코딩, 레시피 게시판을 추가한 온라인 쇼핑몰 RecipeToYou 구축하기
my code archive
article thumbnail
반응형
반응형

💡프로젝트 소개

온라인 쇼핑몰 + 레시피 커뮤니티 웹 애플리케이션 구축을 목표로 한 RecipeToYou 프로젝트 기획

  • 마켓컬리 클론코딩 (+레시피 커뮤니티)
  • 기존 마켓컬리에 사용자들이 쇼핑몰에서 구매한 식재료를 활용하여 레시피를 올리는 레시피 커뮤니티를 추가했다.
  • 기획 의도 : 코로나 장기화로 인해 온라인 시장 규모가 확대 / 건강한 가정식에 대한 관심도 증가 / 정보통신기술의 발달로 다양한 온라인 커뮤니티 활성화

 

🔨기술 스택

 

⌛️작업 기간

⭐️2021. 12. 20 ~ 2022. 03. 10

 

💡작업 목표물

  • 메인(사용자) 페이지 
  • 로그인, 회원가입
  • 상품 목록 출력, 마이페이지, 상품 상세 페이지
  • 레시피 커뮤니티
  • 관리자 페이지
  • 회원 관리, 탈퇴 회원 관리
  • 주문, 결제 관리
  • 상품 CRUD 기능

💻Front-End

  • 메인 페이지

 

  • 레시피 커뮤니티

 

  • 관리자 페이지
  • 인터셉터 적용하여 로그인 시에만 관리자모드 접속 가능하도록 구현

 

💻Back-End

백엔드 기능은 내가 작업을 맡은 부분 위주로 정리하겠다.

나는 관리자(Admin) 페이지의 상품 CRUD 기능, 상품 문의 & 후기 파트를 맡았다.

 

전체 상품 SELECT 

  • 전체 상품 목록 SELECT 쿼리문에는 검색, 페이징 기능을 같이 넣었다.
  • 검색어(keyword)가 null일 때에는 전체 상품을 SELECT하고 null이 아닐 때에는 keyword에 해당하는 상품만 SELECT하도록 choose, when 조건문을 넣었다.
  • 검색 기능은 상품명으로 검색, 카테고리로 검색 2가지 조건을 넣었다.

 

  • 검색어(keyword)가 null일 때에 전체 상품 목록 SELECT

 

  • 상품명으로 SELECT

 

  • 상품명에 해당하는 상품이 없을 때

 

  • DAO -> Service -> Controller 흐름으로 코드 작성
  • 상품 목록 데이터를 prodList에, 전체 상품 개수를 cnt에, 검색된 상품 개수를 searchcnt에, 카테고리 데이터를 cateList에 담아 뷰(JSP)로 전달함.
//글목록보기(PageMaker 객체 사용)
	//전체 상품 목록 조회
	@Override
	@RequestMapping(value = "/adgoods/listProduct.do", method = {RequestMethod.GET, RequestMethod.POST})
	@ResponseBody
	public ModelAndView listPageGet(PagingVO vo, HttpServletRequest request, HttpServletResponse response)
			throws Exception {
		request.getServletContext();
		ObjectMapper objm = new ObjectMapper();
		String viewName = (String)request.getAttribute("viewName");
		ModelAndView mav = new ModelAndView(viewName);
		List prodList = adGoodsService.listProduct(vo);
		List list = adGoodsService.cateList();
		String cateList = objm.writeValueAsString(list);
		
		int cnt = adGoodsService.prodCount(vo);
		int searchcnt = adGoodsService.countSearch(vo);
		
		if(!prodList.isEmpty()) {
			mav.addObject("prodList", prodList);
			mav.addObject("cnt", cnt);
			mav.addObject("searchcnt", searchcnt);
			mav.addObject("cateList", cateList);
		}else {
			mav.addObject("listCheck", "empty");
		}
		
		//페이지 데이터
		mav.addObject("pm", new PageMaker(vo, adGoodsService.prodCount(vo)));
		
		return mav;
	}

 

  • 화면단에서는 페이지 데이터, keyword, cateCode(카테고리)를 hidden 태그에 담아 검색 기능 구현
<form id="moveForm" action="${contextPath}/adgoods/listProduct.do" method="get">
    <input type="hidden" name="page" value="${pm.vo.page}">
    <input type="hidden" name="pageSize" value="${pm.vo.pageSize}">
    <input type="hidden" id="keyword" name="keyword" value="${pm.vo.keyword}">
    <input type="hidden" id="cateCode" name="cateCode" value="${pm.vo.cateCode}">
</form>

 

상품 카테고리

  • 작업을 하면서 가장 어려웠던 부분은 '카테고리 데이터를 어떻게 분류하느냐'였다. 대부분의 쇼핑몰 사이트에는 이렇게 상품이 1차, 2차 카테고리로 분류되는데 ..

 

  • 카테고리 테이블을 여러 번 DROP하고 CREATE한 끝에... 계층형 쿼리문을 사용하여 생성했다.
  • 카테고리 이름(cateName), 카테고리 코드(cateCode), 상위 카테고리(cateParent) 총 3개의 컬럼으로 테이블을 만들어 cateParent 값이 null일 경우 1차 카테고리, null이 아닐 경우 2차 카테고리로 분류되도록 작업했다.
--상품 카테고리 1
INSERT INTO tb_prod_cate(cateCode, cateName)
	VALUES('1', '과일/견과/쌀');

INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('1002', '제철과일', '1');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('1003', '국산과일', '1');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('1004', '수입과일', '1');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('1005', '간편과일', '1');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('1006', '냉동/건과일', '1');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('1007', '견과류', '1');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('1008', '쌀/잡곡', '1');

--상품 카테고리 2
INSERT INTO tb_prod_cate(cateCode, cateName)
	VALUES('2', '샐러드/간편식');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('2001', '샐러드/닭가슴살', '2');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('2002', '도시락/밥류', '2');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('2003', '파스타/면류', '2');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('2004', '떡볶이/튀김/순대', '2');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('2005', '피자/핫도그/만두', '2');

--상품 카테고리 3
INSERT INTO tb_prod_cate(cateCode, cateName)
	VALUES('3', '베이커리/치즈/델리');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('3001', '식빵/빵류', '3');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('3002', '잼/버터/스프레드', '3');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('3003', '케이크/파이/디저트', '3');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('3004', '치즈', '3');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('3005', '올리브/피클', '3');

--상품 카테고리 4
INSERT INTO tb_prod_cate(cateCode, cateName)
	VALUES('4', '수산/해산/건어물');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('4001', '제철수산', '4');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('4002', '생선류', '4');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('4003', '오징어/낙지/문어', '4');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('4004', '건어물/다시팩', '4');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('4005', '굴비/반건류', '4');
	
--상품 카테고리 5
INSERT INTO tb_prod_cate(cateCode, cateName)
	VALUES('5', '전통주');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('5001', '막걸리/약주', '5');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('5002', '증류주/과실주', '5');

--상품 카테고리 6
INSERT INTO tb_prod_cate(cateCode, cateName)
	VALUES('6', '채소'); 

INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('6001', '친환경', '6');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('6002', '고구마/감자/당근', '6');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('6003', '시금치/쌈채소/나물', '6');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('6004', '브로콜리/파프리카/양배추', '6');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('6005', '양파/대파/마늘/배추', '6');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('6006', '오이/호박/고추', '6');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('6007', '냉동/이색/간편채소', '6');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('6008', '콩나물/버섯', '6');

--상품 카테고리 7
INSERT INTO tb_prod_cate(cateCode, cateName)
	VALUES('7', '정육/계란');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('7001', '국내산소고기', '7');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('7002', '수입산소고기', '7');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('7003', '돼지고기', '7');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('7004', '계란류', '7');

--상품 카테고리8
INSERT INTO tb_prod_cate(cateCode, cateName)
	VALUES('8', '간식/과자/떡');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('8001', '과자/스낵/쿠키', '8');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('8002', '초콜릿/젤리/캔디', '8');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('8003', '떡/한과', '8');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('8004', '아이스크림', '8');

--상품 카테고리9
INSERT INTO tb_prod_cate(cateCode, cateName)
	VALUES('9', '건강식품');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('9001', '영양제', '9');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('9002', '유산균', '9');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('9003', '홍삼/인삼/꿀', '9');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('9004', '다이어트/이너뷰티', '9');

--상품 카테고리 10
INSERT INTO tb_prod_cate(cateCode, cateName)
	VALUES('10', '국/반찬/메인요리');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('100001', '국/탕/찌개', '10');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('100002', '밑반찬', '10');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('100003', '두부/어묵/부침개', '10');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('100004', '밀키트/메인요리', '10');

--상품 카테고리 11
INSERT INTO tb_prod_cate(cateCode, cateName)
	VALUES('11', '면/양념/오일');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('110001', '파스타/면류', '11');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('110002', '식초/소스/드레싱', '11');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('110003', '소금/설탕/향신료', '11');
INSERT INTO tb_prod_cate(cateCode, cateName, cateParent)
	VALUES('110004', '밀가루/가루/믹스', '11');

 

  • 카테고리 데이터를 담는 쿼리문을 작성 후 Controller 단에서 cateList로 넘긴 뒤, 

  • JSP 화면에서 그 값을 받아온다.

 

  • 1차 카테고리 먼저 선택 후 1차 카테고리에 해당하는 2차 카테고리만 출력되도록 JSON으로 구현함.

 

 

상품 상세 정보 조회 SELECT

 

  • 상품 테이블과 상품 카테고리 테이블을 따로 만들었고 상품 상세 조회 시 두 테이블을 조인했다.

 

  • 컨트롤러에서 카테고리 데이터, 페이징 데이터, 상품 정보 총 3가지 데이터를 담아 뷰로 전달하도록 구현했다.
//상품 상세 정보 조회
	@Override
	@RequestMapping(value = "/adgoods/adgoodsInfo.do", produces = "application/json", method = {RequestMethod.GET, RequestMethod.POST})
	public void getadGoodsDetail(int prod_code, Model model, PagingVO vo) throws Exception{
		
		logger.info("클릭한 상품 : "+prod_code);
		
		ObjectMapper objm = new ObjectMapper();
		
		
		//카테고리 리스트 정보
		model.addAttribute("cateList", objm.writeValueAsString(adGoodsService.cateList()));
		
		//목록 페이지 조건 정보
		model.addAttribute("vo", vo);

		//상품 정보
		model.addAttribute("goodsVO", adGoodsService.adgoodsGetDetail(prod_code));
	
	}

 

상품 등록 INSERT

  • 상품 코드는 시퀀스를 적용하여 자동으로 상품 번호가 들어가도록 구현했다.

 

  • 상품 등록이 완료되면 상품 목록 화면으로 리다이렉트되도록 구현했다.

 

  • 상품 설명은 CK에디터를 적용했다.

 

  • 상품 등록 (삭제, 수정) 후에는 상품명+등록 완료 메시지를 Alert 창으로 띄우도록 구현했다.
	/* 상품 등록 성공 이벤트 */
		let eResult = '<c:out value="${goodsResult}"/>';
		checkResult(eResult);
		function checkResult(result){
			
			if(result == ""){
				return;
			}
			alert("상품"+eResult+"등록이 완료되었습니다.");
		}
		
		/* 수정 성공 이벤트 */
		let modify_result = '${modify_result}';
		
		if(modify_result == 1){
			alert("수정 완료");
		}
		
		/* 삭제 성공 이벤트 */
		let delete_result = '${delete_result}';
		
		if(delete_result == 1){
			alert("삭제 완료");
		}

 

상품 정보 수정 UPDATE, 상품 정보 삭제 DELETE

  • 쿼리문이 간단하므로 자세한 설명은 생략한다.
  • 상품 정보 수정, 삭제 후에도 상품 목록 화면으로 리다이렉트시킴.

 

상품 이미지 업로드

  • 사용자가 선택한 파일을 서버에 전송하기 위해서는 선택된 파일에 접근하는 방법을 알아야 한다.
  • 1. <input> 태그를 통해 선택된 파일은 File 객체 형태로 표현됨.
  • 2. File 객체는 FileList(배열 형태의 객체) 객체의 요소로 저장이 됨.
  • 3. FileList의 요소에는 File 객체가 저장됨 -> File 객체는 type이 'file'인 <input> 태그의 "files"의 속성.
  • 4. 사용자가 <input> 태그를 통해 파일 1개를 선택하게 되면 FileList 첫 번째 요소(FileList[0])인 File 객체에 파일 데이터가 저장됨.
  • 5. 여러 개의 파일을 선택한다면 선택한 갯수(n)만큼 FileList 첫 번째 요소(FileList[0])부터 순서대로 각 요소(FileList[n]) File 객체에 저장됨.

💡사용자가 선택한 파일을 선택한 파일인 File 객체에 접근하기 위해서는 결국
FileList 객체(<input>태그의 files 속성)에 접근해야 함.

 

  • 파일 업로드, 썸네일 이미지, JSON 의존성 추가
		<!-- 다중 파일 업로드 -->
		<dependency>
			<groupId>commons-fileupload</groupId>
			<artifactId>commons-fileupload</artifactId>
			<version>1.3.3</version>
		</dependency>

		<dependency>
			<groupId>commons-io</groupId>
			<artifactId>commons-io</artifactId>
			<version>1.4</version>
		</dependency>

		<!-- 썸네일 이미지 -->
		<dependency>
			<groupId>net.coobird</groupId>
			<artifactId>thumbnailator</artifactId>
			<version>0.4.8</version>
		</dependency>

 

  • uploadFile 데이터는 ajax로 전달함.

 

  • 너무 많은 파일이 한 곳에 모여있지 않도록 날짜 폴더 경로 생성

 

  • 상품 이미지 테이블을 따로 만들고 컨트롤러에서 이미지 정보를 담는 코드를 작성했다.
  • 파일 이름의 경우 동일한 이름을 가진 파일을 덮어쓰지 않도록, 각 파일이 고유한 이름을 갖도록 UUID(범용 고유 식별자)가 포함되도록 구현했다.
  • 썸네일 파일명은 "s_" + uploadfileName으로 구현했다.

 

  • 상품 상세 페이지에서는 자바스크립트를 통해 선택한 상품 번호(prod_code)에 해당하는 이미지 정보가 출력되도록 구현함.

 

💭프로젝트 소감, 배운점

나는 앞서 다른 학원을 4개월 다니고 이번 국비 과정 6개월을 수료하면서 자바 웹 프로그래밍 교육 과정을 2번 진행했다. 그전에는 이론 수업만 받고 프로젝트를 따로 하지 않아서 이미 배운 내용이지만 국비 학원을 한 번 더 다녔던 것인데 프로젝트를 경험하기 위해 수료를 결심한 만큼 이번 프로젝트는 나에게 남다른 의미가 있었다. 결과적으로는 자바 이론은 두 번째로 배우는 것이지만 프로젝트는 첫 경험이었던 나에게 매우 소중한 경험이었다. 6개월동안 학원을 다녔지만 거의 비대면 수업을 해서 학원 수강생들과 소통할 일이 없었는데 프로젝트 팀으로 너무 좋은 조원들을 만나서 작업하는 내내 많은 것을 얻어가는 느낌이었다. 

 

1일 1커밋!! 조원들 모두 다들 열심히 했다!!

회의는 메타버스 플랫폼인 게더타운에서 진행했다!! 매일 새벽까지 화면 공유하고 열심히 회의했다!!

 

수업 시간에 mvc 패턴, 게시판을 만들 때에는 스프링의 흐름조차 제대로 이해하지 못했는데 프로젝트를 하며 직접 쿼리문을 작성하고 화면을 만들고 백엔드 코드를 작성하면서 CRUD가 어떤 순서로 흘러가고 어떻게 타일즈를 연결하고 다음 페이지를 매핑하는지 드디어 이해할 수 있었다. 물론 개발자가 되려면 공부할 내용이 끝도 없고 취업 후에도 계속 공부해야 하는 것을 잘 안다. 그런데 기본이 부족했던 나로써는 이번 프로젝트를 통해 이해하지 못한 흐름을 파악하고 처음에 나를 곤란하게 했던 에러도 어느 정도는 덜 짜증내며(?) 해결할 수 있게 되어서 취업을 위한 포트폴리오 그 이상의 가치를 갖는 결과물이라고 생각한다. 이제 오늘로써 학원 마지막날, 수료를 하지만 이번 인연으로 만난 팀원들과 따로 학원에서 배우지 않은 내용도 스터디하고 추가 프로젝트도 시작할 생각이다. 별 일 없이 나의 첫 코딩 팀 프로젝트를 끝마칠 수 있어서 뿌듯하고! 이제 이해하지 못했던 것도 이해할 수 있으니 좀더 재밌게 코딩 공부를 계속 이어나가고 싶다. 취업까지 화이팅!!

반응형
profile

my code archive

@얼레벌레 개발자👩‍💻

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

반응형