Trip-Linker 개발일지 6 - 플래너 API 연동 및 UI 개선 — goPlanStep 검증, 날씨 팝업

2026. 6. 4. 19:35프로젝트/Trip-Linker 팀 프로젝트

반응형

플래너 API 연동 및 UI 개선 — goPlanStep 검증, 날씨 팝업

📋 한줄 요약

플래너 3단계 흐름을 백엔드 API에 연결하고, 미로그인/미입력 상태에서 단계 이동을 막는 검증 로직을 추가했다.


🎯 왜 만들었나

기존 플래너는 MOCK 데이터로 동작하고 있었고, 로그인 없이도 3단계까지 이동이 가능했다.

실제 API 연동 + 단계별 입력값 검증으로 정상 흐름을 완성하는 작업이었다.

추가로 클라이언트 측에 있던 로그인 잠금 dead code를 제거해서 서버 응답 기반 단일 로직으로 정리했다.


📁 작업 순서 (파일별)

1. ApiResponse.java

📦 global

추가한 코드

// 기존 — 두 인자 필요
public static <T> ApiResponse<T> success(String message, T data)

// 추가 — 데이터만 넘길 때 쓸 수 있는 단일 인자 오버로드
public static <T> ApiResponse<T> success(T data) {
    return new ApiResponse<>(true, "success", data);
}

왜 추가했나

TravelPlanController에서 메시지 없이 데이터만 반환하는 경우가 많아서

매번 success("success", data) 처럼 쓰는 게 불편했다.


2. TravelPlanController.java

📦 domain/plan/controller

변경한 코드

// 변경 전 — LoginUser라는 커스텀 어노테이션이 없어서 에러
@AuthenticationPrincipal LoginUser loginUser
loginUser.getId()

// 변경 후 — 실제 존재하는 CustomUserDetails 사용
@AuthenticationPrincipal CustomUserDetails userDetails
userDetails.getUser().getId()

왜 바꿨나

@AuthenticationPrincipal은 UserDetails 구현체 타입으로 정확히 맞춰야 주입된다.

프로젝트의 실제 구현체는 CustomUserDetails이고, 사용자 ID는 .getUser().getId()로 꺼낸다.


3. TravelPlanServiceImpl.java

📦 domain/plan/service

변경한 코드

// 변경 전
throw new CustomException(ErrorCode.PLAN_NOT_FOUND);

// 변경 후
throw new IllegalArgumentException("존재하지 않는 플랜입니다.");
throw new IllegalStateException("본인의 플랜만 수정할 수 있습니다.");

왜 바꿨나

CustomException과 ErrorCode가 아직 팀에서 구현되지 않은 상태라 컴파일 에러가 났다.

표준 Java 예외로 교체해서 일단 동작하게 만들었다.


4. app_main.js

📦 frontend

제거한 코드 (dead code)

// 클라이언트 측 로그인 잠금 — 서버와 이중으로 관리되어 불필요
let _loginFailCount = 0;
let _loginLockedUntil = null;

if (_loginLockedUntil && Date.now() < _loginLockedUntil) {
    showLockMessage(_loginLockedUntil - Date.now());
    return;
}
_loginFailCount++;
if (_loginFailCount >= 5) { ... }

유지한 코드 (서버 응답 기반)

if (res.data.locked) {
    showLockMessage(res.data.remainSeconds);
}

왜 바꿨나

클라이언트 카운터는 새로고침하면 초기화되어 의미가 없고,

서버에서 locked, remainSeconds를 이미 내려주므로 클라이언트 로직은 불필요했다.

추가한 코드 — go() 함수 내 플래너 초기화

case 'planner':
    setTimeout(initPlannerDateConstraints, 100);
    break;

왜 추가했나

SPA는 fragment를 동적으로 삽입하기 때문에 DOMContentLoaded 이후에 DOM이 생긴다.

페이지 전환 시점에 직접 호출해야 달력 min 날짜가 정상 세팅된다.


5. page_planner.html

📦 frontend

추가한 함수 — goPlanStep() 검증

function goPlanStep(step) {
    // 미로그인 차단
    if (!Token.getAccess()) {
        openLoginModal();
        return;
    }
    // STEP 1 → 2: 필수 입력값 검증
    if (currentStep === 1 && step === 2) {
        if (!_validatePlanStep1()) return;
    }
    // STEP 2 → 3: 스타일 chip 최소 1개
    if (currentStep === 2 && step === 3) {
        if (!_validatePlanStep2()) return;
    }
    currentStep = step;
    renderStep();
}

API 연동 추가

// MOCK 제거 후 실제 API 호출
// 플랜 생성
POST /api/trips

// 이전 취향 불러오기
POST /api/trips/{tripId}/load-preference

// MBTI 기반 일정 밀도 자동 선택
GET /api/users/me  →  res.data.mbti 로 density 자동 세팅

토큰 참조 통일

// 변경 전 — 혼재
localStorage.getItem('accessToken')

// 변경 후 — 통일
Token.getAccess()

6. app_community.js

📦 frontend

제거한 코드

// _reportAction 함수가 파일 내에 두 번 선언되어 있었음
// 두 번째 선언 제거
function _reportAction(type, targetId) { ... }  // ← 이 줄 삭제

⚠️ 트러블슈팅

문제 — 날씨 팝업이 display:flex로 켜도 안 보임

CSS가 기본적으로 opacity: 0; pointer-events: none이고

.overlay.open 클래스가 붙어야 보이는 구조였는데,

element.style.display = 'flex'로 직접 조작하고 있었다.

// 변경 전
weatherPopup.style.display = 'flex';

// 변경 후
weatherPopup.classList.add('open');

💡 배운 점

SPA에서 fragment를 동적 삽입하면 DOMContentLoaded가 이미 지난 뒤라서

초기화 함수를 페이지 전환 시점에 직접 호출해야 한다.

@AuthenticationPrincipal은 SecurityContext에 저장된 UserDetails 구현체 타입과 정확히 일치해야 주입된다.