본문 바로가기
Project/3차 프로젝트 리뷰

3차 프로젝트 - 'CarPlanet' 리뷰

by ys2ys2 2024. 12. 3.

프로젝트 개요     

  • 프로젝트 주제 : 주유소/전기차 충전소 정보 제공 및 현재 위치 기준 목적지 추천 시스템
  • 기간 : 2024.11.11 ~ 2024.12.10
  • 팀 내 역할 : 팀장 (프로젝트 관리 및 개발)
  • 개발 페이지 : 전기차 충전소 페이지, 마이페이지, 회원정보 변경 로직
  • 기술 스택

기술 스택

 


프로젝트 취지     

현대 사회에서 자동차는 필수적인 이동 수단입니다.

실제로 자가용이나 승용차의 평소 이용률은 약 70% 이상을 차지하고 있으며, 많은 사람들이 자동차를 통해 일상생활을 유지하고 있습니다.

하지만 자동차를 유지하는 데 가장 큰 부담이 되는 요소는 바로 유지비입니다. 특히, 연비가 유지비의 큰 부분을 차지하고 있기 때문에 많은 사람들이 저렴한 주유소를 찾거나, 상대적으로 경제적인 전기차나 하이브리드 자동차에 관심을 보이고 있습니다.

이러한 흐름 속에서 저희는 전국의 저렴한 주유소 정보를 제공하고, 동시에 전기차 충전소 정보를 한눈에 확인할 수 있는 시스템을 개발하려고 했습니다.

또한, 단순히 정보 제공에 그치는 것이 아니라 사용자들 간에 커뮤니티 기능을 추가하여 자동차 관련 정보를 자유롭게 교환하고 소통할 수 있는 공간도 함께 제공하려고 합니다.

이번 프로젝트는 사용자들의 현재 위치를 기준으로 최적의 주유소나 전기차 충전소를 추천해주며, 더 나아가 자동차를 이용하는 사람들이 보다 효율적이고 경제적인 선택을 할 수 있도록 돕는 것을 목표로 하고 있습니다.

 

프로젝트 시작 배경
프로젝트 시작 배경


 

 

 

✅전기차 충전소 페이지

 

전기차 충전소 페이지입니다. 최초 페이지에 접속하게 되면 사용자에게 현재 위치를 받게 됩니다.

위치를 받게 되면 카카오 지도 API의 마커 기능을 활용하여 현재 위치에 마커가 찍히게 구현하였습니다.

HTML5의 표준 API 기능인 Geolocation API를 활용하였습니다.

function setMapToCurrentLocation() {
    if (navigator.geolocation) {
        navigator.geolocation.getCurrentPosition(
            function(position) {
                const latitude = position.coords.latitude;
                const longitude = position.coords.longitude;
                
                const userLocation = new kakao.maps.LatLng(latitude, longitude);
                map.setCenter(userLocation);
                
                const userMarker = new kakao.maps.Marker({
                    position: userLocation,
                    map: map
                });
                
            },
            function(error) {
                alert("현재 위치를 가져올 수 없습니다. 위치 정보를 허용해 주세요.");
            },
            {
                enableHighAccuracy: true,
                timeout: 10000,
                maximumAge: 0 
            }
        );
    } else {
        alert("이 브라우저에서는 현재 위치를 가져올 수 없습니다.");
    }
}

 

현재 위치 요청

 

현재 위치 마커

 


 

사용자는 화면 좌측에 위치한 네비게이션 영역을 통해

충전소 검색충전소 길 찾기 기능을 사용할 수 있도록 구현하였습니다.

사용자는 드롭다운 리스트를 통해 지역을 선택하고(시/도 및 시/군)

충전 타입을 선택 후 검색하기 버튼을 통해 해당 지역의 전기차 충전소 리스트를 실시간으로 제공받게 됩니다.

또한 사용자들의 UI/UX를 고려하여

해당 충전소의 주소, 충전기 사용 가능 여부, 충전 용량, 충전기 타입을 직관적으로 제공하였습니다.

 

위 기능을 구현할 때는 공공데이터 포털의 한국환경공단_전기자동차 충전소 정보 API를 활용하여 구현하였습니다.
API 공식 문서를 참고하여 시/도, 시/군에 해당하는 타입충전기 타입 값을 변수화하였으며,

이를 API 요청 경로에 활용하였습니다.

 

document.addEventListener("DOMContentLoaded", function() {
    // 시/군 데이터 저장
    const districtData = {
        "11": [
            { value: "11680", text: "강남구" },
            { value: "11740", text: "강동구" },
            { value: "11305", text: "강북구" },
            ...
        ],
        "26": [
            { value: "26440", text: "강서구" },
            { value: "26410", text: "금정구" },
            { value: "26710", text: "기장군" },
    		...
        ],
	    "27": [
	        { value: "27200", text: "남구" },
	        { value: "27290", text: "달서구" },
	        { value: "27710", text: "달성군" },
			...
	    ]
};
// 충전기 타입 코드 조합
const chargerTypeMap = {
    "01": ["01", "03", "05", "06"], // DC차데모
    "02": ["02", "08"],             // AC완속
    "03": ["03", "06"],             // DC차데모+AC3상
    "04": ["04", "05", "06", "08"], // DC콤보
    "05": ["05", "06"],             // DC차데모+DC콤보
    "06": ["06"],                   // DC차데모+AC3상+DC콤보
    "07": ["07", "03", "06"],       // AC3상
    "08": ["02", "08"],             // DC콤보(완속)
    "89": ["89"]                    // H2
};
const chargerStatusMap = {
	//충전기 사용 여부에 따른 시각화
    "1": { text: "사용 불가", class: "status-unavailable" },
    "2": { text: "사용 가능", class: "status-available" },
    "3": { text: "사용 중", class: "status-in-use" },
    "4": { text: "운영 중지", class: "status-out-of-service" },
    "5": { text: "점검 중", class: "status-under-maintenance" },
    "9": { text: "상태 미확인", class: "status-unknown" }
};

충전소 리스트

 


 

충전소 리스트에서 특정 충전소를 클릭하면,

해당 위치에 마커와 함께 충전소 정보를 담은 팝업창이 표시되도록 구현하였습니다.

팝업창에는 사용자에게 더욱 상세한 정보를 제공하기 위해 충전소 정보, 충전기 상태, 이용 시간 등을 포함하였으며, UI/UX를 고려하여 사용자들이 가장 많이 찾는 정보를 시각적으로 확인할 수 있도록 설계하였습니다.

특히, 전기차 충전소의 주요 문제 중 하나인 충전기 고장 여부를 쉽게 확인할 수 있도록 충전기 상태 탭을 추가하였으며,

충전기 상태 갱신 일자와 함께 제공하여 신뢰성을 높였습니다.

또한, 사용자들이 충전소를 더욱 쉽게 찾아갈 수 있도록 카카오 지도 API의 로드뷰 기능을 활용하여

충전소의 실제 위치를 직관적으로 파악할 수 있도록 구현하였습니다.

 

로드뷰 기능을 구현할 때는

충전소의 위도와 경도 및 각종 정보가 담겨져 있는 stationData 객체를 통해 위치 데이터를 받은 후 전달하여 roadviewClient.getNearestPanoId를 호출해 로드뷰 파노라마 ID를 요청하게 하였습니다.

listItem.addEventListener("click", function () {
    const stationData = {
        name: displayName,             // 충전소명 (중복 구분 포함)
        operator: busiNm,              // 운영기관명
        contact: busiCall,             // 운영기관 연락처
        address: addr,                 // 주소
        addressDetail: addrDetail,     // 상세주소
        useTime: useTime,              // 이용 가능 시간
        output: output,                // 충전 용량
        type: type,                    // 충전기 타입 코드
        status: stat,                  // 충전기 상태 코드
        statusUpdateDate: statUpdDt,   // 상태갱신일시
        lastStart: lastTsdt,           // 마지막 충전 시작 일시
        lastEnd: lastTedt,             // 마지막 충전 종료 일시
        chargingStart: nowTsdt,        // 현재 충전 시작 일시
        note: note,                    // 충전소 안내
        limitDetail: limitDetail,      // 이용 제한 사항
        lat: lat,                      // 위도
        lng: lng                       // 경도
    };

    showStationInfoPopup(stationData);
const roadviewContainer = document.getElementById('roadview');
const roadview = new kakao.maps.Roadview(roadviewContainer);
const roadviewClient = new kakao.maps.RoadviewClient();

const position = new kakao.maps.LatLng(stationData.lat, stationData.lng);
roadviewClient.getNearestPanoId(position, 50, function(panoId) {
    if (panoId) {
        roadview.setPanoId(panoId, position);
    } else {
        roadviewContainer.innerHTML = '<p>로드뷰를 사용할 수 없는 위치입니다.</p>';
    }
});

 

충전소 팝업창

 

 


 

길 찾기 버튼을 누르면 충전소 길 찾기 탭으로 이동하며, 해당 탭의 목적지 필드에 선택된 충전소의 좌표가 자동으로 전달되도록 구현하였습니다.

출발지와 도착지가 입력되지 않은 경우, 출발지와 도착지를 입력해 주세요! 라는 알림을 제공하여 사용자가 출발지를 설정할 수 있도록 안내하게 구현했습니다.

출발지와 도착지를 설정할 때는 자동완성 기능을 활용하여, 사용자가 더욱 편리하게 검색할 수 있도록 구현하였습니다.

사용자가 출발지와 도착지를 설정하고 경로 검색 버튼을 클릭하면, 선택된 경로에 대한 안내 사항이 내비게이션 형태로 제공되며, 경로 안내와 예상 이동 시간을 함께 확인할 수 있도록 구현하였습니다.

 

 

자동완성 기능은 카카오 지도 API에서 제공하는 키워드로 장소 검색하기 API를 활용했습니다.

사용자가 키워드를 입력하면 카카오 지도 API의 keywordSearch를 이용하여 실시간으로 검색창에 제공되게 하여 자동완성 기능을 구현하였습니다.

function autoComplete(query, suggestionsContainer, selectPlaceCallback) {
    if (!query.trim()) {
        suggestionsContainer.innerHTML = "";
        return;
    }

    ps.keywordSearch(query, function(data, status) {
        if (status === kakao.maps.services.Status.OK) {
            suggestionsContainer.innerHTML = "";
            data.forEach(function(place) {
			    const item = document.createElement("div");
			    item.className = "autocomplete-item";
			    
			    const icon = document.createElement("div");
			    icon.className = "autocomplete-icon";
			
			    const iconUrl = getIconUrl(place.category_group_code);
			    icon.style.backgroundImage = `url('${iconUrl}')`;

			
			    item.innerHTML = `<strong>${place.place_name}</strong>`;
                
			    item.prepend(icon);
			    item.onclick = () => selectPlaceCallback(place);
			    suggestionsContainer.appendChild(item);
			});
        }
    });
}

 

네비게이션 기능은 카카오 모빌리티에서 제공하는 길찾기 API를 활용했습니다.

길 찾기 버튼을 클릭하면 카카오 모빌리티 길찾기 API가 호출되며, 출발지와 도착지의 좌표를 전달합니다.

API 호출 후 반환된 routeData에는 경로 정보와 안내 지침이 포함되어 있으며, 이를 활용하여 경로를 화면에 네비게이션 형태로 표현합니다.

routeData를 기반으로 지도에 Polyline(경로 선)을 표시하고, 경로 안내 지침은 텍스트와 함께 제공됩니다.

또한, 안내 지침의 타입별로 이미지 파일을 매핑하여 시각적으로 사용자에게 직관적인 네비게이션 경험을 제공하도록 구현되었습니다.

async function getRoute(origin, destination) {
    const apiKey = 'API키';
    const url = `https://apis-navi.kakaomobility.com/v1/directions?origin=${origin}&destination=${destination}&priority=RECOMMEND&car_fuel=GASOLINE`;

    try {
        const response = await fetch(url, {
            method: 'GET',
            headers: { 
                'Authorization': `KakaoAK ${apiKey}`, 
                'Content-Type': 'application/json' 
            }
        });

        const routeData = await response.json();
        return routeData;
    } catch (error) {
        console.error("경로 요청 중 오류 발생:", error);
        return null;
    }
}
function displayRoute(routeData) {
    if (routeData && routeData.routes && routeData.routes[0]) {
        const linePath = [];
        const guides = routeData.routes[0].sections[0].guides;

        routeData.routes[0].sections[0].roads.forEach(road => {
            const vertexes = road.vertexes;
            for (let i = 0; i < vertexes.length; i += 2) {
                linePath.push(new kakao.maps.LatLng(vertexes[i + 1], vertexes[i]));
            }
        });

        if (polyline) polyline.setMap(null);
        polyline = new kakao.maps.Polyline({
            path: linePath,
            strokeWeight: 5,
            strokeColor: '#FF0000',
            strokeOpacity: 0.7,
            strokeStyle: 'solid'
        });
        polyline.setMap(map);

        guides.forEach(guide => {
            console.log(`${guide.guidance}: ${guide.distance}m`);
        });
    }
}

자동완성

 

길찾기 네비게이션

 

 


 

✅마이페이지

 

마이페이지에서는 차량 정보 수정회원 정보 수정, 커뮤니티 기능을 구현했습니다.

차량 정보 수정 버튼과 회원 정보 수정 버튼을 통해 해당하는 정보를 수정할 수 있게 구현하였습니다.

 

☑️ 차량 정보 수정

 

차량 정보 수정 버튼을 클릭하면,

기존에 read-only로 설정된 입력 필드가 활성화되어 사용자가 값을 입력할 수 있도록 구현하였습니다.

또한, 사진 등록 버튼을 통해 차량 사진을 업로드할 수 있는 기능을 추가하였습니다.

사용자가 수정된 정보를 입력하고 등록하기 버튼을 클릭하면, 입력된 값들이 테이블에 저장되도록 구현하였습니다.

 

클라이언트에서 받은 값들은 서버에서 JPA를 통해 처리되며,

사용자가 등록하기 버튼을 클릭하면 입력 필드에 작성된 값들을 AJAX를 사용하여 /car/save 경로로 POST 요청을 보냅니다.

요청을 받은 서버는 세션에서 car_idx 값을 가져와 로그인 상태를 확인한 뒤, 해당 car_idx 값을 CarInfoEntity 객체에 설정합니다.

이후 클라이언트로부터 전달받은 데이터(carType, carNumber, fuelType, carImage 또는 existingCarImage)를 사용해 CarInfoEntity 객체를 생성합니다.

생성된 CarInfoEntity 객체는 CarInfoService의 saveCarInfo 메서드를 호출하여 처리되며, 이 메서드는 JPA Repository의 save() 메서드를 통해 데이터를 데이터베이스에 저장합니다.

 

클라이언트 : AJAX 요청

document.querySelector('.register-car-info-btn').addEventListener('click', () => {
    const formData = new FormData();

    formData.append('carType', document.querySelector('input[placeholder="차량 종류를 입력해 주세요."]').value);
    formData.append('carNumber', document.querySelector('input[placeholder="차량 번호를 입력해 주세요."]').value);
    formData.append('fuelType', document.querySelector('select').value);

    const carImageFile = document.querySelector('#carImage').files[0];
    const existingCarImage = document.querySelector('#existingCarImage').value;

    if (carImageFile) {
        formData.append('carImage', carImageFile);
    } else if (existingCarImage) {
        formData.append('existingCarImage', existingCarImage);
    }

    fetch('/car/save', {
        method: 'POST',
        body: formData,
    })
        .then(response => {
            if (!response.ok) {
                throw new Error('서버 오류: ' + response.status);
            }
            return response.text();
        })
        .then(message => {
            alert(message);
            location.reload();
        })
        .catch(error => {
            console.error('Error:', error);
            alert('저장 중 오류가 발생했습니다. 다시 시도해 주세요.');
        });
});

 

서버 : Controller

@RestController
@RequestMapping("/car")
public class CarController {

    private final CarInfoService carInfoService;

    public CarController(CarInfoService carInfoService) {
        this.carInfoService = carInfoService;
    }

    @PostMapping(value = "/save", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public ResponseEntity<String> saveCarInfo(
            @RequestParam("carType") String carType,
            @RequestParam("carNumber") String carNumber,
            @RequestParam("fuelType") String fuelType,
            @RequestParam(value = "carImage", required = false) MultipartFile carImage,
            @RequestParam(value = "existingCarImage", required = false) String existingCarImage,
            HttpSession session) {

        UserEntity user = (UserEntity) session.getAttribute("user");
        if (user == null) {
            return ResponseEntity.status(401).body("로그인이 필요합니다.");
        }

        try {
            Long carIdxFromSession = user.getCarIdx();

            CarInfoEntity carInfo = new CarInfoEntity();
            carInfo.setCarIdx(carIdxFromSession);
            carInfo.setCarType(carType);
            carInfo.setCarNumber(carNumber);
            carInfo.setFuelType(fuelType);

            if (carImage != null && !carImage.isEmpty()) {
                carInfo.setCarImage(carImage.getBytes());
            } else if (existingCarImage != null && !existingCarImage.isEmpty()) {
                carInfo.setCarImage(Base64.getDecoder().decode(existingCarImage));
            }

            carInfoService.saveCarInfo(carInfo);

            return ResponseEntity.ok()
                    .header("Content-Type", "text/plain; charset=UTF-8")
                    .body("차량 정보가 성공적으로 저장되었습니다.");
        } catch (IOException e) {
            return ResponseEntity.status(500).body("이미지 저장 중 오류가 발생했습니다.");
        }
    }
}

 

Service(interface)

public interface CarInfoService {
    void saveCarInfo(CarInfoEntity carInfo);
}

 

Service(구현체)

@Service
public class CarInfoServiceImpl implements CarInfoService {

    private final CarInfoRepository carInfoRepository;

    public CarInfoServiceImpl(CarInfoRepository carInfoRepository) {
        this.carInfoRepository = carInfoRepository;
    }

    @Override
    public void saveCarInfo(CarInfoEntity carInfo) {
        carInfoRepository.save(carInfo);
    }
}

 

Repository

public interface CarInfoRepository extends JpaRepository<CarInfoEntity, Long> {
}

 

Entity

@Entity
@Table(name = "car_info")
@Data
public class CarInfoEntity {

    @Id
    @Column(name = "car_idx")
    private Long carIdx;

    @Column(name = "car_type", nullable = false)
    private String carType;

    @Column(name = "car_number", nullable = false)
    private String carNumber;

    @Column(name = "fuel_type", nullable = false)
    private String fuelType;

    @Lob
    @Column(name = "car_image")
    private byte[] carImage; // 이미지 데이터 (BLOB)

    @Transient
    private String base64Image;

 

 

마이페이지 메인

 

화면 시연

차량 정보 수정

 


 

☑️ 회원 정보 수정

 

회원 정보 수정 버튼을 클릭하면 팝업 형태로 회원 정보를 수정할 수 있는 페이지가 열리도록 구현하였습니다.

개인 정보 관리 버튼을 클릭하면 사용자에게 비밀번호 재확인 화면이 표시되어,

이를 통해 개인정보의 보안성을 강화하였습니다.
비밀번호 입력 과정에서는 세션에 저장된 비밀번호와 비교하며, 유효성 검사를 통과하면 회원 정보를 수정할 수 있는 화면이 제공됩니다.

필수 입력 정보이메일인증 절차를 완료한 경우에만 수정할 수 있도록 구현하였습니다.
사용자 정보에서는 닉네임, 생년월일, 휴대폰 번호를 수정할 수 있도록 하였으며,
하단 우측의 수정하기 버튼을 클릭하면 입력된 값들이 데이터베이스 테이블에 저장되도록 구현하였습니다.

하단 우측의 탈퇴하기 버튼을 클릭하면 회원 탈퇴가 진행되며, 탈퇴 완료 후 세션이 로그아웃 처리되어 탈퇴한 회원이 더 이상 접근할 수 없도록 구현하였습니다.

비밀번호 탭의 변경 버튼을 클릭하면 비밀번호를 수정할 수 있는 화면이 표시되며,
현재 비밀번호를 입력받아 보안성을 높였습니다.
신규 비밀번호 입력 시에는 8~16자 영문 소문자 및 특수문자 1개 이상 포함이라는 유효성 검사 로직을 적용하여 개인정보 보안을 강화하였습니다.
비밀번호 변경 후 변경 버튼을 클릭하면 수정된 비밀번호가 데이터베이스 테이블에 저장되도록 구현하였습니다.

 

해당 기능도 모두 Ajax와 JPA를 통해 구현하였습니다.

페이지 구성은 하나의 JSP 페이지에서 display: nonedisplay: block 속성을 활용하여 단계별로 로직이 진행될 때마다 사용자에게 다음 단계 화면이 자연스럽게 제공되도록 구현하였습니다.
이를 통해 사용자 경험(UX)을 우선적으로 고려하며, 사용자가 명확하고 직관적으로 작업을 진행할 수 있도록 설계하였습니다.

 

대표적으로 비밀번호 변경 기능에 대해 설명드리겠습니다.

 

 

클라이언트 : AJAX 요청

 

클라이언트에서는 fetch를 사용하여 Ajax 방식으로 /check_password 경로에 POST 요청을 보냅니다.

요청 본문에는 JSON.stringify를 통해 현재 비밀번호(currentPassword)가 전달됩니다.

만약 입력된 비밀번호가 세션에 저장된 비밀번호와 일치한다면, 신규 비밀번호에 대한 유효성 검사가 진행됩니다.

신규 비밀번호는 정규식 기반의 유효성 검사를 거치며, 아래 조건을 충족해야 합니다:

  • 8~16자 길이
  • 영문 소문자 포함
  • 특수문자 1개 이상 포함

유효성 검사를 통과한 뒤에는 신규 비밀번호(newPassword)와 확인용 비밀번호(confirmPassword)가 일치하는지 확인합니다.
만약 두 비밀번호가 일치하지 않을 경우 사용자에게 알림을 띄우고 요청이 중단됩니다.

모든 유효성 검사가 완료되면 /update_password 경로로 POST 요청을 보내며,
요청 본문에 newPassword를 전달하여 서버에서 비밀번호 변경 처리가 이루어지게 됩니다.

 

서버의 Controller에서는 newPassword를 가져와 유효성을 확인하고, 세션에서 사용자 정보를 조회하여 비밀번호를 변경하게 됩니다.

Service에서는 updateUser 메서드를 호출하고, JPA Repository를 통해 데이터베이스에 UserEntity를 저장하게 됩니다.

Repository에서는 save 메서드를 통해 쿼리를 실행하게 됩니다.

document.querySelector('#changepwpw form').addEventListener('submit', function (event) {
   event.preventDefault();

   const currentPassword = document.getElementById('current-password').value;
   const newPassword = document.getElementById('new-password').value;
   const confirmPassword = document.getElementById('confirm-password').value;

   // 비밀번호 유효성 검사 정규식
   const passwordRegex = /^(?=.*[a-z])(?=.*[!@#$%^&*])[a-zA-Z0-9!@#$%^&*]{8,16}$/;

   // 현재 비밀번호 확인 (서버와 비교)
   fetch(contextPath + '/check_password', {
       method: 'POST',
       headers: { 'Content-Type': 'application/json' },
       body: JSON.stringify({ password: currentPassword }),
   })
       .then((response) => response.json())
       .then((data) => {
           if (data.status !== 'success') {
               alert('현재 비밀번호를 확인해 주세요.');
               return;
           }

           // 신규 비밀번호 유효성 검사
           if (!passwordRegex.test(newPassword)) {
               alert('비밀번호는 8~16자 영문 대소문자 및 특수문자가 1개 이상 포함되어야 합니다.');
               return;
           }

           // 신규 비밀번호와 확인 비밀번호가 일치하는지 확인
           if (newPassword !== confirmPassword) {
               alert('비밀번호를 확인해 주세요.');
               return;
           }

           changePassword(newPassword);
       })
       .catch((error) => {
           console.error('Error:', error);
           alert('서버와 통신 중 문제가 발생했습니다.');
       });
});

function changePassword(newPassword) {
    fetch(contextPath + "/update_password", {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
        },
        body: JSON.stringify({
            newPassword: newPassword,
        }),
    })
        .then((response) => {
            if (!response.ok) {
                throw new Error(HTTP error! status: ${response.status});
            }
            return response.json();
        })
        .then((result) => {
            if (result.status === "SUCCESS") {
                alert("비밀번호가 성공적으로 변경되었습니다.");
                location.reload();
            } else {
                alert(업데이트 실패: ${result.message});
            }
        })
        .catch((error) => {
            console.error("Error:", error);
            alert("서버와 통신 중 문제가 발생했습니다.");
        });
}

 

 

서버 : Controller

@PostMapping(value = "/update_password", produces = "application/json")
public ResponseEntity<?> updatePassword(@RequestBody Map<String, String> requestBody, HttpSession session) {
    String newPassword = requestBody.get("newPassword");

    if (newPassword == null || newPassword.isEmpty()) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(Map.of("status", "fail", "message", "비밀번호가 제공되지 않았습니다."));
    }

    try {
        UserEntity user = (UserEntity) session.getAttribute("user");
        if (user == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                    .body(Map.of("status", "fail", "message", "로그인이 필요합니다."));
        }

        user.updatePassword(newPassword);

        userService.updateUser(user);

        session.setAttribute("user", user);

        return ResponseEntity.ok(Map.of("status", "SUCCESS", "message", "비밀번호가 성공적으로 변경되었습니다."));
    } catch (Exception e) {
        e.printStackTrace();
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(Map.of("status", "fail", "message", "비밀번호 변경 중 오류가 발생했습니다."));
    }
}

 

 

Service(interface)

void updateUser(UserEntity userEntity);

 

Service(구현체)

@Override
public void updateUser(UserEntity userEntity) {
    repository.save(userEntity);
}

 

Repository

public interface UserRepository extends JpaRepository<UserEntity, Integer> {
}

 

화면 시연

 

회원 정보 수정 로직

 

 

 


 

 

☑️ 커뮤니티 리스트

 

로그인한 사용자가 작성한 게시글을 한눈에 확인할 수 있도록 리스트 형식으로 구성하였습니다.
리스트에는 게시글 번호, 제목, 작성 시간이 포함되어 있으며, 사용자에게 필요한 정보를 직관적으로 제공합니다.

 

 

 

 

 


 

프로젝트 후기

 

다시 팀장을 맡아 프로젝트를 진행하며 새로운 팀원들과 협업할 수 있었다는 점이 굉장히 즐거웠습니다.

이전 1차와 2차 프로젝트를 통해 많은 경험을 쌓은 덕분에 3차 프로젝트는 비교적 수월하게 진행할 수 있었습니다.

팀원들이 열정적으로 참여하고 훌륭한 의견들을 적극적으로 제시해 준 덕분에, 팀장으로서 프로젝트를 이끌기 수월했으며 유능한 팀원들 덕분에 프로젝트가 자연스럽게 잘 진행될 수 있었습니다.

 

2차 프로젝트에서는 많은 페이지를 혼자 맡아 진행해야 했던 상황과 달리, 이번 프로젝트에서는 팀원들이 높은 참여도와 열정을 보여준 덕분에 보다 효율적으로 프로젝트를 완성할 수 있었습니다.

 

이 과정에서 저 역시 팀원들로부터 많은 것을 배우며, 함께 성장할 수 있었던 소중한 시간이었습니다.

협업의 중요성팀워크의 힘을 다시금 느낄 수 있었던 프로젝트였습니다.

 

이번 프로젝트는 실제 사용자들이 관심을 가지는 주유소, 충전소, 주차장에 대한 실용적인 기능을 구현했다는 점에서 더욱 뜻깊었습니다. 특히, 카카오 지도와 카카오 모빌리티 API를 경험함으로써 실제로 사용성이 높은 프로젝트를 진행하며 사용자 중심의 서비스를 구체적으로 설계하고 구현할 수 있었기에 의미 있는 경험이었습니다.

 

개인적으로는 이번 프로젝트를 통해 JPA의 데이터 처리 흐름을 깊이 이해하게 되었습니다. 이전 2차 프로젝트에서 사용했던 JSP 기반의 MVC 패턴과 비교했을 때, JPA 메서드를 활용하면 데이터 저장 및 관리를 훨씬 간결하고 효율적으로 처리할 수 있다는 점이 인상적이었습니다.

 

프로젝트 진행 과정에서 예상치 못한 어려움도 있었습니다. 특히, API를 활용하면서 데이터 구조를 정확히 이해하고 이를 서비스에 적용하는 과정에서 많은 시행착오를 겪었습니다. 또한, 지도와 길찾기 기능을 구현할 때, 화면 전환 및 데이터 흐름 간 충돌 문제가 발생해 처음에는 혼란스러웠지만, 지도 부분을 맡은 팀원들과 협력하여 디버깅하고 테스트를 반복하면서 점차 문제를 해결해 나갔습니다.

 

이러한 어려움은 단순히 프로젝트를 완수하는 것을 넘어, 문제 해결 능력과 협업의 중요성을 다시금 깨닫게 해 주었습니다. 팀원들과의 협력으로 모든 난관을 극복하고 최종적으로 완성도 높은 결과물을 만들어 낼 수 있었던 점이 가장 보람찼습니다.

 

이번 프로젝트는 기술적인 성장뿐 아니라 협업과 소통의 중요성을 다시 한 번 되새길 수 있었던 값진 프로젝트 였습니다.