사용자에게 여러 가지 선택지를 제시하고, 그 중에서 선호하는 것을 선택하여 대결을 진행하는 웹 기반 이상형 월드컵을 만들어 보도록 하자.
이상형 월드컵을 사용자가 진행할 때 하나의 페이지에서 화면이 바뀌면서 진행되어야 하므로 데이터를 유지하기 위해 세션 객체를 사용해 보려고 한다.
Spring 프로젝트에서 사용자가 선택한 이상형 월드컵 결과를 저장하고 관리하는 시스템을 구현하면서 코드를 검토하고 각 계층에서 사용한 기술을 점검해 보려고 한다.
💡Spring Framework
Spring Framework는 자바 기반의 엔터프라이즈 응용 프로그램을 개발하기 위한 전체적인 인프라를 제공하는 경량 프레임워크로, 의존성 주입(Dependency Injection)과 관점 지향 프로그래밍(Aspect-Oriented Programming)을 지원하여 코드의 모듈성과 유지보수성을 높일 수 있다.
Layered Architecture
Layered Architecture는 소프트웨어 시스템을 여러 계층으로 분할하여 각 계층이 특정 역할을 수행하도록 설계하는 아키텍처 스타일을 의미한다.
Presentation Layer, Business Layer(Service Layer), Persistence Layer(Repository Layer), 그리고 Database Layer로 구분된다. 어떤 애플리케이션을 만드냐에 따라서 이 계층 방식은 변경될 수 있다.
Layerd Architecture 동작 과정
2024.02.08 - [Programming/Spring] - [Spring] Spring MVC Framework: MVC 패턴의 구조, 동작 과정
Spring MVC Framework의 구조는 위 글을 참고한다.
표현 계층(Presentation Layer)
사용자와 시스템 간의 상호 작용을 처리한다. 기존의 MVC 패턴이 여기에 속한다.
주로 사용자 인터페이스(UI)를 표현하고 사용자의 입력을 받아들이며, 이를 서비스 계층으로 전달한다.
웹 애플리케이션의 경우 HTML, CSS, JavaScript와 같은 웹 기술이 위 계층에 포함된다.
비즈니스 계층(Service Layer 또는 Business Layer)
비즈니스 로직을 처리하고 애플리케이션의 핵심 기능을 제공한다.
사용자의 요청에 따라 데이터를 처리하고 필요한 계산이나 유효성 검사를 수행한다.
예를 들어 주문을 처리하거나 결제를 수행하는 등의 작업을 한다.
데이터 액세스 계층(Repository Layer 또는 Persistence Layer)
데이터베이스나 외부 데이터 소스와의 상호 작용을 담당한다.
데이터의 CRUD(Create, Read, Update, Delete) 작업을 처리하고 영속성을 관리한다.
주로 데이터베이스와의 통신을 담당하는 ORM(Object-Relational Mapping) 프레임워크나 SQL 쿼리가 사용된다.
데이터베이스 계층(Database Layer)
영속성 계층에서 처리된 데이터를 실제 데이터베이스에 저장한다.
주로 SQL 데이터베이스나 NoSQL 데이터베이스가 사용된다.
장단점
복잡도가 낮아 소규모 애플리케이션에 적합하며, 각 계층이 분리되어 있어 유지보수와 확장이 용이하다. 그리고 각 계층의 역할이 명확하게 구분되어 있어 개발과 이해가 쉽다는 장점이 있다.
그러나 데이터베이스 중심의 설계로 인해 데이터베이스에 종속적인 설계가 될 수 있으며, 계층 간의 상호 작용에 대한 이해도가 필요하므로 유지보수가 어렵다는 단점이 있다. 또한 대규모 애플리케이션에는 적합하지 않을 수 있다.
💡Data Structure
ERD(Entity Relationship Diagram)
DDL(Data Definition Language)
/* 어트랙션 */
CREATE TABLE tblAttraction (
attraction_seq NUMBER PRIMARY KEY, /* 어트랙션번호 */
name VARCHAR2(500) NOT NULL UNIQUE, /* 어트랙션명 */
info VARCHAR2(1000) NOT NULL, /* 어트랙션설명 */
capacity NUMBER NOT NULL, /* 수용인원 */
time VARCHAR2(500) NOT NULL, /* 운영시간 */
restriction VARCHAR2(2000) /* 키 크기 제약사항 등 이용정보 */
);
/* 어트랙션월드컵 */
CREATE TABLE tblAWC (
awc_seq NUMBER PRIMARY KEY, /* 어트랙션월드컵번호 */
is_test CHAR(1) NOT NULL, /* 테스트채택 */
attraction_seq NUMBER REFERENCES tblAttraction(attraction_seq) NOT NULL /* 어트랙션번호 */
);
/* 어트랙션월드컵승리 */
CREATE TABLE tblAWCWin (
awc_win_seq NUMBER PRIMARY KEY, /* 어트랙션월드컵승리번호 */
awc_match_count NUMBER NOT NULL, /* 어트랙션월드컵대결횟수 */
awc_win_count NUMBER NOT NULL, /* 어트랙션월드컵승리횟수 */
attraction_seq NUMBER REFERENCES tblAttraction(attraction_seq) NOT NULL /* 어트랙션번호 */
);
/* 어트랙션월드컵최종승리 */
CREATE table tblAWCFinalWin (
awc_final_win_seq NUMBER PRIMARY KEY, /* 어트랙션월드컵최종승리번호 */
awc_final_win_count NUMBER NOT NULL, /* 어트랙션월드컵최종승리횟수 */
attraction_seq NUMBER REFERENCES tblAttraction(attraction_seq) NOT NULL /* 어트랙션번호 */
);
/* 코스 */
CREATE TABLE tblCourse (
course_seq NUMBER PRIMARY KEY, /* 코스번호 */
name VARCHAR2(500) NOT NULL UNIQUE, /* 코스명 */
img VARCHAR2(500) DEFAULT 'course.png' NOT NULL /* 코스이미지 */
);
/* 코스월드컵 */
CREATE TABLE tblCWC (
cwc_seq NUMBER PRIMARY KEY, /* 코스월드컵번호 */
is_test CHAR(1) NOT NULL, /* 테스트채택 */
course_seq NUMBER REFERENCES tblCourse(course_seq) NOT NULL /* 코스번호 */
);
/* 코스월드컵승리 */
CREATE TABLE tblCWCWin (
cwc_win_seq NUMBER PRIMARY KEY, /* 코스월드컵승리번호 */
cwc_match_count NUMBER NOT NULL, /* 코스월드컵대결횟수 */
cwc_win_count NUMBER NOT NULL, /* 코스월드컵승리횟수 */
course_seq NUMBER REFERENCES tblCourse(course_seq) NOT NULL /* 코스번호 */
);
/* 코스월드컵최종승리 */
CREATE TABLE tblCWCFinalWin (
cwc_final_win_seq NUMBER PRIMARY KEY, /* 코스월드컵최종승리번호 */
cwc_final_win_count NUMBER NOT NULL, /* 코스월드컵최종승리횟수 */
course_seq NUMBER REFERENCES tblCourse(course_seq) NOT NULL /* 코스번호 */
);
/* MBTI */
CREATE TABLE tblMBTI (
mbti_seq NUMBER PRIMARY KEY, /* MBTI번호 */
result VARCHAR2(500) NOT NULL, /* 결과 */
name VARCHAR2(500) NOT NULL, /* MBTI명 */
mbti_img VARCHAR2(500), /* MBTI이미지 */
course_seq NUMBER REFERENCES tblCourse(course_seq) NOT NULL, /* 코스번호 */
attraction_seq NUMBER REFERENCES tblAttraction(attraction_seq) NOT NULL /* 어트랙션번호 */
);
DML(Data Manipulation Language)
--어트랙션(tblAttraction): 30개(위치번호: 1~30)
/* 어트랙션 */
SELECT * FROM tblAttraction;
INSERT INTO tblAttraction (attraction_seq, name, info, capacity, time, restriction)
VALUES (seqtblAttraction.NEXTVAL, '지브리특급', '지브리 특급을 타고 지브리의 세계로~!! 우리 함께 지브리를 만나러 가요!', 120, '09:00 - 22:00', '130cm 미만 탑승 불가, 임산부 및 노약자 탑승 불가');
INSERT INTO tblAttraction (attraction_seq, name, info, capacity, time, restriction)
VALUES (seqtblAttraction.NEXTVAL, '포뇨의 비행', '포뇨는 비행하고싶어요. 포뇨와 함께 비행하며 바다를 떠나 하늘을 여행해볼까요?', 30, '09:00 - 22:00', '임산부 및 노약자 탑승 불가');
INSERT INTO tblAttraction (attraction_seq, name, info, capacity, time, restriction)
VALUES (seqtblAttraction.NEXTVAL, '천공의 성', '천공의 성까지 가려면 얼마나 걸릴까요? 천공의 성에서 맛있는 만찬을 즐겨요!', 65, '09:00 - 22:00', '임산부 및 노약자 탑승 불가');
INSERT INTO tblAttraction (attraction_seq, name, info, capacity, time, restriction)
VALUES (seqtblAttraction.NEXTVAL, '고양이 스핀', '고양이는 돌고 돈다! 고양이와 함께 돌고 돌며 빙글 빙글 세계를 경험해봐요!', 80, '09:00 - 22:00', '제한 없음');
INSERT INTO tblAttraction (attraction_seq, name, info, capacity, time, restriction)
VALUES (seqtblAttraction.NEXTVAL, '센과 치히로의 동굴 탐험', '이 지하 동굴의 끝에는 뭐가 있을까요? 호기심을 한 가득 안고 센과 치히로와 함께 동굴 탐험을 떠나요!', 60, '09:00 - 22:00', '제한 없음');
INSERT INTO tblAttraction (attraction_seq, name, info, capacity, time, restriction)
VALUES (seqtblAttraction.NEXTVAL, '모노노케히메의 늑대 체험', '늑대를 만나 볼 수 있는 곳! 와아!! 늑대다!', 70, '09:00 - 22:00', '130cm 미만 탑승 불가, 임산부 및 노약자 탑승 불가');
INSERT INTO tblAttraction (attraction_seq, name, info, capacity, time, restriction)
VALUES (seqtblAttraction.NEXTVAL, '우리는 빙글빙글', '우리는 모두 빙글빙글 돌아요. 빙글빙글 돌아도 정신만 바짝 차리면 된답니다!', 80, '09:00 - 22:00', '제한 없음');
INSERT INTO tblAttraction (attraction_seq, name, info, capacity, time, restriction)
VALUES (seqtblAttraction.NEXTVAL, '추억의 마니', '마니야 보고싶었어! 추억의 마니를 만나러 바로 이 곳으로 오세요!', 70, '09:00 - 22:00', '제한 없음');
INSERT INTO tblAttraction (attraction_seq, name, info, capacity, time, restriction)
VALUES (seqtblAttraction.NEXTVAL, '즐거움은 방울방울', '즐거움은 늘 방울방울 떠올라요. DD 랜드에서의 즐거움은 터지지 않는 비눗방울과 같죠!', 50, '09:00 - 22:00', '제한 없음');
INSERT INTO tblAttraction (attraction_seq, name, info, capacity, time, restriction)
VALUES (seqtblAttraction.NEXTVAL, '마녀 배달부 디디', '디디에게 배달 업무를 배우며 마녀 배달부의 일상속으로!', 70, '09:00 - 22:00', '제한 없음');
INSERT INTO tblAttraction (attraction_seq, name, info, capacity, time, restriction)
VALUES (seqtblAttraction.NEXTVAL, '빗자루 여행', '빗자루 타고 하늘을 날아봤나요? 그렇다면 바로 지금! 빗자루 여행에서 만나요. 우리 함께 하늘로 가볼까요?', 55, '09:00 - 22:00', '120cm 미만 탑승 불가, 임산부 및 노약자 탑승 불가');
INSERT INTO tblAttraction (attraction_seq, name, info, capacity, time, restriction)
VALUES (seqtblAttraction.NEXTVAL, '너구리 대작전', '으악! 너구리다! 너구리 대작전. 과연 무슨 작전일까요?', 75, '09:00 - 22:00', '임산부 및 노약자 탑승 불가');
INSERT INTO tblAttraction (attraction_seq, name, info, capacity, time, restriction)
VALUES (seqtblAttraction.NEXTVAL, '지트란티스', '스릴을 원한다면 바로 찾아와야 하는 곳. 지트란티스!', 85, '09:00 - 22:00', '140cm 미만 탑승 불가, 임산부 및 노약자 탑승 불가');
INSERT INTO tblAttraction (attraction_seq, name, info, capacity, time, restriction)
VALUES (seqtblAttraction.NEXTVAL, '토토로스윙', '토토로와 함께 스윙하러 가볼까요?', 80, '09:00 - 22:00', '130cm 미만 탑승 불가, 임산부 및 노약자 탑승 불가');
INSERT INTO tblAttraction (attraction_seq, name, info, capacity, time, restriction)
VALUES (seqtblAttraction.NEXTVAL, '토토로스핀', '토토로와 함께 스핀하러 가볼까요?', 80, '09:00 - 22:00', '130cm 미만 탑승 불가, 임산부 및 노약자 탑승 불가');
INSERT INTO tblAttraction (attraction_seq, name, info, capacity, time, restriction)
VALUES (seqtblAttraction.NEXTVAL, '거북이 그네', '거북이 그네다! 거북이가 태워주는 그네는 어떨까요?', 55, '09:00 - 22:00', '임산부 및 노약자 탑승 불가');
INSERT INTO tblAttraction (attraction_seq, name, info, capacity, time, restriction)
VALUES (seqtblAttraction.NEXTVAL, '지브리왕국의 해적선', '나는 지브리 왕국의 해적이다! 해적선의 선장은 누구일지! 지브리의 잭 스패로우를 찾아서 떠나요.', 120, '09:00 - 22:00', '130cm 미만 탑승 불가, 임산부 및 노약자 탑승 불가');
INSERT INTO tblAttraction (attraction_seq, name, info, capacity, time, restriction)
VALUES (seqtblAttraction.NEXTVAL, '후룸라이드', '후룸라이드 타며 슈슝~', 65, '09:00 - 22:00', '임산부 및 노약자 탑승 불가');
INSERT INTO tblAttraction (attraction_seq, name, info, capacity, time, restriction)
VALUES (seqtblAttraction.NEXTVAL, '회전목마', '빙글빙글 돌며 환상의 시간을 보내보세요! 빙글빙글 돌다보면 언젠가 내가 찾던 것들이 눈에 보이기 시작할거에요! 바로 그 순간을 위해 회전목마는 오늘도 돌아간답니다.', 75, '09:00 - 22:00', '제한 없음');
INSERT INTO tblAttraction (attraction_seq, name, info, capacity, time, restriction)
VALUES (seqtblAttraction.NEXTVAL, '후렌치레볼루션', '후렌치레볼루션은 DD랜드의 인기 어트랙션 중 하나에요. 시간이 흘러도 끊임없는 인기! 비결이 뭘까요?', 80, '09:00 - 22:00', '140cm 미만 탑승 불가, 임산부 및 노약자 탑승 불가');
INSERT INTO tblAttraction (attraction_seq, name, info, capacity, time, restriction)
VALUES (seqtblAttraction.NEXTVAL, '황야의 무법자', '비켜라! 황야의 무법자 나간다!', 80, '09:00 - 22:00', '제한 없음');
INSERT INTO tblAttraction (attraction_seq, name, info, capacity, time, restriction)
VALUES (seqtblAttraction.NEXTVAL, '뛰뛰빵빵', '뛰뛰빵빵! 지나갑니다! 조심하세요!', 60, '09:00 - 22:00', '제한 없음');
INSERT INTO tblAttraction (attraction_seq, name, info, capacity, time, restriction)
VALUES (seqtblAttraction.NEXTVAL, '환타지드림', '이곳은! 내가 바라던 모든 것이 있잖아?! 환타지 드림을 만나러 가요!', 55, '09:00 - 22:00', '제한 없음');
INSERT INTO tblAttraction (attraction_seq, name, info, capacity, time, restriction)
VALUES (seqtblAttraction.NEXTVAL, '코쿠리코 언덕에서', '코쿠리코 언덕에서 우리가 만난 날을 기억하나요?', 80, '09:00 - 22:00', '임산부 및 노약자 탑승 불가');
INSERT INTO tblAttraction (attraction_seq, name, info, capacity, time, restriction)
VALUES (seqtblAttraction.NEXTVAL, '마루 밑 아리에티를 찾아서', '아리에티가 어디있다고? 마루 밑?! 아리에티를 찾아서 지금 당장 떠나요!', 30, '09:00 - 22:00', '임산부 및 노약자 탑승 불가');
INSERT INTO tblAttraction (attraction_seq, name, info, capacity, time, restriction)
VALUES (seqtblAttraction.NEXTVAL, '니모 이야기', '니모야, 너의 이야기를 들려줄래? 니모의 이야기를 들으며 여행을 떠나보아요.', 75, '09:00 - 22:00', '제한 없음');
INSERT INTO tblAttraction (attraction_seq, name, info, capacity, time, restriction)
VALUES (seqtblAttraction.NEXTVAL, '벼랑 기차', '극한의 공포! 벼랑 끝에서의 아찔함을 느끼고 싶다면 바로 지금! 벼랑 기차로 오세요.', 75, '09:00 - 22:00', '130cm 미만 탑승 불가, 임산부 및 노약자 탑승 불가');
INSERT INTO tblAttraction (attraction_seq, name, info, capacity, time, restriction)
VALUES (seqtblAttraction.NEXTVAL, '지브리의 보은', '지브리의 보은은 정말 놀라워요!', 90, '09:00 - 22:00', '임산부 및 노약자 탑승 불가');
INSERT INTO tblAttraction (attraction_seq, name, info, capacity, time, restriction)
VALUES (seqtblAttraction.NEXTVAL, '이웃집 탐방기', '나의 이웃에는 어떤 분들이 살고 있을까요? 이웃집 탐방하러 Go Go!', 100, '09:00 - 22:00', '임산부 및 노약자 탑승 불가');
INSERT INTO tblAttraction (attraction_seq, name, info, capacity, time, restriction)
VALUES (seqtblAttraction.NEXTVAL, '귀를 기울이면', '귀를 기울이면? 신나는 음악소리가 들려요! 여러 사람들의 목소리도 들리구요! 과연 무슨 일이 벌어질까요?', 120, '09:00 - 22:00', '제한 없음');
/* 어트랙션월드컵 */
INSERT INTO tblAWC (awc_seq, is_test, attraction_seq)
VALUES (seqtblAWC.NEXTVAL, 'Y', 1);
INSERT INTO tblAWC (awc_seq, is_test, attraction_seq)
VALUES (seqtblAWC.NEXTVAL, 'Y', 2);
INSERT INTO tblAWC (awc_seq, is_test, attraction_seq)
VALUES (seqtblAWC.NEXTVAL, 'Y', 3);
INSERT INTO tblAWC (awc_seq, is_test, attraction_seq)
VALUES (seqtblAWC.NEXTVAL, 'Y', 4);
INSERT INTO tblAWC (awc_seq, is_test, attraction_seq)
VALUES (seqtblAWC.NEXTVAL, 'Y', 5);
INSERT INTO tblAWC (awc_seq, is_test, attraction_seq)
VALUES (seqtblAWC.NEXTVAL, 'N', 6);
INSERT INTO tblAWC (awc_seq, is_test, attraction_seq)
VALUES (seqtblAWC.NEXTVAL, 'N', 7);
INSERT INTO tblAWC (awc_seq, is_test, attraction_seq)
VALUES (seqtblAWC.NEXTVAL, 'N', 8);
INSERT INTO tblAWC (awc_seq, is_test, attraction_seq)
VALUES (seqtblAWC.NEXTVAL, 'N', 9);
INSERT INTO tblAWC (awc_seq, is_test, attraction_seq)
VALUES (seqtblAWC.NEXTVAL, 'Y', 10);
INSERT INTO tblAWC (awc_seq, is_test, attraction_seq)
VALUES (seqtblAWC.NEXTVAL, 'N', 11);
INSERT INTO tblAWC (awc_seq, is_test, attraction_seq)
VALUES (seqtblAWC.NEXTVAL, 'Y', 12);
INSERT INTO tblAWC (awc_seq, is_test, attraction_seq)
VALUES (seqtblAWC.NEXTVAL, 'N', 13);
INSERT INTO tblAWC (awc_seq, is_test, attraction_seq)
VALUES (seqtblAWC.NEXTVAL, 'Y', 14);
INSERT INTO tblAWC (awc_seq, is_test, attraction_seq)
VALUES (seqtblAWC.NEXTVAL, 'N', 15);
INSERT INTO tblAWC (awc_seq, is_test, attraction_seq)
VALUES (seqtblAWC.NEXTVAL, 'Y', 16);
INSERT INTO tblAWC (awc_seq, is_test, attraction_seq)
VALUES (seqtblAWC.NEXTVAL, 'N', 17);
INSERT INTO tblAWC (awc_seq, is_test, attraction_seq)
VALUES (seqtblAWC.NEXTVAL, 'Y', 18);
INSERT INTO tblAWC (awc_seq, is_test, attraction_seq)
VALUES (seqtblAWC.NEXTVAL, 'N', 19);
INSERT INTO tblAWC (awc_seq, is_test, attraction_seq)
VALUES (seqtblAWC.NEXTVAL, 'N', 20);
INSERT INTO tblAWC (awc_seq, is_test, attraction_seq)
VALUES (seqtblAWC.NEXTVAL, 'N', 21);
INSERT INTO tblAWC (awc_seq, is_test, attraction_seq)
VALUES (seqtblAWC.NEXTVAL, 'N', 22);
INSERT INTO tblAWC (awc_seq, is_test, attraction_seq)
VALUES (seqtblAWC.NEXTVAL, 'N', 23);
INSERT INTO tblAWC (awc_seq, is_test, attraction_seq)
VALUES (seqtblAWC.NEXTVAL, 'N', 24);
INSERT INTO tblAWC (awc_seq, is_test, attraction_seq)
VALUES (seqtblAWC.NEXTVAL, 'N', 25);
INSERT INTO tblAWC (awc_seq, is_test, attraction_seq)
VALUES (seqtblAWC.NEXTVAL, 'N', 26);
INSERT INTO tblAWC (awc_seq, is_test, attraction_seq)
VALUES (seqtblAWC.NEXTVAL, 'N', 27);
INSERT INTO tblAWC (awc_seq, is_test, attraction_seq)
VALUES (seqtblAWC.NEXTVAL, 'N', 28);
INSERT INTO tblAWC (awc_seq, is_test, attraction_seq)
VALUES (seqtblAWC.NEXTVAL, 'N', 29);
INSERT INTO tblAWC (awc_seq, is_test, attraction_seq)
VALUES (seqtblAWC.NEXTVAL, 'N', 30);
/* 어트랙션월드컵승리 */
INSERT INTO tblAWCWin (awc_win_seq, awc_match_count, awc_win_count, attraction_seq)
VALUES (seqtblAWCWin.NEXTVAL, 682, 502, 1);
INSERT INTO tblAWCWin (awc_win_seq, awc_match_count, awc_win_count, attraction_seq)
VALUES (seqtblAWCWin.NEXTVAL, 670, 408, 2);
INSERT INTO tblAWCWin (awc_win_seq, awc_match_count, awc_win_count, attraction_seq)
VALUES (seqtblAWCWin.NEXTVAL, 703, 203, 3);
INSERT INTO tblAWCWin (awc_win_seq, awc_match_count, awc_win_count, attraction_seq)
VALUES (seqtblAWCWin.NEXTVAL, 710, 350, 4);
INSERT INTO tblAWCWin (awc_win_seq, awc_match_count, awc_win_count, attraction_seq)
VALUES (seqtblAWCWin.NEXTVAL, 700, 550, 5);
INSERT INTO tblAWCWin (awc_win_seq, awc_match_count, awc_win_count, attraction_seq)
VALUES (seqtblAWCWin.NEXTVAL, 690, 480, 6);
INSERT INTO tblAWCWin (awc_win_seq, awc_match_count, awc_win_count, attraction_seq)
VALUES (seqtblAWCWin.NEXTVAL, 720, 400, 7);
INSERT INTO tblAWCWin (awc_win_seq, awc_match_count, awc_win_count, attraction_seq)
VALUES (seqtblAWCWin.NEXTVAL, 690, 480, 8);
INSERT INTO tblAWCWin (awc_win_seq, awc_match_count, awc_win_count, attraction_seq)
VALUES (seqtblAWCWin.NEXTVAL, 710, 350, 9);
INSERT INTO tblAWCWin (awc_win_seq, awc_match_count, awc_win_count, attraction_seq)
VALUES (seqtblAWCWin.NEXTVAL, 700, 390, 10);
INSERT INTO tblAWCWin (awc_win_seq, awc_match_count, awc_win_count, attraction_seq)
VALUES (seqtblAWCWin.NEXTVAL, 705, 420, 11);
INSERT INTO tblAWCWin (awc_win_seq, awc_match_count, awc_win_count, attraction_seq)
VALUES (seqtblAWCWin.NEXTVAL, 695, 470, 12);
INSERT INTO tblAWCWin (awc_win_seq, awc_match_count, awc_win_count, attraction_seq)
VALUES (seqtblAWCWin.NEXTVAL, 725, 380, 13);
INSERT INTO tblAWCWin (awc_win_seq, awc_match_count, awc_win_count, attraction_seq)
VALUES (seqtblAWCWin.NEXTVAL, 715, 410, 14);
INSERT INTO tblAWCWin (awc_win_seq, awc_match_count, awc_win_count, attraction_seq)
VALUES (seqtblAWCWin.NEXTVAL, 730, 360, 15);
INSERT INTO tblAWCWin (awc_win_seq, awc_match_count, awc_win_count, attraction_seq)
VALUES (seqtblAWCWin.NEXTVAL, 700, 400, 16);
INSERT INTO tblAWCWin (awc_win_seq, awc_match_count, awc_win_count, attraction_seq)
VALUES (seqtblAWCWin.NEXTVAL, 710, 390, 17);
INSERT INTO tblAWCWin (awc_win_seq, awc_match_count, awc_win_count, attraction_seq)
VALUES (seqtblAWCWin.NEXTVAL, 690, 420, 18);
INSERT INTO tblAWCWin (awc_win_seq, awc_match_count, awc_win_count, attraction_seq)
VALUES (seqtblAWCWin.NEXTVAL, 705, 370, 19);
INSERT INTO tblAWCWin (awc_win_seq, awc_match_count, awc_win_count, attraction_seq)
VALUES (seqtblAWCWin.NEXTVAL, 715, 430, 20);
INSERT INTO tblAWCWin (awc_win_seq, awc_match_count, awc_win_count, attraction_seq)
VALUES (seqtblAWCWin.NEXTVAL, 725, 350, 21);
INSERT INTO tblAWCWin (awc_win_seq, awc_match_count, awc_win_count, attraction_seq)
VALUES (seqtblAWCWin.NEXTVAL, 695, 440, 22);
INSERT INTO tblAWCWin (awc_win_seq, awc_match_count, awc_win_count, attraction_seq)
VALUES (seqtblAWCWin.NEXTVAL, 730, 380, 23);
INSERT INTO tblAWCWin (awc_win_seq, awc_match_count, awc_win_count, attraction_seq)
VALUES (seqtblAWCWin.NEXTVAL, 720, 390, 24);
INSERT INTO tblAWCWin (awc_win_seq, awc_match_count, awc_win_count, attraction_seq)
VALUES (seqtblAWCWin.NEXTVAL, 710, 400, 25);
INSERT INTO tblAWCWin (awc_win_seq, awc_match_count, awc_win_count, attraction_seq)
VALUES (seqtblAWCWin.NEXTVAL, 700, 410, 26);
INSERT INTO tblAWCWin (awc_win_seq, awc_match_count, awc_win_count, attraction_seq)
VALUES (seqtblAWCWin.NEXTVAL, 690, 420, 27);
INSERT INTO tblAWCWin (awc_win_seq, awc_match_count, awc_win_count, attraction_seq)
VALUES (seqtblAWCWin.NEXTVAL, 710, 350, 28);
INSERT INTO tblAWCWin (awc_win_seq, awc_match_count, awc_win_count, attraction_seq)
VALUES (seqtblAWCWin.NEXTVAL, 690, 480, 29);
INSERT INTO tblAWCWin (awc_win_seq, awc_match_count, awc_win_count, attraction_seq)
VALUES (seqtblAWCWin.NEXTVAL, 710, 350, 30);
/* 어트랙션월드컵최종승리 */
INSERT INTO tblAWCFinalWin (awc_final_win_seq, awc_final_win_count, attraction_seq)
VALUES (seqtblAWCFinalWin.NEXTVAL, 150, 1);
INSERT INTO tblAWCFinalWin (awc_final_win_seq, awc_final_win_count, attraction_seq)
VALUES (seqtblAWCFinalWin.NEXTVAL, 123, 2);
INSERT INTO tblAWCFinalWin (awc_final_win_seq, awc_final_win_count, attraction_seq)
VALUES (seqtblAWCFinalWin.NEXTVAL, 66, 3);
INSERT INTO tblAWCFinalWin (awc_final_win_seq, awc_final_win_count, attraction_seq)
VALUES (seqtblAWCFinalWin.NEXTVAL, 110, 4);
INSERT INTO tblAWCFinalWin (awc_final_win_seq, awc_final_win_count, attraction_seq)
VALUES (seqtblAWCFinalWin.NEXTVAL, 120, 5);
INSERT INTO tblAWCFinalWin (awc_final_win_seq, awc_final_win_count, attraction_seq)
VALUES (seqtblAWCFinalWin.NEXTVAL, 112, 6);
INSERT INTO tblAWCFinalWin (awc_final_win_seq, awc_final_win_count, attraction_seq)
VALUES (seqtblAWCFinalWin.NEXTVAL, 100, 7);
INSERT INTO tblAWCFinalWin (awc_final_win_seq, awc_final_win_count, attraction_seq)
VALUES (seqtblAWCFinalWin.NEXTVAL, 95, 8);
INSERT INTO tblAWCFinalWin (awc_final_win_seq, awc_final_win_count, attraction_seq)
VALUES (seqtblAWCFinalWin.NEXTVAL, 110, 9);
INSERT INTO tblAWCFinalWin (awc_final_win_seq, awc_final_win_count, attraction_seq)
VALUES (seqtblAWCFinalWin.NEXTVAL, 70, 10);
INSERT INTO tblAWCFinalWin (awc_final_win_seq, awc_final_win_count, attraction_seq)
VALUES (seqtblAWCFinalWin.NEXTVAL, 113, 11);
INSERT INTO tblAWCFinalWin (awc_final_win_seq, awc_final_win_count, attraction_seq)
VALUES (seqtblAWCFinalWin.NEXTVAL, 115, 12);
INSERT INTO tblAWCFinalWin (awc_final_win_seq, awc_final_win_count, attraction_seq)
VALUES (seqtblAWCFinalWin.NEXTVAL, 92, 13);
INSERT INTO tblAWCFinalWin (awc_final_win_seq, awc_final_win_count, attraction_seq)
VALUES (seqtblAWCFinalWin.NEXTVAL, 105, 14);
INSERT INTO tblAWCFinalWin (awc_final_win_seq, awc_final_win_count, attraction_seq)
VALUES (seqtblAWCFinalWin.NEXTVAL, 60, 15);
INSERT INTO tblAWCFinalWin (awc_final_win_seq, awc_final_win_count, attraction_seq)
VALUES (seqtblAWCFinalWin.NEXTVAL, 49, 16);
INSERT INTO tblAWCFinalWin (awc_final_win_seq, awc_final_win_count, attraction_seq)
VALUES (seqtblAWCFinalWin.NEXTVAL, 98, 17);
INSERT INTO tblAWCFinalWin (awc_final_win_seq, awc_final_win_count, attraction_seq)
VALUES (seqtblAWCFinalWin.NEXTVAL, 110, 18);
INSERT INTO tblAWCFinalWin (awc_final_win_seq, awc_final_win_count, attraction_seq)
VALUES (seqtblAWCFinalWin.NEXTVAL, 96, 19);
INSERT INTO tblAWCFinalWin (awc_final_win_seq, awc_final_win_count, attraction_seq)
VALUES (seqtblAWCFinalWin.NEXTVAL, 100, 20);
INSERT INTO tblAWCFinalWin (awc_final_win_seq, awc_final_win_count, attraction_seq)
VALUES (seqtblAWCFinalWin.NEXTVAL, 92, 21);
INSERT INTO tblAWCFinalWin (awc_final_win_seq, awc_final_win_count, attraction_seq)
VALUES (seqtblAWCFinalWin.NEXTVAL, 73, 22);
INSERT INTO tblAWCFinalWin (awc_final_win_seq, awc_final_win_count, attraction_seq)
VALUES (seqtblAWCFinalWin.NEXTVAL, 83, 23);
INSERT INTO tblAWCFinalWin (awc_final_win_seq, awc_final_win_count, attraction_seq)
VALUES (seqtblAWCFinalWin.NEXTVAL, 105, 24);
INSERT INTO tblAWCFinalWin (awc_final_win_seq, awc_final_win_count, attraction_seq)
VALUES (seqtblAWCFinalWin.NEXTVAL, 50, 25);
INSERT INTO tblAWCFinalWin (awc_final_win_seq, awc_final_win_count, attraction_seq)
VALUES (seqtblAWCFinalWin.NEXTVAL, 65, 26);
INSERT INTO tblAWCFinalWin (awc_final_win_seq, awc_final_win_count, attraction_seq)
VALUES (seqtblAWCFinalWin.NEXTVAL, 90, 27);
INSERT INTO tblAWCFinalWin (awc_final_win_seq, awc_final_win_count, attraction_seq)
VALUES (seqtblAWCFinalWin.NEXTVAL, 82, 28);
INSERT INTO tblAWCFinalWin (awc_final_win_seq, awc_final_win_count, attraction_seq)
VALUES (seqtblAWCFinalWin.NEXTVAL, 62, 29);
INSERT INTO tblAWCFinalWin (awc_final_win_seq, awc_final_win_count, attraction_seq)
VALUES (seqtblAWCFinalWin.NEXTVAL, 57, 30);
/* 코스 */
INSERT INTO tblCourse (course_seq, name, img)
VALUES (seqtblCourse.NEXTVAL, '고양이의 보은 코스', '고양이의 보은 코스.png');
INSERT INTO tblCourse (course_seq, name, img)
VALUES (seqtblCourse.NEXTVAL, '마녀 배달부 키키와 함께하는 배달 코스', '마녀 배달부 키키와 함께하는 배달 코스.png');
INSERT INTO tblCourse (course_seq, name, img)
VALUES (seqtblCourse.NEXTVAL, '센과 치히로의 행방불명 코스', '센과 치히로의 행방불명 코스.png');
INSERT INTO tblCourse (course_seq, name, img)
VALUES (seqtblCourse.NEXTVAL, '원령공주 코스', '원령공주 코스.png');
INSERT INTO tblCourse (course_seq, name, img)
VALUES (seqtblCourse.NEXTVAL, '천공의 성 코스', '천공의 성 코스.png');
INSERT INTO tblCourse (course_seq, name, img)
VALUES (seqtblCourse.NEXTVAL, '토토로 코스', '토토로 코스.png');
INSERT INTO tblCourse (course_seq, name, img)
VALUES (seqtblCourse.NEXTVAL, '포뇨 수영 코스', '포뇨 수영 코스.png');
INSERT INTO tblCourse (course_seq, name, img)
VALUES (seqtblCourse.NEXTVAL, '하울과 함께 움직이는 코스', '하울과 함께 움직이는 코스.png');
/* 코스월드컵 */
INSERT INTO tblCWC (cwc_seq, is_test, course_seq)
VALUES (seqtblCWC.NEXTVAL, 'Y', 1);
INSERT INTO tblCWC (cwc_seq, is_test, course_seq)
VALUES (seqtblCWC.NEXTVAL, 'Y', 2);
INSERT INTO tblCWC (cwc_seq, is_test, course_seq)
VALUES (seqtblCWC.NEXTVAL, 'Y', 3);
INSERT INTO tblCWC (cwc_seq, is_test, course_seq)
VALUES (seqtblCWC.NEXTVAL, 'Y', 4);
INSERT INTO tblCWC (cwc_seq, is_test, course_seq)
VALUES (seqtblCWC.NEXTVAL, 'N', 5);
INSERT INTO tblCWC (cwc_seq, is_test, course_seq)
VALUES (seqtblCWC.NEXTVAL, 'Y', 6);
INSERT INTO tblCWC (cwc_seq, is_test, course_seq)
VALUES (seqtblCWC.NEXTVAL, 'Y', 7);
INSERT INTO tblCWC (cwc_seq, is_test, course_seq)
VALUES (seqtblCWC.NEXTVAL, 'Y', 8);
/* 코스월드컵승리 */
INSERT INTO tblCWCWin (cwc_win_seq, cwc_match_count, cwc_win_count, course_seq)
VALUES (seqtblCWCWin.NEXTVAL, 520, 350, 1);
INSERT INTO tblCWCWin (cwc_win_seq, cwc_match_count, cwc_win_count, course_seq)
VALUES (seqtblCWCWin.NEXTVAL, 490, 280, 2);
INSERT INTO tblCWCWin (cwc_win_seq, cwc_match_count, cwc_win_count, course_seq)
VALUES (seqtblCWCWin.NEXTVAL, 510, 200, 3);
INSERT INTO tblCWCWin (cwc_win_seq, cwc_match_count, cwc_win_count, course_seq)
VALUES (seqtblCWCWin.NEXTVAL, 505, 220, 4);
INSERT INTO tblCWCWin (cwc_win_seq, cwc_match_count, cwc_win_count, course_seq)
VALUES (seqtblCWCWin.NEXTVAL, 495, 180, 5);
INSERT INTO tblCWCWin (cwc_win_seq, cwc_match_count, cwc_win_count, course_seq)
VALUES (seqtblCWCWin.NEXTVAL, 525, 220, 6);
INSERT INTO tblCWCWin (cwc_win_seq, cwc_match_count, cwc_win_count, course_seq)
VALUES (seqtblCWCWin.NEXTVAL, 515, 210, 7);
INSERT INTO tblCWCWin (cwc_win_seq, cwc_match_count, cwc_win_count, course_seq)
VALUES (seqtblCWCWin.NEXTVAL, 530, 160, 8);
/* 코스월드컵최종승리 */
INSERT INTO tblCWCFinalWin (cwc_final_win_seq, cwc_final_win_count, course_seq)
VALUES (seqtblCWCFinalWin.NEXTVAL, 90, 1);
INSERT INTO tblCWCFinalWin (cwc_final_win_seq, cwc_final_win_count, course_seq)
VALUES (seqtblCWCFinalWin.NEXTVAL, 73, 2);
INSERT INTO tblCWCFinalWin (cwc_final_win_seq, cwc_final_win_count, course_seq)
VALUES (seqtblCWCFinalWin.NEXTVAL, 66, 3);
INSERT INTO tblCWCFinalWin (cwc_final_win_seq, cwc_final_win_count, course_seq)
VALUES (seqtblCWCFinalWin.NEXTVAL, 53, 4);
INSERT INTO tblCWCFinalWin (cwc_final_win_seq, cwc_final_win_count, course_seq)
VALUES (seqtblCWCFinalWin.NEXTVAL, 80, 5);
INSERT INTO tblCWCFinalWin (cwc_final_win_seq, cwc_final_win_count, course_seq)
VALUES (seqtblCWCFinalWin.NEXTVAL, 82, 6);
INSERT INTO tblCWCFinalWin (cwc_final_win_seq, cwc_final_win_count, course_seq)
VALUES (seqtblCWCFinalWin.NEXTVAL, 130, 7);
INSERT INTO tblCWCFinalWin (cwc_final_win_seq, cwc_final_win_count, course_seq)
VALUES (seqtblCWCFinalWin.NEXTVAL, 65, 8);
/* MBTI */
INSERT INTO tblMBTI (mbti_seq, result, name, mbti_img, course_seq, attraction_seq)
VALUES (seqtblMBTI.NEXTVAL, '몇 시에 일어나서 무슨 옷을 입고 몇 시에 출발할지 모두 계획하는 사람', 'ISTJ', 'ISTJ.png', '1', '1');
INSERT INTO tblMBTI (mbti_seq, result, name, mbti_img, course_seq, attraction_seq)
VALUES (seqtblMBTI.NEXTVAL, '무서워하는 친구를 챙겨주는 세심한 사람', 'ISFJ', 'ISFJ.png', '2', '2');
INSERT INTO tblMBTI (mbti_seq, result, name, mbti_img, course_seq, attraction_seq)
VALUES (seqtblMBTI.NEXTVAL, '친구들의 부탁을 잘 들어주는 사람', 'INFJ', 'INFJ.png', '4', '3');
INSERT INTO tblMBTI (mbti_seq, result, name, mbti_img, course_seq, attraction_seq)
VALUES (seqtblMBTI.NEXTVAL, '타고 싶은 거 안 타고 싶은 거 이유를 칼같이 말하는 논리적인 사람', 'INTJ', 'INTJ.png', '7', '4');
INSERT INTO tblMBTI (mbti_seq, result, name, mbti_img, course_seq, attraction_seq)
VALUES (seqtblMBTI.NEXTVAL, '조용히 있는 듯 없는 듯 다 타는 스릴러를 즐기는 사람', 'ISTP', 'ISTP.png', '5', '5');
INSERT INTO tblMBTI (mbti_seq, result, name, mbti_img, course_seq, attraction_seq)
VALUES (seqtblMBTI.NEXTVAL, '내 갈길 간다! 친구가 못 타면 나라도 타고오는 사람', 'ISFP', 'ISFP.png', '3', '6');
INSERT INTO tblMBTI (mbti_seq, result, name, mbti_img, course_seq, attraction_seq)
VALUES (seqtblMBTI.NEXTVAL, '타고 싶은 게 있어도 말 안하고 친구들이 타고 싶다하는 거 타러 가는 사람', 'INFP', 'INFP.png','4', '7');
INSERT INTO tblMBTI (mbti_seq, result, name, mbti_img, course_seq, attraction_seq)
VALUES (seqtblMBTI.NEXTVAL, '타고 싶은 건 많지만 귀찮아서 몇 개만 타는 사람', 'INTP', 'INTP.png', '5', '8');
INSERT INTO tblMBTI (mbti_seq, result, name, mbti_img, course_seq, attraction_seq)
VALUES (seqtblMBTI.NEXTVAL, '처음 타는 것도 바로 적응하고 계속 타는 사람', 'ESTP', 'ESTP.png', '6', '9');
INSERT INTO tblMBTI (mbti_seq, result, name, mbti_img, course_seq, attraction_seq)
VALUES (seqtblMBTI.NEXTVAL, '인싸중에 핵인싸 놀이동산 오자고 한 사람', 'ESFP', 'ESFP.png', '8', '10');
INSERT INTO tblMBTI (mbti_seq, result, name, mbti_img, course_seq, attraction_seq)
VALUES (seqtblMBTI.NEXTVAL, '무서워도 웃고 재밌어도 웃고 신나서 계속 웃는 사람', 'ENFP', 'ENFP.png', '1', '11');
INSERT INTO tblMBTI (mbti_seq, result, name, mbti_img, course_seq, attraction_seq)
VALUES (seqtblMBTI.NEXTVAL, '반복적인 거 싫어하고 도전하는 거 좋아해서 처음 보는 거 다 타는 사람', 'ENTP', 'ENTP.png', '6', '12');
INSERT INTO tblMBTI (mbti_seq, result, name, mbti_img, course_seq, attraction_seq)
VALUES (seqtblMBTI.NEXTVAL, '이거 타자 저거 타자 의견 제시하는 사람', 'ESTJ', 'ESTJ.png', '4', '13');
INSERT INTO tblMBTI (mbti_seq, result, name, mbti_img, course_seq, attraction_seq)
VALUES (seqtblMBTI.NEXTVAL, '갑자기 놀이동산 가자하면 지체없이 바로 출발하는 사람', 'ESFJ', 'ESFJ.png', '5', '14');
INSERT INTO tblMBTI (mbti_seq, result, name, mbti_img, course_seq, attraction_seq)
VALUES (seqtblMBTI.NEXTVAL, '놀이기구 제일 잘 타는(줄 아는) 사람', 'ENFJ', 'ENFJ.png', '2', '15');
INSERT INTO tblMBTI (mbti_seq, result, name, mbti_img, course_seq, attraction_seq)
VALUES (seqtblMBTI.NEXTVAL, '오늘 타려고 계획했던 건 다 타야 하는 사람', 'ENTJ', 'ENTJ.png', '4', '16');
💡Controller
애플리케이션의 HTTP 요청을 처리하고 비즈니스 로직을 호출하는 역할을 수행하는 계층이다. AdminWorldCupAttractionController는 관리자가 어트랙션을 관리할 수 있는 기능을 제공하며, UserWorldCupAttractionController는 사용자가 어트랙션에 투표할 수 있는 기능을 제공한다.
UserWorldCupAttractionController
package com.project.dd.test.worldcup.attraction.controller;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.project.dd.activity.attraction.domain.AttractionDTO;
import com.project.dd.test.worldcup.attraction.service.WorldCupAttractionService;
/**
* 사용자가 참여하는 월드컵 어트랙션 테스트를 관리하는 컨트롤러 입니다.
*
* 1. 월드컵 어트랙션 목록 조회 화면 및 테스트 진행
* 2. 월드컵 어트랙션 초기화
* 3. 테스트 결과 업데이트
* 4. 최종 우승 어트랙션 업데이트
*
* @author 이승원
*/
@Controller
@RequestMapping("/user/test/worldcup/attraction")
public class UserWorldCupAttractionController {
@Autowired
private WorldCupAttractionService awcService;
/**
* 월드컵 어트랙션 목록 조회 화면을 표시하고 테스트를 진행합니다.
*
* @param model 화면에 전달할 데이터를 담는 모델 객체
* @param session 현재 세션 객체
* @return 월드컵 어트랙션 목록 조회 화면
*/
@GetMapping(value = "/view.do")
public String view(Model model, HttpSession session) {
// 어트랙션 리스트 가져오기
List attractionList = awcService.getAttractionList();
// 선택하지 않은 어트랙션 리스트 생성
List remainingAttractions = new ArrayList<>(attractionList);
// 선택하지 않은 어트랙션 중에서 랜덤으로 두 개 선택
List selectedTwoAttractions = awcService.getRandomTwoAttractions(remainingAttractions);
model.addAttribute("selectedTwoAttractions", selectedTwoAttractions);
model.addAttribute("testCount", awcService.getTestCount());
return "user/test/worldcup/attraction/view";
}
/**
* 세션을 초기화하고 월드컵 어트랙션 목록 조회 화면으로 리다이렉트합니다.
*
* @param model 화면에 전달할 데이터를 담는 모델 객체
* @param session 현재 세션 객체
* @return 월드컵 어트랙션 목록 조회 화면으로 리다이렉트
*/
@GetMapping(value = "/initialization.do")
public String initialization(Model model, HttpSession session) {
// 세션 초기화
List selectedAttractions = new ArrayList<>();
session.setAttribute("selectedAttractions", selectedAttractions);
return "redirect:/user/test/worldcup/attraction/view.do";
}
/**
* 사용자의 월드컵 어트랙션 테스트 결과를 업데이트하고 새로운 어트랙션을 선택합니다.
*
* @param winAttractionSeq 이긴 어트랙션의 일련번호
* @param lostAttractionSeq 진 어트랙션의 일련번호
* @param model 화면에 전달할 데이터를 담는 모델 객체
* @param session 현재 세션 객체
* @return 업데이트된 테스트 결과와 선택 가능한 어트랙션 목록을 응답
*/
@PostMapping("/view.do")
public ResponseEntity> attractionSelection(@RequestParam String winAttractionSeq,
@RequestParam String lostAttractionSeq, Model model, HttpSession session) {
// 테스트 결과 업데이트
awcService.updateAWCMatchCount(winAttractionSeq);
Service.updateAWCWinCount(winAttractionSeq);
awcService.updateAWCMatchCount(lostAttractionSeq);
// 세션에서 선택한 어트랙션 리스트 가져오기
@SuppressWarnings("unchecked") // 제네릭 경고 무시
List selectedAttractions = (ArrayList) session.getAttribute("selectedAttractions");
// 선택한 어트랙션이 중복되지 않았을 경우 추가
if (!selectedAttractions.contains(lostAttractionSeq)) {
selectedAttractions.add(lostAttractionSeq);
session.setAttribute("selectedAttractions", selectedAttractions);
}
// 새로운 어트랙션 선택
List remainingAttractions = awcService.getRemainingAttractions(selectedAttractions);
List selectedTwoAttractions = awcService.getRandomTwoAttractions(remainingAttractions);
// 응답 데이터 설정
Map responseData = new HashMap<>();
responseData.put("selectedTwoAttractions", selectedTwoAttractions);
responseData.put("remainingAttractions", remainingAttractions);
// HTTP status OK와 함께 JSON 형식 응답
return new ResponseEntity<>(responseData, HttpStatus.OK);
}
/**
* 최종 우승 어트랙션을 업데이트합니다.
*
* @param finalWinAttractionSeq 최종 우승 어트랙션의 일련번호
* @return 업데이트 완료 메시지와 함께 HTTP status OK 응답
*/
@PostMapping("/final.do")
public ResponseEntity finalUpdate(@RequestParam String finalWinAttractionSeq) {
// 최종 우승 어트랙션 업데이트
awcService.updateAWCFinalWinCount(finalWinAttractionSeq);
// HTTP status OK 응답
return new ResponseEntity<>("Final Win update completed", HttpStatus.OK);
}
}
AdminWorldCupAttractionController
package com.project.dd.test.worldcup.attraction.controller;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.project.dd.test.worldcup.attraction.service.WorldCupAttractionService;
/**
* 관리자가 월드컵 어트랙션 정보를 관리하는 컨트롤러 입니다.
*
* 1. 월드컵 어트랙션 목록 조회 및 페이징 기능
* 2. 월드컵 어트랙션 테스트 진행 여부 업데이트 기능
*
* @author 이승원
*/
@Controller
@RequestMapping("/admin/test/worldcup/attraction")
public class AdminWorldCupAttractionController {
@Autowired
private WorldCupAttractionService awcService;
/**
* 월드컵 어트랙션 목록을 조회하여 페이징된 결과를 화면에 전달합니다.
*
* @param word 검색어
* @param page 현재 페이지 번호
* @param model 화면에 전달할 데이터를 담는 모델 객체
* @return 월드컵 어트랙션 목록 조회 화면
*/
@GetMapping(value = "/view.do")
public String view(String word, @RequestParam(defaultValue = "1") int page, Model model) {
String solting = "admin";
String searchStatus = (word == null || word.equals("")) ? "n" : "y";
// 페이징 정보를 담은 Map 생성
Map map = awcService.paging(solting, searchStatus, word, page);
// 화면에 전달할 데이터 설정
model.addAttribute("currentPage", page);
model.addAttribute("map", map);
model.addAttribute("listAttraction", awcService.getAllAttraction(map));
model.addAttribute("awcFinalWinTotalCount", awcService.getAWCFinalWinTotalCount());
return "admin/test/worldcup/attraction/view";
}
/**
* 선택한 월드컵 어트랙션의 테스트 진행 여부를 업데이트합니다.
*
* @param attractionSeq 선택한 어트랙션의 일련번호
* @param isTest 업데이트할 테스트 진행 여부
* @param model 화면에 전달할 데이터를 담는 모델 객체
* @return 월드컵 어트랙션 목록 조회 화면으로 리다이렉트
*/
@PostMapping(value = "/view.do")
public String updateAttractionStatus(@RequestParam String attractionSeq, @RequestParam String isTest, Model model) {
//System.out.println("seq:" + attractionSeq + " check:" + isTest);
// 선택한 어트랙션의 테스트 진행 여부 업데이트
Map map = new HashMap<>();
map.put("isTest", isTest);
map.put("attractionSeq", attractionSeq);
awcService.updateAttractionStatus(map);
// 월드컵 어트랙션 목록 조회 화면으로 리다이렉트
return "redirect:/admin/test/worldcup/attraction/view.do";
}
}
💡DTO(Data Transfer Object)
DTO 계층은 서비스 계층과 뷰 계층 간의 데이터 전달을 담당하며, 데이터베이스에서 가져온 엔티티를 뷰에서 사용할 수 있는 형태로 변환하는 역할을 한다.
WorldCupAttractionDTO
package com.project.dd.test.worldcup.attraction.domain;
import lombok.Data;
@Data
public class WorldCupAttractionDTO {
// tblAWC
private String awc_seq; // 어트랙션월드컵번호
private String is_test; // 테스트채택
private String attraction_seq; // 어트랙션번호
}
WorldCupAttractionWinDTO
package com.project.dd.test.worldcup.attraction.domain;
import lombok.Data;
@Data
public class WorldCupAttractionWinDTO {
// tblAWCWin
private String awc_win_seq; // 어트랙션월드컵승리번호
private String awc_match_count; // 어트랙션월드컵대결횟수
private String awc_win_count; // 어트랙션월드컵승리횟수
private String attraction_seq; // 어트랙션번호
}
WorldCupAttractionFinalWinDTO
package com.project.dd.test.worldcup.attraction.domain;
import lombok.Data;
@Data
public class WorldCupAttractionFinalWinDTO {
// tblAWCFinalWin
private String awc_final_win_seq; // 어트랙션월드컵최종승리번호
private String awc_final_win_count; // 어트랙션월드컵최종승리횟수
private String attraction_seq; // 어트랙션번호
}
WorldCupAttractionRankDTO
package com.project.dd.test.worldcup.attraction.domain;
import lombok.Data;
@Data
public class WorldCupAttractionRankDTO {
private String attraction_seq; // 어트랙션번호
private String name; // 어트랙션명
private String info; // 어트랙션설명
private String img; // 어트랙션이미지
private String match_count; // 어트랙션월드컵대결횟수
private String win_count; // 어트랙션월드컵승리횟수
private String final_win_count; //어트랙션월드컵최종승리횟수
private String win_rate; // 승률 (어트랙션월드컵승리횟수 / 어트랙션월드컵승리횟수)
private String win_rate_per; // 승률 (100%)
private String final_win_rate; // 최종승률 (어트랙션월드컵최종승리횟수 / 전체 테스트 횟수)
private String final_win_rate_per; // 최종승률 (100%)
private String rnum; // 글번호
}
💡Service
서비스 계층은 비즈니스 로직을 캡슐화하여 처리하고, 데이터베이스와의 상호작용을 담당한다.
WorldCupAttractionService
package com.project.dd.test.worldcup.attraction.service;
import java.util.List;
import java.util.Map;
import com.project.dd.activity.attraction.domain.AttractionDTO;
public interface WorldCupAttractionService {
int getTotalCount();
int getTestCount();
Map paging(String solting, String searchStatus, String word, int page);
List getAllAttraction(Map map);
List getAttractionList();
void updateAttractionStatus(Map map);
int getAWCFinalWinTotalCount();
List getRandomTwoAttractions(List remainingAttractions);
List getRemainingAttractions(List selectedAttractions);
int addAWC(AttractionDTO dto, String seq);
int addAWCWin(AttractionDTO dto, String seq);
int addAWCFinalWin(AttractionDTO dto, String seq);
void updateAWCMatchCount(String attractionSeq);
void updateAWCWinCount(String attractionSeq);
void updateAWCFinalWinCount(String attractionSeq);
List getAttractionNameList();
List getTopThreeAttraction();
}
WorldCupAttractionServiceImpl
package com.project.dd.test.worldcup.attraction.service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.service;
import com.project.dd.activity.attraction.domain.AttractionDTO;
import com.project.dd.test.worldcup.attraction.repository.WorldCupAttractionDAO;
/**
* 월드컵 어트랙션과 관련된 비즈니스 로직을 처리하는 서비스 클래스 입니다.
*
* 1. 어트랙션의 총 개수 조회
* 2. 어트랙션 테스트 개수 조회
* 3. 어트랙션 리스트 조회 (페이징 포함)
* 4. 어트랙션의 테스트 상태 업데이트
* 5. 월드컵 최종 우승 어트랙션의 총 개수 조회
* 6. 남은 어트랙션 중에서 랜덤으로 두 개 선택
* 7. 어트랙션 추가 및 관련 통계 업데이트
* 8. 어트랙션 경기 횟수 업데이트
* 9. 어트랙션 승리 횟수 업데이트
* 10. 어트랙션 최종 우승 횟수 업데이트
* 11. 어트랙션명 목록 조회
* 12. 상위 3개 어트랙션 조회
*
* @author 이승원
*/
@Service
@Primary
public class WorldCupAttractionServiceImpl implements WorldCupAttractionService {
@Autowired
private WorldCupAttractionDAO dao;
/**
* 어트랙션의 총 개수를 조회합니다.
*
* @return 어트랙션의 총 개수
*/
@Override
public int getTotalCount() {
return dao.getTotalCount();
}
/**
* 어트랙션 테스트 개수를 조회합니다.
*
* @return 어트랙션 테스트 개수
*/
@Override
public int getTestCount() {
return dao.getTestCount();
}
/**
* 어트랙션의 페이징 처리를 위한 맵을 생성합니다.
*
* @param solting 정렬 기준
* @param searchStatus 검색 상태
* @param word 검색어
* @param page 현재 페이지 번호
* @return 페이징 처리를 위한 맵
*/
@Override
public Map<String, String> paging(String solting, String searchStatus, String word, int page) {
Map<String, String> map = new HashMap<String, String>();
map.put("solting", solting);
map.put("searchStatus", searchStatus);
map.put("word", word);
int pageSize = 10; // 조회할 글 개수
int startIndex = (page - 1) * pageSize + 1;
int endIndex = startIndex + pageSize - 1;
map.put("startIndex", String.format("%d", startIndex));
map.put("endIndex", String.format("%d", endIndex));
int totalPosts = getTotalCount();
int totalPages = (int) Math.ceil((double) totalPosts / pageSize);
map.put("totalPosts", String.format("%d", totalPosts));
map.put("totalPages", String.format("%d", totalPages));
return map;
}
/**
* 전체 어트랙션 목록을 조회합니다.
*
* @param map 페이징 및 검색 정보를 담고 있는 맵
* @return 전체 어트랙션 목록
*/
@Override
public List<AttractionDTO> getAllAttraction(Map<String, String> map) {
return dao.getAllAttraction(map);
}
/**
* 어트랙션 목록을 조회합니다.
*
* @return 어트랙션 목록
*/
@Override
public List<AttractionDTO> getAttractionList() {
return dao.getAttractionList();
}
/**
* 어트랙션의 테스트 상태를 업데이트합니다.
*
* @param map 테스트 상태를 업데이트하기 위한 맵
*/
@Override
public void updateAttractionStatus(Map<String, String> map) {
dao.updateAttractionStatus(map);
}
/**
* 월드컵 최종 우승 어트랙션의 총 개수를 조회합니다.
*
* @return 최종 우승 어트랙션의 총 개수
*/
@Override
public int getAWCFinalWinTotalCount() {
return dao.getAWCFinalWinTotalCount();
}
/**
* 남은 어트랙션 중에서 랜덤으로 두 개 선택하여 반환합니다.
*
* @param remainingAttractions 선택 대상이 되는 어트랙션 리스트
* @return 랜덤으로 선택된 두 개의 어트랙션 리스트
*/
@Override
public List<AttractionDTO> getRandomTwoAttractions(List<AttractionDTO> remainingAttractions) {
ArrayList<AttractionDTO> selectedTwoAttractions = new ArrayList<>();
Random random = new Random();
// 어트랙션 리스트가 있고, 크기가 1보다 큰 경우
if (remainingAttractions != null && remainingAttractions.size() > 1) {
int index1 = random.nextInt(remainingAttractions.size());
int index2;
// index1과 다른 index2 선택 (중복 회피)
do {
index2 = random.nextInt(remainingAttractions.size());
} while (index1 == index2);
// 두 개의 어트랙션을 리스트에 추가
selectedTwoAttractions.add(remainingAttractions.get(index1));
selectedTwoAttractions.add(remainingAttractions.get(index2));
} else if (remainingAttractions != null && remainingAttractions.size() == 1) {
// 어트랙션이 하나만 남았을 경우
selectedTwoAttractions.add(remainingAttractions.get(0));
}
return selectedTwoAttractions;
}
/**
* 선택되지 않은 어트랙션 목록을 가져옵니다.
*
* @param selectedAttractions 선택된 어트랙션 목록
* @return 선택되지 않은 어트랙션 목록
*/
@Override
public List<AttractionDTO> getRemainingAttractions(List<String> selectedAttractions) {
List<AttractionDTO> allAttractions = getAttractionList();
if (selectedAttractions == null) {
return allAttractions;
}
return allAttractions.stream()
.filter(attraction -> !selectedAttractions.contains(attraction.getAttraction_seq()))
.collect(Collectors.toList());
}
/**
* 어트랙션 월드컵 정보를 추가합니다.
*
* @param dto 월드컵 정보를 담고 있는 어트랙션 DTO
* @param seq 어트랙션 일련번호
* @return 데이터베이스에 추가된 행 수
*/
@Override
public int addAWC(AttractionDTO dto, String seq) {
dto.setAttraction_seq(seq);
return dao.addAWC(dto);
}
/**
* 어트랙션 월드컵 승리 결과를 추가합니다.
*
* @param dto 월드컵 승리 결과 정보를 담고 있는 어트랙션 DTO
* @param seq 어트랙션 일련번호
* @return 데이터베이스에 추가된 행 수
*/
@Override
public int addAWCWin(AttractionDTO dto, String seq) {
dto.setAttraction_seq(seq);
return dao.addAWCWin(dto);
}
/**
* 어트랙션 월드컵 최종 우승 결과를 추가합니다.
*
* @param dto 월드컵 최종 우승 결과 정보를 담고 있는 어트랙션 DTO
* @param seq 어트랙션 일련번호
* @return 데이터베이스에 추가된 행 수
*/
@Override
public int addAWCFinalWin(AttractionDTO dto, String seq) {
dto.setAttraction_seq(seq);
return dao.addAWCFinalWin(dto);
}
/**
* 어트랙션 월드컵 경기 횟수를 업데이트합니다.
*
* @param attractionSeq 어트랙션 일련번호
*/
@Override
public void updateAWCMatchCount(String attractionSeq) {
dao.updateAWCMatchCount(attractionSeq);
}
/**
* 어트랙션 월드컵 승리 횟수를 업데이트합니다.
*
* @param attractionSeq 어트랙션 일련번호
*/
@Override
public void updateAWCWinCount(String attractionSeq) {
dao.updateAWCWinCount(attractionSeq);
}
/**
* 어트랙션 월드컵 최종 우승 횟수를 업데이트합니다.
*
* @param attractionSeq 어트랙션 일련번호
*/
@Override
public void updateAWCFinalWinCount(String attractionSeq) {
dao.updateAWCFinalWinCount(attractionSeq);
}
/**
* 어트랙션명 목록을 조회합니다.
*
* @return 어트랙션명 목록
*/
@Override
public List<AttractionDTO> getAttractionNameList() {
return dao.getAttractionNameList();
}
/**
* 상위 3개 어트랙션을 조회합니다.
*
* @return 상위 3개 어트랙션 목록
*/
@Override
public List<AttractionDTO> getTopThreeAttraction() {
return dao.getTopThreeAttraction();
}
}
💡Repository
Repository 계층은 데이터베이스와의 상호작용을 담당하며, 데이터 액세스 객체(DAO)를 통해 데이터를 검색, 저장, 업데이트 및 삭제 작업을 한다.
여기서 DAO는 일반적으로 데이터 액세스 객체를 의미한다.
WorldCupAttractionDAO
package com.project.dd.test.worldcup.attraction.repository;
import java.util.List;
import java.util.Map;
import com.project.dd.activity.attraction.domain.AttractionDTO;
public interface WorldCupAttractionDAO {
int getTotalCount();
int getTestCount();
List getAllAttraction(Map map);
List getAttractionList();
void updateAttractionStatus(Map map);
int getAWCFinalWinTotalCount();
int addAWC(AttractionDTO dto);
int addAWCWin(AttractionDTO dto);
int addAWCFinalWin(AttractionDTO dto);
void updateAWCMatchCount(String attractionSeq);
void updateAWCWinCount(String attractionSeq);
void updateAWCFinalWinCount(String attractionSeq);
List getAttractionNameList();
List getTopThreeAttraction();
}
WorldCupAttractionDAOImpl
package com.project.dd.test.worldcup.attraction.repository;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Repository;
import com.project.dd.activity.attraction.domain.AttractionDTO;
import com.project.dd.test.worldcup.attraction.mapper.WorldCupAttractionMapper;
@Repository
@Primary
public class WorldCupAttractionDAOImpl implements WorldCupAttractionDAO {
@Autowired
private WorldCupAttractionMapper mapper;
@Override
public int getTotalCount() {
return mapper.getTotalCount();
}
@Override
public int getTestCount() {
return mapper.getTestCount();
}
@Override
public List getAllAttraction(Map map) {
return mapper.getAllAttraction(map);
}
@Override
public List getAttractionList() {
return mapper.getAttractionList();
}
@Override
public void updateAttractionStatus(Map map) {
mapper.updateAttractionStatus(map);
}
@Override
public int getAWCFinalWinTotalCount() {
return mapper.getAWCFinalWinTotalCount();
}
@Override
public int addAWC(AttractionDTO dto) {
return mapper.addAWC(dto);
}
@Override
public int addAWCWin(AttractionDTO dto) {
return mapper.addAWCWin(dto);
}
@Override
public int addAWCFinalWin(AttractionDTO dto) {
return mapper.addAWCFinalWin(dto);
}
@Override
public void updateAWCMatchCount(String attractionSeq) {
mapper.updateAWCMatchCount(attractionSeq);
}
@Override
public void updateAWCWinCount(String attractionSeq) {
mapper.updateAWCWinCount(attractionSeq);
}
@Override
public void updateAWCFinalWinCount(String attractionSeq) {
mapper.updateAWCFinalWinCount(attractionSeq);
}
@Override
public List getAttractionNameList() {
return mapper.getAttractionNameList();
}
@Override
public List getTopThreeAttraction() {
return mapper.getTopThreeAttraction();
}
}
💡Mapper
Mapper 계층은 엔티티와 DTO 간의 매핑을 담당하며, MyBatis나 Hibernate와 같은 ORM 프레임워크를 사용하여 데이터베이스와의 상호작용을 추상화한다.
WorldCupAttractionMapper
package com.project.dd.test.worldcup.attraction.mapper;
import java.util.List;
import java.util.Map;
import com.project.dd.activity.attraction.domain.AttractionDTO;
public interface WorldCupAttractionMapper {
int getTotalCount();
int getTestCount();
List getAllAttraction(Map map);
List getAttractionList();
void updateAttractionStatus(Map map);
int getAWCFinalWinTotalCount();
int addAWC(AttractionDTO dto);
int addAWCWin(AttractionDTO dto);
int addAWCFinalWin(AttractionDTO dto);
void updateAWCMatchCount(String attractionSeq);
void updateAWCWinCount(String attractionSeq);
void updateAWCFinalWinCount(String attractionSeq);
List getAttractionNameList();
List getTopThreeAttraction();
}
WorldCupAttractionMapper.xml
<?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.project.dd.test.worldcup.attraction.mapper.WorldCupAttractionMapper">
<!-- 총 어트랙션 데이터 개수 조회 쿼리 -->
<select id="getTotalCount" resultType="int">
select count(*) from tblAWC awc
join vwAttractionList al on awc.attraction_seq = al.attraction_seq
join tblAWCWin w on w.attraction_seq = al.attraction_seq
join tblAWCFinalWin fw on fw.attraction_seq = al.attraction_seq
<if test="solting == 'admin' and searchStatus == 'y'.toString()">
where name like '%' || #{word} || '%' or info like '%' || #{word} || '%' or restriction like '%' || #{word} || '%'
</if>
</select>
<!-- 어트랙션 월드컵이 'Y'인 개수 조회 쿼리 -->
<select id="getTestCount" resultType="int">
select count(*) from tblAWC awc
join vwAttractionList al on awc.attraction_seq = al.attraction_seq
join tblAWCWin w on w.attraction_seq = al.attraction_seq
join tblAWCFinalWin fw on fw.attraction_seq = al.attraction_seq
where awc.is_test = 'Y'
</select>
<!-- 어트랙션 리스트 조회 쿼리 -->
<select id="getAllAttraction" parameterType="map" resultType="com.project.dd.activity.attraction.domain.AttractionDTO">
SELECT * FROM (
SELECT n.*, ROWNUM AS rnum FROM (
SELECT al.*,
awc.is_test,
w.awc_match_count,
w.awc_win_count,
fw.awc_final_win_count
FROM tblAWC awc
JOIN vwAttractionList al ON awc.attraction_seq = al.attraction_seq
JOIN tblAWCWin w ON w.attraction_seq = al.attraction_seq
JOIN tblAWCFinalWin fw ON fw.attraction_seq = al.attraction_seq
order by al.attraction_seq desc) n
<if test="solting == 'admin' and searchStatus == 'y'.toString()">
where name like '%' || #{word} || '%' or info like '%' || #{word} || '%' or restriction like '%' || #{word} || '%'
</if>
) where rnum between #{startIndex} and #{endIndex}
</select>
<!-- 모든 어트랙션 데이터 목록 조회 쿼리 -->
<select id="getAttractionList" resultType="com.project.dd.activity.attraction.domain.AttractionDTO">
select
al.*,
awc.is_test,
w.awc_match_count,
w.awc_win_count,
fw.awc_final_win_count
from vwAttractionList al
join tblAWC awc on awc.attraction_seq = al.attraction_seq
join tblAWCWin w on w.attraction_seq = al.attraction_seq
join tblAWCFinalWin fw on fw.attraction_seq = al.attraction_seq
where awc.is_test = 'Y'
</select>
<!-- 어트랙션 테스트 채택 업데이트 쿼리 -->
<update id="updateAttractionStatus" parameterType="map">
update tblAWC
set is_test = #{isTest}
where attraction_seq = #{attractionSeq}
</update>
<!-- 어트랙션 월드컵 실행 횟수(최종 우승 횟수 총합) 조회 쿼리 -->
<select id="getAWCFinalWinTotalCount" resultType="int">
select sum(awc_final_win_count) as total_awc_final_win_count from tblAWCFinalWin
</select>
<!-- 어트랙션 월드컵 추가 쿼리 -->
<insert id="addAWC" parameterType="com.project.dd.activity.attraction.domain.AttractionDTO">
insert into tblAWC (awc_seq, is_test, attraction_seq)
values (seqtblAWC.NEXTVAL, 'Y', #{attraction_seq})
</insert>
<!-- 어트랙션 월드컵 승리 추가 쿼리 -->
<insert id="addAWCWin" parameterType="com.project.dd.activity.attraction.domain.AttractionDTO">
insert into tblAWCWin (awc_win_seq, awc_match_count, awc_win_count, attraction_seq)
values (seqtblAWCWin.NEXTVAL, 0, 0, #{attraction_seq})
</insert>
<!-- 어트랙션 월드컵 최종 우승 추가 쿼리 -->
<insert id="addAWCFinalWin" parameterType="com.project.dd.activity.attraction.domain.AttractionDTO">
insert into tblAWCFinalWin (awc_final_win_seq, awc_final_win_count, attraction_seq)
values (seqtblAWCFinalWin.NEXTVAL, 0, #{attraction_seq})
</insert>
<!-- 어트랙션 경기 횟수 업데이트 쿼리 -->
<update id="updateAWCMatchCount" parameterType="String">
update
tblAWCWin
set awc_match_count = awc_match_count + 1
where attraction_seq = #{attractionSeq}
</update>
<!-- 어트랙션 승리 횟수 업데이트 쿼리 -->
<update id="updateAWCWinCount" parameterType="String">
update
tblAWCWin
set awc_win_count = awc_win_count + 1
where attraction_seq = #{attractionSeq}
</update>
<!-- 어트랙션 최종 우승 횟수 업데이트 쿼리 -->
<update id="updateAWCFinalWinCount" parameterType="String">
update
tblAWCFinalWin
set awc_final_win_count = awc_final_win_count + 1
where attraction_seq = #{attractionSeq}
</update>
<!-- 어트랙션명 목록 조회 쿼리 -->
<select id="getAttractionNameList" resultType="com.project.dd.activity.attraction.domain.AttractionDTO">
select attraction_seq, name from tblAttraction order by attraction_seq
</select>
<!-- 상위 3개 어트랙션 조회 쿼리 -->
<select id="getTopThreeAttraction" resultType="com.project.dd.activity.attraction.domain.AttractionDTO">
select
t.*,
to_char(t.win_rate * 100, '999.99') as win_rate_per,
to_char(t.final_win_rate * 100, '999.99') as final_win_rate_per
from (
select
al.attraction_seq,
al.name,
al.info,
al.img,
w.awc_match_count as match_count,
w.awc_win_count as win_count,
fw.awc_final_win_count as final_win_count,
case
when w.awc_match_count = 0 then 0
else (w.awc_win_count / w.awc_match_count)
end as win_rate,
case
when total_final_win_count = 0 then 0
else (fw.awc_final_win_count / total_final_win_count)
end as final_win_rate,
rownum as rnum
from vwAttractionList al
join tblAWC awc on awc.attraction_seq = al.attraction_seq
join tblAWCWin w on w.attraction_seq = al.attraction_seq
join tblAWCFinalWin fw on fw.attraction_seq = al.attraction_seq
cross join (
select SUM(awc_final_win_count) as total_final_win_count
from tblAWCFinalWin
)
where awc.is_test = 'Y'
order by win_rate desc, final_win_rate desc
) t
where t.rnum <= 3
</select>
</mapper>
💡JSP(View)
JSP 계층은 웹 페이지를 구성하고 사용자와의 상호작용을 통해 정보를 표시하고 제어하는 역할을 한다.
여기서 JSP 파일은 HTML과 자바 코드를 혼합하여 동적으로 웹 페이지를 생성하는 데 사용된다.
user.test.worldcup.attraction.view.jsp
<%@page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
<!-- 구글 폰트 로드 -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Black+Han+Sans&display=swap" rel="stylesheet">
<!-- user > test > worldcup > attraction > view.jsp -->
<style>
@import url('https://fonts.googleapis.com/css2?family=Black+Han+Sans&display=swap');
#title {
font-size: 48px;
display: block;
color: #fff;
font-weight: 700;
}
#title+p {
text-shadow: 0 2px 10px rgba(255, 255, 255, 0.8);
padding: 5px 20px;
color: #222222;
font-size: 17px;
background-color: rgba(255, 255, 255, 0.6);
display: inline-block;
border-radius: 50px;
}
#pagetitle {
margin-top: 70px;
}
#title {
margin-bottom: 20px;
}
.item {
position: relative;
width: 25.5%;
aspect-ratio: 0.75;
padding: 0;
box-sizing: border-box;
min-width: 500px;
border: 1px solid #E1E1E1;
margin: 10px 45px 50px 45px;
border-radius: 10px;
transition: all 0.3s;
}
.item:hover {
cursor: pointer;
box-shadow: 12px 12px 17px rgba(0, 0, 0, 0.20);
}
.item {
width: 50%;
height: 600px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: transparent;
border-radius: 8px;
margin: 10px;
padding: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: all 0.35s ease;
transform-origin: center bottom;
cursor: pointer;
font-size: 40px;
font-weight: 600;
color: #333;
position: relative;
overflow: hidden;
}
#result-message {
padding-top: 20px;
color: white;
text-shadow: 0px 1px 5px black;
}
#worldcup-container {
width: 100%;
display: flex;
padding-bottom: 20px;
justify-content: center;
}
#attinfo {
font-size : 18px;
text-align: center;
color: #444;
font-weight: bold;
margin-bottom: 3px;
}
.item div.img-container {
width: 100%;
height: 100%;
background-size: cover;
background-position: center;
}
.item>div:nth-child(1) {
background-color: transparent;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
border-radius: 10px 10px 0 0;
}
.item>div:nth-child(2) {
display: flex;
flex-direction: column;
padding: 20px;
font-size: 1.3rem;
font-weight: bold;
border-radius: 10px 10px 10px 10px;
}
.attraction-container {
text-align: center;
}
.remain-test {
width: 140px;
padding: 5px;
text-align: center;
font-size: 30px;
margin-bottom: 20px;
margin-top: -20px;
color: #fff;
font-weight: bold;
background: linear-gradient(135deg, #3498db, #8e44ad);
border: 1px solid #2980b9;
border-radius: 70px;
display: inline-block;
transition: background 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease, color 0.3s ease;
}
.test-name {
padding: 30px !important;
font-size: 40px !important;
text-align: center;
position: absolute;
color: white;
text-shadow: 0px 1px 5px black;
}
.vs {
position: absolute;
font-family: 'Black Han Sans', sans-serif;
font-size: 100px;
font-style: italic;
color: white;
transform: translateY(162%);
z-index: 1;
transition: all 0.3s;
letter-spacing: 5px;
text-shadow: 3px 3px 10px rgba(0, 0, 0, 0.3);
}
.stats-counter {
background: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url("/dd/resources/files/test/worldcup/attraction/attraction_worldcup_title.png") center center;
background-size: cover;
background-attachment: fixed;
}
/* 모바일 적응형 설정 */
@media screen and (max-width: 600px) {
.item {
width: 90%;
min-width: auto;
height: auto;
font-size: 18px;
}
.item>div:nth-child(2) {
padding: 10px;
font-size: 1rem;
}
#result-message {
font-size: 14px;
}
.test-name {
font-size: 18px !important;
}
#worldcup-container {
padding-bottom: 10px;
}
#remaining-attractions-count {
font-size: 14px;
margin-bottom: 10px;
}
.vs {
font-size: 30px;
transform: translateY(245%);
}
}
</style>
<!-- Main Start -->
<section id="stats-counter" class="stats-counter">
<div id="pagetitle" class="container" data-aos="zoom-out">
<div class="gy-4" style="justify-content: center; width: 100%;">
<div class="col-lg-3 col-md-6" style="width: 100%;">
<div class="stats-item text-center w-100 h-100">
<div id="title">어트랙션 월드컵</div>
<p>나에게 딱 맞는 어트랙션을 찾아보세요!</p>
</div>
</div>
</div>
</div>
</section>
<section id="menu" class="menu">
<div class="container" data-aos="fade-up">
<div class="tab-content" data-aos="fade-up" data-aos-delay="300">
<div class="tab-pane fade active show" id="menu-starters">
<div class="attraction-container">
<div id="remaining-attractions-count" class="remain-test">1 / ${testCount - 1}</div>
<div id="result-info"></div>
<div id="worldcup-container" class="button-container">
<!-- 어트랙션 출력 -->
<c:forEach var="attraction" items="${selectedTwoAttractions}" varStatus="loop">
<div class="item" id="item${loop.index + 1}" onclick="selectAttraction('${attraction.attraction_seq}')">
<div style="display:none" data-attraction-seq="${attraction.attraction_seq}"></div>
<div class="img-container" style="background-image: url('/dd/resources/files/activity/attraction/${attraction.img}');"></div>
<div class="test-name">${attraction.name}</div>
</div>
</c:forEach>
<div class="vs">VS</div>
</div>
</div>
</div>
</div>
</div>
</section>
<script>
let selectedTwoAttractions;
let remainingAttractions;
// CSRF token
var csrfHeaderName = "${_csrf.headerName}";
var csrfTokenValue = "${_csrf.token}";
//페이지가 로드될 때 월드컵 세션 초기화
$(document).ready(function() {
initializeSession();
});
function initializeSession() {
$.ajax({
type : 'GET',
url : '/dd/user/test/worldcup/attraction/initialization.do',
data : {
'isNewSession' : true
},
success : function(data) {
//console.log('세션 초기화');
},
error : function(a, b, c) {
console.error(a, b, c);
}
});
}
function selectAttraction(attractionSeq) {
// 첫 번째 어트랙션의 attraction_seq
const attractionSeq1 = $('#item1 > div:nth-child(1)').data('attraction-seq');
// 두 번째 어트랙션의 attraction_seq
const attractionSeq2 = $('#item2 > div:nth-child(1)').data('attraction-seq');
let winAttractionSeq;
let lostAttractionSeq;
if (attractionSeq !== attractionSeq1) {
winAttractionSeq = attractionSeq2;
lostAttractionSeq = attractionSeq1;
} else if (attractionSeq !== attractionSeq2) {
winAttractionSeq = attractionSeq1;
lostAttractionSeq = attractionSeq2;
} else {
console.error('No matching attractionSeq found.');
return;
}
$.ajax({
type: 'POST',
url: '/dd/user/test/worldcup/attraction/view.do',
data: {
'winAttractionSeq': winAttractionSeq,
'lostAttractionSeq': lostAttractionSeq
},
dataType: 'json',
success: function(data) {
//console.log('선택한 어트랙션:', data.selectedTwoAttractions);
//console.log('남은 어트랙션:', data.remainingAttractionSeqs);
// 전역 변수에 할당
selectedTwoAttractions = data.selectedTwoAttractions;
remainingAttractions = data.remainingAttractions;
// 어트랙션 정보에 따라 화면 갱신
if (selectedTwoAttractions.length > 1) {
refreshScreen();
if (remainingAttractions.length != 2) {
$('#remaining-attractions-count').text(${testCount - 1} - (remainingAttractions.length - 2) + ' / ' + ${testCount - 1});
} else {
$('#remaining-attractions-count').text('결승');
}
} else {
resultScreen(selectedTwoAttractions[0]);
$('#remaining-attractions-count').text('');
// 최종 우승 어트랙션
updateAWCFinalWinCount(selectedTwoAttractions[0].attraction_seq);
}
},
beforeSend : function(xhr) {
xhr.setRequestHeader(csrfHeaderName, csrfTokenValue);
},
error : function(a, b, c) {
console.error(a, b, c);
}
});
}
function refreshScreen() {
//console.log('refreshScreen 함수 호출');
// 모든 어트랙션을 화면에 갱신
$('#worldcup-container').empty();
if (selectedTwoAttractions.length == 2) {
for (let i = 0; i < selectedTwoAttractions.length; i++) {
const attraction = selectedTwoAttractions[i];
const imgUrl = attraction.img ? '/dd/resources/files/activity/attraction/' + attraction.img : 'attraction.png';
// 동적으로 id 생성
const itemId = 'item' + (i + 1);
const item = $('<div class="item" id="' + itemId + '" onclick="selectAttraction(' + attraction.attraction_seq + ')">')
.append('<div style="display:none" data-attraction-seq=' + attraction.attraction_seq + '></div>')
.append('<div class="img-container" style="background-image: url(\'' + imgUrl + '\');"></div>')
.append('<div class="test-name">' + attraction.name + '</div>')
$('#worldcup-container').append(item);
$('#worldcup-container').append('<div class="vs">VS</div>');
}
} else {
const attraction = selectedTwoAttractions[0];
const imgUrl = attraction.img ? '/dd/resources/files/activity/attraction/' + attraction.img : 'attraction.png';
// 동적으로 id 생성
const itemId = 'item3';
const item = $('<div class="item" id="' + itemId + '" onclick="selectAttraction(' + attraction.attraction_seq + ')">')
.append('<div style="display:none" data-attraction-seq=' + attraction.attraction_seq + '></div>')
.append('<div class="img-container" style="background-image: url(\'' + imgUrl + '\');"></div>')
.append('<div class="test-name">' + attraction.name + '</div>');
$('#worldcup-container').append(item);
}
}
function resultScreen(selectedAttraction) {
// 어트랙션을 화면에 갱신
$('#worldcup-container').empty();
$('.attraction-container').css({
'margin-top': '0px',
'text-align': 'center',
'font-weight': 'bold',
'font-size': '30px',
'background-color': '#ecf0f1',
'border-radius': '10px',
'box-shadow': '0 4px 6px rgba(0, 0, 0, 0.1)',
'transition': 'background-color 0.3s ease'
});
$('.remain-test').remove();
const resultContainer = $('<div class="item result-container" id="item3">');
const imgContainer = $('<div class="img-container" style="background-image: url(\'/dd/resources/files/activity/attraction/' + selectedAttraction.img + '\');"></div>');
const message = $('<p id="result-message">최고의 어트랙션이죠!<br>[' + selectedAttraction.name + ']</p>'
+ '<p id="attinfo">클릭시 해당 어트랙션 페이지로 이동합니다.</p>');
// 메시지
$('#result-info').append(message);
// 최종 선택 어트랙션
resultContainer.append(imgContainer);
// 클릭 시 어트랙션 상세 페이지로 이동
resultContainer.click(function() {
window.location.href = '/dd/user/activity/attraction/detail.do?seq=' + selectedAttraction.attraction_seq;
});
// #worldcup-container에 추가
$('#worldcup-container').append(resultContainer);
}
function updateAWCFinalWinCount(finalWinAttractionSeq) {
$.ajax({
type: 'POST',
url: '/dd/user/test/worldcup/attraction/final.do',
data: {
'finalWinAttractionSeq': finalWinAttractionSeq
},
success: function(data) {
// console.log('최종 우승 업데이트 완료: ', data);
},
beforeSend: function(xhr) {
xhr.setRequestHeader(csrfHeaderName, csrfTokenValue);
},
error: function(a, b, c) {
console.error('Error during final update:', a, b, c);
}
});
}
</script>
<!-- Main End -->
admin.test.worldcup.attraction.view.jsp
<%@page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
<style>
#main h1 {
font-size: 2rem !important;
margin-top: 45px !important;
margin-left: 10px;
}
.d-md-block {
margin-right: 15px;
}
.pagetitle {
margin-top: 10px;
}
.col-12 {
margin-top: 15px;
}
.col-lg-8 {
width: 100%;
}
.card-body {
min-height: 600px;
}
div.header {
height: 60px;
border-radius: 5px;
}
#search {
margin-bottom: 15px;
padding: 7px;
}
.search-form {
width: 100%;
margin: 0;
}
.header .search-form input {
border: 0;
height: 50px;
}
.header .search-form input:focus, .header .search-form input:hover {
outline: none;
border: none;
box-shadow: none;
transition: none;
}
.card-body .header {
display: flex;
align-items: center;
justify-content: space-between;
}
.breadcrumb {
margin-right: 15px;
margin-top: 30px;
margin-bottom: 10px;
}
.breadcrumb a {
color: #012970;
}
.breadcrumb a:hover {
color: #0d6efd;
}
.table {
text-align: center;
}
th {
background-color: #f2f2f2 !important;
}
.table th:nth-child(1) { width: 10%; }
.table th:nth-child(2) { width: 30%; }
.table th:nth-child(3) { width: 25%; }
.table th:nth-child(4) { width: 25%; }
.table th:nth-child(5) { width: 10%; }
.table td i {
color: #0d6efd;
margin-top: 7px;
}
.table td a {
color: #000;
}
.table td a:hover {
color: #0d6efd;
}
.pagination {
justify-content: center;
margin-top: 40px;
}
.form-check {
min-height: 0 !important;
}
.hidden-seq {
display: none;
}
/* 모달 CSS */
#modal table.m-desc {
width: 100%;
font-size: 14px;
}
#modal table tr > th {
width: 120px;
text-align: left;
font-weight: bold;
background: #FFF !important;
padding: 10px;
}
#modal table tr > td {
padding: 10px;
}
</style>
<!-- Main Start -->
<main id="main" class="main">
<div class="pagetitle">
<h1>어트랙션 월드컵 관리</h1>
</div>
<section class="section">
<div class="row">
<div class="col-lg-8">
<div class="row">
<div class="col-12">
<div id="search" class="header">
<form method="GET" action="/dd/admin/test/worldcup/attraction/view.do" class="search-form d-flex align-items-center">
<input type="text" name="word" id="search-field" placeholder="제목 또는 내용을 입력하세요." autocomplete="off">
<button type="submit"><i class="bi bi-search"></i></button>
</form>
</div>
<!-- 어트랙션 월드컵 상세 모달 -->
<div id="modal" class="modal fade show" tabindex="-1" aria-labelledby="exampleModalScrollableTitle" aria-modal="true" role="dialog">
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 id="modal-name" class="modal-title"></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<table class="m-desc">
<colgroup>
<col style="width: 100px">
</colgroup>
<tbody>
<tr>
<th>최종우승횟수</th>
<td class="m-awc_final_win_count"></td>
</tr>
<tr>
<th>승리횟수</th>
<td class="m-awc_win_count"></td>
</tr>
<tr>
<th>1:1 대결수</th>
<td class="m-awc_match_count"></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-body">
<nav class="d-flex justify-content-end">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/dd/admin/activity/attraction/view.do">어트랙션 관리</a></li>
</ol>
</nav>
<table class="table">
<thead>
<tr>
<!-- <th></th> -->
<th>No</th>
<th>이름</th>
<th>우승비율 (우승횟수/게임횟수)</th>
<th>승률 (승리횟수/대결수)</th>
<th>테스트 채택</th>
</tr>
</thead>
<tbody>
<c:forEach items="${listAttraction}" var="dto" varStatus="status">
<tr>
<!-- <td><input type="checkbox"></td> -->
<td>${map.totalPosts - status.index - map.startIndex + 1}</td>
<td><a onclick="showModal('${dto.attraction_seq}', '${dto.name}', '${dto.awc_final_win_count}', '${dto.awc_win_count}', '${dto.awc_match_count}')"><c:out value="${fn:substring(dto.name, 0, 22)}${fn:length(dto.name) > 22 ? '...' : ''}" /></a></td>
<td>
<div class="progress" style="height: 20px;">
<div class="progress-bar" role="progressbar"
style="width: ${dto.awc_final_win_count != 0 ? String.format('%.2f', (dto.awc_final_win_count / (awcFinalWinTotalCount / 5)) * 100) : '0'}%;"
aria-valuenow="${dto.awc_final_win_count != 0 ? String.format('%.2f', (dto.awc_final_win_count / (awcFinalWinTotalCount / 5)) * 100) : '0'}"
aria-valuemin="0" aria-valuemax="100"
data-bs-toggle="tooltip" data-bs-placement="top"
title="${dto.awc_final_win_count}/${awcFinalWinTotalCount}">
${dto.awc_final_win_count != 0 ? String.format('%.2f', (dto.awc_final_win_count / awcFinalWinTotalCount) * 100) : '0'}%
</div>
</div>
</td>
<td>
<div class="progress" style="height: 20px;">
<div class="progress-bar" role="progressbar"
style="width: ${dto.awc_win_count != 0 && dto.awc_match_count != 0 ? String.format('%.2f', (dto.awc_win_count / dto.awc_match_count) * 100) : '0'}%;"
aria-valuenow="${dto.awc_win_count != 0 && dto.awc_match_count != 0 ? String.format('%.2f', (dto.awc_win_count / dto.awc_match_count) * 100) : '0'}"
aria-valuemin="0" aria-valuemax="100"
data-bs-toggle="tooltip" data-bs-placement="top"
title="${dto.awc_win_count}/${dto.awc_match_count}">
${dto.awc_win_count != 0 && dto.awc_match_count != 0 ? String.format('%.2f', (dto.awc_win_count / dto.awc_match_count) * 100) : '0'}%
</div>
</div>
</td>
<td>
<div class="d-flex justify-content-center">
<div class="form-check form-switch">
<c:choose>
<c:when test="${dto.is_test eq 'Y'}">
<input class="form-check-input" type="checkbox" id="flexSwitchCheckDefault" checked>
</c:when>
<c:otherwise>
<input class="form-check-input" type="checkbox" id="flexSwitchCheckDefault">
</c:otherwise>
</c:choose>
</div>
</div>
</td>
<td class="hidden-seq">${dto.attraction_seq}</td>
</tr>
</c:forEach>
</tbody>
</table>
<ul class="pagination justify-content-center">
<c:forEach begin="1" end="${map.totalPages}"
varStatus="pageStatus">
<c:choose>
<c:when test="${pageStatus.index == currentPage}">
<li class="page-item active"><span class="page-link">${pageStatus.index}</span></li>
</c:when>
<c:otherwise>
<li class="page-item"><a class="page-link"
href="/dd/admin/test/worldcup/attraction/view.do?page=${pageStatus.index}">${pageStatus.index}</a></li>
</c:otherwise>
</c:choose>
</c:forEach>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</main>
<script>
// 문서가 완전히 로드 된 뒤에 실행
$(document).ready(function() {
// 체크박스 클릭 이벤트
$(document).on('change', '.form-check-input', function() {
// 테스트 채택
var isTest = $(this).is(':checked') ? 'Y' : 'N';
// 선택한 어트랙션 일련번호
var attractionSeq = $(this).closest('tr').find('td:nth-child(6)').text();
// CSRF token
var csrfHeaderName = "${_csrf.headerName}";
var csrfTokenValue = "${_csrf.token}";
// 데이터베이스 업데이트
$.ajax({
type : 'POST',
url : '/dd/admin/test/worldcup/attraction/view.do',
data : {
attractionSeq : attractionSeq,
isTest : isTest
},
beforeSend : function(xhr) {
xhr.setRequestHeader(csrfHeaderName, csrfTokenValue);
},
/*
success: function(response) {
// console.log(response); // 응답 처리
},
*/
error : function(a, b, c) {
console.error(a, b, c);
}
});
});
});
// 검색
<c:if test="${map.searchStatus == 'y'}">
$('#search-field').val('${map.word}');
</c:if>
$(document).keydown(function(event) {
if (event.key === 'F5') {
location.href='/dd/admin/test/worldcup/attraction/view.do';
}
});
// 어트랙션 월드컵 상세 모달
function showModal(seq, name, awc_final_win_count, awc_win_count, awc_match_count) {
$('#modal-name').text(name);
$('.m-awc_final_win_count').text(awc_final_win_count);
$('.m-awc_win_count').text(awc_win_count);
$('.m-awc_match_count').text(awc_match_count);
$('#modal').modal('show');
}
</script>
<!-- Main End -->
이상형 월드컵 페이지
참고 자료
Spring Boot Architecture, JavaTpoint, 2024.02.28.
[Spring] 레이어드 아키텍쳐(Layered Architecture), skyriv312079, 2022.07.25.
Layered Architecture Deep Dive, Solomon Maeng, 2021.09.17.