Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import { Chip } from '@/shared/ui/Chip'
import { useGlobalModalStore } from '@/store'

import { useDeleteMyPreOpinionAnswer } from '../hooks/useDeleteMyPreOpinionAnswer'
import { ROLE_TO_AVATAR_VARIANT } from '../preOpinions.constants'
import type { PreOpinionMember, PreOpinionTopic } from '../preOpinions.types'
import { ROLE_TO_AVATAR_VARIANT } from '../preOpinion.constants'
import type { PreOpinionAnswerTopic, PreOpinionMember } from '../preOpinion.types'

type PreOpinionDetailProps = {
member: PreOpinionMember
topics: PreOpinionTopic[]
topics: PreOpinionAnswerTopic[]
gatheringId: number
meetingId: number
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { UserChip } from '@/shared/ui/UserChip'

import { ROLE_TO_AVATAR_VARIANT } from '../preOpinions.constants'
import type { PreOpinionMember } from '../preOpinions.types'
import { ROLE_TO_AVATAR_VARIANT } from '../preOpinion.constants'
import type { PreOpinionMember } from '../preOpinion.types'

type PreOpinionMemberListProps = {
members: PreOpinionMember[]
Expand Down
86 changes: 33 additions & 53 deletions src/features/pre-opinion/components/PreOpinionWriteHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useEffect, useRef, useState } from 'react'

import type { PreOpinionBook } from '@/features/pre-opinion/preOpinion.types'
import { useScrollShadow } from '@/shared/hooks'
import { cn } from '@/shared/lib/utils'
import { Button } from '@/shared/ui'

Expand Down Expand Up @@ -42,62 +41,43 @@ const PreOpinionWriteHeader = ({
isSubmitting,
isReviewValid = false,
}: PreOpinionWriteHeaderProps) => {
const sentinelRef = useRef<HTMLDivElement>(null)
const [isStuck, setIsStuck] = useState(false)

useEffect(() => {
const sentinel = sentinelRef.current
if (!sentinel) return

const observer = new IntersectionObserver(
([entry]) => {
setIsStuck(!entry.isIntersecting)
},
{ threshold: 0 }
)

observer.observe(sentinel)
return () => observer.disconnect()
}, [])
const isScrolled = useScrollShadow()

return (
<>
<div ref={sentinelRef} className="h-0" aria-hidden />
<div
className={cn(
'sticky top-[123px] z-30 bg-white w-screen ml-[calc(-50vw+50%)] transition-shadow duration-200',
isStuck && '[box-shadow:0_6px_6px_-4px_rgba(17,17,17,0.08)]'
)}
>
<div className="mx-auto max-w-layout-max px-layout-padding h-[65px] pb-tiny">
<div className="flex justify-between items-center">
<div className="flex flex-col gap-xtiny">
<h3 className="typo-heading3 text-black">사전 의견 작성하기</h3>
<p className="text-grey-600">
{book.title} · {book.author}
</p>
</div>
<div className="flex items-center">
{updatedAt && (
<p className="typo-body6 text-grey-600 mr-large">{formatUpdatedAt(updatedAt)}</p>
)}
<Button
className="mr-xsmall"
variant={'secondary'}
outline
onClick={onSave}
disabled={isSaving || !isReviewValid}
>
{isSaving ? '저장 중...' : '저장하기'}
</Button>
<Button onClick={onSubmit} disabled={isSubmitting || isSaving || !isReviewValid}>
{isSubmitting ? '공유 중...' : '공유하기'}
</Button>
</div>
<div
className={cn(
'sticky top-[123px] z-30 bg-white transition-shadow duration-200',
isScrolled && 'shadow-drop-bottom'
)}
>
<div className="mx-auto max-w-layout-max px-layout-padding h-[65px] pb-tiny">
<div className="flex justify-between items-center">
<div className="flex flex-col gap-xtiny">
<h3 className="typo-heading3 text-black">사전 의견 작성하기</h3>
<p className="text-grey-600">
{book.title} · {book.author}
</p>
</div>
<div className="flex items-center">
{updatedAt && (
<p className="typo-body6 text-grey-600 mr-large">{formatUpdatedAt(updatedAt)}</p>
)}
<Button
className="mr-xsmall"
variant={'secondary'}
outline
onClick={onSave}
disabled={isSaving || !isReviewValid}
>
{isSaving ? '저장 중...' : '저장하기'}
</Button>
<Button onClick={onSubmit} disabled={isSubmitting || isSaving || !isReviewValid}>
{isSubmitting ? '공유 중...' : '공유하기'}
</Button>
</div>
</div>
</div>
</>
</div>
)
}

Expand Down
5 changes: 5 additions & 0 deletions src/features/pre-opinion/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './BookReviewSection'
export * from './PreOpinionDetail'
export * from './PreOpinionMemberList'
export * from './PreOpinionWriteHeader'
export * from './TopicItem'
2 changes: 2 additions & 0 deletions src/features/pre-opinion/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export * from './preOpinionQueryKeys'
export * from './useDeleteMyPreOpinionAnswer'
export * from './usePreOpinion'
export * from './usePreOpinionAnswers'
export * from './useSavePreOpinion'
export * from './useSubmitPreOpinion'
17 changes: 16 additions & 1 deletion src/features/pre-opinion/hooks/preOpinionQueryKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,26 @@
* @description 사전 의견 관련 Query Key Factory
*/

import type { GetPreOpinionParams } from '@/features/pre-opinion/preOpinion.types'
import type {
GetPreOpinionAnswersParams,
GetPreOpinionParams,
} from '@/features/pre-opinion/preOpinion.types'

/**
* Query Key Factory
*
* @description
* 사전 의견 관련 Query Key를 일관되게 관리하기 위한 팩토리 함수
*/
export const preOpinionQueryKeys = {
all: ['preOpinions'] as const,

// 내 사전 의견 작성/조회 관련
details: () => [...preOpinionQueryKeys.all, 'detail'] as const,
detail: (params: GetPreOpinionParams) => [...preOpinionQueryKeys.details(), params] as const,

// 사전 의견 목록 관련
answers: () => [...preOpinionQueryKeys.all, 'answers'] as const,
answerList: (params: GetPreOpinionAnswersParams) =>
[...preOpinionQueryKeys.answers(), params] as const,
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'

import type { ApiError } from '@/api'

import { deleteMyPreOpinionAnswer } from '../preOpinions.api'
import type { DeleteMyPreOpinionAnswerParams } from '../preOpinions.types'
import { deleteMyPreOpinionAnswer } from '../preOpinion.api'
import type { DeleteMyPreOpinionAnswerParams } from '../preOpinion.types'
import { preOpinionQueryKeys } from './preOpinionQueryKeys'

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { useQuery } from '@tanstack/react-query'

import type { ApiError } from '@/api'

import { getPreOpinionAnswers } from '../preOpinions.api'
import type { GetPreOpinionAnswersParams, PreOpinionAnswersData } from '../preOpinions.types'
import { getPreOpinionAnswers } from '../preOpinion.api'
import type { GetPreOpinionAnswersParams, PreOpinionAnswersData } from '../preOpinion.types'
import { preOpinionQueryKeys } from './preOpinionQueryKeys'

/**
Expand All @@ -18,9 +18,7 @@ import { preOpinionQueryKeys } from './preOpinionQueryKeys'
* TanStack Query를 사용하여 약속의 사전 의견 목록을 조회합니다.
* 멤버별 책 평가, 주제별 의견 등을 포함합니다.
*
* @param params - 조회 파라미터
* @param params.gatheringId - 모임 식별자
* @param params.meetingId - 약속 식별자
* @param params - 모임 ID와 약속 ID
*
* @returns TanStack Query 결과 객체
*/
Expand Down
16 changes: 16 additions & 0 deletions src/features/pre-opinion/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Components
export * from './components'

// Hooks
export * from './hooks'

// API
export * from './preOpinion.api'
export * from './preOpinion.endpoints'
export * from './preOpinion.mock'

// Constants
export * from './preOpinion.constants'

// Types
export * from './preOpinion.types'
52 changes: 51 additions & 1 deletion src/features/pre-opinion/preOpinion.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@

import { api } from '@/api/client'
import { PRE_OPINION_ENDPOINTS } from '@/features/pre-opinion/preOpinion.endpoints'
import { getMockPreOpinionDetail } from '@/features/pre-opinion/preOpinion.mock'
import {
getMockPreOpinionAnswers,
getMockPreOpinionDetail,
} from '@/features/pre-opinion/preOpinion.mock'
import type {
DeleteMyPreOpinionAnswerParams,
GetPreOpinionAnswersParams,
GetPreOpinionParams,
GetPreOpinionResponse,
PreOpinionAnswersData,
SavePreOpinionBody,
SavePreOpinionParams,
SubmitPreOpinionBody,
Expand Down Expand Up @@ -53,6 +59,8 @@ export const savePreOpinion = async (
{ gatheringId, meetingId, isFirstSave }: SavePreOpinionParams,
body: SavePreOpinionBody
): Promise<void> => {
if (USE_MOCK) return

if (isFirstSave) {
return api.post(PRE_OPINION_ENDPOINTS.CREATE(gatheringId, meetingId), body)
}
Expand All @@ -74,5 +82,47 @@ export const submitPreOpinion = async (
meetingId: number,
body: SubmitPreOpinionBody
): Promise<void> => {
if (USE_MOCK) return

return api.patch(PRE_OPINION_ENDPOINTS.SUBMIT(gatheringId, meetingId), body)
}

/**
* 사전 의견 목록 조회
*
* @description
* 약속의 사전 의견 목록(멤버별 책 평가 + 주제 의견)을 조회합니다.
*
* @param params - 모임 ID와 약속 ID
*
* @returns 사전 의견 목록 데이터 (topics + members)
*/
export const getPreOpinionAnswers = async (
params: GetPreOpinionAnswersParams
): Promise<PreOpinionAnswersData> => {
const { gatheringId, meetingId } = params

if (USE_MOCK) {
await new Promise((resolve) => setTimeout(resolve, 500))
return getMockPreOpinionAnswers()
}

return api.get<PreOpinionAnswersData>(PRE_OPINION_ENDPOINTS.ANSWERS(gatheringId, meetingId))
}

/**
* 내 사전 의견 삭제
*
* @description
* 현재 로그인한 사용자의 사전 의견을 삭제합니다.
*
* @param params - 모임 ID와 약속 ID
*/
export const deleteMyPreOpinionAnswer = async (
params: DeleteMyPreOpinionAnswerParams
): Promise<void> => {
if (USE_MOCK) return

const { gatheringId, meetingId } = params
return api.delete(PRE_OPINION_ENDPOINTS.DELETE_MY_ANSWER(gatheringId, meetingId))
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { MemberRole } from './preOpinions.types'
import type { MemberRole } from './preOpinion.types'

/** API MemberRole → Avatar variant 매핑 */
export const ROLE_TO_AVATAR_VARIANT: Record<MemberRole, 'leader' | 'host' | 'member'> = {
Expand Down
7 changes: 7 additions & 0 deletions src/features/pre-opinion/preOpinion.endpoints.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { API_PATHS } from '@/api'

export const PRE_OPINION_ENDPOINTS = {
// 사전 의견 목록 조회 (GET /api/gatherings/{gatheringId}/meetings/{meetingId}/answers)
ANSWERS: (gatheringId: number, meetingId: number) =>
`${API_PATHS.GATHERINGS}/${gatheringId}/meetings/${meetingId}/answers`,

// 내 사전 의견 삭제 (DELETE /api/gatherings/{gatheringId}/meetings/{meetingId}/topics/answers/me)
DELETE_MY_ANSWER: (gatheringId: number, meetingId: number) =>
`${API_PATHS.GATHERINGS}/${gatheringId}/meetings/${meetingId}/topics/answers/me`,
// 사전 의견 조회 (GET /api/gatherings/{gatheringId}/meetings/{meetingId}/answers/me)
DETAIL: (gatheringId: number, meetingId: number) =>
`${API_PATHS.GATHERINGS}/${gatheringId}/meetings/${meetingId}/answers/me`,
Expand Down
Loading
Loading