Image 001
Image 002
Image 003
Image 004
Image 005
Image 006
Image 007
Image 008
Image 009
Image 010
Image 011
Image 012
Image 013
Image 014
Image 015
Image 016
Image 017
Image 018
Image 019
Image 020
Image 021
Image 023
Image 024
Image 025
Image 026
Image 027
Image 028
Image 029
Image 030
Image 031
Image 032
Image 033
Image 034
Image 035
Image 036
Image 037
Image 038
Image 039
Image 040
Image 041
Image 042
Image 043
Image 044
Image 045
Image 046
Image 047
Image 048
Image 049
Image 050
Image 051
Image 052
Image 086
Image 087
Image 089

프로젝트 개요

  • 기획 배경
    • 1. 맞춤형 추천 서비스의 편의를 제공하여 즐거운 놀이공원 체험을 할 수 있도록 한다.
    • 2. 어트랙션 예약 서비스를 도입하여 사용자의 어트랙션 예약 대기 시간을 최소화한다.
    • 3. 기프트샵 상품 구매 서비스를 온라인으로 제공하여, 놀이공원에 방문하지 않아도 원하는 상품을 구매할 수 있도록 한다.
  • 구현 목표
    • 1. 회원 기능과 관리자 기능을 별개의 페이지로 분리하고, 적절한 태그 및 모달을 활용하여 정보를 제공한다.
    • 2. 반응형 디자인을 통해 모바일 환경에서도 편리하게 이용할 수 있도록 한다.
    • 3. Spring MVC Model 및 디자인 패턴을 적용하고, 각 계층을 분리하여 프로젝트의 유지보수성 및 확장성을 확보한다.
    • 4. Socket (채팅) 및 Open API를 활용한다.
  • Category
  • Spring 프로젝트
  • Period
  • 2023.12.18.~2023.12.27.
  • GitHub
  • https://github.com/Isaac-Seungwon/dd-land
Web Image 001
Web Image 002
Web Image 003
Web Image 004
Web Image 005
Web Image 006
Web Image 007
Web Image 008
Web Image 009
Web Image 010
Web Image 011
Web Image 012
Web Image 013
Web Image 014
Web Image 015
Web Image 016
Web Image 017
Web Image 018
Web Image 019
Web Image 020
Web Image 021
Web Image 022
Web Image 023
Web Image 024
Web Image 025
Web Image 026
Web Image 027
Web Image 028
Web Image 029
Web Image 030
Web Image 031
Web Image 032
Web Image 033
Web Image 034

기능 상세

  • DD 월드컵
    어트랙션 월드컵, 코스 월드컵을 진행하여 사용자에게 추천해주고, 관리자 페이지를 통해 어트랙션과 코스를 관리한다.
  • MBTI 추천
    사용자의 MBTI 선택을 통해 어트랙션과 코스를 추천하고, 관리자 페이지를 통해 MBTI CRUD를 관리한다.
  • Spring Security
    로그인 인증 및 인가를 통해 로그인한 사용자가 회원 또는 관리자인지 구분한다.
  • Socket (채팅)
    Web socket을 Open Chat에 적용하여 실시간으로 양방향 통신을 가능하게 한다.

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"; 
	}

}


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);
	}

}

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; // 글번호
	
}

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>


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();

}

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();
	}
	
}


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();

}

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();
	}
	
}


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();

}


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 -->

ChatController

package com.project.dd.server.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

/**
 * 오픈채팅 관련 요청을 처리하는 Controller 클래스
 * 
 * @author 이승원
 */

@Controller
public class ChatController {

	/**
	 * 닉네임을 적어 채팅방에 들어갈 수 있는 페이지를 보여주는 메서드
	 *
	 * @param model Model 객체
	 * @return 오픈채팅 view
	 */
	@GetMapping(value = "/user/chat/view.do")
	public String view(Model model) {

		return "user/chat/view";
	}

	/**
	 * 실제 채팅을 하는 창을 보여주는 메서드
	 *
	 * @param model Model 객체
	 * @return 채팅 창 view
	 */
	@GetMapping(value = "/user/chat/chat.do")
	public String chat(Model model) {

		return "user/chat/chat";
	}

}

Message

package com.project.dd.server.domain;

public class Message {

	private String code; // 상태코드
	private String sender; // 보내는 사람
	private String receiver; // 받는 사람
	private String content; // 내용
	private String regdate; // 날짜

	public String getCode() {
		return code;
	}

	public void setCode(String code) {
		this.code = code;
	}

	public String getSender() {
		return sender;
	}

	public void setSender(String sender) {
		this.sender = sender;
	}

	public String getReceiver() {
		return receiver;
	}

	public void setReceiver(String receiver) {
		this.receiver = receiver;
	}

	public String getContent() {
		return content;
	}

	public void setContent(String content) {
		this.content = content;
	}

	public String getRegdate() {
		return regdate;
	}

	public void setRegdate(String regdate) {
		this.regdate = regdate;
	}

	@Override
	public String toString() {
		return "Message [code=" + code + ", sender=" + sender + ", receiver=" + receiver + ", content=" + content
				+ ", regdate=" + regdate + "]";
	}
}

ChatServer

package com.project.dd.server;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

//import javax.inject.Inject;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
//import javax.websocket.server.ServerEndpointConfig;
import org.springframework.stereotype.Component;
//import org.springframework.web.socket.server.standard.SpringConfigurator;

import com.google.gson.Gson;
import com.project.dd.server.domain.Message;
	
/**
 * 채팅의 서버 역할을 하는 클래스
 * 
 * @author 이승원
 */

@ServerEndpoint(value = "/chatserver.do") /* , configurator = com.project.dd.server.ServerEndpointConfig.class */
public class ChatServer {

	// 현재 채팅 서버에 접속 중인 클라이언트 목록
	// @Inject
	private static List sessionList = new ArrayList();

	// 클라이언트 접속
	@OnOpen
	public void handleOpen(Session session) {

		System.out.println("클라이언트가 접속했습니다.");

		sessionList.add(session); // 현재 접속자의 정보를 배열에 추가

		// 현재 대화방에 누가 있는지?
		checkSessionList();

		// 대화방에 혹시라도 연결이 끊긴 사람이 있으면 제거
		clearSessionList();
	}

	// 클라이언트로부터 메시지 전달
	@OnMessage
	public void handleMessage(String msg, Session session) {

		System.out.println(msg);

		// JSON 형식의 문자열 -> 자바 클래스 객체로 변환
		Gson gson = new Gson();

		Message message = gson.fromJson(msg, Message.class);

		if (message.getCode().equals("1")) {

			for (Session s : sessionList) {

				// 모든 접속자 중에서 방금 메시지를 보낸 세션을 제외하고 검색
				if (s != session) {
					// 본인 이외의 세션(소켓)에게 현재 접속자를 알리는 메시지 전달
					try {
						s.getBasicRemote().sendText(msg);
					} catch (Exception e) {
						e.printStackTrace();
					}

				}
			}
		} else if (message.getCode().equals("2")) {

			// 기존 접속자가 대화방을 나갔습니다.
			sessionList.remove(session);

			for (Session s : sessionList) {
				try {
					// '누군가가 나갔습니다.'라는 메시지를 남아있는 모든 사람에게 전달
					s.getBasicRemote().sendText(msg);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		} else if (message.getCode().equals("3")) {

			// 대화 메시지
			// - 보낸 사람 빼고 나머지 사람에게 전달
			for (Session s : sessionList) {
				if (s != session) { // 방금 메세지 보낸 사람 빼고 나머지 사람들을 찾아서 메세지 전달하기
					try {
						s.getBasicRemote().sendText(msg);
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			}

		} else if (message.getCode().equals("4")) {

			// 대화 메시지
			// - 보낸 사람 빼고 나머지 사람에게 전달
			for (Session s : sessionList) {
				if (s != session) { // 방금 메세지 보낸 사람 빼고 나머지 사람들을 찾아서 메세지 전달하기
					try {
						s.getBasicRemote().sendText(msg);
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			}
		}
	}

	// 어떤 접속자들(세션들)이 접속해 있는지 확인하는 메서드
	private void checkSessionList() {
		System.out.println("Session List");
		for (Session session : sessionList) {
			System.out.println();
			System.out.println(session.getId());
		}
		System.out.println();
	}

	private void clearSessionList() {

		// List 계열의 컬렉션은 향상된 for문 내에서 요소 추가/삭제 하는 행동이 불가능하다.
		// 따라서 해당 업무를 위해서는 다음의 방법이 있다.
		// 1. 일반 for문 사용
		// 2. Iterator 사용

		Iterator iter = sessionList.iterator();

		while (iter.hasNext()) {

			if (!(iter.next()).isOpen()) { // 혹시 연결이 끊어진 세션이 있을 경우
				iter.remove(); // 리스트에서 제거
			}
		}
	}
}

user.chat.chat.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<style>
html, body {
	padding: 0 !important;
	margin: 0 !important;
	background-color: #FFF !important; 
	display: block;
	overflow: hidden;
}

body > div {
	margin: 0; 
	padding: 0; 
}

#main {
	width: 400px;
	height: 510px;
	margin: 3px;
	display: grid;
	grid-template-rows: repeat(12, 1fr);
}

#header > h2 {		
	margin: 0px;
	margin-bottom: 10px;
	padding: 5px;
}

#list {
	border: 1px solid var(--border-color);
	box-sizing: content-box;
	padding: .5rem;
	grid-row-start: 2;
	grid-row-end: 12;
	font-size: 14px;
	overflow: auto;
}

#msg {
	margin-top: 3px;
}

#list .item {
	font-size: 14px;
	margin: 15px 0;
}

#list .item > div:first-child {
	display: flex;
}

#list .item.me > div:first-child {
	justify-content: flex-end;
}

#list .item.other > div:first-child {
	justify-content: flex-end;
	flex-direction: row-reverse;
}

#list .item > div:first-child > div:first-child {
	font-size: 10px;
	color: #777;
	margin: 3px 5px;
}

#list .item > div:first-child > div:nth-child(2) {
	border: 1px solid var(--border-color);
	display: inline-block;
	min-width: 100px;
	max-width: 250px;
	text-align: left;
	padding: 3px 7px;
}

#list .state.item > div:first-child > div:nth-child(2) {
	background-color: #EEE;
}

#list .item > div:last-child {
	font-size: 10px;
	color: #777;
	margin-top: 5px;
}

#list .me {
	text-align: right;
}

#list .other {
	text-align: left;
}

#list .msg.me.item > div:first-child > div:nth-child(2) {
	background-color: rgba(255, 99, 71, .2);
}

#list .msg.other.item > div:first-child > div:nth-child(2) {
	background-color: rgba(100, 149, 237, .2);
}

#list .msg img {
	width: 150px;
}

span {
	color : #ce1212;
}
</style>

<main>
<div id="main">
	<div id="header"><h2><span>.</span>D_D<span>.</span> <small>Isaac</small></h2></div>
	<div id="list"></div>
	<input type="text" id="msg" placeholder="대화 내용을 입력하세요.">
</div>
</main>

<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script>

	/*
		메시지 규칙
		- code: 상태코드
			- 1: 새로운 유저가 들어옴
			- 2: 기존 유저가 나감
			- 3: 메시지 전달
		- sender: 보내는 유저명
		- receiver: 받는 유저명
		- content: 대화 내용
		- regdate: 날짜/시간
	*/

	let name;
	let ws;
	const url = 'ws://43.200.60.152:8080/dd/chatserver.do';
	/* cmd의 ipconfig 명령어로 나오는 ip주소를 url에 적어야 다른 컴퓨터에서 이 주소를 ip를 입력한 주소를 통해 들어올 수 있다. */
	
	function connect(name) {
		
		window.name = name;
		$('#header small').text(name);
		
		//연결하기 > 소켓 생성
		ws = new WebSocket(url);
		
		//연결 후 작업
		ws.onopen = function(evt) {
			log('서버 연결 성공');
			
			//메시지 규칙
			//ws.send('1|Isaac'); //상태코드(1): 본인대화명
			//ws.send('2|1|2|점심 뭐먹어?'); //상태코드(2): 서로간에 주고받는 대화 > 상태코드|보내는사람|받는사람|대화내용
			//ws.send('3|Isaac'); //상태코드(3): 대화방을 나가는 사람
			
			let message = {
					code: '1',
					sender: window.name,
					receiver: '',
					content: '',
					regdate: new Date().toLocaleString()
			};
			
			ws.send(JSON.stringify(message));
			print('', '대화방에 참여했습니다.', 'me', 'state', message.regdate);
			
			$('#msg').focus();
		};
		
		//서버에서 클라이언트에게 전달한 메시지
		ws.onmessage = function(evt) {
			log('메시지 수신');
			
			//console.log(evt.data);
			
			let message = JSON.parse(evt.data);
			//console.log(message);
			
			if (message.code == '1') {
				print('',`[\${message.sender}]님이 들어왔습니다.`, 'other', 'state', message.regdate);
			} else if (message.code == '2') {
				print('',`[\${message.sender}]님이 나갔습니다.`, 'other', 'state', message.regdate);
			} else if (message.code == '3') { //남이 나한테 보낸것을 화면에 출력하기
				print(message.sender, message.content, 'other', 'msg', message.regdate);
			} else if (message.code == '4') { //남이 나한테 보낸것을 화면에 출력하기
				printEmoticon(message.sender, message.content, 'other', 'msg', message.regdate);
			}
		}
	}//connect
	
	function log(msg) {
		console.log(`[\${new Date().toLocaleTimeString()}] \${msg}`);
	}
	
	//대화창 출력
	function print(name, msg, side, state, time) {
		
		let temp = `
		
		<div class="item \${state} \${side}">
			<div>
				<div>\${name}</div>
				<div>\${msg}</div>
			</div>
			<div>\${time}</div>
		</div>
		
		`;
		
		$('#list').append(temp);
		
		//새로운 내용 추가하고 스크롤을 바닥으로 내림
		scrollList();
	}
	
	//이모티콘 출력
	function printEmoticon(name, msg, side, state, time) {
		
		let temp = `
		
		<div class="item \${state} \${side}">
			<div>
				<div>\${name}</div>
				<div style="background-color: #FFF; border: 0;"><img src="/dd/resources/files/emoticon\${msg}.png"></div>
			</div>
			<div>\${time}</div>
		</div>
		
		`;
		
		$('#list').append(temp);
		
		//이모티콘이 추가된 후 스크롤을 바닥으로 내림
		setTimeout(scrollList, 100);
	}
	
	//창이 닫히기 바로 직전 발생
	$(window).on('beforeunload', function() {
		disconnect();
	});
	
	function disconnect() {
		
		//대화방에서 나가면 다른 사람에게 알리기
		let message = {
				code: '2',
				sender: window.name,
				receiver: '',
				content: '',
				regdate: new Date().toLocaleString()
		};
		
		ws.send(JSON.stringify(message));
	}
	
	$('#msg').keydown(function(evt) {
		
		if (evt.keyCode == 13) {
			
			//입력한 대화 내용을 서버로 전달
			//ws.send('전달 내용'); 서버가 내용을 구분할 수 있도록 object 틀에 맞춰서 전달
			let message = {
					code: '3',
					sender: window.name,
					receiver: '',
					content: $('#msg').val(),
					regdate: new Date().toLocaleString()
			};
			
			//일반대화 및 이모티콘을 구분하여 서버로 전달하는 과정
			if ($('#msg').val().startsWith('/')) {
				//대화(X) > 이모티콘(O)
				message.code = '4';
				//alert(message.content);
			}
			
			//객체를 보낼수는 없으므로 주고 받기 쉬운 JSON 형태의 format으로 변경하여 전달
			ws.send(JSON.stringify(message));
			
			//다시 대화를 입력해야하니까 초기화 시키고 focus 잡아주기
			$('#msg').val('').focus();
			
			if (message.code == '3') {
				print(window.name, message.content, 'me', 'msg', message.regdate);
			} else if (message.code == '4') {
				printEmoticon(window.name, message.content, 'me', 'msg', message.regdate);
			}
		}
	});
	
	function scrollList() {
		$('#list').scrollTop($('#list').outerHeight() + 300);
		//큰 값을 주면 움직이긴 하나, 대화의 길이가 길어지면 해당 값 이후로는 스크롤바가 움직이지 않으므로 절대값이 아닌 상대값을 주며, 대화창의 높이를 가져온다.
	}
</script>

HikariCPTest

package com.test.java.db;

import static org.junit.Assert.assertNotNull;

import java.util.ArrayList;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.project.dd.TestMapper;
import com.project.dd.login.domain.LoginDTO;

/**
 * HikariCP 라이브러리를 이용해 JDBC 커넥션 풀이 관리되는지 Test하는 클래스 입니다.
 * 
 * @author 이승원
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({ "file:src/main/webapp/WEB-INF/spring/root-context.xml",
		"file:src/main/webapp/WEB-INF/spring/security-context.xml" })
public class HikariCPTest {

	@Autowired
	private PasswordEncoder encoder;

	@Autowired
	private TestMapper mapper;

	/**
	 * 데이터베이스에서 사용자 목록을 가져와 각 사용자의 비밀번호를 암호화하여 업데이트하는 테스트를 수행하는 메서드 입니다.
	 */
	@Test
	public void testUpdatePw() {
		assertNotNull(mapper);

		ArrayList list = mapper.select();

		int count = mapper.count();

		for (LoginDTO dto : list) {
			String pw = encoder.encode(dto.getPw());
			String seq = dto.getUser_seq();

			int result = mapper.update(seq, pw);

			if (result == 1) {
				System.out.println("변경");
			} else {
				System.out.println("변경 안됨");
			}
		}
	}
	
}


JDBCTest

package com.test.java.db;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

import java.sql.Connection;
import java.sql.DriverManager;

import org.junit.Test;

/**
 * JDBC 연결 테스트를 위한 JUnit 테스트 클래스 입니다.
 * 	
 * @author 이승원
 */
public class JDBCTest {

	/**
	 * 데이터베이스 연결을 테스트하는 메서드 입니다.
	 */
	@Test
	public void testConnection() {

		try {

			// 오라클 드라이버 로딩
			Class.forName("oracle.jdbc.driver.OracleDriver");

			Connection conn = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:xe", "dd", "java1234");

			assertNotNull(conn); // DB 연결 성공 시 True

			assertEquals("DB 연결", false, conn.isClosed()); // 기대값이 같은지를 검증하여 conn.isClose()가 false와 같을 경우 성공

		} catch (Exception e) {
			e.printStackTrace();
		}

	}

}

프로젝트 측면

Spring 프로젝트를 통해 Controller, Mapper, Service 등의 각 영역의 역할과 의존성을 주입하는 과정에 대해 이해할 수 있었습니다. 또한, 프로젝트를 진행하면서 팀원들과 자주 회의를 진행했습니다. 부분적인 내용이라도 팀원들과 공유하였고, 이를 통해 사전에 오류를 방지하면서 팀 프로젝트에서 소통의 중요성을 알 수 있었습니다.


개인적인 측면

언제나 제가 상상하던 걸 실제로 만들 수 있게 되면 좋겠다고 생각했습니다. 프로그램을 사용할 때면 어떻게 기능이 구현되는지에 대한 궁금증이 있었고, 복잡한 코드의 의미를 알고 싶었습니다. 이번 Spring 프로젝트는 최종 프로젝트인 만큼 Front-End, Back-End 구축에 있어서 이전까지 학습한 다양한 기술을 적극적으로 활용했습니다.

Socket 통신을 구현함으로써 실시간 채팅 기능을 추가하고, Elasticsearch를 활용하여 검색 기능을 강화하는 과정을 통해 기술에 대한 이해도를 높일 수 있었습니다. Spring 프로젝트 경험은 개발자로서의 성장뿐만 아니라, 직접 기획한 프로젝트를 성공적으로 구현함으로써 자신감을 얻을 수 있는 시간이었다고 생각합니다.


코드에 대한 피드백

프로젝트를 진행하는 동안 짧은 기간 내에 기능을 완성하기 위해서 팀원들 모두 각자의 역할에 집중했습니다. 결국 프로젝트를 완성할 수는 있었지만, 서로의 코드에 피드백을 주고받지 못한 것이 아쉽게 느껴졌습니다. 중복 기능을 효율적으로 구현하기 위해 검색과 모달 기능의 코드를 공유했지만, 목적이나 코드에 대한 설명은 부족한 측면이 있었습니다.

프로젝트 중간에 코드 리뷰와 피드백을 진행했다면, 최적화가 가능한 부분을 발견하여 코드 품질과 프로젝트의 완성도를 높일 수 있었을 것으로 생각합니다. 앞으로는 서로의 코드를 심층적으로 검토하며 의견을 나누어, 팀 전체의 업무 효율을 향상시키고 프로젝트의 품질을 한 단계 더 높일 수 있도록 노력하겠습니다.


코드 충돌 문제

프로젝트를 진행하면서 코드를 통합할 때 코드가 충돌하여 문제가 발생하는 경우가 있었습니다. 유닛 테스트, 통합 테스트 등을 수행하여 코드의 안정성을 높일 수 있도록 개선하면 좋겠다는 생각이 들었습니다.