일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- 마이페이지
- 관리자페이지
- stock option
- register
- 캘린더 라이브러리
- 인증처리
- 로그인
- jsonwebtoken
- userManagement
- react
- RCPS
- 로그인 로직
- 밸류즈 홈페이지
- Token
- 밸류즈
- Typesciprt
- Ajax
- Styled Components
- Update
- 회원가입로직
- 빌드 및 배포
- 파생상품평가
- mypage
- MRC
- 배포
- ui탬플릿
- 스프링시큐리티
- 공통메서드
- 달력 라이브러리
- 이미지 업로드
- Today
- Total
I T H
[프로젝트] 13. 관리자 - 상품등록 구현 (Week 3) 본문
관리자 계정으로 로그인 한 후
ART 상품 등록을 위한 상품 페이지를 개발함.
아래와 같은 절차로 1개의 상품이 등록되며, 상품과 관련한 테이블은 앞서 DB 모델링을 통해서 구현한 마스터/디테일 테이블 2개를 사용함.
1. 상품에 대한 기본 정보를 등록 : 상품명, 가격 등
2. 등록된 상품을 클릭하여 상품 이미지를 등록 (최대 5개까지) - 추후 파일 업로드 시 다중업로드가 가능하도록 구현할 예정, 현재는 이미지 1개씩 등록하는 방법으로 진행
먼저 상품 등록 시
상세 설명을 적기 위해 textfield를 이용해도 되지만
이왕이면 텍스트 라이브러리를 가져와서 사용해볼 겸 텍스트 에디터 라이브러리를 선택하여 적용.
https://github.com/suyati/line-control
GitHub - suyati/line-control: A Light Weight HTML5 Text Editor designed as a JQuery Plugin
A Light Weight HTML5 Text Editor designed as a JQuery Plugin - GitHub - suyati/line-control: A Light Weight HTML5 Text Editor designed as a JQuery Plugin
github.com
JS, CSS 파일을 각각 다운로드 받아 붙여넣기 후 import 하였음.
admin.jsp에 import함.
<!-- text Editor -->
<link rel="stylesheet" href="/resources/css/editor.css">
...
생략
...
<!-- text Editor -->
<!-- https://github.com/suyati/line-control -->
<script src="/resources/js/editor.js"></script>
등록되지 않은 이미지인 경우에 비어놓기 보다는 이미지가 등록되지 않았음을 보여주고 싶어서
dummy 이미지 파일을 하나 만들어서 프로젝트에 붙여넣고 사용.

더미 이미지
화면 개발을 먼저 진행.
화면 구성은
- 신규 상품 등록
- 상품 리스트 보기
- 선택한 상품에 대한 이미지 등록
으로 구성
[ prodAdm.jsp ]
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang="kr">
<head>
<meta charset="UTF-8">
<meta name="description" content="">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- The above 4 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<!-- Title -->
<title>ART - [관리자] 상품등록</title>
<!-- import CSS -->
<%@include file="/resources/inc/incCss.jsp"%>
<!-- text Editor -->
<link rel="stylesheet" href="/resources/css/editor.css">
<style>
/* table 스타일 inline 적용 */
.main-content-wrapper .cart-table-area table tbody tr td
,.main-content-wrapper .cart-table-area table thead tr th {
width: 20%;
max-width: 20%;
font-size: 10pt;
font-family: 'FontAwesome';
}
</style>
</head>
<body>
<!-- ##### Main Content Wrapper Start ##### -->
<div class="main-content-wrapper d-flex clearfix">
<!-- import Header -->
<%@include file="/resources/inc/incHeader.jsp"%>
<div class="cart-table-area section-padding-100">
<div class="container-fluid">
<div class="row">
<div class="col-12 col-lg-12">
<div class="checkout_details_area mt-50 clearfix">
<div class="cart-title">
<h4>[관리자] 상품등록</h4>
</div>
<form action="#" method="post">
<div class="row">
<div class="col-md-3 mb-3">
<small>상품명</small>
<input type="text" class="form-control" id="txtProdName" value="" placeholder="상품명 입력" required>
</div>
<div class="col-md-3 mb-3">
<small>상품가격</small>
<input type="text" class="form-control" id="txtProdPrice" value="" placeholder="상품가격 입력" required>
</div>
<div class="col-md-3 mb-3">
<small>상품수량</small>
<input type="text" class="form-control" id="txtProdCnt" value="" placeholder="상품수량 입력" required>
</div>
<div class="col-md-3 mb-3">
<small>사용여부</small>
<select class="w-100" id="cmbUseYn">
<option value="Y">사용</option>
<option value="N">미사용</option>
</select>
</div>
<div class="col-md-12 mb-3">
<small>상품설명</small>
<textarea id="txtEditor"></textarea>
</div>
<div class="col-md-12 mb-3">
<div class="cart-btn mt-50">
<a id="btnInsertProd" href="javascript:void(0)" class="btn amado-btn w-25">상품 등록</a>
</div>
</div>
</div>
</form>
<br /><br />
<div class="cart-title">
<h4>[관리자] 등록된 상품 리스트 조회</h4>
</div>
<div class="cart-table clearfix">
<table class="table table-responsive">
<thead>
<tr>
<th>상품아이디</th>
<th>상품명</th>
<th>상품가격</th>
<th>상품수량</th>
<th>사용여부</th>
</tr>
</thead>
<tbody id="mainTable" style="max-height: 500px; overflow-y: scroll;">
<tr>
<td class="cart_product_desc">
</td>
<td class="cart_product_desc">
</td>
<td class="cart_product_desc">
</td>
<td class="cart_product_desc">
</td>
<td class="cart_product_desc">
</td>
</tr>
</tbody>
</table>
</div>
<p>선택한 상품명 : <label id="selectedProdCode" style="display: none;">-</label><label id="selectedProdName">-</label></p>
<div class="row">
<div class="col-md-3 mb-3">
<small>상품이미지 1</small>
<input type="file" class="form-control" id="file1" name="file1">
</div>
<div class="col-md-3 mb-3">
<small> </small>
<div class="cart-btn">
<a id="1" href="javascript:void(0)" class="btn amado-btn w-50 btnFileUp">이미지 업로드</a>
</div>
</div>
<div class="col-md-6 mb-3">
<input type="hidden" id="currentImgId1">
<img src="" id="image1" class="img" style="max-height: 150px;">
</div>
</div>
<div class="row">
<div class="col-md-3 mb-3">
<small>상품이미지 2</small>
<input type="file" class="form-control" id="file2" name="file2">
</div>
<div class="col-md-3 mb-3">
<small> </small>
<div class="cart-btn">
<a id="2" href="javascript:void(0)" class="btn amado-btn w-50 btnFileUp">이미지 업로드</a>
</div>
</div>
<div class="col-md-6 mb-3">
<input type="hidden" id="currentImgId2">
<img src="" id="image2" class="img" style="max-height: 150px;">
</div>
</div>
<div class="row">
<div class="col-md-3 mb-3">
<small>상품이미지 3</small>
<input type="file" class="form-control" id="file3" name="file3">
</div>
<div class="col-md-3 mb-3">
<small> </small>
<div class="cart-btn">
<a id="3" href="javascript:void(0)" class="btn amado-btn w-50 btnFileUp">이미지 업로드</a>
</div>
</div>
<div class="col-md-6 mb-3">
<input type="hidden" id="currentImgId3">
<img src="" id="image3" class="img" style="max-height: 150px;">
</div>
</div>
<div class="row">
<div class="col-md-3 mb-3">
<small>상품이미지 4</small>
<input type="file" class="form-control" id="file4" name="file4">
</div>
<div class="col-md-3 mb-3">
<small> </small>
<div class="cart-btn">
<a id="4" href="javascript:void(0)" class="btn amado-btn w-50 btnFileUp">이미지 업로드</a>
</div>
</div>
<div class="col-md-6 mb-3">
<input type="hidden" id="currentImgId4">
<img src="" id="image4" class="img" style="max-height: 150px;">
</div>
</div>
<div class="row">
<div class="col-md-3 mb-3">
<small>상품이미지 5</small>
<input type="file" class="form-control" id="file5" name="file5">
</div>
<div class="col-md-3 mb-3">
<small> </small>
<div class="cart-btn">
<a id="5" href="javascript:void(0)" class="btn amado-btn w-50 btnFileUp">이미지 업로드</a>
</div>
</div>
<div class="col-md-6 mb-3">
<input type="hidden" id="currentImgId5">
<img src="" id="image5" class="img" style="max-height: 150px;">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ##### Main Content Wrapper End ##### -->
<!-- import Footer -->
<%@include file="/resources/inc/incFooter.jsp"%>
<!-- import JS -->
<%@include file="/resources/inc/incJs.jsp"%>
<!-- text Editor -->
<!-- https://github.com/suyati/line-control -->
<script src="/resources/js/editor.js"></script>
<!-- page script -->
<script src="/resources/views/prodAdm.js"></script>
</body>
</html>
[ prodAdm.js ]
/*******************************************************************************
* prodAdm.js
* @author thkim
* @since 2022
* @DESC 관리자 - 상품등록 화면 스크립트
******************************************************************************/
(function() {
function ProdAdm() {
/*
* private variables
*/
/*
* 초기화 메소드
*/
function _init() {
// 이벤트 처리 함수 호출
bindEvent();
// 등록된 상품 리스트 조회
findProdInfo();
}
function bindEvent() {
// text editor 실행
$("#txtEditor").Editor();
// 정보 수정 버튼 클릭 이벤트
$("#btnInsertProd").on("click", function() {
var prodName = $("#txtProdName").val();
if(prodName == "" || prodName == null) {
alert("상품명을 입력하시기 바랍니다.");
return;
}
var prodPrice = $("#txtProdPrice").val();
if(prodPrice == "" || prodPrice == null) {
alert("상품가격을 입력하시기 바랍니다.");
return;
}
var prodCnt = $("#txtProdCnt").val();
if(prodCnt == "" || prodCnt == null) {
alert("상품수량을 입력하시기 바랍니다.");
return;
}
var obj = {
prodName: $("#txtProdName").val(),
prodPrice: $("#txtProdPrice").val(),
prodCnt: $("#txtProdCnt").val(),
prodUseYn: $("#cmbUseYn").val(),
prodDesc: $("#txtEditor").Editor("getText")
}
if(obj) {
cfSave("/prodAdm/saveProdInfo", obj, function(data) {
if(data.success) {
alert("상품 등록이 완료되었습니다.");
$(".form-control").val(""); // 모든 텍스트필드 초기화
$("#txtEditor").Editor("setText", "");
// 상품 리스트 재조회
findProdInfo();
} else {
alert("상품 등록에 실패하였습니다. 확인 후 다시 등록하시기 바랍니다.");
}
});
}
});
// 이미지 업로드
$(".btnFileUp").on("click", function() {
var btnId = $(this)[0].id;
var fileId = "file" + btnId;
var selectedId = $("#selectedProdCode").html();
if(selectedId == "" || selectedId == null) {
alert("이미지를 등록할 상품을 선택하시기 바랍니다.");
return;
}
var obj = {
prodId: selectedId // 상품 아이디
};
// 파일 확장자 체크
var ext = $('#' + fileId).val().split('.').pop().toLowerCase();
if($.inArray(ext, ['gif','png','jpg','jpeg']) == -1) {
alert('gif, png, jpg, jpeg 파일만 업로드 할수 있습니다.');
return false;
}
cfUpload("/prodAdm/fileUpload/", function(result) {
if(result.error != null) {
alert('저장 실패');
} else {
obj["sort"] = btnId; // 이미지 순서
obj["mainImgYn"] = (btnId == "1" ? "Y" : "N"); // 이미지 순서가 1이면 메인 이미지로 등록
obj["src"] = result.src; // 상품 이미지명
obj["curImg"] = $("#currentImgId" + btnId).val(); // 이미 등록되어 있던 상품이미지의 아이디 (ART_PROD_DETAIL 테이블의 DETAIL_ID 컬럼)
// 이미지 업로드가 완료된 이후 실제 테이블에 데이터 (이미지 경로 등)를 저장한다.
cfSave("/prodAdm/saveImageInfo", obj, function(data) {
if(data.success) {
alert("상품 이미지 등록이 완료되었습니다.");
findImageInfo(selectedId);
initForm(fileId);
} else {
alert("상품 이미지 등록에 실패하였습니다. 확인 후 다시 등록하시기 바랍니다.");
}
});
}
}, function () {
alert('이미지 파일 업로드에 실패하였습니다. 다시 시도하시기 바랍니다.');
});
});
}
/*
* 업로드 후 파일 초기화
*/
function initForm(id) {
// browser version 체크 후 file inputbox 초기화 진행
var browserType = cfGetBrowser();
if(browserType == "MSIE") {//IE version
$("#" + id).replaceWith( $("#" + id).clone(true) );
} else {// other browser
$("#" + id).val("");
}
$("#" + id).val("");
}
/*
* 등록된 상품 리스트 조회
*/
function findProdInfo() {
var obj = {
};
cfFind("/prodAdm/findProdInfo", obj, function(data) {
if(data.length > 0) {
var html = "";
$.each(data, function(idx, node) {
html += "<tr id='" + idx + "'>";
html += "<td class='cart_product_desc' style='cursor: pointer;'>" + node["PROD_ID"] + "</td>";
html += "<td class='cart_product_desc'>" + node["PROD_NAME"] + "</td>";
html += "<td class='cart_product_desc'>" + node["PROD_PRICE"] + "</td>";
html += "<td class='cart_product_desc'>" + node["PROD_CNT"] + "</td>";
html += "<td class='cart_product_desc'>" + node["USE_YN"] + "</td>";
html += "</tr>";
});
$("#mainTable").html(html);
// 행을 선택 시 상품 선택
// 이후 이미지 등록하도록 함
$(".cart_product_desc").on("click", function() {
var rowId = $(this).parent()[0].id;
var selectedId = data[rowId]["PROD_ID"];
$("#selectedProdCode").html(selectedId);
var selectedName = data[rowId]["PROD_NAME"];
$("#selectedProdName").html(selectedName);
findImageInfo(selectedId);
});
// 조회 완료 시 첫번째 상품 자동 선택
$("#selectedProdCode").html(data[0]["PROD_ID"]);
$("#selectedProdName").html(data[0]["PROD_NAME"]);
findImageInfo(data[0]["PROD_ID"]);
} else {
$("#mainTable").html("<tr><td colspan='5'>조회된 데이터가 없습니다.</td></tr>");
return;
}
}, true, "POST");
}
/*
* 등록 이미지 조회
*/
function findImageInfo(id) {
var obj = {
prodId: id
};
cfFind("/prodAdm/findImageInfo", obj, function(data) {
$(".img").attr("src", "/resources/img/noImage.png");
$("[id^='currentImgId']").val(""); // ^= : currentImgId로 시작하는 id태그들을 의미함
//currentImgId로 시작하는 id태그들을 초기화 함 : 이유는 다른 상품의 이미지를 등록할때 이미지 정보가 남아 있으면 안되므로(지워지기때문에)
if(data.length > 0) {
$.each(data, function(idx, node) {
$("#image" + (node["SORT"])).attr("src", "/resources/upload/" + node["PROD_IMG"]);
$("#currentImgId" + (node["SORT"])).val(node["DETAIL_ID"]);
});
}
}, true, "POST");
}
function _finalize() {
}
return {
init : _init,
finalize : _finalize
};
};
var prodAdm = new ProdAdm();
prodAdm.init();
})();
//# sourceURL=prodAdm.js
다음은 백엔드를 진행함.
파일 업로드를 위한 스프링 멀티파트 클래스를 dispatcher-servlet.xml 파일에 선언하여 사용.
<!-- for Servlet2.5 Multipart FileUpload -->
<!-- maxUploadSize : 파일 업로드 시 용량 제한 - 최대 용량을 지정한다. -->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver" >
<property name="maxUploadSize" value="100000000" />
</bean>
[ ProdAdmController.java ]
package kr.co.art.biz.prodAdm.web;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.fileupload.FileUploadException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import kr.co.art.biz.prodAdm.persistence.ProdAdmMapper;
import kr.co.art.biz.prodAdm.service.ProdAdmService;
@Controller
@RequestMapping("/prodAdm")
public class ProdAdmController {
@Autowired
private ProdAdmMapper prodAdmMapper;
@Autowired
private ProdAdmService prodAdmService;
@RequestMapping("")
public String main() {
return "prodAdm";
}
/**
* 등록된 상품 리스트 조회
* (/prodAdm/findProdInfo)
* @return
*/
@RequestMapping("/findProdInfo")
@ResponseBody
public List<Map<String, Object>> findProdInfo(@RequestBody Map<String, Object> param) {
List<Map<String, Object>> list = prodAdmMapper.findProdInfo(param);
return list;
}
/**
* 신규 상품 등록
* (/prodAdm/saveProdInfo)
* @return
*/
@RequestMapping("/saveProdInfo")
@ResponseBody
public Map<String, Object> saveProdInfo(@RequestBody Map<String, Object> param) {
Map<String, Object> result = new HashMap<String, Object>();
prodAdmService.saveProdInfo(param);
result.put("success", true);
return result;
}
/**
* 상품 이미지 등록
* (/prodAdm/saveProdInfo)
* @return
*/
@RequestMapping("/saveImageInfo")
@ResponseBody
public Map<String, Object> saveImageInfo(@RequestBody Map<String, Object> param) {
Map<String, Object> result = new HashMap<String, Object>();
prodAdmService.saveImageInfo(param);
result.put("success", true);
return result;
}
/**
* 파일 업로드 (상품 이미지 업로드)
* (/prodAdm/fileUpload)
* for Servlet2.5 Multipart FileUpload
* @throws FileUploadException
*/
@RequestMapping("/fileUpload")
@ResponseBody
public Map<String, Object> fileUpload(@RequestParam("file") MultipartFile part, HttpServletRequest request) throws FileUploadException {
ServletContext context = request.getSession().getServletContext();
String path = context.getRealPath("/resources/upload"); // 업로드 경로
System.out.println("path : " + path);
String fileName = part.getOriginalFilename();
System.out.println("fileName : " + fileName);
InputStream in = null;
SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmss");
Date time = new Date();
String forTime = format.format(time);
int pos = fileName.lastIndexOf(".");
String ext = fileName.substring(pos + 1);
try {
File f = new File(path + "/" + forTime + "." + ext);
part.transferTo(f);
} catch (Exception e) {
throw new FileUploadException(); // 파일 업로드 오류
} finally {
if (in != null)
try {
in.close();
} catch (IOException e) {
}
}
Map<String, Object> result = new HashMap<String, Object>();
result.put("src", forTime + "." + ext);
return result;
}
/**
* 등록된 상품 이미지 리스트 조회
* (/prodAdm/findImageInfo)
* @return
*/
@RequestMapping("/findImageInfo")
@ResponseBody
public List<Map<String, Object>> findImageInfo(@RequestBody Map<String, Object> param) {
List<Map<String, Object>> list = prodAdmMapper.findImageInfo(param);
return list;
}
}
[ ProdAdmService.java ]
package kr.co.art.biz.prodAdm.service;
import java.util.Map;
public interface ProdAdmService {
// 신규 상품 등록
void saveProdInfo(Map<String, Object> map);
// 상품 이미지 등록
void saveImageInfo(Map<String, Object> map);
}
[ ProdAdmServiceImpl.java ]
package kr.co.art.biz.prodAdm.service;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import kr.co.art.biz.prodAdm.persistence.ProdAdmMapper;
@Service
public class ProdAdmServiceImpl implements ProdAdmService {
@Autowired
private ProdAdmMapper prodAdmMapper;
@Override
public void saveProdInfo(Map<String, Object> map) {
prodAdmMapper.saveProdInfo(map);
}
@Override
public void saveImageInfo(Map<String, Object> map) {
// 등록된 상품이 있는 경우 delete 후 insert
if(!map.get("curImg").equals("")) {
// delete
prodAdmMapper.saveImageInfoDelete(map);
}
prodAdmMapper.saveImageInfo(map);
}
}
[ ProdAdmMapper.java ]
package kr.co.art.biz.prodAdm.persistence;
import java.util.List;
import java.util.Map;
import kr.co.art.biz.register.domain.UserInfo;
public interface ProdAdmMapper {
// 등록된 상품 리스트 조회
List<Map<String, Object>> findProdInfo(Map<String, Object> params);
// 신규 상품 등록
void saveProdInfo(Map<String, Object> params);
// 이미지 정보 삭제
void saveImageInfoDelete(Map<String, Object> params);
// 신규 이미지 등록
void saveImageInfo(Map<String, Object> params);
// 등록된 이미지 조회
List<Map<String, Object>> findImageInfo(Map<String, Object> params);
}
[ ProdAdmMapper.xml ]
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="kr.co.art.biz.prodAdm.persistence.ProdAdmMapper">
<select id="findProdInfo" resultType="hashmap" parameterType="hashmap">
SELECT PROD_ID
, PROD_NAME
, PROD_DESC
, PROD_PRICE
, PROD_CNT
, USE_YN
, INPUT_DATETIME
FROM ART.ART_PROD
WHERE 1=1
</select>
<insert id="saveProdInfo" parameterType="hashmap">
INSERT INTO ART.ART_PROD (
PROD_ID
, PROD_NAME
, PROD_DESC
, PROD_PRICE
, PROD_CNT
, USE_YN
, INPUT_DATETIME
)
VALUES (
UUID() -- mysql(mariadb) 에서 사용, 중복되지 않는 36자리 키를 생성
, #{prodName}
, #{prodDesc}
, #{prodPrice}
, #{prodCnt}
, #{prodUseYn}
, NOW()
)
</insert>
<delete id="saveImageInfoDelete" parameterType="hashmap">
DELETE
FROM ART.ART_PROD_DETAIL
WHERE 1=1
AND DETAIL_ID = #{curImg}
</delete>
<insert id="saveImageInfo" parameterType="hashmap">
INSERT INTO ART.ART_PROD_DETAIL (
DETAIL_ID
, PROD_ID
, PROD_IMG
, SORT
, MAIN_IMG
, USE_YN
, INPUT_DATETIME
)
VALUES (
UUID() -- mysql(mariadb) 에서 사용, 중복되지 않는 36자리 키를 생성
, #{prodId}
, #{src}
, #{sort}
, #{mainImgYn}
, 'Y'
, NOW()
)
</insert>
<select id="findImageInfo" resultType="hashmap" parameterType="hashmap">
SELECT DETAIL_ID
, PROD_ID
, PROD_IMG
, SORT
, MAIN_IMG
, USE_YN
, INPUT_DATETIME
FROM ART.ART_PROD_DETAIL
WHERE 1=1
AND PROD_ID = #{prodId}
</select>
</mapper>
이제 상품 등록까지 완료!

'Spring ArtGallery Project' 카테고리의 다른 글
[프로젝트] 15. 메인페이지 꾸미기 (Week 3) (1) | 2024.01.23 |
---|---|
[프로젝트] 14. 시큐리티 tag 라이브러리 - 관리자 메뉴 처리 (Week 3) (0) | 2024.01.23 |
[프로젝트] 12. 로그인/로그아웃 버튼 처리 (Week 3) (1) | 2024.01.23 |
[프로젝트] 11. 마이페이지 구현 (Week 3) (0) | 2024.01.23 |
[프로젝트] 10. 로그인 구현 (Week 3) (0) | 2024.01.23 |