현재 일반사용자 측면에서의 기능은 대부분 구현이 끝났습니다
몇몇 기능의 경우 일반사용자와 관리자간 상호작용이 필요하므로 이번 포스팅부터는
관리자측면에서의 기능을 구현해보도록 하겠습니다
우리가 현재 진행중인 프로젝트에서 관리자란 매장을 운영중인 매장주(사장님)를 뜻합니다
매장주는 관리자페이지에서 현재 운영중인 매장의 정보와 매출, 주문등을 관리할수 있어야 합니다
위의 파일을 받아 기존파일에 덮어쓰기 해주세요.
admin폴더에 있는 파일들은 새로 추가된것이며 그 외의 파일들은 수정된 부분이 있는
파일들로 앞으로 관리자페이지에 대한 요소는 모두 admin폴더에 추가하겠습니다
header.jsp에 현재 로그인된 사용자가 일반유저인지 관리자인지 확인하여
관리자일경우 관리자페이지로 이동가능한 메뉴버튼을 추가해주었습니다
이제 운영중인 가게버튼을 누르면 매장목록페이지에서와 같이 내가 운영중인 매장의
정보가 간략하게 화면에 표시되어야 합니다. 그러려면 현재 로그인된 사용자가 운영중인
매장이 어느 매장인지를 알아야하므로 이 정보를 저장할 테이블을 하나 만들어야 합니다
CREATE TABLE DL_MY_STORE (
USER_ID NUMBER,
STORE_ID NUMBER
);
ALTER TABLE DL_MY_STORE
ADD CONSTRAINT MY_STORE_U
FOREIGN KEY (USER_ID)
REFERENCES DL_USER(ID)
ON DELETE CASCADE;
ALTER TABLE DL_MY_STORE
ADD CONSTRAINT MY_STORE_S
FOREIGN KEY (STORE_ID)
REFERENCES DL_STORE(ID)
ON DELETE CASCADE;
이제 해당 테이블에 유저번호와 매장번호를 추가하고 기존 DL_USER테이블에서
관리자로 등록할 사용자아이디의 ROLE을 ROLE_USER -> ROLE_ADMIN으로 변경합니다
이제 관리자페이지에 대한 로직을 처리하기 위해 모든 레이어에 Admin이라는 이름의
클래스를 추가해주도록 합니다
@Controller
public class AdminController {
@Autowired
AdminService adminService;
@Autowired
StoreService storeService;
@GetMapping("/admin/myStore")
public String myStore(@AuthenticationPrincipal CustomUserDetails principal, Model model) {
long userId = principal.getId();
List<StoreDto> storeList = adminService.myStore(userId);
model.addAttribute("storeList", storeList);
return "admin/myStore";
}
@GetMapping("/admin/management/{id}/detail")
public String detail(@PathVariable long id, @AuthenticationPrincipal CustomUserDetails principal, Model model) {
long userId = principal.getId();
StoreDetailDto storeDetailDto = storeService.storeDetail(id, userId);
model.addAttribute("store", storeDetailDto);
model.addAttribute("adminPage", true);
return "admin/detail";
}
}
myStore는 내가 운영중인 모든 매장을 보여주는 페이지이며 기존 매장목록페이지와 같은
로직을 가집니다. 다만 기존 매장목록의 경우 등록된 모든 매장을 보여주었지만 이 페이지는
내가 운영중인 매장만 보여줘야하므로 그부분에 대한 쿼리만 추가해주면 됩니다
detail는 내가 운영중인 특정 매장의 정보를 보여주는 페이지로 매장상세페이지와
같은 로직을 가집니다 따라서 기존 StoreController에 매장상세페이지를 가져오는
부분의 코드와 storeDetail.jsp를 그대로 사용하기 때문에 관리자페이지임을
표시하기 위한 변수하나를 model에 심어줍니다
@Service
public class AdminService {
@Autowired
AdminMapper adminMapper;
public List<StoreDto> myStore(long userId) {
return adminMapper.myStore(userId);
}
}
//운영중인 매장 목록
public List<StoreDto> myStore(long userId);
<select id="myStore" resultType="com.han.delivery.dto.StoreDto">
WITH R_COUNT AS (
SELECT STORE_ID
,ROUND(AVG(SCORE), 1) SCORE
,COUNT(REVIEW_CONTENT) REVIEW_COUNT
,COUNT(BOSS_COMMENT) BOSS_COMMENT_COUNT
FROM DL_REVIEW
GROUP BY STORE_ID
),
STORE AS (
SELECT S.*,
T.*
FROM DL_STORE S
LEFT JOIN R_COUNT T
ON S.ID = T.STORE_ID
LEFT JOIN DL_MY_STORE M
ON S.ID = M.STORE_ID
WHERE M.USER_ID = #{user_id }
)
SELECT * FROM STORE
</select>
AdminMapper는 기존에 포인트 적립, 사용 처리를 할때 추가해주었으니
코드만 추가해줍시다 포인트 관련 코드는 나중에 딴곳으로 옮기면 좋을것 같습니다
쿼리는 기존 매장목록쿼리에서 복잡한 부분 다 제외하고 MY_STORE테이블만 조인해줬습니다
이제 내가 운영중인 가게중에 특정매장을 클릭해 매장상세페이지로 접근하면
adminPage가 false -> true로 바뀌어 기존 일반유저 화면에서는 보이지 않았던
매장의 정보를 관리하기 위한 몇가지 요소들이 보이게 됩니다
메뉴 추가 버튼을 누를시 다음과 같은 화면이 나옵니다
추가할 메뉴에 대한 설명과 옵션을 설정할시 AJAX를 통해 ApiController로
사용자가 입력한 데이터와 함께 요청이 가게 됩니다
현재 화면에 떠있는 메뉴를 클릭할경우 메뉴선택창이 아닌 메뉴수정창이 나타납니다
메뉴 추가와 마찬가지로 사용자가 입력한 데이터와 함께 서버로 요청이 갑니다
메뉴삭제의 경우 삭제하고자 하는 메뉴의 체크박스를 클릭하고 메뉴삭제 버튼을 누를시
마찬가지로 서버로 요청이 가게 됩니다. 다만 메뉴삭제의 경우 메뉴번호만 포함합니다
이제 ajax요청을 받을 api컨트롤러를 하나 추가해야 하는데 그전에 기존 utils패키지안에
FileUpload클래스에 메서드를 하나 추가하도록 하겠습니다
public String uploadImg(MultipartFile file, String imgPathName) {
String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyMMdd"));
String uploadForder= Paths.get("C:", "delivery", "upload").toString();
String imageUploadForder = Paths.get(imgPathName, today).toString();
String uploadPath = Paths.get(uploadForder, imageUploadForder).toString();
File dir = new File(uploadPath);
if (dir.exists() == false) {
dir.mkdirs();
}
UUID uuid = UUID.randomUUID();
String ImgName = uuid+"_"+file.getOriginalFilename();
try {
File target = new File(uploadPath, ImgName);
file.transferTo(target);
} catch (Exception e) {
}
return "\\upload\\" + imageUploadForder + "\\" + ImgName;
}
기존에 uploadReviewImg메서드랑 모든 부분이 동일합니다
다만 기존코드의 경우 리뷰이미지를 업로드할때 구현했기 때문에
ReviewDto를 파라메터로 받아야하고 외부폴더이름이 "reviewImg"으로 고정되어있어
상황에 따라 저장할 폴더이름을 직접 지정할수 있도록 하고 파라미터를
multipartFile로 받도록 새로 추가해줬습니다 기존에 메서드는 그대로 남겨두시고
기존 메서드를 지우실거면 리뷰작성쪽 코드를 수정해주셔야 합니다
@RestController
public class AdminApiController {
@Autowired
AdminService adminService;
@Autowired
FileUpload fileUpload;
//메뉴추가
@PostMapping("/api/admin/management/menu")
public ResponseEntity<FoodDto> addMenu(FoodDto foodDto, String[] foodOption, Integer[] foodOptionPrice, MultipartFile file) throws IOException {
if(file.isEmpty()) {
String img = File.separator + "img" + File.separator + "none.gif";
foodDto.setFoodImg(img);
foodDto.setFoodThumb(img);
} else {
String img = fileUpload.uploadImg(file, "foodImg");
foodDto.setFoodImg(img);
foodDto.setFoodThumb(img);
}
adminService.addMenu(foodDto, foodOption, foodOptionPrice);
return ResponseEntity.ok().body(foodDto);
}
//메뉴수정
@PatchMapping("/api/admin/management/menu")
public ResponseEntity<FoodDto> updateMenu(FoodDto foodDto, String[] foodOption, Integer[] foodOptionPrice, Integer[] optionId, MultipartFile file) throws IOException {
System.out.println(foodDto);
if(!file.isEmpty()){
String img = fileUpload.uploadImg(file, "foodImg");
foodDto.setFoodImg(img);
foodDto.setFoodThumb(img);
}
adminService.updateMenu(foodDto, foodOption, foodOptionPrice, optionId);
return ResponseEntity.ok().body(foodDto);
}
//메뉴삭제
@DeleteMapping("/api/admin/management/menu")
public ResponseEntity<String> deleteMenu(long storeId, long[] deleteNumber) {
adminService.deleteMenu(storeId, deleteNumber);
return ResponseEntity.ok().body("삭제 완료");
}
}
메뉴 추가의 경우 이미지를 등록했는지 안했는지 확인해야 합니다. 등록을 하지 않았을시
none값을 가지게하고 이미지를 등록했을경우 foodImg폴더안에 파일을 저장하고
그 주소를 FoodDto에 변수값에 넣어줍니다
메뉴 수정의 경우 file이 존재하면 이미지를 교체하는 경우이므로 이미지주소를 교체하고
file이 존재하지 않는경우 이미지주소를 유지시킵니다
메뉴 추가/수정 모두 한번에 메뉴 1가지만 추가/수정이 가능하지만 옵션의 경우 여러개를
추가하거나 수정할수 있습니다 따라서 이를 배열의 형태로 저장하게 됩니다
//메뉴추가
@Transactional
public void addMenu(FoodDto foodDto, String[] foodOption, Integer[] foodOptionPrice) {
adminMapper.addMenu(foodDto);
if(foodOption != null) {
List<Map<String, Object>> optionList = new ArrayList<>();
for(int i=0;i<foodOption.length;i++) {
Map<String, Object> optionMap = new HashMap<>();
optionMap.put("optionName", foodOption[i]);
optionMap.put("optionPrice", foodOptionPrice[i]);
optionMap.put("foodId", foodDto.getId());
optionList.add(optionMap);
}
adminMapper.addMenuOption(optionList);
}
}
//메뉴수정
@Transactional
public void updateMenu(FoodDto foodDto, String[] foodOption, Integer[] foodOptionPrice, Integer[] optionId) {
Map<String, Object> map = new HashMap<>();
if(foodOption == null) {
adminMapper.deleteMenuOption(foodDto.getId());
} else {
List<Map<String, Object>> optionList = new ArrayList<>();
for(int i=0;i<foodOption.length;i++) {
Map<String, Object> optionMap = new HashMap<>();
long oid = -1;
if(optionId.length != 0 && optionId[i] != null) {
oid = optionId[i];
}
optionMap.put("optionId", oid);
optionMap.put("optionName", foodOption[i]);
optionMap.put("optionPrice", foodOptionPrice[i]);
optionList.add(optionMap);
}
map.put("optionList", optionList);
}
map.put("food", foodDto);
adminMapper.updateMenu(map);
}
//메뉴삭제
public void deleteMenu(long storeId, long[] deleteNumber) {
adminMapper.deleteMenu(storeId, deleteNumber);
}
메뉴추가의 경우 FOOD테이블과 FOOD_OPTION테이블에 INSERT만 해주면 됩니다
다만 옵션의 경우 배열이므로 foreach구문을 사용합니다
메뉴삭제의 경우 FOOD테이블에서만 foreach구문을 사용해 DELETE해주면 됩니다
FOOD테이블과 FOOD_OPTION테이블은 외래키로 묶여있기 때문에 부모테이블인
FOOD테이블에서 삭제시 FOOD_OPTION테이블에서도 자동으로 삭제됩니다
메뉴수정의 경우 여러가지로 나눠서 생각해야 합니다
만약 수정전 1번메뉴에 A,B,C,D,E 라는 옵션이 있었다고 가정해봅시다
이때 사용자는 A,B,C,D,E -> A,B,C,D,E,F로 F를 추가할수도 있고
A,B,C,D,E -> A,B,C,D로 E를 삭제할수도 있습니다
아니면 A,B,C,D,E -> H,I,J,K,L로 변경할수도 있고 모두 지울수도 있습니다
가장 심플한 방법은 메뉴를 수정할때 FOOD_OPTION테이블에서 기존에 메뉴에
존재했던 옵션을 모두 지우고 현재 입력받은 옵션을 모두 다시 추가하는겁니다
다만 이방법은 한가지 문제가 존재하는데 FOOD_OPTION의 ID번호는 시퀀스로
자동증가하므로 ID값이 빠르게 증가하게 됩니다
따라서 좀 복잡하지만 옵션이 존재하지 않는경우 / 새로추가한 옵션이 있는 경우
기존 옵션의 내용을 변경한경우를 나누어 코드를 작성해줘야 합니다
이부분은 밑에서 설명하도록 하겠습니다
<insert id="addMenu">
<selectKey keyProperty="id" resultType="long" order="BEFORE" >
SELECT FOOD_ID_SEQ.NEXTVAL FROM DUAL
</selectKey>
INSERT INTO DL_FOOD (
ID
,STORE_ID
,FOOD_NAME
,FOOD_PRICE
,FOOD_DEC
,FOOD_IMG
,FOOD_THUMB
) VALUES (
#{id }
,#{storeId }
,#{foodName }
,#{foodPrice }
,#{foodDec }
,#{foodImg }
,#{foodThumb }
)
</insert>
<insert id="addMenuOption">
INSERT INTO DL_FOOD_OPTION
SELECT OPTION_ID_SEQ.NEXTVAL, F.*
FROM(
<foreach collection="list" item="item" separator="UNION ALL" >
SELECT #{item.foodId }
,#{item.optionName }
,#{item.optionPrice }
FROM DUAL
</foreach>
)F
</insert>
<delete id="deleteMenuOption">
DELETE DL_FOOD_OPTION WHERE FOOD_ID = #{foodId }
</delete>
<update id="updateMenu">
DECLARE BEGIN
UPDATE DL_FOOD
SET STORE_ID = #{food.storeId }
,FOOD_NAME = #{food.foodName }
,FOOD_PRICE = #{food.foodPrice }
,FOOD_DEC = #{food.foodDec }
,FOOD_IMG = #{food.foodImg }
,FOOD_THUMB = #{food.foodThumb }
WHERE ID = #{food.id };
<if test="optionList != null">
DELETE DL_FOOD_OPTION WHERE FOOD_ID = #{food.id } AND ID NOT IN
<foreach collection="optionList" item="item" open="(" close=");" separator="," >
${item.optionId }
</foreach>
<foreach collection="optionList" item="item" separator=";" close=";">
<if test="item.optionId == -1">
INSERT INTO DL_FOOD_OPTION
VALUES (OPTION_ID_SEQ.NEXTVAL
,#{food.id }
,#{item.optionName }
,#{item.optionPrice })
</if>
<if test="item.optionId != -1">
UPDATE DL_FOOD_OPTION
SET OPTION_NAME = #{item.optionName }
,OPTION_PRICE = #{item.optionPrice }
WHERE FOOD_ID = #{food.id }
AND ID = #{item.optionId }
</if>
</foreach>
</if>
END;
</update>
<delete id="deleteMenu">
DELETE DL_FOOD WHERE STORE_ID = ${storeId } AND ID IN
<foreach collection="deleteNumber" item="arr" open="(" close=")" separator="," >
${arr }
</foreach>
</delete>
addMenu의 selectKey부분은 id를 생성해주는 부분입니다. 메뉴추가를 할때 FOOD테이블뿐만
아니라 FOOD_OPTION테이블에도 등록해야 합니다. FOOD테이블의 ID의경우 시퀀스를 통해
자동생성이 가능합니다 하지만 FOOD_OPTION테이블의 경우 등록할때 FOOD테이블의 ID가
필요합니다 즉 FOOD테이블 INSERT하고 이 ID를 확인하기 위해서 다시 SELECT를 하고
그 후에 FOOD_OPTION테이블에 등록해야 합니다. 하지만 selectKey를 설정하고
order=BEFORE로 할경우 밑의 INSERT쿼리가 처리되기전에 id가 생성되므로 중간에
SELECT를 건너뛸수 있습니다
메뉴수정의 경우 수정데이터에서 옵션정보가 존재하지 않을경우 단순히 테이블에 존재하는
옵션정보를 모두 지워주면 됩니다. 하지만 옵션정보가 존재할경우 일단 FOOD_OPTION테이블
에서 현재 입력받은 옵션데이터의 ID를 제외한 나머지 옵션을 모두 삭제합니다
그 이후 새로 추가한 옵션의 경우 addMenu에서와 마찬가지로 ID가 존재하지 않으므로
INSERT를 실행하고 기존에 있었던 옵션의 내용을 변경한경우 옵션ID가 존재하므로
내용만 UPDATE해줍니다
좀더 쉽게 설명해보면 기존에 1-A , 2-B , 3-C , 4-D 라는 옵션이 있었는데
(숫자는 옵션ID , 알파뱃은 옵션내용)
이 옵션이 1-E(변경), 3-C, F(추가)로 바뀌었다면 F는옵션ID가 없는 상태이므로 INSERT
테이블에 저장된 1,2,3,4중 남은 1,3번을 제외한 2,4번은 DELETE
1,3번의 경우 내용이 바뀌었든 안바뀌었든 UPDATE로 생각할수 있습니다