프론트엔드백엔드

프로필 수정 + Header 아바타 + MateBoard 본인글 표시 — POP-SPOT v2.16

김동현
··4분 읽기

v2.15.3 MY 탭 내 계정 카드의 자연스러운 다음 단계. 프로필 편집 페이지 신설, Header 우상단 작은 아바타, 메이트 게시판에서 내가 쓴 글에 본인 배지 표시. 3 곳을 같이 정리.

v2.15.3 에서 MY 탭에 내 계정 카드를 만들고 보니 "편집" 링크가 열 페이지가 없었다. 프로필 편집 페이지 + Header 우상단 아바타 + 메이트 게시판 본인글 표시를 같이 정비.

이 글에서 다루는 것

  • 프로필 편집 페이지 (/profile/edit) 의 백엔드·프론트 코드
  • Header 우상단 아바타 — 데스크탑은 닉네임 / PRO 뱃지, 모바일은 작은 아바타만
  • MateBoard 에서 내가 쓴 글에 본인 배지 노출
  • 모르는 단어 한 줄로

    용어한 줄 설명
    presigned URL일정 시간 동안만 유효한 업로드 URL. 클라이언트가 서버 거치지 않고 S3 같은 곳에 직접 올릴 때 씀
    multipart/form-data파일과 텍스트 필드를 같이 보낼 때 쓰는 HTTP 요청 형식
    본인 배지게시글 작성자가 현재 로그인한 사람과 같을 때 "내 글" 표시를 붙여주는 작은 라벨

    무엇이 바뀌었나

    영역v2.15v2.16
    프로필 편집없음<code>/profile/edit</code> 페이지 — 이름·프로필 사진 변경
    Header 우상단로그인/로그아웃 텍스트만아바타 + 닉네임(md+) + PRO 뱃지(Premium)
    MateBoard모든 글이 동등본인 글에 "내 글" 라임 배지

    1. 프로필 편집 페이지

    백엔드 — UserProfileController

    파일: popspot-backend/src/main/java/com/popspot/controller/UserProfileController.java (v2.16 신규)

    java
    // v2.16 — UserProfileController.java (신규)
    @RestController
    @RequestMapping("/api/v1/users/me/profile")
    public class UserProfileController {
    
      private final UserProfileService service;
    
      public UserProfileController(UserProfileService service) {
        this.service = service;
      }
    
      @PatchMapping
      public UserProfileResponse updateProfile(
          Authentication auth,
          @RequestBody @Valid ProfileUpdateRequest req) {
        Long userId = Long.parseLong(auth.getName());
        return service.updateProfile(userId, req);
      }
    
      @PostMapping(value = "/image",
                   consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
      public UserProfileResponse updateImage(
          Authentication auth,
          @RequestPart("file") MultipartFile file) {
        Long userId = Long.parseLong(auth.getName());
        return service.updateProfileImage(userId, file);
      }
    }

    해석 — 한 줄씩

  • @RequestMapping("/api/v1/users/me/profile") — 베이스 경로. /me/ 는 "현재 로그인한 사용자 자신" 을 의미하는 표준 패턴. URL 에 userId 를 노출 안 함 → IDOR 위험 0.
  • Authentication auth — Spring Security 가 JWT 검증 후 주입해주는 객체. 그 안에 auth.getName() 으로 userId 가 들어있다 (JWT subject 가 userId).
  • Long.parseLong(auth.getName()) — String → Long 변환. JWT subject 는 항상 String 이라서.
  • @PatchMapping — 부분 수정. PUT 은 "전체 교체" 의미라 닉네임만 바꿀 때는 PATCH 가 올바름.
  • @RequestPart("file") — multipart 요청에서 file 이름의 파일 파트를 받음.
  • 백엔드 — UserProfileService

    파일: popspot-backend/src/main/java/com/popspot/service/UserProfileService.java

    java
    // v2.16 — UserProfileService.updateProfile()
    @Transactional
    public UserProfileResponse updateProfile(Long userId, ProfileUpdateRequest req) {
      User user = userRepository.findById(userId)
          .orElseThrow(() -> new ResourceNotFoundException("User"));
    
      if (req.name() != null) {
        String safe = req.name().trim();
        if (safe.length() < 2 || safe.length() > 20) {
          throw new BadRequestException("이름은 2~20자");
        }
        user.changeName(safe);
      }
      return UserProfileResponse.from(user);
    }

    해석

  • @Transactional — 메서드 실행 동안 DB 트랜잭션 유지. JPA dirty checking 으로 user.changeName() 가 자동 UPDATE 됨 (repository.save() 호출 불필요).
  • req.name() != null — null 이면 이름 안 바꾸겠다는 뜻. PATCH 의 "부분 수정" 의미를 살리는 방식.
  • trim() + 길이 검사 — 입력 검증을 컨트롤러가 아닌 서비스에서 한 번 더. DTO 의 @Valid 와 중복이지만 안전망.
  • 프론트 — 폼

    파일: popspot-frontend/src/app/profile/edit/page.tsx (v2.16 신규)

    typescript
    // v2.16 — profile/edit/page.tsx (신규)
    'use client';
    export default function ProfileEditPage() {
      const [name, setName] = useState('');
      const [file, setFile] = useState<File | null>(null);
    
      async function handleSave() {
        await fetch('/api/v1/users/me/profile', {
          method: 'PATCH',
          headers: { 'Content-Type': 'application/json' },
          credentials: 'include',
          body: JSON.stringify({ name }),
        });
    
        if (file) {
          const fd = new FormData();
          fd.append('file', file);
          await fetch('/api/v1/users/me/profile/image', {
            method: 'POST',
            credentials: 'include',
            body: fd,
          });
        }
    
        router.push('/?tab=MY');
      }
    }

    해석

  • 두 호출을 직렬로 — 이름 변경과 사진 업로드가 별도 엔드포인트라서. 사진은 multipart 라 별도 호출이 표준.
  • credentials: 'include' — 쿠키(JWT) 같이 보냄. Spring Security 가 인증 확인에 사용.
  • FormData — 파일은 JSON 으로 못 보냄. multipart/form-data 자동 헤더 설정.
  • router.push('/?tab=MY') — 저장 후 MY 탭으로 복귀. v2.15.2 의 딥링크 우회 덕에 인트로 안 거치고 바로.

  • 2. Header 우상단 아바타

    코드

    파일: popspot-frontend/src/components/layout/Header.tsx

    typescript
    // v2.16 — Header.tsx 우상단 아바타 블록
    {user && (
      <div className="flex items-center gap-2">
        <Avatar src={user.profileImageUrl} name={user.name} size={28} />
        <span className="hidden md:inline-flex text-sm font-medium">
          {user.name}
        </span>
        {user.isPremium && (
          <span className="hidden md:inline-flex text-[10px] px-1.5 py-0.5
                           rounded bg-amber-400/20 text-amber-700">
            PRO
          </span>
        )}
      </div>
    )}

    해석

  • Avatar 컴포넌트 — src 가 없거나 깨지면 이름의 첫 글자를 lime 배경 동그라미로 표시 (fallback).
  • hidden md:inline-flex — Tailwind 의 반응형. md (768px) 미만에서는 hidden, 이상에서는 inline-flex. 모바일은 아바타만, 데스크탑은 아바타 + 닉네임 + PRO 뱃지 패턴.
  • text-[10px] px-1.5 py-0.5 — PRO 뱃지를 작게. 너무 크면 광고처럼 보임.
  • amber 색 — 다른 인터랙티브 요소(lime)와 구분해서 "정보" 성격으로.
  • 디자인 결정

    v2.15 까지 UserChip 이 hidden md:inline-flex 였다 → 모바일에서 본인 정보 자체가 안 보임. 모바일이 70% 이상인 트래픽이라 이건 사용성 문제. 아바타만이라도 항상 보이게.


    3. MateBoard 본인 글 표시

    코드

    파일: popspot-frontend/src/features/mate/MateBoard.tsx (각 게시글 카드 렌더링)

    typescript
    // v2.16 — MateBoard.tsx 내 각 카드 렌더링 쪽
    {post.authorId === currentUserId && (
      <span className="inline-flex items-center gap-1 text-[10px]
                       px-2 py-0.5 rounded-full
                       bg-lime-400/15 text-lime-700 font-medium">
        내 글
      </span>
    )}

    해석

  • post.authorId === currentUserId — 글의 작성자와 현재 로그인한 사용자가 같으면 표시
  • bg-lime-400/15 — 라임 배경 15% 불투명도. 너무 옅으면 안 보이고 진하면 광고 같음. /15 가 균형
  • inline-flex — 텍스트와 같은 줄에 자연스럽게
  • 왜 필요한가

    메이트 게시판은 자기 글을 한 번 올리고 잊는 패턴이 많다. 며칠 뒤 게시판 들어와서 "내가 뭘 썼었더라" 를 찾을 때 일일이 클릭해서 확인해야 했음. 본인 배지가 있으면 한눈에 식별.


    관련 글

  • 이전 — v2.15, MY 탭 + middleware 딥링크 우회
  • 다음 — v2.17, 운영 출시 직전 12 개 핫픽스
  • 공유

    댓글