-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 의견 수렴 페이지 전체 API 연동 및 서버 Prefetch 적용 #54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
RookieAND
merged 15 commits into
feature/gathering-custom-hooks
from
feature/opinion-page-api-integration
Jan 30, 2026
Merged
Changes from 11 commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
ce65ed9
refactor: IntroStep scheduledDate prop 연동
RookieAND b6ccda0
refactor: DistanceStep API 기반 region prop 연동
RookieAND 9fad819
refactor: PreferenceStepFooter onSubmit prop 변경
RookieAND 4bbdeea
refactor: CompleteView 텍스트 인라인 처리
RookieAND 8e8993b
refactor: SubmissionBottomSheet props명 변경 (maxCount, currentCount)
RookieAND ad2d5b0
feat: useOpinionForm에 createParticipant 제출 로직 연동
RookieAND d23b783
feat: 의견 수렴 페이지 API 연동 및 타입 수정
RookieAND 767680e
feat: queryOption에 select 추가하여 response.data 자동 추출
RookieAND 1737a23
feat: 의견 수렴 페이지에 서버 프리페치 적용
RookieAND 6bdf53d
fix: useCreateParticipant Hook 을 Import 하는 경로를 올바르게 수정'
RookieAND b36a9fa
feat: PendingView, CompleteView에 HydrationBoundary 및 API 연동 추가
RookieAND 2471ea1
feat: ResultView에 HydrationBoundary 및 API 연동 추가
RookieAND d29c09e
feat: 식당 페이지 이동 과정에서 Cursor Pointer 스타일 적용
RookieAND bac45f1
feat: distanceRange를 실제 거리 값으로 변환하는 로직 추가
RookieAND c70d8a2
refactor: Zod 기반 런타임 검증 추가 및 Controller 패턴에서 useController 패턴으로 마이그레이…
RookieAND File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| "use client"; | ||
|
|
||
| import { useParams, useRouter } from "next/navigation"; | ||
|
|
||
| import { | ||
| IntroStep, | ||
| DistanceStepContent, | ||
| DistanceStepFooter, | ||
| DislikeStepContent, | ||
| DislikeStepFooter, | ||
| PreferenceStepContent, | ||
| PreferenceStepFooter, | ||
| } from "#/pageComponents/gathering/opinion"; | ||
| import { StepTransition } from "#/components/stepTransition"; | ||
| import { useOpinionForm, useOpinionFunnel } from "#/hooks/gathering"; | ||
| import { Button } from "#/components/button"; | ||
| import { Layout } from "#/components/layout"; | ||
| import { FormProvider } from "react-hook-form"; | ||
| import { BackwardButton } from "#/components/backwardButton"; | ||
| import { Toaster } from "#/components/toast"; | ||
| import { useGetGathering } from "#/hooks/apis/gathering"; | ||
|
|
||
| export default function OpinionView() { | ||
| const { accessKey } = useParams<{ accessKey: string }>(); | ||
| const router = useRouter(); | ||
|
|
||
| const { methods, onSubmit } = useOpinionForm(); | ||
| const { step, direction, next, back, isFirstStep } = useOpinionFunnel(); | ||
| const { data: gathering } = useGetGathering(accessKey); | ||
|
|
||
| const handleBackward = () => { | ||
| if (isFirstStep) { | ||
| router.push(`/gathering/${accessKey}`); | ||
| } else { | ||
| back(); | ||
| } | ||
| }; | ||
|
|
||
| const handleComplete = () => { | ||
| onSubmit(); | ||
| }; | ||
|
|
||
| if (step === "intro") { | ||
| return ( | ||
| <> | ||
| <Layout.Header background="gray"> | ||
| <div className="ygi:h-full ygi:w-full" /> | ||
| </Layout.Header> | ||
| <Layout.Content background="gray"> | ||
| <IntroStep scheduledDate={gathering.scheduledDate} /> | ||
| </Layout.Content> | ||
| <Layout.Footer background="gray"> | ||
| <div className="ygi:py-auto ygi:px-6"> | ||
| <Button variant="primary" width="full" onClick={next}> | ||
| 내 취향 입력 | ||
| </Button> | ||
| </div> | ||
| </Layout.Footer> | ||
| </> | ||
| ); | ||
| } | ||
|
|
||
| const renderContent = () => { | ||
| switch (step) { | ||
| case "distance": | ||
| return <DistanceStepContent region={gathering.region} />; | ||
| case "dislike": | ||
| return <DislikeStepContent />; | ||
| case "preference": | ||
| return <PreferenceStepContent />; | ||
| default: | ||
| return null; | ||
| } | ||
| }; | ||
|
|
||
| const renderFooter = () => { | ||
| switch (step) { | ||
| case "distance": | ||
| return <DistanceStepFooter onNext={next} />; | ||
| case "dislike": | ||
| return <DislikeStepFooter onNext={next} />; | ||
| case "preference": | ||
| return <PreferenceStepFooter onSubmit={handleComplete} />; | ||
| default: | ||
| return null; | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <FormProvider {...methods}> | ||
| <Layout.Header> | ||
| <BackwardButton onClick={handleBackward} /> | ||
| </Layout.Header> | ||
| <Layout.Content> | ||
| <StepTransition step={step} direction={direction}> | ||
| {renderContent()} | ||
| </StepTransition> | ||
| </Layout.Content> | ||
| {renderFooter()} | ||
| <Toaster offset={{ bottom: 96 }} mobileOffset={{ bottom: 96 }} /> | ||
| </FormProvider> | ||
| ); | ||
| } |
48 changes: 48 additions & 0 deletions
48
app/gathering/[accessKey]/opinion/complete/CompleteView.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| "use client"; | ||
|
|
||
| import { Button } from "#/components/button"; | ||
| import { Layout } from "#/components/layout"; | ||
| import { | ||
| CompleteView, | ||
| SubmissionBottomSheet, | ||
| } from "#/pageComponents/gathering/opinion"; | ||
| import { useParams, redirect } from "next/navigation"; | ||
| import { useGetGatheringCapacity } from "#/hooks/apis/gathering"; | ||
|
|
||
| export default function CompleteViewContainer() { | ||
| const { accessKey } = useParams<{ accessKey: string }>(); | ||
| const { data: capacity } = useGetGatheringCapacity(accessKey); | ||
|
|
||
| const isPending = capacity.currentCount < capacity.maxCount; | ||
|
|
||
| if (isPending) { | ||
| redirect(`/gathering/${accessKey}/opinion/pending`); | ||
| } | ||
|
|
||
| const handleRedirectResult = () => { | ||
| redirect(`/gathering/${accessKey}/opinion/result`); | ||
| }; | ||
|
|
||
| return ( | ||
| <Layout.Root> | ||
| <CompleteView /> | ||
|
|
||
| <SubmissionBottomSheet | ||
| maxCount={capacity.maxCount} | ||
| currentCount={capacity.currentCount} | ||
| /> | ||
|
|
||
| <Layout.Footer> | ||
| <div className="ygi:px-6"> | ||
| <Button | ||
| variant="primary" | ||
| width="full" | ||
| onClick={handleRedirectResult} | ||
| > | ||
| 추천 결과 보기 | ||
| </Button> | ||
| </div> | ||
| </Layout.Footer> | ||
| </Layout.Root> | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,51 +1,33 @@ | ||
| "use client"; | ||
|
|
||
| import { Button } from "#/components/button"; | ||
| import { Layout } from "#/components/layout"; | ||
| import { | ||
| CompleteView, | ||
| SubmissionBottomSheet, | ||
| } from "#/pageComponents/gathering/opinion"; | ||
| import { useParams } from "next/navigation"; | ||
| import { redirect } from "next/navigation"; | ||
|
|
||
| export default function OpinionCompletePage() { | ||
| const { accessKey } = useParams<{ accessKey: string }>(); | ||
|
|
||
| // TODO: API 연동 시 실제 데이터로 교체 | ||
| const totalCount = 5; | ||
| const submittedCount = 5; | ||
|
|
||
| const isPending = submittedCount < totalCount; | ||
| HydrationBoundary, | ||
| QueryClient, | ||
| dehydrate, | ||
| } from "@tanstack/react-query"; | ||
|
|
||
| import { gatheringOptions } from "#/apis/gathering"; | ||
| import CompleteView from "./CompleteView"; | ||
|
|
||
| interface OpinionCompletePageProps { | ||
| params: Promise<{ | ||
| accessKey: string; | ||
| }>; | ||
| } | ||
|
|
||
| if (isPending) { | ||
| redirect(`/gathering/${accessKey}/opinion/pending`); | ||
| } | ||
| /** | ||
| * 의견 수렴 완료 페이지 (서버 컴포넌트) | ||
| * - gathering capacity 데이터를 서버에서 prefetch하여 무한 렌더링 방지 | ||
| */ | ||
| export default async function OpinionCompletePage({ | ||
| params, | ||
| }: OpinionCompletePageProps) { | ||
| const { accessKey } = await params; | ||
| const queryClient = new QueryClient(); | ||
|
|
||
| const handleRedirectResult = () => { | ||
| redirect(`/gathering/${accessKey}/opinion/result`); | ||
| }; | ||
| await queryClient.prefetchQuery(gatheringOptions.capacity(accessKey)); | ||
|
|
||
| return ( | ||
| <Layout.Root> | ||
| <HydrationBoundary state={dehydrate(queryClient)}> | ||
| <CompleteView /> | ||
|
|
||
| <SubmissionBottomSheet | ||
| totalCount={totalCount} | ||
| submittedCount={submittedCount} | ||
| /> | ||
|
|
||
| <Layout.Footer> | ||
| <div className="ygi:px-6"> | ||
| <Button | ||
| variant="primary" | ||
| width="full" | ||
| onClick={handleRedirectResult} | ||
| > | ||
| 추천 결과 보기 | ||
| </Button> | ||
| </div> | ||
| </Layout.Footer> | ||
| </Layout.Root> | ||
| </HydrationBoundary> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,118 +1,32 @@ | ||
| "use client"; | ||
|
|
||
| import { useParams, useRouter } from "next/navigation"; | ||
|
|
||
| import { | ||
| IntroStep, | ||
| DistanceStepContent, | ||
| DistanceStepFooter, | ||
| DislikeStepContent, | ||
| DislikeStepFooter, | ||
| PreferenceStepContent, | ||
| PreferenceStepFooter, | ||
| } from "#/pageComponents/gathering/opinion"; | ||
| import { StepTransition } from "#/components/stepTransition"; | ||
| import { useOpinionForm, useOpinionFunnel } from "#/hooks/gathering"; | ||
| import { Button } from "#/components/button"; | ||
| import { Layout } from "#/components/layout"; | ||
| import { FormProvider } from "react-hook-form"; | ||
| import { BackwardButton } from "#/components/backwardButton"; | ||
| import { Toaster } from "#/components/toast"; | ||
| import { useMemo } from "react"; | ||
| import { MeetingContext } from "#/types/gathering"; | ||
| import { MOCK_MEETING_DATA } from "#/constants/gathering/opinion/meeting"; | ||
|
|
||
| export default function OpinionPage() { | ||
| const { accessKey } = useParams<{ accessKey: string }>(); | ||
| const router = useRouter(); | ||
|
|
||
| const form = useOpinionForm(); | ||
| const { step, direction, next, back, isFirstStep } = useOpinionFunnel(); | ||
|
|
||
| const meetingContext = useMemo<MeetingContext>( | ||
| () => ({ | ||
| accessKey, | ||
| scheduledDate: MOCK_MEETING_DATA.DATE, | ||
| stationName: MOCK_MEETING_DATA.STATION_NAME, | ||
| }), | ||
| [accessKey], | ||
| ); | ||
|
|
||
| const handleBackward = () => { | ||
| if (isFirstStep) { | ||
| router.push(`/gathering/${accessKey}`); | ||
| } else { | ||
| back(); | ||
| } | ||
| }; | ||
|
|
||
| const handleComplete = () => { | ||
| router.replace(`/gathering/${accessKey}/opinion/pending`); | ||
| }; | ||
|
|
||
| if (step === "intro") { | ||
| return ( | ||
| <> | ||
| <Layout.Header background="gray"> | ||
| <div className="ygi:h-full ygi:w-full" /> | ||
| </Layout.Header> | ||
| <Layout.Content background="gray"> | ||
| {/* TODO : API 연동 과정에서 대체가 필요한 코드 */} | ||
| <IntroStep | ||
| meetingContext={meetingContext} | ||
| step="intro" | ||
| onNext={next} | ||
| /> | ||
| </Layout.Content> | ||
| <Layout.Footer background="gray"> | ||
| <div className="ygi:py-auto ygi:px-6"> | ||
| <Button variant="primary" width="full" onClick={next}> | ||
| 내 취향 입력 | ||
| </Button> | ||
| </div> | ||
| </Layout.Footer> | ||
| </> | ||
| ); | ||
| } | ||
| HydrationBoundary, | ||
| QueryClient, | ||
| dehydrate, | ||
| } from "@tanstack/react-query"; | ||
|
|
||
| import { gatheringOptions } from "#/apis/gathering"; | ||
| import OpinionView from "./OpinionView"; | ||
|
|
||
| interface OpinionPageProps { | ||
| params: Promise<{ | ||
| accessKey: string; | ||
| }>; | ||
| } | ||
|
|
||
| const renderContent = () => { | ||
| switch (step) { | ||
| case "distance": | ||
| return <DistanceStepContent meetingContext={meetingContext} />; | ||
| case "dislike": | ||
| return <DislikeStepContent />; | ||
| case "preference": | ||
| return <PreferenceStepContent />; | ||
| default: | ||
| return null; | ||
| } | ||
| }; | ||
| /** | ||
| * 의견 수렴 페이지 (서버 컴포넌트) | ||
| * - gathering 데이터를 서버에서 prefetch하여 무한 렌더링 방지 | ||
| */ | ||
| export default async function OpinionPage({ params }: OpinionPageProps) { | ||
| const { accessKey } = await params; | ||
| const queryClient = new QueryClient(); | ||
|
|
||
| const renderFooter = () => { | ||
| switch (step) { | ||
| case "distance": | ||
| return <DistanceStepFooter onNext={next} />; | ||
| case "dislike": | ||
| return <DislikeStepFooter onNext={next} />; | ||
| case "preference": | ||
| return <PreferenceStepFooter onComplete={handleComplete} />; | ||
| default: | ||
| return null; | ||
| } | ||
| }; | ||
| // 서버에서 gathering 데이터 미리 가져오기 | ||
| await queryClient.prefetchQuery(gatheringOptions.detail(accessKey)); | ||
|
|
||
| return ( | ||
| <FormProvider {...form}> | ||
| <Layout.Header> | ||
| <BackwardButton onClick={handleBackward} /> | ||
| </Layout.Header> | ||
| <Layout.Content> | ||
| <StepTransition step={step} direction={direction}> | ||
| {renderContent()} | ||
| </StepTransition> | ||
| </Layout.Content> | ||
| {renderFooter()} | ||
| <Toaster offset={{ bottom: 96 }} mobileOffset={{ bottom: 96 }} /> | ||
| </FormProvider> | ||
| <HydrationBoundary state={dehydrate(queryClient)}> | ||
| <OpinionView /> | ||
| </HydrationBoundary> | ||
| ); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.