강아지를 키우는 사람들은 의료 정보와 애견 동반 가능한 장소에 대한 정보 접근이 어려운 경우가 많습니다. 이러한 문제를 해결하기 위해 강아지 병원 정보, 애견 동반 가능한 휴가 장소 추천, 그리고 내 강아지를 자랑할 수 있는 커뮤니티 기능을 제공하는 웹사이트를 기획하게 되었습니다.
의의
지금은 586만 마리의 강아지와 함께 하는 시대!
국민은행 경영연구소가 발표한 2023 한국 반려동물 보고서에 따르면,
현재 대한민국에는 582만 마리 이상의 강아지가 살고 있습니다.
가족처럼 소중한 반려동물과 건강하고 오래 살고자 하는 반려가구가 늘어나면서,
다양한 건강 관리 정보에 대한 필요성도 증가하고 있습니다.
이 웹사이트는 이러한 반려가구가 보다 성숙한 반려견 양육 문화를 형성하는 데 기여하고자 합니다.
package com.dino.root;
import java.text.DateFormat;
import java.util.Date;
import java.util.Locale;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
/**
* Handles requests for the application home page.
*/@ControllerpublicclassRootController{
privatestaticfinal Logger logger = LoggerFactory.getLogger(RootController.class);
/**
* Simply selects the home view to render by returning its name.
*/@RequestMapping(value = "/", method = RequestMethod.GET)public String home(Locale locale, Model model){
logger.info("Welcome home! The client locale is {}.", locale);
Date date = new Date();
DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale);
String formattedDate = dateFormat.format(date);
model.addAttribute("serverTime", formattedDate );
return"home";
}
@RequestMapping(value = "signup", method = RequestMethod.GET)public String signup(){
System.out.println("signup");
return"signup";
}
@RequestMapping(value = "signup", method = RequestMethod.POST)public String signup2(HttpServletRequest req){
System.out.println("signup 회원가입 정보를 저장");
System.out.println(req.getParameter("aaa"));
return"home";
}
}
homecontroller 에서 수정된 RootController.java
package com.dino.root;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
@ControllerpublicclassRootController{
privatestaticfinal Logger logger = LoggerFactory.getLogger(RootController.class);
// 메인 페이지 이동@RequestMapping(value = "/", method = RequestMethod.GET)public String home(){
logger.info("Welcome to the home page!");
return"home";
}
// 로그인 페이지 이동@RequestMapping(value = "/login", method = RequestMethod.GET)public String login(){
return"login";
}
// 로그인 요청 처리@RequestMapping(value = "/login", method = RequestMethod.POST)public String loginPost(@RequestParam("userid") String userid,
@RequestParam("password") String password,
HttpServletRequest req){
// 아이디와 비밀번호 확인 (예제로 확인용)if ("user".equals(userid) && "password".equals(password)) {
return"redirect:/game";
} else {
return"login";
}
}
// 회원가입 페이지 이동@RequestMapping(value = "/signup", method = RequestMethod.GET)public String signup(){
return"signup";
}
// 회원가입 요청 처리@RequestMapping(value = "/signup", method = RequestMethod.POST)public String signupPost(HttpServletRequest req){
// 회원가입 처리 로직 (여기서는 예제로 간단하게 출력만 함)
String userid = req.getParameter("userid");
String password = req.getParameter("password");
logger.info("회원가입한 회원 이름: {} 비밀번호: {}", userid, password);
// 회원가입 후 바로 로그인 처리if ("user".equals(userid) && "password".equals(password)) {
return"redirect:/game";
} else {
return"login";
}
}
// 랭킹 페이지 이동@RequestMapping(value = "/ranking", method = RequestMethod.GET)public String ranking(){
return"ranking";
}
// 게임 페이지 이동 (로그인 성공 후)@RequestMapping(value = "/game", method = RequestMethod.GET)public String game(){
return"game";
}
}
private static final Logger logger = LoggerFactory.getLogger(RootController.class);
logger.info → syso 는 단순 문자열출력으로 확인만 했다면 log 가 더 자세하게 오류 원인을 알려준다고 해서 사용해보았다. (프로젝트 만들면 기본으로 homecontroller에 샘플코드로 찍히는데 log 관련된 것 같길래 구글링해보니 이렇게 사용하면 된다고 해서 사용해본 것)
required 속성→ html5 표준, 필드의 폼을 비워두고 제출하면 브라우저가 자동으로 해달 필드를 강조해 메세지를 표시한다 (유효성 검사와는 조금 다르고 빈 값이 없게 해주는 역할)
servlet-context
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
<!-- Enables the Spring MVC @Controller programming model -->
<annotation-driven />
<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
<resources mapping="/resources/**" location="/resources/" />
<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<beans:property name="prefix" value="/WEB-INF/views/" />
<beans:property name="suffix" value=".jsp" />
</beans:bean>
<context:component-scan base-package="com.dino.root" />
</beans:beans>
두번째 수정)
전체 코드는 0803(토) 자정 쯤 조 단톡방에 보내드렸습니다.
파일 구조
🛠 수정 사항
.do 로 매핑 → localhost:9999/login.do 이런 식으로 들어가야 한다.
home.jsp 파일의 이름을 명시성을 위해 game.jsp 파일로 이름 수정
회원가입 기능 수행을 위해 MemberDAO, MemberVO, MemberService 클래스를 만들었다. (RootController에서 처리한 기능 분산)
MemberDAO에 DB 연결 코드 작성
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee https://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<!-- The definition of the Root Spring Container shared by all Servlets and Filters -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/root-context.xml</param-value>
</context-param>
<!-- Creates the Spring Container shared by all Servlets and Filters -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- Processesapplicationrequests -->
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
미니프로젝트였지만 스프링을 다 이해하고 돌입하게 아니어서 파일구조부터 컨트롤러 구성까지 시행착오가 많았고, 시행착오가 많았던 만큼 스프링에 대해 공부도 많이 됐던 프로젝트다. 사실 여전히 깃은 잘 모르겠지만.. 모르는 만큼 본격적인 프로젝트에 들어가기 전 공부를 해야겠다고 다짐한다! ㅠ
또, 앞으로는 얼마 남지 않은 기간이지만 날마다 그날 작성한 코드들을 압축해 관리하는 습관을 들여야할것 같다.
그렇게 해놓지 않으니 스프링 JDBC 방식이 뭐였는지 mybatis가 뭐였는지 구별이 안된다. 둘을 제대로 알고 넘어간게 아니라 하날 공부하는 와중에 다른 걸 또 배우니 머리 속에서 정리가 안되는 거다. 그래도 시험을 보며 잘 정리할 기회를 얻어서 다행.. 블로그에도 두 방식을 나눠 정리를 잘 해둬야겠다.
이런저런 팀원들의 사정으로 내가 거의 다 해냈던 프로젝트인데 그래서인지 애정이 정말 크다.
내가 만든 프로젝트라고 자신있게 말할 수 있을 것 같다. 아주 작은 규모라도 끝을 냈다는 것에 의의를 두고 자신감을 가져야겠다. 회고는 이쯤에서 끝내고 3~4일 가량이었지만 고생했던 프로젝트를 끝냄 기념으로 기록해보려 한다.
프로젝트 주제 : 공룡게임을 할 수 있는 사이트
간단한 조작을 통해 누구나 공룡게임을 즐길 수 있고, 회원가입을 하면 본인의 랭킹을 확인할 수 있습니다.
기간 ) 0801 ~ 0804
팀원 ) 강유나, ____, ____
역할 )
강유나 : 컨트롤러 구현, 회원가입 DAO, VO, Service 기능 구현, jsp파일 css, html, javascript 공룡 게임 구현, 파일 정의서, 화면 정의서 작성
____ : jsp파일 css, html 디자인, 공룡 게임 수정, 팀원에게 깃 사용법 전파
____ : 랭킹 페이지 구현
☄️ 구현할 페이지 : 게임을 할 수 있는 메인화면, 로그인, 회원가입, 랭킹 확인
패키지/ 자바 파일 정의서
shoppingmall 폴더에서 작업 ) -> 초기에는 shoppingmall 이름으로 프로젝트를 모든 조가 일괄적으로 만들었기 때문에 이름이 이렇다.
src > main > wabapp 에는 화면을 띄울 jsp 파일이 들어갑니다.
index.jsp → 메인 화면으로 바로 공룡게임을 할 수 있는 화면이 보여집니다.
login.jsp → 로그인 화면입니다.
memberjoin.jsp → 회원가입 화면입니다.
ranking.jsp → 회원들의 게임 점수를 기반으로 랭킹이 매겨집니다.
자바 파일 및 JSP 파일 정의서 수정
각 기능은 동일하지만 보다 정리된 파일 구조입니다.
-> 컨트롤러를 이해하고 훨씬 더 간결해진 파일 구조다.
sql deverloper 에서 작업 )
kit3 계정을 만들어 회원 정보를 담을 테이블과 회원의 점수를 담을 테이블 생성
reate tablemember(
id varchar2(15) primary key,
pass varchar2(20) notnull,
name varchar2(200) notnull
);
createtable board(
id varchar2(200) primary key,
score number notnull,
content varchar2(200),
num number
);
altertable board
addconstraint board_mem_fk foreign key (id)
referencesmember (id);
create sequence seq_board_num2
increment by1startwith1
minvalue 1;
select id,score,content
from (select*from board orderby score desc)
where rownum <=20 ;
화면정의서
메인 화면 ( index.jsp )
로그인 화면 ( login.jsp )
회원가입 화면 ( memberJoin.jsp )
랭킹 화면 ( ranking.jsp )
☄️ 실제 구현 화면
함께 공유해요
-> 프로젝트를 하며 새롭게 알게됐던 부분, 함께 나누고 싶은 부분을 '함께 공유해요'라는 이름으로 발표해보았다.
스프링에서 정적자원 삽입하기
스프링에서 css, js, image 등의 자원은 파일이 존재하는 url 그 자체로 사용된다. 따라서 url 요청을 해야 하는데 이는 MVC의 DispatcherServlet에서 판단해 Controller에서 RequestMapping Annotation을 검색하게 된다. 이 때 404에러가 발생하게 된다.
따라서 CSS, JavaScript, Image 같은 정적 자원들에 대해 URL을 따로 주어야 하는데 이를 지원해주는 녀석이 mvc:resources이다. 이 태그를 DispatcherServlet에 등록해줘야 사용할 수 있다.
로그인을 실행하게 될 경우, 바로 게임 화면으로 이동하면서 게임이 시작되는 부분이 있었다. 이것은 유저의 순발력을 요구하는 플레이가 될 수 있지만, 실제로 게임을 즐기는 유저 입장에서는 굉장히 불쾌할 수 있는 요소중 하나이다. 적어도 게임에 입장하게 되면, 바로 조작할 수 있게 진행해주었어야했었다고 생각했다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
<!-- 패키지를 스캔해준다. 다 해주는 것은 아니고 표시되어진 클래스만 스캔해준다 -> 이것이 어노테이션 -->
<!-- <context:component-scan base-package="polymorphism"></context:component-scan> -->
<context:component-scan base-package="com.springbook.biz"></context:component-scan>
<!-- database.properties 의 파일을 읽어올 수 있게 해준다 -->
<context:property-placeholder location="classpath:config/database.properties"/>
<bean id="dataSource"class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<!--JdbcTemplate 클래스를 통해 DB에 접근 (crud) -->
<bean id="jdbcTemplate"class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
</beans>
BoardDAOSpring
package com.springbook.biz.board.impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import com.springbook.biz.board.BoardVO;
// 비즈니스 계층에서 사용할 수 있도록 빈에 등록@RepositorypublicclassBoardDAOSpring{
@Autowiredprivate JdbcTemplate jdbcTemplate;
// 상수형태로 만들어 두었다.privatefinal String BOARD_INSERT = " insert into board(seq, title, writer, content) values "
+ " ((select nvl(max(seq),0) + 1 from board), ?, ?, ?)";
privatefinal String BOARD_UPDATE = "update board set title=?, content=? where seq=?";
privatefinal String BOARD_DELETE = "delete board where seq=?";
privatefinal String BOARD_GET = "select * from board where seq=?";
privatefinal String BOARD_LIST = "select * from board order by seq desc";
// ---------- update() 구문 사용 ----------// 글 등록publicvoidinsertBoard(BoardVO vo){
System.out.println("===> springJDBC로 insertBoard() 기능 처리");
// insert를 springJDBC에서는 update가 처리, try catch문 사용 안해도 됨.// 기존 jdbc 에서 사용한 '?' 대신 순서대로 객체를 가지고 와 적어준다.
jdbcTemplate.update(BOARD_INSERT, vo.getTitle(), vo.getWriter(), vo.getContent());
}
// 글 수정publicvoidupdateBoard(BoardVO vo){
System.out.println("===> springJDBC로 updateBoard() 기능 처리");
jdbcTemplate.update(BOARD_UPDATE, vo.getTitle(), vo.getContent(), vo.getSeq());
}
// 글 삭제publicvoiddeleteBoard(BoardVO vo){
System.out.println("===> springJDBC로 deleteBoard() 기능 처리");
jdbcTemplate.update(BOARD_DELETE, vo.getSeq());
}
// ---------- queryForObject(), query() 구문 사용 ----------// 글 상세 조회public BoardVO getBoard(BoardVO vo){
System.out.println("===> springJDBC로 getBoard() 상세 보기 처리");
// 배열로 값을 받기 때문에 배열에 넣어준다
Object[] args= {vo.getSeq()};
// return jdbcTemplate.queryForObject(sql, args, rowMapper);return jdbcTemplate.queryForObject(BOARD_GET, args, new BoardRowMapper());
}
// 글 목록 조회public List<BoardVO> getBoardList(BoardVO vo){
System.out.println("==> springJDBC로 getBoardList() 기능 처리");
// '?' 가 없다. 글 목록 전체 조회이기 때문에 모든 글들을 다 불러오면 된다.return jdbcTemplate.query(BOARD_LIST, new BoardRowMapper());
}
}
BoardRowMapper
package com.springbook.biz.board.impl;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.springframework.jdbc.core.RowMapper;
import com.springbook.biz.board.BoardVO;
publicclassBoardRowMapperimplementsRowMapper<BoardVO> {
// BoardDAO 의 resultSet의 역할을 수행한다.@Overridepublic BoardVO mapRow(ResultSet rs, int rowNum)throws SQLException {
BoardVO board = new BoardVO();
board.setSeq(rs.getInt("SEQ"));
board.setTitle(rs.getString("TITLE"));
board.setWriter(rs.getString("WRITER"));
board.setContent(rs.getString("CONTENT"));
board.setRegDate(rs.getDate("REGDATE"));
board.setCnt(rs.getInt("CNT"));
return board;
}
}
<aop:config>
<!-- 실행 범위 -->
<aop:pointcut expression="execution(* com.springbook.biz..*Impl.*(..))" id="allPointcut"/>
<!-- 레퍼런스 log -->
<aop:aspect ref="log">
<!-- 대상, 적용할 기능 -->
<aop:before pointcut-ref="allPointcut" method="printLog"/>
</aop:aspect>
</aop:config>
"execution(* com.springbook.biz..*Impl.*(..))"
= 패키지 하위의 범위를 표현하는 정규식
= 패키지 안에 Impl 이 들어간 파일이 있다면 공통관심사 코드 실행