프론트엔드 단에서 자바스크립트만 열심히 만지다보니 이벤트나 메소드는 익숙해지고 있으나 서버단, 그리고 SQL 쿼리는 점차 까먹기에 이르렀다.
학원 다니면서 만들어봤던 쇼핑몰 프로젝트도 열심히 했지만, 여러명이서 나눠서 했기 때문에 디테일한 부분은 잘 모르는게 사실이다. 다시 복기시키는 겸, 요즘 열심히 하는 네이버 블로그의 폼을 따서 나만의 블로그(이지만 실상은 게시판인)를 만들어보기로 했다.
디자인은 크게 자신이 없어서 네이버 소스보기로 틀을 긁어오고, 나머지는 BootStrap을 사용할 예정이다.
회원가입/로그인 기능도 구글링해서 기본 테이블 제작 후, 후에 세부적으로 커스터마이징 하기로 마음먹었다.
하나하나 한걸 나눠서 전부 포스팅하면 좋을거라 생각하지만.... 로그인/회원가입 예제는 구글링에 잘 나와있어 참고한 링크만 걸어놓는다.
1. 초기 세팅
이클립스를 설치하고 DB를 세팅한다.
이미 시스템 > 고급 시스템 설정 > 시스템속성 고급탭 > 환경변수 > jdk1.8.0_281 가 JAVA_HOME으로 등록되어있음.
DB는 예전에 설치한 SQL Developer 사용했다.
계정은 system이 가장 상위이다. 12345 비번으로 되어있었음. cmd에서 새로 계정 만들고 권한주고 할 수 있음. BlogDB 계정을 따로 만들어서 거기서 작업해주자. 비밀번호도 재설정해주었다.
2. 회원가입/로그인 기본 기능 만들기
dynamic Web project로 진행함.
처음에는 그대로 따라서 만들었다.
이클립스 자동 정렬 Shrift + Ctrt + F
이클립스 자동 import Ctrl+ Shift + O
me로 끝나는 가상페이지로 이동시켜서 컨트롤러에서 그에 해당하는 Action페이지로 갈 수 있게 한다. 실제 페이지 주소를 숨긴다.
여기까지 회원가입 버튼 누르면 DB에 데이터 들어가는거까지 완료했다.
여기서 뭐가 안되나 했더니, context.xml에서 Resource name = “jdbc/oracleDB”로 되어있었는데 내 오라클 이름은 myora 였다. 이거 언제 어디서 설정했는지 모르겠음…몰?루ㅋㅋ
→ 찾았다. JdbcUtil 안에 DB 이름이 설정되어있다. 내 DB이름 어디서 설정했더라...
▼ 지금까지 과정 요약
dynamic Web Project 만들고 WebContent 아래에 화면 만들어서 각자 연결한다.
각 페이지 연결은 href로 각각 연결해주는 것도 있지만 Controller 단에서 가상의 페이지(.me)를 만든 뒤 만약 링크가 거기로 접속한다면 이리로 가라! 라는 식으로 연결해줬다. 로그인 가상창, 회원가입 가상창, 회원가입버튼 누를 시 액션을 취하는 가상창까지 만듦. input을 누르면 가상창으로 간다.
(컨트롤러에서 doGet부분만 doProcess로 가게 해줬는데, doPost도 doProcess(req,resp);를 추가해줬다.)
그래서 doProcess에서 주소 가져오고.로그인.me로 가면 로그인 창으로 이동해주고~ 회원가입 버튼 누르면 me로 가서 이동해주고~(여기서 경로 문제때문에 한참걸림 ./는 현 위치니까 /로 시작하면 루트 경로라고 생각하고 설정해주기.) 그 후 회원가입을 도와주는 Action.me도 연결은 했는데 여기서 SQL로 보내주는 것. 리모콘 역할.
VO : 사용할 변수 선언 및 getter/setter로 받고 보내줄 변수 준비 / actionForward에서는 어디로 이동할지에 대한 다이렉트 변수 선언
db : 자바와 오라클을 연결해줌. 접속 하고 해제하고 등등 쿼리문 전달도 함
DAO : 정보 넘겨주는 애. 자바랑 오라클 사이에서 전달해줌. 자바 → 오라클
svc : DAO가 고객정보를 DB로 전달해줬으면 얘가 그거 보고 가입 완료했는지 불가했는지 보고 잘 됐으면 DB 커밋하고 안되면 쿼리 롤백하고 DB와의 연결을 끊는다.
action : 입력할 목록 정해주고 vo에서 설정된 변수들을 받아서 목록에 대한 파라미터 설정과 등록이 안되면 alert를 띄우고 등록 잘되면 로그인 페이지로 이동시켜줌
controller : 이 위에서 흩어져있는거 다 가져온 다음에 대가리가 시킴. 야 foward 있으면 주소로 이동해. 가입하고 forward = action.execute(req, resp); 여기서 받은 action을 뜯어서 SQL로 보내준다.
그럼 SQL로 슝 가는것^_^ 예에~~!!
그다음 로그인 기능 구현 시작
회원가입 하고 나니까 로그인은 비슷하게 술술 넘어가네.
▼ 커넥션풀 / 인스턴스 / 싱글톤 패턴
커넥션 풀 : DB와 미리 연결해놓은 커넥션 객체들(도구들)을 풀에 저장해뒀다가 클라에서 요청오면 커넥션 객체 빌려주고(접근 가능 도구를 빌려주고) DB에 접근해서 쿼리 막 날린 뒤 볼일 끝나면 다시 커넥션을 반납받아 풀에 저장하는 방식 / 너무 많으면 메모리 차지하니까 주의.
원래 클라가 서버로 게시글 저장 요청을 하면 서버에서 가공해서~그걸 DB에 저장하기 위해 DB랑 커넥션 맺고~DB에 게시글 저장하고~다시 DB랑 커넥션 끊고~ 다시 응답 가공해서~ 클라이언트에게 전달하는 로직인데…. → 이걸 DB랑 커넥션 맺고 끊고 안하게 하기 위해서 미리 연결해놓은 공용 와이파이(?)만 접속하면 내가 거기랑 일일히 연결했다가 끊을 필요가 없다. 그러므로 성능이 향상되는 것.(100명이 각각 데이터 연결할 필요 없이 공용 wifi 쓰면 원래 연결되어있는 wifi 커넥션들을 딱 사용할만큼만 쓸 수 있음)
인스턴스 : 집합의 개별요소. 객체지향이면 어떤 클래스에 속하는 각 객체를 인스턴스라고 함. 만약 list라는 클래스에서 myList라는 객체를 생성(메모리할당)하면 그 클래스의 인스턴스가 생성된다.
예를 들어 붕어빵틀(Class)에 반죽과 팥(변수variable)을 각각 넣어 구우면(인스턴스화) 각각의 붕어빵(Instance)가 탄생한다. 각각의 붕어빵은 객체이면서 각각의 개성이 있는 인스턴스이다.
클래스는 객체를 만들기 위한 설계도 또는 틀이고, 연관된 변수와 메소드를 묶는 집합이다.
객체는 클래스에서 선언한 모양 그대로 생성된 실체이다. 사용될 대상을 의미(=클래스의 인스턴스). 모든 인스턴스를 대표하는 포괄적인 의미
인스턴스는 만들어진 객체를 소프트웨어에 실체화하면 각각의 개성이 있으므로 각각 메모리가 할당되며 객체를 설명할 때 쓴다. 객체의 추상적인 개념이고 관계에 대한 말을 할 때 쓴다.(ex. 객체는 클래스의 인스턴스이다.) 추상적이고 여러 개성이 있으므로 원본(추상적 개념)의 생성된 복제본을 말한다.
https://gmlwjd9405.github.io/2018/09/17/class-object-instance.html
싱글톤 : 객체가 인스턴스화가 되면서 메모리를 할당할 때 그 영역을 1개만 고정해서 쓰는 것(static). 객체의 인스턴스가 오직 1개만 생성되는 패턴이다. 메모리 낭비를 안하고 데이터 공유가 쉽다. 주소가 1개로 고정되니까. 오직 한 개의 인스턴스 생성을 보증한다.
커넥션풀처럼 공통된 객체를 여러개 생성해서 사용할 때에 싱글톤 패턴을 쓴다. DB에 데이터가 공통적으로 있고, 사람들이 로그인을 동시다발적으로 하면 커넥션 풀에서 DB랑 연결된 와이파이를 빌려줘서 필요한 정보만 뺴서 쓰는거니까 싱글톤 패턴을 사용한다고 볼 수 있다.
▼ JSTL 설치 및 세팅
Can not find the tag library descriptor for "http://java.sun.com/jsp/jstl/core" 에러 뜨는데 jstl 설치 안해서 그럼.
설치해서 WEB_INF > lib 에 넣어주면 됨.(jstl-1.2.jar)
▼ 이클립스와 Git 연동
어쩐지 비번 입력해도 git 연동아 안되더라니…토큰 방식으로 바뀌었구나.
git 등록 방법 : Git Staging 에서 Unstaged Changes에 있는 것들을 아래로 옮김(++클릭) > Commit Message 쓰기 > Commit and Push 누르고 git id / 토큰 입력하면 업로드 됨.
여기까지 구현된 기능 : 회원가입 / 로그인 / 관리자 로그인 시 회원목록 나오고 관리자는 회원 목록에서 회원 삭제가 가능함.
3. 네이버 CSS 긁어오기
일단 회원가입 페이지 소스보기해서 html 다 긁어오고 style 하나하나 설정해볼 예정
-> 나중에 알고보니 css도 긁어올 수 있더라.
처음엔 일일히 따서 css 긁어왔는데^^...그래도 뭐 좋은 공부였다.
갑자기 CSS가 안먹더라.
결론적으로는 css 를 인식하지 못하는 mime 문제였던 것 같다. 왜냐면 IE에서 열었을 때 [MIME 형식이 일치하지 않아 CSS가 무시되었습니다.] 라고 떴거든. type=text/css로 되어있는데 왜 안되나 싶은데…
web.xml에서 .css 파일을 읽을 수 있도록 mime 설정 해주니 해결됨. 해결하는데 한참 걸렸다.
getter setter 단축키 : Alt + Shift + S → R
4. 테이블 컬럼 맞추고 DB연결
java.sql.SQLException: 인덱스에서 누락된 IN 또는 OUT 매개변수:: 4 에러메시지 계속 나서 뭔가 했더니 ? 갯수도 맞고 set도 제대로 했는데 왜 안되나 했더니…숫자도 맞춰야하는데 그냥 복붙해서 그랬음..ㅋ
근데 왜 날짜가 안들어가지? 이상하다...했는데
name값이 없어서였음. id값으로 가는게 아니라 name값으로 가는거다.
회원가입 부분에 유효성 검사가 생각보다 할게 많았다. ajax부터 다시 주석달면서 DB연결해줌.
여기까지 구글링 보고 기본 세팅 및 회원가입/로그인/회원리스트 출력 및 삭제까지 완료했다.
기본기는 여기 링크에 걸린 것들로 충분히 세팅할 수 있었고, ajax 통신부터 세부적인 부분은 따로 포스팅해볼 예정이다.
5. 회원가입/로그인 소스 정리
1) MEMBERINFO 테이블
회원 정보를 담은 MEMBERINFO 테이블이다.
회원가입 시 해당 테이블에 데이터를 insert하고, 로그인 시 이 테이블에서 select하여 가입 여부를 확인한다.
▼자세한 소스 보기
- MemberDAO 에서 설정한 sql 구문
1) 회원가입 SQL 로직
//=========================== 회원가입하는 SQL로직 ===============================
// MemberJoinService에서 회원가입할 때 DB와 JSP를 연결할 때 인자로 쓰임.
public int insertMember(MemberBean member) {
//회원가입 시 DB에 member 정보를 넣는 SQL문(DB 이름 확인하기***)
//String sql = "insert into memberinfo() values (?,?,?,?,?,?,?,?,?)";
//memberinfo 테이블이 언제 또 추가될지 모르니 따로 넣자.
String sql =
"INSERT INTO memberinfo("
+ "MEM_NO,"
+ "MEM_ID,"
+ "MEM_PWD,"
+ "MEM_NAME,"
+ "MEM_BIR_YY,"
+ "MEM_BIR_MM,"
+ "MEM_BIR_DD,"
+ "MEM_GENDER,"
+ "MEM_MAIL,"
+ "MEM_PHONE,"
+ "MEM_REGDATE,"
+ "MEM_PIC"
+ ") "
+ "VALUES( "
+ "(SELECT NVL(MAX(mem_no), 0) + 1 FROM memberinfo)," //MEM_NO
+ "?," //MEM_ID
+ "?," //MEM_PWD
+ "?," //MEM_NAME
+ "?," //MEM_BIR_YY
+ "?," //MEM_BIR_MM
+ "?," //MEM_BIR_DD
+ "?," //MEM_GENDER
+ "?," //MEM_MAIL
+ "?,"//MEM_PHONE
+ "TRUNC(SYSDATE)," //MEM_REGDATE
+ "'default.jpg'" //MEM_PIC
+ ")";
int insertCount=0;
try {
pstmt = con.prepareStatement(sql);
//prepareStatement : SQL문 실행하는 기능을 갖는 객체(변수는 ?로, setString으로 아래에 지정함.)
pstmt.setString(1, member.getMEMBER_ID());
pstmt.setString(2, member.getMEMBER_PW());
pstmt.setString(3, member.getMEMBER_NAME());
pstmt.setString(4, member.getMEMBER_BIR_YY());
pstmt.setString(5, member.getMEMBER_BIR_MM());
pstmt.setString(6, member.getMEMBER_BIR_DD());
pstmt.setString(7, member.getMEMBER_GENDER());
pstmt.setString(8, member.getMEMBER_MAIL());
pstmt.setString(9, member.getMEMBER_PHONE());
insertCount=pstmt.executeUpdate(); //executeUpdate : 데이터베이스 변경할 때
//select는 executeQuery()를 사용한다.
// insert, update, delete는 executeUpdate()를 사용한다.
System.out.println("MemberDAO : 성공");
//이 것을수행해서 정상적으로 된다면 insertCount가 1이 된다.
} catch (Exception ex) {
System.out.println("JoinMember 에러 : " + ex);
} finally {
close(pstmt); // import static db.JdbcUtil.*;
}
return insertCount;
}
2) 로그인 SQL 로직(select)
//=========================== 로그인하는 SQL로직 ===============================
// MemberLoginService에서 로그인할 때 DB와 JSP를 연결할 때 인자로 쓰임.
public String selectLoginId(MemberBean member) {
String loginId = null;
// 로그인 시 사용자가 입력한 아이디와 패스워드로 디비에 저장된 아이디 확인하는 SQL문(DB 이름 확인하기***)
String sql = "select MEM_ID from memberinfo " + "where MEM_ID=? and MEM_PWD=?";
try {
pstmt = con.prepareStatement(sql);
pstmt.setString(1, member.getMEMBER_ID());
pstmt.setString(2, member.getMEMBER_PW());
rs = pstmt.executeQuery();
//select는 executeQuery()를 사용한다.
// insert, update, delete는 executeUpdate()를 사용한다.
//쿼리문 처리결과 ResultSet의 객체인 rs에 저장.
if (rs.next()) {
//rs객체에서 첫 번째 값 가져오기
//가져올 값이 있다면~
loginId = rs.getString("MEM_ID"); //DB 컬럼 이름 쓰기
//member_id를 가져와서 loginId로 저장하겠다.
}
} catch (Exception ex) {
System.out.println("LoginMember 에러: " + ex);
} finally {
close(rs);
close(pstmt);
}
return loginId;
}
2) Controller에서 Action 페이지로 보냄
회원가입 페이지, 로그인페이지 이동 및 처리할 Action 페이지로 연결한다.
▼자세한 소스 보기
- 각 분기별로 페이지를 연결해줌.
//=============== 여기서부터 화원가입/로그인 페이지 연결해줌. ===============
ActionForward forward = null;
// Action이 모든 작업을 끝내고서 이동하는 위치을 가상적으로 지정한 것이 ActionForward이다.
//ActionForward를 null로 리턴하면 이미 response에 응답을 끝냈다는 의미가 된다. 다른 어떤 페이지로도 이동하지 않는다.
Action action = null; // execute라는 실행 메소드 return.
//-------------------- 로그인 버튼 누르면 로그인 페이지로 이동 ---------------------------
if(command.equals("/member/memberLogin.me")) {
//전달된 명령이 memberLogin.me라면
forward = new ActionForward();
//객체 생성
forward.setRedirect(true);
//이동 허락
forward.setPath("./login.jsp");
//거기의 주소는 loginForm.jsp로 해라.(이동할 주소 저장)
}
//-------------------- 회원가입 버튼 누르면 회원가입 페이지로 이동 ---------------------------
else if(command.equals("/member/memberJoin.me")) { //이거네...
//member 폴더 안 login.jsp에서 전달된 명령이 member 폴더 안 memberJoin.me라면(컨트롤러 기준은 ROOT)
forward = new ActionForward();
//객체 생성
forward.setRedirect(false);
//이동 허락 안함
forward.setPath("./joinUs.jsp");
//거기 주소는 joinForm.jsp으로 해라.(이동할 주소 저장)
}
//-------------------- 회원가입을 처리할 memberJoinAction 페이지 생성 ---------------------------
else if (command.equals("/member/memberJoinAction.me")) {
action = new MemberJoinAction();
try {
forward = action.execute(req, resp); //받은 action을 뜯어서 SQL로 보내준다.
System.out.printf("MemberFrontController : 회원가입 - SQL DB로 보내는 로직 실행 \n",req, resp);
//메인페이지로 이동하자.
//forward.setRedirect(true);
//forward.setPath("../index.jsp");
} catch (Exception e) {
e.printStackTrace();
System.out.println("MemberFrontController : 회원가입 - SQL DB로 보내는 로직 실패(위에 에러)");
}
} //회원가입을 하면 회원 가입이 되어야하는데 이를 DB에 저장도 할 수 있어야함. -> jdbcUtil에서 연결하고 DB로 보냄.
//-------------------- 로그인을 처리할 memberLoginAction 페이지 생성 ---------------------------
else if(command.equals("/member/memberLoginAction.me")) {
action = new MemberLoginAction();
//MemberLoginAction : 로그인 처리 준비. DB접속 전 준비.
try {
forward = action.execute(req, resp); //받은 action을 뜯어서 SQL로 보내준다.
System.out.printf("MemberFrontController : 로그인 - SQL DB로 보내는 로직 실행 \n",req, resp);
}catch (Exception e) {
e.printStackTrace();
System.out.println("MemberFrontController : 로그인 - SQL DB로 보내는 로직 실패(위에 에러)");
}
}
forward.setRedirect(true);
forward.setPath("./login.jsp");
를 통해 Path로 주소를 이동시킨다.
생성된 action 은 execute로 요청과 응답값을 받는데, execute() 메소드는 리턴값으로 스트링형을 반환하며, 반환되는 이 리턴값이 무엇이냐에 따라서 그 다음 로직이 어떻게 진행될지가 결정된다고 생각하면 된다.
3) Action 페이지에서 회원가입 처리
회원가입 시 클라이언트는 HTTP 프로토콜을 이용해서 서버로 데이터를 요청한다. 이 때 jsp 페이지에서 폼에 작성한 내용을 파라미터로 하여금 요청(request)에 끼워서 보낼 수 있다. 그래서 jsp 폼에 작성한 내용들이 서버로 전달되는 것.
이 전달된 값을 서버에서 읽기 위해 request.getParameter() 메소드를 사용한다.
반대로 서버에 있는 내용을 클라이언트인 jsp 페이지로 전달해줘야한다고 하면, 다른곳으로 정보를 넘겨주기 위해 request 객체의 attribute 속성을 사용한다. 서블릿 간에 주고받는 데이터들을 setAttribute 한 값을 getAttribute 등으로 꺼내 쓰는 것.
▼자세한 소스 보기
/* 인터페이스를 함께 추가했다. 회원가입 처리를 위한 클래스이다.*/
public class MemberJoinAction implements Action { //Action을 implements 해줌
@Override
public ActionForward execute(HttpServletRequest req, HttpServletResponse resp) throws Exception {
MemberBean member = new MemberBean(); //vo에 선언한 변수들 import한거.
//회원 정보를 저장하고 DB로 전달.
//회원 정보를 가지고 있을 공간이 된다.
boolean joinResult = false;
//입력 목록 적어주기(vo에서 받아옴.)
member.setMEMBER_ID(req.getParameter("MEMBER_ID"));
member.setMEMBER_PW(req.getParameter("MEMBER_PW"));
member.setMEMBER_NAME(req.getParameter("MEMBER_NAME"));
member.setMEMBER_BIR_YY(req.getParameter("MEMBER_BIR_YY"));
member.setMEMBER_BIR_MM(req.getParameter("MEMBER_BIR_MM"));
member.setMEMBER_BIR_DD(req.getParameter("MEMBER_BIR_DD"));
member.setMEMBER_GENDER(req.getParameter("MEMBER_GENDER"));
member.setMEMBER_MAIL(req.getParameter("MEMBER_MAIL"));
member.setMEMBER_PHONE(req.getParameter("MEMBER_PHONE"));
MemberJoinService memberJoinService = new MemberJoinService();
//서비스 svc에서 만들어준다. 아래에 회원가입 처리를 해주자.
//=========================== 회원가입 처리 ================================
joinResult = memberJoinService.joinMember(member);
ActionForward forward = null;
if (joinResult == false) { //회원가입 실패 시
resp.setContentType("text/html;charset=UTF-8");
PrintWriter out = resp.getWriter(); //위에 있음
out.println("<script>");
out.println("alert('회원등록실패')");
out.println("history.back()");
out.println("</script>");
} else { //회원가입 성공 시
forward = new ActionForward();
forward.setRedirect(true);
forward.setPath("./memberLogin.me");
}
return forward;
}
}
4) Action 페이지에서 로그인 처리
로그인은 더 간단하게 id 비밀번호만 폼에서 받아서 SQL 검색대로 보내준다.
▼자세한 소스 보기
/* 인터페이스를 함께 추가했다. 로그인 처리를 위한 클래스이다.*/
public class MemberLoginAction implements Action { //Action을 implements 해줌
@Override
public ActionForward execute(HttpServletRequest req, HttpServletResponse resp) throws Exception {
//session을 써서 서버 생성함.
HttpSession session = req.getSession();
//session - 페이지 로그인 유지 방법
//**참고: cookie - 페이지 로그인 유지 방법, 클라이언트 생성. **
// session상의 회원 등급은 존재하지 않음. ???
// DB관리자의 회원 등급은 서로 다름. ???
MemberBean member = new MemberBean();
member.setMEMBER_ID(req.getParameter("MEMBER_ID"));
member.setMEMBER_PW(req.getParameter("MEMBER_PW"));
// vo의 MemberBean에서 아이디와 패스워드를 넘겨받아와라.
MemberLoginService memberLoginService = new MemberLoginService();
//서비스 svc에서 만들어준다. 아래에 로그인 처리를 해주자.
//=========================== 로그인 처리 ================================
boolean loginResult = memberLoginService.login(member);
//사용자가 등록되어 있다면 true, 아니라면 false리턴.
ActionForward forward = null; //어디로 갈지?
if (loginResult) { //로그인 성공 시
forward = new ActionForward();
session.setAttribute("id", member.getMEMBER_ID());
//session에 사용자의 ID 저장.
forward.setRedirect(true);
//Redirect유무
forward.setPath("./memberListAction.me");
//패스 지정
// ++ 추가로 로그인 했다고 alert를 띄워줄꺼임.
// resp.setContentType("text/html;charset=UTF-8");
// PrintWriter out = resp.getWriter();
// out.println("<script>");
// out.println("alert('로그인성공');");
// out.println("</script>");
} else { //로그인 실패 시
resp.setContentType("text/html;charset=UTF-8");
PrintWriter out = resp.getWriter();
out.println("<script>");
out.println("alert('로그인실패');");
out.println("location.href='./memberLogin.me';");
//로그인 창으로 다시 이동하라.
out.println("</script>");
}//else는 로그인 실패 부분을 담당함.
//자바 코드에서 문자열을 만들어내서 결과적으로 자바 스크립트로 생성하여 console.log에 표시함.
return forward; //null이 아니라 forward로 가야지.
//ActionForward를 null로 리턴하면 이미 response에 응답을 끝냈다는 의미가 된다. 다른 어떤 페이지로도 이동하지 않는다.
}
}
이 다음은 회원가입 기능 중 id 체크나 validation 관련된 기능을 구현해보겠습니다.
▼ 진행중인 gitHub 링크를 남깁니다.
'Project > 블로그 프로젝트' 카테고리의 다른 글
네이버 블로그 만들기 프로젝트(5) - 내 블로그 페이지 만들기 / 나만 내 게시글 삭제하기 (1) | 2022.10.28 |
---|---|
네이버 블로그 만들기 프로젝트(4) - 게시글 상세페이지 연결 (1) | 2022.10.27 |
네이버 블로그 만들기 프로젝트(3) - 게시글 목록 페이지 만들기 (1) | 2022.10.20 |
네이버 블로그 만들기 프로젝트(2) - 비밀번호 유효성검사 기능 구현 (2) | 2022.10.19 |
네이버 블로그 만들기 프로젝트(1) - 아이디 중복검사 기능 구현 (1) | 2022.10.17 |
댓글