Skip to content

Conversation

@youdaeng2
Copy link
Member

@youdaeng2 youdaeng2 commented Nov 29, 2025

📌 개요

  • 투표 글 작성/수정 페이지 및 폼 로직 구현
  • 투표 마감 기한(duration) 기반 endTime 계산 유틸 추가
  • 투표 옵션 생성/수정 UX 정리 및 수정 모드에서 옵션 편집 제한
  • 투표 보드 API 스펙 변경 사항(FormData, 카테고리 enum, 이미지 필드) 반영 및 커스텀 FormData 빌더 추가

🗒 상세 설명

1. 투표 글 작성/수정 페이지 및 폼 컴포넌트 추가

/main/community/votesboard 도메인에 투표 글 작성/수정 플로우를 붙였습니다.

핵심 기술 및 구현사항

  • 새 페이지 추가

    • apps/web/src/app/main/community/votesboard/new/page.tsx

      • searchParams.category를 읽어 유효한 카테고리일 경우 초기 카테고리로 전달
      • VoteboardForm initialCategory={category}로 생성 모드 진입
    • apps/web/src/app/main/community/votesboard/[votesboardId]/edit/page.tsx

      • useParams()votesboardId 추출 후 useGetVotePost(voteId)로 상세 데이터 조회
      • 로딩 중에는 VoteboardFormSkeleton, 완료 후에는 VoteboardForm voteboardId, initialData 렌더
  • 공통 폼 컴포넌트

    • apps/web/src/app/main/community/votesboard/components/VoteboardForm.tsx

    • react-hook-form + zodResolver 기반 폼 상태 관리

    • voteboardId 유무로 isEdit 모드 계산

    • 필드 구성

      • 카테고리: 커스텀 <Select> + Controller
      • 제목: Input
      • 내용: TextArea
      • 마감 기간: duration 셀렉트(1d, 3d, 7d)
      • 투표 옵션: 동적 필드(useFieldArray + VoteboardOptionField)
      • 옵션 설정(복수 선택 / 재투표 허용): 체크박스 2개
      • 이미지 업로드: ImageUploader 재사용

적용 이미지

image

2. 투표 폼 스키마 및 마감 기한(duration) 유틸 추가

투표 폼 전용 Zod 스키마와 마감 기한 계산 유틸을 분리했습니다.

핵심 기술 및 구현사항

  • apps/web/src/app/main/community/votesboard/schema/voteboardSchema.ts

    • VOTE_DURATION_VALUES = ['1d', '3d', '7d'] as const
    • duration 필드: 셀렉터에서 선택한 기간을 표현
    • category: CategoryEnum 기반 enum으로 제한
    • title, content: 공백만 입력 불가 + 길이 제한
    • voteOptions: 최소 2개 ~ 최대 5개, 각 옵션은 1~50자 + 공백-only 방지
    • images: Blob[] 기반, 최대 4장 제한
    • VoteboardFormData 타입 export
  • apps/web/src/utils/voteTime.ts

    • getDaysFromDuration(duration: VoteDuration): number

      • '1d' -> 1, '3d' -> 3, '7d' -> 7
    • buildEndTimeFromDuration(duration: VoteDuration): string

      • 현재 시각 + N일을 더한 후
      • yyyy-MM-ddTHH:mm:ss 로컬 시간 문자열로 포맷
    • useVoteboardMutation에서 duration을 받아 실제 endTime 문자열을 생성해 서버로 전달


3. 투표 옵션 필드 컴포넌트 및 수정 모드 옵션 잠금

투표 옵션 필드 UI를 컴포넌트로 분리하고, 수정 모드에서 옵션 편집/추가/삭제를 막았습니다.

핵심 기술 및 구현사항

  • apps/web/src/app/main/community/votesboard/components/VoteboardForm.tsx

    • useFieldArray({ name: 'voteOptions' }) 기반 동적 옵션 관리

    • 생성 모드

      • 초기값: [{ content: '찬성' }, { content: '반대' }]
      • 옵션 추가 버튼 노출 (fields.length < 5일 때만 append)
      • 옵션 삭제 버튼: 옵션이 3개 이상일 때 노출
    • 수정 모드

      • isEdit = !!voteboardId
      • 옵션 추가 버튼 숨김
      • VoteboardOptionFieldeditable={!isEdit}, canRemove={!isEdit && fields.length > 2}로 내려서 옵션 텍스트/개수 변경 불가
  • apps/web/src/app/main/community/votesboard/components/VoteoptionField.tsx

    • VoteboardOptionField 컴포넌트 분리
    • editable 플래그로 Input.disabled 제어
    • canRemove 플래그로 삭제 버튼 노출 제어
    • 투표 옵션 인풋에 voteOptions.${index}.content를 register

적용 이미지

image

4. 투표 폼 스켈레톤(VoteboardFormSkeleton) 추가

로딩 시 CLS를 줄이기 위해 VoteboardForm과 거의 동일한 레이아웃의 스켈레톤을 추가했습니다.

  • apps/web/src/app/main/community/votesboard/components/VoteBoardForm.Skeleton.tsx

5. 투표 게시글 생성/수정 API 클라이언트(FormData) 정리

기존 Orval에서 생성한 createVotePost는 application/json + JSON body 기준이라
실제 서버에서 요구하는 multipart/form-data + voteOptions[0].content 형식과 맞지 않았습니다.
그래서 FormData를 직접 생성해서 백엔드 스펙에 맞는 필드 구조로 다시 맞춰 주었습니다.

핵심 기술 및 구현사항

  • 생성 플로우

    • Orval이 만든 기본 createVotePost는 그대로 두고,
    • 실제 생성 흐름에서는 apps/web/src/app/main/community/votesboard/new/api/votePostCreate.ts의 커스텀 헬퍼만 사용합니다.
    • 이 헬퍼에서 FormData를 직접 만들고, voteOptions[${index}].content 형식으로 옵션을 넣어 백엔드 스펙에 맞췄습니다.

6. 투표 게시글 생성/수정 통합 훅(useVoteboardMutation) 추가

폼에서 API 호출을 깔끔하게 사용할 수 있도록 생성/수정 로직을 하나의 훅에 통합했습니다.

핵심 기술 및 구현사항

  • apps/web/src/hooks/useVoteboardMutation.ts

    • useVoteboardMutation(voteboardId?: number)

      • voteboardId 유무로 생성 vs 수정 분기
    • 생성:

      • useMutation + 커스텀 createVotePost wrapper 사용

      • 성공 시:

        • /community/votesboard 리스트 쿼리 invalidate
        • 토스트: "투표가 성공적으로 생성되었습니다."
        • 라우팅: /main/community/votesboard
    • 수정:

      • useUpdateVotePost(Orval 생성 훅) 사용

      • votesboardId + VotePostUpdateRequest 형태로 호출

      • 성공 시:

        • 상세, 목록 쿼리 invalidate
        • 토스트: "투표가 성공적으로 수정되었습니다."
        • 라우팅: /main/community/votesboard/${voteboardId}
    • 공통:

      • 에러 시 공통 토스트 메시지 노출
      • isPending로 버튼 로딩/비활성 제어

7. API 모델/타입 스펙 업데이트

Orval 재생성에 따라 투표 관련 모델이 변경되었습니다.

핵심 기술 및 구현사항

  • 카테고리 enum 분리 및 타입 정의

    • votePostCreateRequestCategory.ts
    • votePostDetailResponseCategory.ts
    • votePostSummaryResponseCategory.ts
  • 이미지 관련 필드 변경

    • VotePostCreateRequest.images?: Blob[] (기존 imageUrls 제거)
    • VotePostUpdateRequest.images?: Blob[], deleteImageIds?: number[]
    • VotePostDetailResponse.images: ImageInfo[] 추가 (기존 imageUrls 제거)
  • 요약 응답에 카테고리 추가

    • VotePostSummaryResponse.category 필드 추가

🔗 이슈

closes #76


✅ 체크리스트

  • 코드가 스타일 가이드를 따릅니다
  • 자체 코드 리뷰를 완료했습니다
  • 복잡/핵심 로직에 주석을 추가했습니다 (폼 스키마, 마감 시간 유틸, API 클라이언트 등)
  • 관심사 분리를 확인했습니다 (폼/UI vs 훅 vs 유틸 vs API 클라이언트)
  • 잠재적 사이드이펙트를 점검했습니다 (옵션 수정 제한, FormData 전송 구조 등)
  • Vercel Preview로 테스트를 완료했습니다

🧪 테스트 방법

  • 투표 글 작성 플로우 수동 테스트

    • 카테고리 선택 → 제목/내용 입력 → 기본 옵션(찬성/반대) + 옵션 추가 → 마감 기간 선택 → 이미지 업로드 후 제출
    • 정상 생성 및 목록/상세에서 데이터 확인
  • 투표 글 수정 플로우 수동 테스트

    • 기존 투표 상세 → 수정 페이지 진입
    • 카테고리/제목/내용/설정/이미지 수정
    • 투표 옵션이 읽기 전용으로 표시되고 추가/삭제가 불가능한지 확인
  • 마감 기간(duration) 선택에 따른 endTime 포맷 확인

    • 1일/3일/7일 선택 시 yyyy-MM-ddTHH:mm:ss 형식으로 서버에 전송되는지 네트워크 탭으로 확인
  • 이미지 업로드/삭제 동작 확인

    • 생성/수정 모두에서 이미지 추가/기존 이미지 삭제가 정상 동작하는지 확인

📝 추가 노트

  • 중복투표 여부를 수정할 수 있다면 기본에 중복투표를 했던 경우에 어떻게 처리할지 의논이 필요할 것 같습니다.

후속 작업

  • 옵션 필드 애니메이션 작업
  • 투표 작성 후 수정 불가하다는 안내 모달 연결

@youdaeng2 youdaeng2 requested a review from DreamPaste November 29, 2025 13:38
@youdaeng2 youdaeng2 self-assigned this Nov 29, 2025
@github-actions
Copy link

@youdaeng2 youdaeng2 added Feat 💡 새로운 기능을 구현하고 추가합니다! 유진 labels Nov 29, 2025
@github-actions
Copy link

github-actions bot commented Nov 29, 2025

📦 번들 분석 결과

📊 번들 크기 요약

항목
📦 전체 번들 크기 3.7M
📄 JavaScript 크기 1.6M
🗂️ JavaScript 파일 수 61개

🔍 주요 청크 파일 (크기순)

fe98bb7c-75056deddb8826d9.js - 169K
framework-69e0f7d37422957b.js - 137K
main-448b0e12e0910adc.js - 130K
7147-4b792b1c613a0a9e.js - 122K
1762-0e7232d83dcb3887.js - 121K
polyfills-42372ed130431b0a.js - 110K
2877-2422cd6c39a97d65.js - 90K
6086-65069d4634f32053.js - 76K
page-3026af1cfd56a766.js - 31K
2906-7f73a91282e5669e.js - 28K

🤖 자동 생성된 번들 분석 리포트

@github-actions
Copy link

github-actions bot commented Nov 29, 2025

⚡ Lighthouse 성능 분석 결과

📊 전체 평균 점수

지표 점수
🚀 Performance 74점
♿ Accessibility 86점
✅ Best Practices 100점
🔍 SEO 100점

📈 측정 현황

  • 측정 성공: 15/16 페이지
  • 상태: success

📄 페이지별 상세 분석

🏠 커뮤니티 페이지: /main/community

지표 점수
🚀 Performance 69점
♿ Accessibility 78점
✅ Best Practices 100점
🔍 SEO 100점

📊 상세 분석 보기

👥 창업자 페이지: /main/founder

지표 점수
🚀 Performance 75점
♿ Accessibility 87점
✅ Best Practices 100점
🔍 SEO 100점

📊 상세 분석 보기

🏡 홈 페이지: /main/home

지표 점수
🚀 Performance 75점
♿ Accessibility 91점
✅ Best Practices 100점
🔍 SEO 100점

📊 상세 분석 보기

🗺️ 지도 페이지: /main/maps

지표 점수
🚀 Performance 75점
♿ Accessibility 87점
✅ Best Practices 100점
🔍 SEO 100점

📊 상세 분석 보기

👤 프로필 페이지: /main/profile

지표 점수
🚀 Performance 75점
♿ Accessibility 88점
✅ Best Practices 100점
🔍 SEO 100점

📊 상세 분석 보기

🔗 전체 상세 분석 결과

📊 전체 상세 Lighthouse 분석 결과 보기

📄 측정된 페이지

  • /main/community
  • /main/founder
  • /main/home
  • /main/maps
  • /main/profile

모든 페이지에서 성능 측정이 완료되었습니다.


🤖 자동 생성된 Lighthouse 성능 리포트

Copy link
Member

@DreamPaste DreamPaste left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다! 업데이트된 백엔드 API 및 타입을 반영하고, 디자인 개선을 진행하면 잘 마무리 될 것 같습니다!

Comment on lines 216 to 221
<Select.Content>
<Select.Item value="1d">1일 후 마감</Select.Item>
<Select.Item value="3d">3일 후 마감</Select.Item>
<Select.Item value="7d">7일 후 마감</Select.Item>
</Select.Content>
</Select.Portal>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

마감 기간은 2주까지 있었던걸로 기억합니다.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2주 추가하였습니다.

Comment on lines +22 to +26
// 백엔드 요구사항에 맞춰 인덱스 표기법 사용
votePostCreateRequest.voteOptions.forEach((value, index) => {
formData.append(`voteOptions[${index}].content`, value.content);
});

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

인덱스 표기법으로 접근하면 직접 문자열로 넣어줘야 하는군요..

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FormData는 객체 구조를 이해하지 못해서 배열이나 객체 형태로 만드려면 문자열로 직접 조립해서 넣어줘야한다고 하네요~

Comment on lines 66 to 71
queryClient.invalidateQueries({
queryKey: [`/community/votesboard/${voteboardId}`],
});
queryClient.invalidateQueries({
queryKey: ['/community/votesboard'],
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

orval로 생성된 쿼리 키 get 함수는 없을까요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

있는데 안 쓰고 있네요. 사용하도록 수정했습니다. 감사합니다~!

@github-actions
Copy link

github-actions bot commented Dec 5, 2025

@youdaeng2 youdaeng2 merged commit d9a60bc into dev Dec 5, 2025
4 checks passed
@youdaeng2 youdaeng2 deleted the feat/vote/create-page branch December 5, 2025 04:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Feat 💡 새로운 기능을 구현하고 추가합니다! 유진

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 투표 글 작성 페이지 구현

3 participants