멀티 게시판 DB 설계 - meolti gesipan DB seolgye

multiBoard

프로젝트 설명

멀티 게시판 사이트

: 6개월의 훈련 수료 기간 동안 배운 것을 잊지 않기 위해 복습 겸 웹 개발의 기본이라고 생각하는 게시판을 만들었습니다.
: 게시판을 필요할 때마다 생성하기에는 불편할 거라 판단하여 설정 게시판을 만들었습니다.

제작과정

프로젝트명: multiBoard
제작 기간: 2020년 04월~ 06월
프로젝트 인원: 1명

개발환경

언어 JAVA 8 {JSP, MVC2}
프레임워크 Spring Tool Suite 4.3.23
Data Base Oracle
View&Event HTML / CSS / JavaScript / JQuery / bootstrap

작동 설정

보안이 취약한 사이트니, 중요한 정보를 넣지 마세요!

manage3/src/main/resources/ 경로의 db.properties / email.properties을 수정해주세요.
db.properties의 oracle 계정 id와 pwd, email.properties의 address, pwd를 수정해주시고 gmail이면 host도 gmail 주석으로 변경해주세요.

manage3/src/main/webapp/WEB-INF/config_sql.sql의 테이블과 시퀀스를 생성해주세요.

관리자 아이디는 id: admin / pwd: 1234 입니다.

DB 설계

테이블 마다 기본 키 설정(Sequence: tableName_keyField_Seq)

멀티 게시판 DB 설계 - meolti gesipan DB seolgye

기능 설명

회원 가입, 수정, 탈퇴

  form으로 받은 데이터를 컨트롤러 전송 후 db에 추가하였습니다.
  id와 email에는 keyup 이벤트로 AJAX를 호출해 id와 email 입력 시 db에 중복이 있는지 판단하도록 구현하였습니다.
  주소에는 daum 우편 서비스, 생일에는 datepicker를 사용하였습니다.
  hibernate-validator를 사용하여 필수 항목을 입력하지 않으면 오류 메세지를 옆에 띄우도록 하였습니다.
  
  회원 가입과 수정은 같은 URL을 사용합니다. idx의 유무에 따라 동작합니다.
  탈퇴는 매일 탈퇴일이 7일이 넘은 유저를 삭제하는 Scheduler에 의해 7일 후 삭제됩니다.
  재접속 시 login후 이동하는 URL("/")로 이동해 탈퇴일이 !null이면 null을 집어넣음으로써 탈퇴 취소가 됩니다.

Security

  Security의 URL 접근 제한과 인증만을 사용했습니다. 
  
  게시판 설정(level)에 따라 security의 tags를 이용해 권한이 없으면 해당 내용이 보이지 않도록 설정했습니다.
  (글/댓글쓰기 버튼, 게시판 보기)
  
  "/admin/.."으로 시작하는 URL은 ROLE_ADMIN을 가진 유저만 접근 가능합니다.
  (id가 admin이면 Role_ADMIN을 부여했습니다. 게시판의 관리자 이름은 아무 기능도 없습니다.)

회원 정보 찾기

  아이디/비밀번호 찾기 버튼을 누르면 AJAX로 db의 정보와 비교합니다.
  정보가 일치한다면 id 찾기의 경우 alert로 바로 알리고 
  pwd는 임시 비밀번호를 만들어 db의 비밀번호를 업데이트하고 메일을 보냅니다.

게시판 관리

멀티 게시판 DB 설계 - meolti gesipan DB seolgye

  게시판을 추가할 때 카테고리 지정을 할 수 있고 목록 읽기, 글 읽기, 쓰기, 댓글 작성, 비밀 글 사용 여부, 
  새 글/추천 글에 제한을 줄 수 있습니다.
  회원관리 페이지에서 모든 회원의 정보를 볼 수 있습니다.

게시판

  게시글 객체인 boardVO에 commentList와 fileList를 주입해서 사용했습니다.

메인 페이지

멀티 게시판 DB 설계 - meolti gesipan DB seolgye

admin으로 접속 후 관리 페이지에서 main에서 보기를 check/nonCheck 할 수 있습니다.
check된 게시판은 index에서 최대 5개의 글이 보여지게 됩니다.

배너 추가예정입니다!

contact

name :윤혜진
h.p. :010-2340-6919
email :

(멀티)게시판

게시판(스프링) 2021. 5. 4. 20:06

[ 아이디어 정리 ]

 - 게시판이 여러 개이기만 하면 멀티 게시판이라고 하는 것 같다. 게시글 컬럼 하나를 게시판 id로 두어 구현하는 방법과 테이블 하나 자체를 게시판 이름으로 하고 게시글을 담아두는 방법이 있다. 전자의 방법을 이용해서 구현하는 중이다.

 - 모든 게시판이 같은 구조를 가지기 때문에 게시판 별로 따로 테이블을 가질 필요가 없다. 하나의 테이블로 모두 관리한다.

[ DB Table and Sequence ]

create table forums (
    forum_number number(20) primary key,
    forum_name varchar2(30) unique not null,
    forum_description varchar2(255) not null,
    forum_slug varchar2(30) unique not null,
    date_forum_registered date default sysdate not null
);

create sequence seq_forum;

[ ForumVO ]

@Data
public class ForumVO {

	private Long number;
	private String name;
	private String description;
	private String slug;
	private Date dateRegistered;
}

[ ForumMapper interface and Impl ]

public interface ForumMapper {

	public int insertForum(ForumVO forum);
	
	public ForumVO readForumByForumNumber(Long forumNumber);
	
	public int modifyForum(ForumVO forum);
	
	public int deleteForumByForumNumber(Long forumNumber);
}
<?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="com.bibidi.mapper.ForumMapper">
	
	<resultMap type="com.bibidi.domain.ForumVO" id="forumMap">
		<id property="number" column="forum_number"/>
		<result property="number" column="forum_number" />
		<result property="name" column="forum_name" />
		<result property="description" column="forum_description" />
		<result property="slug" column="forum_slug" />
		<result property="dateRegistered" column="date_forum_registered" />
	</resultMap>
	
	<insert id="insertForum">
		INSERT INTO forums
			(forum_number, forum_name, forum_description, forum_slug)
		VALUES
			(seq_forum.nextval, #{name}, #{description}, #{slug})
	</insert>
	
	<select id="readForumByForumNumber" resultMap="forumMap">
		SELECT * FROM forums WHERE forum_number = #{forumNumber}
	</select>
	
	<update id="updateForum">
		UPDATE forums
		SET forum_name = #{name}, forum_description = #{description}, forum_slug = #{slug}
		WHERE forum_number = #{number}
	</update>
	
	<delete id="deleteForumByForumNumber">
		DELETE FROM forums
		WHERE forum_number = #{forumNumber}
	</delete>
	
</mapper>

[ ForumMapper Tests ]

@RunWith(SpringRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/spring/root-context.xml")
@Log4j
public class ForumMapperTests {

	@Setter(onMethod_ = @Autowired)
	private ForumMapper forumMapper;
	
	@Test
	public void testInsertForum() {
		ForumVO forum = new ForumVO();
		forum.setName("tName");
		forum.setDescription("tDescription");
		forum.setSlug("tSlug");
		
		log.info("THE NUMBER OF INSERTED FORUMS : "
				+ forumMapper.insertForum(forum));
	}
	
	@Test
	public void testReadForumByForumNumber() {
		
		log.info(forumMapper.readForumByForumNumber(1L));
	}
	
	@Test
	public void testUpdateForum() {
		
		ForumVO forum = new ForumVO();
		forum.setNumber(2L);
		forum.setName("updated name");
		forum.setDescription("updated description");
		forum.setSlug("updated slug");
		
		log.info("THE NUMBER OF UPDATED FORUMS : "
				+ forumMapper.updateForum(forum));
	}
	
	@Test
	public void testDeleteForumByForumNumber() {
		
		log.info("THE NUMBER OF DELETED FORUMS : "
				+ forumMapper.deleteForumByForumNumber(0L));
	}
}

[ ForumService Interface and Impl ]

public interface ForumService {

	public int registerForum(ForumVO forum);
	
	public ForumVO getForumByForumNumber(Long forumNumber);
	
	public int modifyForum(ForumVO forum);
	
	public int deleteForumByForumNumber(Long forumNumber);
}
@Service
@Log4j
public class ForumServiceImpl implements ForumService {
	
	@Setter(onMethod_ = @Autowired)
	ForumMapper forumMapper;
	
	@Override
	public int registerForum(ForumVO forum) {

		log.info("register forum.........");
		return forumMapper.insertForum(forum);
	}

	@Override
	public ForumVO getForumByForumNumber(Long forumNumber) {

		log.info("get forum by forum number.........");
		return forumMapper.readForumByForumNumber(forumNumber);
	}

	@Override
	public int modifyForum(ForumVO forum) {

		log.info("modify forum...............");
		return forumMapper.updateForum(forum);
	}

	@Override
	public int deleteForumByForumNumber(Long forumNumber) {

		log.info("delete forum by forum number.............");
		return forumMapper.deleteForumByForumNumber(forumNumber);
	}
	
}

[ ForumService Tests ]

@RunWith(SpringRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/spring/root-context.xml")
@Log4j
public class ForumServiceTests {

	@Setter(onMethod_ = @Autowired)
	private ForumService forumService;
	
	@Test
	public void testExist() {
		
		log.info(forumService);
		assertNotNull(forumService);
	}
	
	@Test
	public void testRegisterForum() {
		ForumVO forum = new ForumVO();
		forum.setName("tsName");
		forum.setDescription("tsDescription");
		forum.setSlug("tsSlug");
		
		log.info("THE NUMBER OF REGISTERED FORUMS : "
				+ forumService.registerForum(forum));
	}
	
	@Test
	public void testGetForumByForumNumber() {
		
		log.info(forumService.getForumByForumNumber(1L));
	}
	
	@Test
	public void testModifyForum() {
		ForumVO forum = new ForumVO();
		forum.setNumber(3L);
		forum.setName("updated name");
		forum.setDescription("updated description");
		forum.setSlug("updated slug");
		
		log.info("THE NUMBER OF MODIFIED FORUMS : " 
				+ forumService.modifyForum(forum));
	}
	
	@Test
	public void testDeleteForumByForumNumber() {
		
		log.info("THE NUMBER OF DELETED FORUMS : "
				+ forumService.deleteForumByForumNumber(0L));
	}
}

[URI 설계 ]

 - 다 괜찮은데, forum 등록할 때 사용할 form을 가져올 때 문제가 된다. 애초에 서버로부터 데이터만 받으면 저걸로 충분한데, 프런트까지 생각하니 collection + method로 처리할 수가 없다. REST API 원칙 자체가 URI가 정보 자원을 의미하도록 하는 것이니까 Registration Form을 자원으로 보고 /admin/forums/registration이라 표현하는 것이 어떨가 생각하고 만들었다.

 - 처음엔 forum form을 가져올 때 다른 사이트들 참고해서 write나 register 같은 동사를 쓸까 했지만 REST API 관련 글들을 보면 동사를 쓸 이유가 없다. 그런데 이렇게 생각하면 로그인, 회원가입은 login, signup으로 썼던 거 같은데, 이거 말고 적절한 명사가 있나 찾아봐야겠다.

 - 찾아보니 login, sign-up이 명사다.

GET /admin/forums/registration : forum registration form을 가져온다.

GET /admin/forums 

POST /admin/forums 
PUT /admin/forums
DELETE /admin/forums

[ AdminController ]

@Controller
@RequestMapping("/admin/*")
@Log4j
public class AdminController {
	
	@Setter(onMethod_ = @Autowired)
	private ForumService forumService;

	@RequestMapping(value="", method=RequestMethod.GET)
	public String getAdminHome() {
		
		log.info("get admin home");
		return "/admin/home";
	}
	
	@RequestMapping(value="/forums/registration", method=RequestMethod.GET)
	public String getForumForm() {
		
		log.info("get forum form");
		return "/admin/forumForm";
	}
	
	
	// 미구현
	@RequestMapping(value="/forums", method=RequestMethod.GET)
	public String getForums() {
		
		// 요구하는 사이즈 체크. 없으면 기본값, 있으면서 제한을 넘지 않으면 그 사이즈 만큼 반환하도
		return "";
	}
	
	@RequestMapping(value="/forums", method=RequestMethod.POST)
	public String postForumForm(String forumName, String forumDescription, String forumSlug) {
		
		log.info("post forum form");
		
		ForumVO forum = new ForumVO();
		forum.setName(forumName);
		forum.setDescription(forumDescription);
		forum.setSlug(forumSlug);
		
		forumService.registerForum(forum);
		
		return "redirect:/admin/forums/registration";
	}
}

 - 나중에 form으로부터 데이터가 제대로 왔는지 검증하는 부분을 추가해야 될 것 같다. 프런트에서 실수로 데이터를 잘못 보냈을 수도 있으니까. 그런데 그 폼 검증을 어디서 해야 되는지가 문제. 컨트롤러 내에서 해야되나 아니면 서비스 내에서 해야되나 그게 헷갈리는 중. 

[ 관리자 페이지 작성 ]

멀티 게시판 DB 설계 - meolti gesipan DB seolgye
관리자 페이지 - 게시판 생성

 - 아직 초기 화면이라 디자인이 구리다. 부트스트랩 그리드 시스템 덕분에 레이아웃 까는 법은 조금 익혔는데, 어떻게 해야 보기 좋은지 근거가 없으니 손 대기가 그렇다. 거기에 디자인 쪽 지식도 꽤 필요할 텐데, 아직 그거까지 익힐 시간은 나지 않는다.

[ 게시판 작성 테스트 ]

멀티 게시판 DB 설계 - meolti gesipan DB seolgye

 - 적당히 작성해서 테스트 해봤는데, 잘 작동한다. 한글도 안 깨지고 잘 들어가고 날짜도 자동으로 잘 입력된다.

//

 - ROLE_USER가 왜 필요한가? 그냥 인증만 되면 글을 쓸 수 있도록 하면 되지 않을까 생각했는데, 사용자 상태에 따라 글을 못 쓰게 하려면 유저 정보를 활용해야한다. 기본 사용자 권한 박탈 기능 등을 생각.