07_Framework
-- 게시판 종류
CREATE TABLE "BOARD_TYPE"(
"BOARD_CODE" NUMBER CONSTRAINT "PK_BOARD_TYPE" PRIMARY KEY,
"BOARD_NAME" VARCHAR2(30) NOT NULL
);
COMMENT ON COLUMN "BOARD_TYPE"."BOARD_CODE"
IS '게시판 코드(SEQ_BOARD_CODE)';
COMMENT ON COLUMN "BOARD_TYPE"."BOARD_NAME"
IS '게시판 이름';
CREATE SEQUENCE SEQ_BOARD_CODE NOCACHE;
-- 게시판 종류 추가
INSERT INTO "BOARD_TYPE" VALUES(SEQ_BOARD_CODE.NEXTVAL, '공지사항');
INSERT INTO "BOARD_TYPE" VALUES(SEQ_BOARD_CODE.NEXTVAL, '자유 게시판');
INSERT INTO "BOARD_TYPE" VALUES(SEQ_BOARD_CODE.NEXTVAL, '테스트 게시판');
INSERT INTO "BOARD_TYPE" VALUES(SEQ_BOARD_CODE.NEXTVAL, '질문 게시판');
INSERT INTO "BOARD_TYPE" VALUES(SEQ_BOARD_CODE.NEXTVAL, '점심 게시판');
COMMIT;
SELECT * FROM "BOARD_TYPE";
-- [게시판 DB 설정]
CREATE TABLE "BOARD" (
"BOARD_NO" NUMBER NOT NULL,
"BOARD_TITLE" VARCHAR2(150) NOT NULL,
"BOARD_CONTENT" VARCHAR2(4000) NOT NULL,
"B_CREATE_DATE" DATE DEFAULT SYSDATE NOT NULL,
"B_UPDATE_DATE" DATE NULL,
"READ_COUNT" NUMBER DEFAULT 0 NOT NULL,
"BOARD_DEL_FL" CHAR(1) DEFAULT 'N' NOT NULL,
"MEMBER_NO" NUMBER NOT NULL,
"BOARD_CODE" NUMBER NOT NULL
);
COMMENT ON COLUMN "BOARD"."BOARD_NO" IS '게시글 번호(SEQ_BOARD_NO)';
COMMENT ON COLUMN "BOARD"."BOARD_TITLE" IS '게시글 제목';
COMMENT ON COLUMN "BOARD"."BOARD_CONTENT" IS '게시글 내용';
COMMENT ON COLUMN "BOARD"."B_CREATE_DATE" IS '게시글 작성일';
COMMENT ON COLUMN "BOARD"."B_UPDATE_DATE" IS '마지막 수정일(수정 시 UPDATE)';
COMMENT ON COLUMN "BOARD"."READ_COUNT" IS '조회수';
COMMENT ON COLUMN "BOARD"."BOARD_DEL_FL" IS '삭제 여부(N : 삭제X , Y : 삭제O)';
COMMENT ON COLUMN "BOARD"."MEMBER_NO" IS '작성자 회원 번호';
COMMENT ON COLUMN "BOARD"."BOARD_CODE" IS '게시판 코드 번호';
----------------------------------------------------------------------
CREATE TABLE "BOARD_IMG" (
"IMG_NO" NUMBER NOT NULL,
"IMG_PATH" VARCHAR2(300) NOT NULL,
"IMG_RENAME" VARCHAR2(30) NOT NULL,
"IMG_ORIGINAL" VARCHAR2(300) NOT NULL,
"IMG_ORDER" NUMBER NOT NULL,
"BOARD_NO" NUMBER NOT NULL
);
COMMENT ON COLUMN "BOARD_IMG"."IMG_NO" IS '이미지 번호(SEQ_IMG_NO)';
COMMENT ON COLUMN "BOARD_IMG"."IMG_PATH" IS '이미지 저장 폴더 경로';
COMMENT ON COLUMN "BOARD_IMG"."IMG_RENAME" IS '변경된 이미지 파일 이름';
COMMENT ON COLUMN "BOARD_IMG"."IMG_ORIGINAL" IS '원본 이미지 파일 이름';
COMMENT ON COLUMN "BOARD_IMG"."IMG_ORDER" IS '이미지 파일 순서 번호';
COMMENT ON COLUMN "BOARD_IMG"."BOARD_NO" IS '이미지가 첨부된 게시글 번호';
----------------------------------------------------------------------
CREATE TABLE "BOARD_LIKE" (
"BOARD_NO" NUMBER NOT NULL,
"MEMBER_NO" NUMBER NOT NULL
);
COMMENT ON COLUMN "BOARD_LIKE"."BOARD_NO" IS '게시글 번호';
COMMENT ON COLUMN "BOARD_LIKE"."MEMBER_NO" IS '좋아요 누른 회원 번호';
----------------------------------------------------------------------
CREATE TABLE "COMMENT" (
"COMMENT_NO" NUMBER NOT NULL,
"COMMENT_CONTENT" VARCHAR2(4000) NOT NULL,
"C_CREATE_DATE" DATE DEFAULT SYSDATE NOT NULL,
"COMMENT_DEL_FL" CHAR(1) DEFAULT 'N' NOT NULL,
"BOARD_NO" NUMBER NOT NULL,
"MEMBER_NO" NUMBER NOT NULL,
"PARENT_NO" NUMBER
);
COMMENT ON COLUMN "COMMENT"."COMMENT_NO" IS '댓글 번호(SEQ_COMMENT_NO)';
COMMENT ON COLUMN "COMMENT"."COMMENT_CONTENT" IS '댓글 내용';
COMMENT ON COLUMN "COMMENT"."C_CREATE_DATE" IS '댓글 작성일';
COMMENT ON COLUMN "COMMENT"."COMMENT_DEL_FL" IS '댓글 삭제 여부(N : 삭제X, Y : 삭제O)';
COMMENT ON COLUMN "COMMENT"."BOARD_NO" IS '댓글이 작성된 게시글 번호';
COMMENT ON COLUMN "COMMENT"."MEMBER_NO" IS '댓글 작성자 회원 번호';
COMMENT ON COLUMN "COMMENT"."PARENT_NO" IS '부모 댓글 번호';
----------------------------------------------------------------------
ALTER TABLE "BOARD" ADD CONSTRAINT "PK_BOARD" PRIMARY KEY (
"BOARD_NO"
);
ALTER TABLE "BOARD_IMG" ADD CONSTRAINT "PK_BOARD_IMG" PRIMARY KEY (
"IMG_NO"
);
ALTER TABLE "BOARD_LIKE" ADD CONSTRAINT "PK_BOARD_LIKE" PRIMARY KEY (
"BOARD_NO",
"MEMBER_NO"
);
ALTER TABLE "COMMENT" ADD CONSTRAINT "PK_COMMENT" PRIMARY KEY (
"COMMENT_NO"
);
ALTER TABLE "BOARD" ADD CONSTRAINT "FK_MEMBER_TO_BOARD_1" FOREIGN KEY (
"MEMBER_NO"
)
REFERENCES "MEMBER" (
"MEMBER_NO"
);
ALTER TABLE "BOARD" ADD CONSTRAINT "FK_BOARD_TYPE_TO_BOARD_1" FOREIGN KEY (
"BOARD_CODE"
)
REFERENCES "BOARD_TYPE" (
"BOARD_CODE"
);
ALTER TABLE "BOARD_IMG" ADD CONSTRAINT "FK_BOARD_TO_BOARD_IMG_1" FOREIGN KEY (
"BOARD_NO"
)
REFERENCES "BOARD" (
"BOARD_NO"
);
ALTER TABLE "BOARD_LIKE" ADD CONSTRAINT "FK_BOARD_TO_BOARD_LIKE_1" FOREIGN KEY (
"BOARD_NO"
)
REFERENCES "BOARD" (
"BOARD_NO"
);
ALTER TABLE "BOARD_LIKE" ADD CONSTRAINT "FK_MEMBER_TO_BOARD_LIKE_1" FOREIGN KEY (
"MEMBER_NO"
)
REFERENCES "MEMBER" (
"MEMBER_NO"
);
ALTER TABLE "COMMENT" ADD CONSTRAINT "FK_BOARD_TO_COMMENT_1" FOREIGN KEY (
"BOARD_NO"
)
REFERENCES "BOARD" (
"BOARD_NO"
);
ALTER TABLE "COMMENT" ADD CONSTRAINT "FK_MEMBER_TO_COMMENT_1" FOREIGN KEY (
"MEMBER_NO"
)
REFERENCES "MEMBER" (
"MEMBER_NO"
);
ALTER TABLE "COMMENT" ADD CONSTRAINT "FK_COMMENT_TO_COMMENT_1" FOREIGN KEY (
"PARENT_NO"
)
REFERENCES "COMMENT" (
"COMMENT_NO"
);
-- 시퀀스 생성
CREATE SEQUENCE SEQ_BOARD_NO NOCACHE; -- 게시글 번호
CREATE SEQUENCE SEQ_IMG_NO NOCACHE; -- 게시글 이미지 번호
CREATE SEQUENCE SEQ_COMMENT_NO NOCACHE; -- 댓글 번호
-- BOARD 테이블 샘플 데이터 삽입(PL/SQL)
BEGIN
FOR I IN 1..2000 LOOP
INSERT INTO BOARD
VALUES( SEQ_BOARD_NO.NEXTVAL,
SEQ_BOARD_NO.CURRVAL || '번째 게시글',
SEQ_BOARD_NO.CURRVAL || '번째 게시글 내용 입니다.',
DEFAULT, DEFAULT, DEFAULT, DEFAULT, 1,
CEIL(DBMS_RANDOM.VALUE(0,4))
);
END LOOP;
END;
COMMIT;
SELECT * FROM BOARD;
-- DROP TABLE BOARD CASCADE CONSTRAINT;
-- DROP SEQUENCE SEQ_BOARD_NO;
------------------------------------------------------
-- COMMENT 테이블 샘플 데이터 삽입(PL/SQL)
BEGIN
FOR I IN 1..1000 LOOP
INSERT INTO "COMMENT"
VALUES(SEQ_COMMENT_NO.NEXTVAL,
SEQ_COMMENT_NO.CURRVAL || '번째 댓글',
DEFAULT, DEFAULT,
CEIL(DBMS_RANDOM.VALUE(0,2000)),
1, NULL);
END LOOP;
END;
COMMIT;
SELECT * FROM "COMMENT";
-- 게시글 샘플 이미지
INSERT INTO BOARD_IMG
VALUES(SEQ_IMG_NO.NEXTVAL, '/resources/images/board/',
'20230508115013_00001.jpg', 'cat1.jpg', 0, 1996);
INSERT INTO BOARD_IMG
VALUES(SEQ_IMG_NO.NEXTVAL, '/resources/images/board/',
'20230508115013_00002.jpg', 'cat2.jpg', 0, 1995);
INSERT INTO BOARD_IMG
VALUES(SEQ_IMG_NO.NEXTVAL, '/resources/images/board/',
'20230508115013_00003.jpg', 'cat3.jpg', 0, 1994);
INSERT INTO BOARD_IMG
VALUES(SEQ_IMG_NO.NEXTVAL, '/resources/images/board/',
'20230508115013_00004.jpg', 'cat4.jpg', 0, 1993);
INSERT INTO BOARD_IMG
VALUES(SEQ_IMG_NO.NEXTVAL, '/resources/images/board/',
'20230508115013_00005.jpg', 'cat5.jpg', 0, 1989);
COMMIT;
SELECT * FROM BOARD_IMG;
Alt + Shift + S 후, >
package edu.kh.project.common.interceptor;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import edu.kh.project.board.model.service.BoardService;
// Interceptor : 요청/응답을 가로채는 객체
// Client <-> (Filter) <-> Dispatcher Servlet <-> (Interceptor) <-> Controller
// Filter : 전반적으로 쓰일 보안/인증/인가 관련 작업, 문자열 인코딩, 이미지 데이터 압축..
// Interceptor : 요청에 대한 '데이터 가공' -> Controller로 넘겨주기 위한 정보/데이터 가공,
// 세부적으로 쓰일 보안/인증
// Handle = '처리' 의미
public class BoardTypeInterceptor implements HandlerInterceptor {
/* preHandle : 전처리 Dispathcer Servlet -> Contoller 사이
* postHandle : 후처리 Controller -> Dispathcer Servlet 사이
* afterCompletion : 뷰 완성 후 View Resolver -> Dispathcer Servlet 사이
* */
@Autowired
private BoardService service;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// application scope 내장 객체 얻어오기
ServletContext application = request.getServletContext();
// application scope에 BOARD_TYPE이 조회되어 세팅되지 않았다면
// -> 서버 시작 후 누구도 요청을 한적이 없을 경우
if(application.getAttribute("boardTypeList") == null) {
// 조회 서비스 호출
System.out.println("BOARD_TYPE 조회 서비스 호출");
List<Map<String, Object>> boardTypeList
= service.selectBoardTypeList();
System.out.println("boardTypeList: " + boardTypeList);
// application scope 세팅
application.setAttribute("boardTypeList", boardTypeList);
}
return HandlerInterceptor.super.preHandle(request, response, handler);
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
// TODO Auto-generated method stub
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
// TODO Auto-generated method stub
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
package edu.kh.project.board.model.service;
import java.util.List;
import java.util.Map;
public interface BoardService {
List<Map<String, Object>> selectBoardTypeList();
}
package edu.kh.project.board.model.service;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import edu.kh.project.board.model.dao.BoardDAO;
@Service
public class BoardServiceImpl implements BoardService{
@Autowired
private BoardDAO dao;
@Override
public List<Map<String, Object>> selectBoardTypeList() {
return dao.selectBoardTypeList();
}
}
package edu.kh.project.board.model.dao;
import java.util.List;
import java.util.Map;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
@Repository
public class BoardDAO {
@Autowired
private SqlSessionTemplate sqlSession;
/** 게시판 종류 목록 조회
* @return
*/
public List<Map<String, Object>> selectBoardTypeList() {
return sqlSession.selectList("boardMapper.selectBoardTypeList");
}
}
<?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="boardMapper">
<!--
resultType이 "map"인 경우
K : 컬럼명(BOARD_CODE, BOARD_NAME)
V : 컬럼 값( 1 , 공지 사항 )
[{BOARD_NAME= 공지사항, BOARD_CODE=1}, {BOARD_NAME= 자유게시판, BOARD_CODE=2}, ...]
-->
<!-- 게시판 종류 목록 조회 -->
<select id="selectBoardTypeList" resultType="map">
SELECT * FROM "BOARD_TYPE" ORDER BY 1
</select>
</mapper>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd" >
<configuration>
<!-- SqlSessionTemplate 관련 설정 -->
<settings>
<!-- insert, update 사용 값중 null 이 있을 경우
SQL 구문에 null 포함되어 있다는 오류 발생
이 설정 후, 오류 발생 X, NULL 값을 컬럼에 대입
단, NOT NULL 제약조건이 없는 컬럼에만 가능
-->
<setting name="jdbcTypeForNull" value="NULL"/>
</settings>
<!-- 별칭 작성 부분 -->
<!-- VO/DTO 클래스의 패키지명+클래스명 작성하는게 불편하기 때문에 짧은 별칭을 부여 -->
<typeAliases>
<typeAlias type="edu.kh.project.member.model.dto.Member" alias="Member"/>
</typeAliases>
<!-- mapper파일(SQL 작성되는파일) 위치 등록 부분 -->
<mappers>
<mapper resource="/mappers/member-mapper.xml"/>
<mapper resource="/mappers/ajax-mapper.xml"/>
<mapper resource="/mappers/email-mapper.xml"/>
<mapper resource="/mappers/myPage-mapper.xml"/>
<mapper resource="/mappers/board-mapper.xml"/>
<!-- 추후 board-mapper를 사용하고 싶다면 추가해야 함!
<mapper resource="/mappers/board-mapper.xml"/>
-->
</mappers>
</configuration>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<link rel="stylesheet" href="/resources/css/main-style.css">
<header>
<!-- 클릭 시 메인페이지로 이동하는 로고 -->
<section>
<a href="/">
<img src="/resources/images/logo.jpg" id="homeLogo">
</a>
</section>
<!-- 검색창 부분 -->
<section>
<section class="search-area">
<!-- form 내부 input 태그 값을 서버 또는 페이지로 전달 -->
<form action="/search" method="GET" name="search-form">
<!-- fieldset : form 내부에서 input을 종류별로 묶는 용도로 자주 사용 -->
<fieldset>
<!-- search : 텍스트 타입과 기능적으로는 똑같으나,
브라우저에 의해 다르게 표현될 수 있음. -->
<!-- autocomplete : HTML 기본 자동완성 사용 X -->
<input type="search" id="query" name="query"
autocomplete="off" placeholder="회원을 닉네임으로 검색해주세요."
>
<button id="searchBtn" class="fa-solid fa-magnifying-glass"></button>
</fieldset>
</form>
</section>
</section>
<section></section>
</header>
<!-- 보통은 header안에 작성하나 사이드에 nav가 있는 경우도 있기 때문에 따로 작성해본다! -->
<nav>
<ul>
<%--
<li><a href="#">공지사항</a></li>
<li><a href="#">자유게시판</a></li>
<li><a href="#">질문게시판</a></li>
<li><a href="#">FAQ</a></li>
<li><a href="#">1:1문의</a></li>
--%>
<%-- interceptor를 이용해서 조회된 boardTypeList를
application scope에서 얻어와 화면에 출력
--%>
<%--
[
{BOARD_NAME=공지사항, BOARD_CODE=1},
{BOARD_NAME=자유 게시판, BOARD_CODE=2},
{BOARD_NAME=테스트 게시판, BOARD_CODE=3},
{BOARD_NAME=질문 게시판, BOARD_CODE=4},
{BOARD_NAME=점심 게시판, BOARD_CODE=5}
]
--%>
<c:forEach var="boardType" items="${boardTypeList}">
<li>
<a href="/board/${boardType.BOARD_CODE}">${boardType.BOARD_NAME}</a>
</li>
</c:forEach>
</ul>
</nav>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<link rel="stylesheet" href="/resources/css/main-style.css">
<header>
<!-- 클릭 시 메인페이지로 이동하는 로고 -->
<section>
<a href="/">
<img src="/resources/images/logo.jpg" id="homeLogo">
</a>
</section>
<!-- 검색창 부분 -->
<section>
<section class="search-area">
<!-- form 내부 input 태그 값을 서버 또는 페이지로 전달 -->
<form action="/search" method="GET" name="search-form">
<!-- fieldset : form 내부에서 input을 종류별로 묶는 용도로 자주 사용 -->
<fieldset>
<!-- search : 텍스트 타입과 기능적으로는 똑같으나,
브라우저에 의해 다르게 표현될 수 있음. -->
<!-- autocomplete : HTML 기본 자동완성 사용 X -->
<input type="search" id="query" name="query"
autocomplete="off" placeholder="회원을 닉네임으로 검색해주세요."
>
<button id="searchBtn" class="fa-solid fa-magnifying-glass"></button>
</fieldset>
</form>
</section>
</section>
<section></section>
</header>
<!-- 보통은 header안에 작성하나 사이드에 nav가 있는 경우도 있기 때문에 따로 작성해본다! -->
<nav>
<ul>
<%--
<li><a href="#">공지사항</a></li>
<li><a href="#">자유게시판</a></li>
<li><a href="#">질문게시판</a></li>
<li><a href="#">FAQ</a></li>
<li><a href="#">1:1문의</a></li>
--%>
<%-- interceptor를 이용해서 조회된 boardTypeList를
application scope에서 얻어와 화면에 출력
--%>
<%--
[
{BOARD_NAME=공지사항, BOARD_CODE=1},
{BOARD_NAME=자유 게시판, BOARD_CODE=2},
{BOARD_NAME=테스트 게시판, BOARD_CODE=3},
{BOARD_NAME=질문 게시판, BOARD_CODE=4},
{BOARD_NAME=점심 게시판, BOARD_CODE=5}
]
--%>
<c:forEach var="boardType" items="${boardTypeList}">
<li>
<a href="/board/${boardType.BOARD_CODE}">${boardType.BOARD_NAME}</a>
</li>
</c:forEach>
</ul>
</nav>
package edu.kh.project.board.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttributes;
@SessionAttributes({"loginMember"})
@RequestMapping("/board")
@Controller
public class BoardController {
/* 목록 조회 : /board/1?cp=1 (cp: current page(현재페이지))
* 상세 조회 : /board/1/1500?cp=1
*
* ** 컨트롤러 따로 생성 **
* 삽입 : /board2/1/insert
* 수정 : /board2/1/1500/update
* 삭제 : /board2/1/1500/delete
* */
@GetMapping("/{boardCode}") // @PathVariable : 주소를 값 자체로 쓸 수 있는 것
public String selectBoardList( @PathVariable("boardCode") int boardCode ) {
// boardCode 확인
System.out.println("boardCode : " + boardCode);
return "board/boardList";
}
}
공지사항 클릭 시, 콘솔창
해당 페이지로 이동
-> 주소에 영어 입력 시, 더이상 페이지가 매핑되지 않음( 숫자만 입력 가능 )
package edu.kh.project.board.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttributes;
@SessionAttributes({"loginMember"})
@RequestMapping("/board")
@Controller
public class BoardController {
/* 목록 조회 : /board/1?cp=1 (cp: current page(현재페이지))
* 상세 조회 : /board/1/1500?cp=1
*
* ** 컨트롤러 따로 생성 **
* 삽입 : /board2/1/insert
* 수정 : /board2/1/1500/update
* 삭제 : /board2/1/1500/delete
* */
/*
* ******** @PathVariable 사용 시 문제점과 해결법 ********
*
* 문제점 : 요청 주소와 @PathVariable로 가져다 쓸 주소와 레벨이 같다면
* 구분하지 않고 모두 매핑되는 문제가 발생
*
* 해결방법 : @PathVariable 지정 시 정규 표현식 사용
* {키:정규표현식}
* */
@GetMapping("/{boardCode:[0-9]+}") // boardCode는 1자리 이상 숫자
// @PathVariable : 주소를 값 자체로 쓸 수 있는 것
public String selectBoardList( @PathVariable("boardCode") int boardCode ) {
// boardCode 확인
System.out.println("boardCode : " + boardCode);
return "board/boardList";
}
}