Skip to main content

법률 전문가 매칭 플랫폼을 운영하다 보면, 기능 완성도만큼이나 사용자 경험(UX)의 디테일이 신뢰도를 좌우한다는 걸 매일 체감합니다. 이번 스프린트에서는 SvelteKit 5 + FastAPI 스택으로 구성된 플랫폼에 채팅 UI 통일, 닉네임 시스템 구축, TipTap 에디터 도입, 모바일 최적화, 비밀번호 변경 API 등 30여 건의 개선 작업을 진행했습니다. 이 글에서는 그 중 핵심 구현을 코드 중심으로 정리합니다.

1. 닉네임 시스템 구축 — 중복 체크 API 설계

법률 상담 플랫폼 특성상 개인정보 보호가 중요합니다. 채팅에서 실명 대신 닉네임을 표시하고, 회원가입·마이페이지에서 중복 확인을 제공합니다. 비로그인용과 로그인용 두 가지 엔드포인트를 분리했습니다.

# FastAPI — 닉네임 중복 체크 (로그인 유저, 본인 제외)
@router.get("/user/check-nickname")
async def check_nickname_auth(
    nickname: str,
    current_user: User = Depends(get_current_user),
    db: Session = Depends(get_db)
):
    if len(nickname.strip()) < 2:
        return {"available": False, "message": "닉네임은 2자 이상이어야 합니다."}
    if len(nickname.strip()) > 20:
        return {"available": False, "message": "닉네임은 20자 이하여야 합니다."}

    existing = db.query(User).filter(
        User.nickname == nickname.strip(),
        User.id != current_user.id   # 본인 닉네임은 통과
    ).first()

    return {
        "available": not bool(existing),
        "message": "이미 사용 중인 닉네임입니다." if existing else "사용 가능한 닉네임입니다."
    }

SvelteKit 프론트엔드에서는 닉네임 입력 후 중복 확인 버튼을 누르면 실시간으로 API를 호출하고, 결과에 따라 폼 제출 가능 여부를 제어합니다.

// SvelteKit — 닉네임 중복 체크 함수
async function checkNickname() {
    if (!user.nickname.trim()) return;
    nicknameCheckLoading = true;

    const token = $authStore.token;
    const res = await fetch(
        `${apiUrl}/api/user/check-nickname?nickname=${encodeURIComponent(user.nickname)}`,
        { headers: { Authorization: `Bearer ${token}` } }
    );
    nicknameCheckResult = await res.json();
    isNicknameChecked = true;
    nicknameCheckLoading = false;
}

2. 채팅 UI 통일 — 오픈채팅 vs 1:1 채팅 스타일 일관성

플랫폼에는 전문가와 1:1로 대화하는 개인 채팅과 누구나 참여하는 오픈채팅 두 가지 채널이 있습니다. 기존에는 두 화면의 메시지 레이아웃이 달라 사용자가 혼란스러울 수 있었습니다. 핵심 변경 포인트는 세 가지입니다.

  • 발신자 이름: 각 메시지 버블 위에 이름 표시 (1:1 채팅에도 동일 적용)
  • 전문가 배지: 전문가 메시지에 회색 전문가 뱃지 노출
  • 프로필 이미지: 모바일에서도 아바타 표시 (hidden md:block 제거)
<!-- SvelteKit — 1:1 채팅 상대방 메시지 (개선 후) -->
<div class="flex items-start gap-2">
  <!-- 프로필 이미지 (모바일 포함) -->
  <svelte:element
    this={otherPerson.expertId ? 'a' : 'div'}
    href={otherPerson.expertId ? `/experts/${otherPerson.expertId}` : undefined}
  >
    {#if otherPerson.image}
      <img src={otherPerson.image} class="w-9 h-9 rounded-full object-cover" />
    {:else}
      <div class="w-9 h-9 bg-gradient-to-br from-gray-600 to-gray-700 rounded-full" />
    {/if}
  </svelte:element>

  <div class="flex-1 min-w-0">
    <!-- 이름 + 전문가 배지 -->
    <div class="flex items-center gap-1.5 mb-1">
      {#if otherPerson.expertId}
        <a href="/experts/{otherPerson.expertId}" class="font-medium text-sm hover:underline">
          {otherPerson.name}
        </a>
        <span class="bg-gray-500 text-white text-[10px] px-1.5 py-0.5 rounded font-medium">전문가</span>
      {:else}
        <span class="font-medium text-gray-900 text-sm">{otherPerson.name}</span>
      {/if}
    </div>
    <!-- 메시지 버블 + 시간 -->
    <div class="flex items-end gap-2">
      <div class="bg-white rounded-2xl rounded-tl-sm px-3.5 py-2.5 shadow-sm">
        <p class="text-gray-900 text-[15px] whitespace-pre-wrap">{message.content}</p>
      </div>
      <span class="text-[11px] text-gray-600">{formatTime(message.created_at)}</span>
    </div>
  </div>
</div>

3. 비밀번호 변경 API — FastAPI + 해시 검증

기존 마이페이지의 “비밀번호 변경” 버튼에는 on:click 핸들러조차 없었습니다. 백엔드 API부터 신규 작성하고, 프론트에는 모달 UI를 추가했습니다.

# FastAPI — 비밀번호 변경 엔드포인트
class PasswordChangeRequest(BaseModel):
    current_password: str
    new_password: str

@router.put("/user/password")
async def change_password(
    data: PasswordChangeRequest,
    current_user: User = Depends(get_current_user),
    db: Session = Depends(get_db)
):
    # 현재 비밀번호 검증
    if not verify_password(data.current_password, current_user.hashed_password):
        raise HTTPException(status_code=400, detail="현재 비밀번호가 올바르지 않습니다.")

    # 최소 길이 체크
    if len(data.new_password) < 8:
        raise HTTPException(status_code=400, detail="새 비밀번호는 8자 이상이어야 합니다.")

    # 해시 후 저장
    current_user.hashed_password = get_password_hash(data.new_password)
    db.commit()
    return {"message": "비밀번호가 변경되었습니다."}

4. 모바일 최적화 — Tailwind CSS 반응형 클래스 전략

법률 상담 서비스 특성상 모바일 사용 비율이 높습니다. 마이페이지 사이드바 내비게이션, 프로필 이미지 크롭 모달, 닉네임 입력 영역 등 여러 곳에서 모바일 레이아웃을 개선했습니다. Tailwind의 반응형 프리픽스를 적극 활용했습니다.

대표적인 패턴은 사이드바 탭을 모바일에서 가로 스크롤 탭으로 전환하는 것입니다. 데스크탑에서는 세로 목록이지만 모바일에서는 수평으로 스크롤되어 화면 공간을 효율적으로 씁니다.

<!-- 마이페이지 탭 — 모바일 가로 스크롤, 데스크탑 세로 목록 -->
<nav class="flex overflow-x-auto gap-1 sm:flex-col sm:overflow-x-visible
            pb-2 sm:pb-0 scrollbar-hide">
  <button class="flex-shrink-0 flex items-center gap-2 px-4 py-3 rounded-xl
                 font-medium text-sm whitespace-nowrap transition
                 {activeTab === 'profile'
                   ? 'bg-gray-800 text-white shadow-sm'
                   : 'text-gray-600 hover:bg-gray-100'}">
    내 정보
  </button>
  <!-- 더 많은 탭... -->
</nav>

5. 요약 자동 생성 — 본문에서 메타 데이터 추출

성공사례·칼럼·Q&A 작성 시 "요약" 항목을 별도로 입력하는 불편함을 없앴습니다. 제출 시 본문 HTML에서 태그를 제거하고 앞 200자를 자동으로 요약으로 활용합니다. 간단하지만 실사용에서 체감 효율이 높은 개선입니다.

// 본문 HTML → 요약 자동 추출 (SvelteKit)
function extractSummary(htmlContent) {
    return htmlContent
        .replace(/<[^>]*>/g, '')   // HTML 태그 제거
        .replace(/\s+/g, ' ')        // 연속 공백 정리
        .trim()
        .substring(0, 200);          // 최대 200자
}

// 폼 제출 시 자동 적용
async function handleSubmit() {
    formData.summary = extractSummary(formData.content);
    // ... API 호출
}

마치며

이번 스프린트를 통해 법률 O2O 플랫폼의 기능 완성도와 UX 디테일이 한 단계 올라갔습니다. 작은 불편함 하나하나를 해소하는 것이 사용자 신뢰로 이어지고, 결국 플랫폼의 경쟁력이 됩니다. 닉네임 시스템, 채팅 UI 통일, 모바일 최적화, 편집기 교체—어느 것 하나 쉬운 작업이 없었지만, SvelteKit 5의 반응형 렌더링과 FastAPI의 빠른 개발 속도가 큰 도움이 됐습니다.

코드벤터는 이처럼 기획 단계부터 배포까지 전 과정을 AI 코딩과 글로벌 협력 네트워크를 통해 빠르고 정교하게 구현합니다. 법률, 의료, 교육 등 전문가 매칭 플랫폼이나 O2O 서비스 개발을 고려하신다면 언제든지 문의해 주세요.

댓글 남기기