해당 프로젝트는 2023/01/25 ~ 2023/03/12 내에 진행되는
아카데미 내 수강생들끼리 팀을 나누어 진행한 모의 프로젝트입니다.
팀원은 5명이었으며, 프로젝트 리더를 맡았습니다.
이전 글 목록
1) 주어진 RFP를 바탕으로 주제 선정 - Spring Project(OTT 서비스)
2) ERD 설계 - Spring Project(OTT 서비스)
3) 회원 가입 기능 구현 - Spring Project (OTT 서비스)
4) 로그인, 로그아웃 기능 구현 - Spring Project (OTT 서비스)
5) 상세 페이지 및 회원 정보 수정 - Spring Project (OTT 서비스)
6) CRUD를 한번에 → 게시판 만들기(QNA게시판) - Spring Project(Mybatis) (OTT 서비스)
7) 게시판 페이징 처리 - Spring Project (OTT 서비스)
8) 카카오 지도 API 사용하기 - Spring Project (OTT 서비스)
9) (네아로) 네이버 로그인 API 활용 사이트 로그인 및 회원가입 - SPRING Project(OTT 서비스)
10) 카카오 로그인 API 사용하기(내 사이트 로그인 및 회원가입) - Spring Project(OTT Service)
11) 아임포트(포트원) API를 이용한 결제처리 - Spring Project(OTT Service)
디자인은 무료 템플릿을 이용했다.
관리자 페이지에서 현재 구현된 기능들은 아래와 같다.
1. 메인 페이지에서 - 일 매출 방문자 수, 주간 매출, 방문자 수(차트), 전체 매출,방문자 수, 카테고리, 장르 별 조회수(차트)
2. 회원 관리 페이지 - 페이징 처리 된 게시판 및 회원 삭제 기능
3. 데이터베이스 관리 - VIDEO Table CRUD (혹시 모를 카테고리와 장르 CRUD)
4. 유저와의 1:1 실시간 채팅(문의)
회원 관리 페이지는 따로 다루지 않겠다, QNA 게시판 CRUD 및 페이징 포스팅에서 모두 다뤘기 때문이다.
데이터베이스는 VIDEO 생성 시 테이블 설계에 따라 여러 테이블로 조건에 맞게 입력이 된다. 해당 부분만 다음 글에서 포스팅 하겠다.
실시간 채팅은 따로 포스팅을 하도록 하겠다.
방문자 수
HttpSessionListener와 같은 것들을 사용할까 하다가, 시간이 타이트해 질 것 같아서 우선 단순하게 메인 페이지 접근 시 만들어 놓은 Visit 테이블에 방문자 수를 +1 해 주는 것으로 처리하였으며, 차후 성능개선할 시간이 되면 세션을 활용하여 구현 후 포스팅 하겠다.
MainController
@RequestMapping("/")
public String Main() {
//일 방문자수 ++ - 02.19
userService.addVisit();
return ("common/start");
}
단순히 메인 페이지 접근 시 addVisit메서드를 통해 방문자 수를 업데이트 해 주었다.
Service / Impl / Dao
//Service
void addVisit();
//ServiceImpl
@Override
public void addVisit() {
userDao.addVisit();
}
//Dao
public void addVisit() {
//일별로 방문 1번째일 때 insert
if(sqlSessionTemplate.selectOne("user.todayVisitCheck") == null){
sqlSessionTemplate.insert("user.todayFirstVisit");
}
//그다음은 update
else sqlSessionTemplate.update("user.todayAddVisit");
}
user_SQL.xml
<select id="todayVisitCheck" resultType="String">
select VISIT_DATE
from VISIT
where date_format(VISIT_DATE, '%Y%m%d') = date_format(now(), '%Y%m%d')
</select>
<insert id="todayFirstVisit">
insert into VISIT (VISIT_DATE, NUMBER)
values (date_format(now(), '%Y%m%d'), 1)
</insert>
<update id="todayAddVisit">
update VISIT
set NUMBER=NUMBER+1
where date_format(VISIT_DATE, '%Y%m%d') = date_format(now(), '%Y%m%d')
</update>
일별로 첫 방문시, insert되고 그 다음부터는 해당 레코드가 update될 것이다.
매출
지난 포스팅에서 PayMentController 마지막 코드이다.
userService.paidUpdate(months);//관리자페이지 일 결제 조회를 위해 추가 - 02.19
결제가 진행되면 PAID 테이블의 레코드가 업데이트 될 것이다. 빠르게 Dao를 살펴보자.
Dao
public void paidUpdate(int months) {
//1. 오늘 첫 결제일 경우
if(sqlSessionTemplate.selectOne("user.todayPaidCheck") == null) {
sqlSessionTemplate.insert("user.todayFirstPaid", months);
}
//2. 아닐경우 +
else sqlSessionTemplate.update("user.todayAddPaid", months);
}
첫 결제일 경우, 해당 레코드 자체를 insert 해주어야 하기 때문에 insert 해주었고, 아닐경우 해당 레코드를 update 해주었다.
SQL.xml
<select id="todayPaidCheck" resultType="String">
select PAID_DATE
from PAID
where date_format(PAID_DATE, '%Y%m%d') = date_format(now(), '%Y%m%d');
</select>
<insert id="todayFirstPaid">
insert into PAID (PAID_DATE, AMOUNT)
values (date_format(now(), '%Y%m%d'), #{months})
</insert>
<update id="todayAddPaid">
update PAID
set AMOUNT=AMOUNT+#{months}
where date_format(PAID_DATE, '%Y%m%d') = date_format(now(), '%Y%m%d')
</update>
View하기
AdminController
//메서드화 후 일괄처리 - 02.21
public String access(HttpServletRequest request) {
return request.getHeader("REFERER");
}
@RequestMapping("/admin")
@ResponseBody
public ModelAndView adminMain(ModelAndView mv, HttpServletRequest request, HttpSession session) {
//URL접근 차단
if(access(request) == null) {
mv.addObject("error", "잘못된 접근입니다");
mv.setViewName("redirect:/");
return mv;
}
//추가로 확실한 검증 - 02.18
if(session.getAttribute("user_id") != "admin" || session.getAttribute("nickname") != "admin") {
mv.addObject("error", "접근 권한이 없습니다");
mv.setViewName("redirect:/");
}
//1. 카테고리 순위
mv.addObject("category", algorithmService.categoryRate());
//2. 장르 순위
mv.addObject("genre", algorithmService.genreRate());
/* 주간 방문자, 매출 차트 추가 - 02.21 장재호 */
mv.addObject("visit", algorithmService.weeklyVisitor());
mv.addObject("sales", algorithmService.weeklySales());
/**********************************/
//3. 매출
int total = adminService.getTotalSales() * 15000;
int daily = adminService.getDailySales() * 15000;
mv.addObject("totalSales", total);
mv.addObject("dailySales", daily);
//4. 방문자 수
int totalVisit = adminService.getTotalVisit();
int todayVisit = adminService.getTodayVisit();
mv.addObject("totalVisit", totalVisit);
mv.addObject("todayVisit", todayVisit);
/*********************끝*****************************/
mv.setViewName("admin/index");
return mv;
}
admin 페이지 요청 시 PAID테이블과 VISIT테이블을 받아와 출력 해주면 그만이다.
카테고리, 장르 순위는 아래에서 다루겠다.
사용자의 영상 조회수(카테고리 / 장르 별)
아래의 코드는 영상 시청을 위한 접근을 할 때의 Controller의 코드 일부이다.
// video_detail 02.07 배철우
// DTO 생성 후 DTO 활용하여 코드재생성 + 배우정보 가져오기 - 02.14 장민실
// 알고리즘 구현을 위해 detail페이지 접근 시 PK값 저장 - 02.15 장재호
@RequestMapping("detail")
public ModelAndView detail(@RequestParam int video_id, ModelAndView mv, HttpSession session, RentalDTO dto) { //세션추가 - 02.15 장재호
/*--------------------------------------- db에 알고리즘 구현을 위한 값들 저장 - 02.15 장재호 ---------------------------------------*/
String id = (String) session.getAttribute("user_id");
Map<String, Object> map = new HashMap<>();
map.put("id", id);
map.put("video_id", video_id);
// if = 추가, else = 업데이트(클릭 수 업)
if(algo.check(map) == null) algo.insert(map);
else algo.update(map);
/*---------------------------내보관함 기능 구현 - rental_id detail.jsp로 꽂기 위한 값 저장 02.18 김범수-------------------------------*/
dto.setId(id); // Id dto에 저장
String rental_id = rentalService.getid(dto);// rental id를 가져오는 것
mv.addObject("rental_id",rental_id);
/*--------------------------------------------------------------------------------------------------------------------*/
List<VideoDto> actor = videoService.actor(video_id);
// 원댓글목록 가져오기 start - 02.21 장민실
List<CommentDto> list = commentService.replyList(video_id);
mv.addObject("replyList", list);
// 원댓글목록 가져오기 end
mv.addObject("dto", videoService.detail(video_id));
mv.addObject("detail", actor);
mv.setViewName("video/detail");
return mv;
}
String id = (String) session.getAttribute("user_id");
Map<String, Object> map = new HashMap<>();
map.put("id", id);
map.put("video_id", video_id);
//if = 추가, else = 업데이트(클릭 수 업)
if(algo.check(map) == null) algo.insert(map);
else algo.update(map);
내가 작성한 코드는 위와 같다.
세션에서 로그인 한 사용자의 정보를 가져와서, 유저의 정보와, 해당 영상의 정보를 map에 담아 현재는 단순하게 카테고리, 장르별 순위를 매기기 위함이지만, 해당 테이블 구현 당시 유저의 시청기록이나, 해당 유저가 좋아하는 카테고리, 장르도 출력해 줄 수 있다고 생각했다. 소위 유튜브의 알고리즘 같은 기능들을 구현하기 위한 테이블로 사용했다.
Service / Impl / Dao
//Service
public Integer check(Map<String, Object> map);
public void insert(Map<String, Object> map);
public void update(Map<String, Object> map);
//ServiceImpl
@Override
public Integer check(Map<String, Object> map) {
return algorithmDao.check(map);
}
@Override
public void insert(Map<String, Object> map) {
algorithmDao.insert(map);
}
@Override
public void update(Map<String, Object> map) {
algorithmDao.update(map);
}
//Dao
//DB에 VIDEO_ID가 이미 있는지 - 02.15
public Integer check(Map<String, Object> map) {
return session.selectOne("algo.check", map);
}
//처음 클릭 시 insert - 02.15
public void insert(Map<String, Object> map) {
session.insert("algo.insert", map);
}
//중복 클릭 시 클릭 수 추가 - 02.15
public void update(Map<String, Object> map) {
session.insert("algo.update", map);
}
SQL.xml
<select id="check" resultType="Integer">
select VIDEO_ID
from ALGORITHM
where USER_ID=(select U.USER_ID from USER U where ID="${id}") and VIDEO_ID = ${video_id}
</select>
<insert id="insert">
insert into ALGORITHM (USER_ID, VIDEO_ID)
values ((select USER_ID from USER where ID="${id}"), ${video_id})
</insert>
<update id="update">
update ALGORITHM
set HIT=HIT+1, LAST_UPDATE_DATE=now()
where USER_ID=(select USER_ID from USER where ID="${id}")
and VIDEO_ID=${video_id}
</update>
위의 AdminController에 카테고리, 장르 조회 부분이다.
Service / Impl / Dao
//Service
public List<AlgorithmDto> categoryRate();
public List<AlgorithmDto> genreRate();
//ServiceImpl
@Override
public List<AlgorithmDto> categoryRate() {
return algorithmDao.categoryRate();
}
@Override
public List<AlgorithmDto> genreRate() {
return algorithmDao.genreRate();
}
//Dao
public List<AlgorithmDto> categoryRate() {
return session.selectList("algo.category");
}
public List<AlgorithmDto> genreRate() {
return session.selectList("algo.genre");
}
SQL.xml
<select id="category" resultType="com.test.test1.algorithm.dto.AlgorithmDto">
select C.CATEGORY_NAME, SUM(A.HIT) as HIT
from CATEGORY C, VIDEO_CATEGORY VC, ALGORITHM A
where C.CATEGORY_ID = VC.CATEGORY_ID
and VC.VIDEO_ID = A.VIDEO_ID
group by C.CATEGORY_ID;
</select>
<select id="genre" resultType="com.test.test1.algorithm.dto.AlgorithmDto">
select G.`GENRE_NAME(KOR)` as GENRE_NAME, SUM(A.HIT) as HIT
from GENRE G, VIDEO_GENRE VG, ALGORITHM A
where G.GENRE_ID = VG.GENRE_ID
and VG.VIDEO_ID = A.VIDEO_ID
group by G.GENRE_ID;
</select>
AlgorithmDto에 있는 필드에 카테고리와, 장르 테이블에 있는 값들을 담아서 해당 데이터들이 View되도록 처리했다.
차트 만들기
차트를 만들기 위해 chart.js를 활용했다.
아래의 카테고리, 장르 차트는 List<AlgorithmDto>의 형태로 View에 전송 된 데이터를 input hidden의 value에 담아 JavaScript 변수로 선언한 뒤, 알맞게 가공하여 사용했다.
추가로 일일 방문자나 결제 수가 0건일 경우도 존재하기 때문에, MySQL의 이벤트 구현을 통해 00시00분00초가 되면 자동으로 해당 레코드가 추가되게 구현하였다. 또한 일~월의 일주일이 아닌 최근 일주일의 현황을 파악하기 위함이기 때문에, 최근 날짜순으로 7건만 조회하였다.
자세한 코드는 아래를 참조바란다.
/* 관리자 페이지의 카테고리 차트 - 02.16 */
const cat = document.querySelector('#categoryH').value.split(", "); //카테고리 별 조회수 값
const hitArr = new Array();
const categoryArr = new Array();
/* 배열에 카테고리 정보, 조회수 정보만 뽑아옴 */
cat.forEach(function(data){
if(data.indexOf('hit') != -1){
hitArr.push(data.split(" : ")[1]);
}
if(data.indexOf('category_name') != -1){
categoryArr.push(data.split(" : ")[1]);
}
})
/* 차트에 칼라 부여*/
const rgb = new Array();
for(var i=0; i<categoryArr.length; i++){
let r = Math.floor(Math.random() * 256);
let g = Math.floor(Math.random() * 256);
let b = Math.floor(Math.random() * 256);
rgb.push("rgb(" + r + "," + g + "," + b + ")");
}
var ctx = document.getElementById('categoryChart');
var chart = new Chart(ctx, {
// 챠트 종류를 선택
type: 'bar',
// 챠트를 그릴 데이타
data: {
labels: categoryArr,
datasets: [{
label: '카테고리 별 조회수',
data: hitArr,
backgroundColor: rgb,
borderColor: rgb,
borderWidth: 1
}]
},
options: {
scales: {
yAxes: [{
ticks: {
beginAtZero: true
}
}]
}
}
});
/* 관리자 페이지의 장르 차트 - 02.16 장재호 */
const gen = document.querySelector('#genreH').value.split(", "); //카테고리 별 조회수 값
const hitArr2 = new Array();
const genreArr = new Array();
/* 배열에 장르 정보, 조회수 정보만 뽑아옴 */
gen.forEach(function(data){
if(data.indexOf('hit') != -1){
hitArr2.push(data.split(" : ")[1]);
}
if(data.indexOf('genre_name') != -1){
genreArr.push(data.split(" : ")[1]);
}
})
/* 차트에 칼라 부여*/
const rgb2 = new Array();
for(var i=0; i<genreArr.length; i++){
let r = Math.floor(Math.random() * 256);
let g = Math.floor(Math.random() * 256);
let b = Math.floor(Math.random() * 256);
rgb2.push("rgb(" + r + "," + g + "," + b + ")");
}
var ctx2 = document.getElementById('genreChart');
var chart2 = new Chart(ctx2, {
// 챠트 종류를 선택
type: 'bar',
// 챠트를 그릴 데이타
data: {
labels: genreArr,
datasets: [{
label: '카테고리 별 조회수',
data: hitArr2,
backgroundColor: rgb2,
borderColor: rgb2,
borderWidth: 1
}]
},
options: {
scales: {
yAxes: [{
ticks: {
beginAtZero: true
}
}]
}
}
});
/* 관리자 페이지의 방문자 차트 - 02.16 장재호 */
const visit = document.querySelector('#visitH').value.split(", "); //카테고리 별 조회수 값
const numArr = new Array();
const visitArr = new Array();
const visitArr2 = new Array();
/* 배열에 장르 정보, 조회수 정보만 뽑아옴 */
visit.forEach(function(data){
if(data.indexOf('number') != -1){
numArr.push(data.split(" : ")[1]);
}
if(data.indexOf('visit_date') != -1){
visitArr.push(data.split(" : ")[1]);
}
})
visitArr.forEach(function(e){
visitArr2.push(e.split(" ")[2]);
})
/* 차트에 칼라 부여*/
const rgb4 = new Array();
for(var i=0; i<visitArr.length; i++){
let r = Math.floor(Math.random() * 256);
let g = Math.floor(Math.random() * 256);
let b = Math.floor(Math.random() * 256);
rgb4.push("rgb(" + r + "," + g + "," + b + ")");
}
var ctx4 = document.getElementById('visitChart');
var chart4 = new Chart(ctx4, {
// 챠트 종류를 선택
type: 'line',
// 챠트를 그릴 데이타
data: {
labels: visitArr2,
datasets: [{
label: '주간 방문자',
backgroundColor: 'transparent',
borderColor: 'red',
data: numArr
}]
},
options: {
scales: {
yAxes: [{
ticks: {
beginAtZero: true
}
}]
}
}
});
/* 매출 차트 */
const sales = document.querySelector('#salesH').value.split(", "); //카테고리 별 조회수 값
const amountArr = new Array();
const salesArr = new Array();
const salesArr2 = new Array();
/* 배열에 장르 정보, 조회수 정보만 뽑아옴 */
sales.forEach(function(data){
if(data.indexOf('amount') != -1){
amountArr.push(data.split(" : ")[1]);
}
if(data.indexOf('paid_date') != -1){
salesArr.push(data.split(" : ")[1]);
}
})
salesArr.forEach(function(e){
salesArr2.push(e.split(" ")[2]);
})
/* 차트에 칼라 부여*/
const rgb3 = new Array();
for(var i=0; i<salesArr.length; i++){
let r = Math.floor(Math.random() * 256);
let g = Math.floor(Math.random() * 256);
let b = Math.floor(Math.random() * 256);
rgb2.push("rgb(" + r + "," + g + "," + b + ")");
}
var ctx3 = document.getElementById('salesChart').getContext('2d');
var chart3 = new Chart(ctx3, {
// 챠트 종류를 선택
type: 'line',
// 챠트를 그릴 데이타
data: {
labels: salesArr2,
datasets: [{
label: '주간 매출',
backgroundColor: 'transparent',
borderColor: 'red',
data: amountArr
}]
},
options: {
scales: {
yAxes: [{
ticks: {
beginAtZero: true
}
}]
}
}
});
2023.04 ~ 백엔드 개발자의 기록
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!